chore: P0 skeleton

Bootstrap the Dashward repo per arch/UBUNTU-DASHBOARD-SPACE.md:

- pnpm-workspaces monorepo (sdk, extension, container, widgets-builtin/*)
- GNOME extension stub (metadata.json, src/*.ts placeholders for warden,
  guard, supervisor, entry UX, DBus service)
- WebKit container stub (GJS main + page-side runtime + dashboard.html)
- TypeScript widget SDK (defineWidget + types)
- Builtin clock widget as the first SDK consumer example
- DBus interface XML (proto/shell.iface.xml) and shared types
- esbuild configs for extension and container; tsc for SDK
- Design doc copied in at repo root for discoverability

No functional logic yet -- all components are placeholders that compose
in extension.ts so the build chain can be exercised. P1 (workspace
warden) starts next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-22 23:00:02 +01:00
commit 3bf3aa1989
41 changed files with 1361 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import esbuild from 'esbuild';
const watch = process.argv.includes('--watch');
const opts = {
entryPoints: ['src/extension.ts'],
bundle: true,
outfile: 'dist/extension.js',
format: 'esm',
target: 'firefox128',
platform: 'neutral',
external: [
'gi://*',
'resource:///*',
'system',
'cairo',
'gettext',
],
sourcemap: 'inline',
logLevel: 'info',
};
if (watch) {
const ctx = await esbuild.context(opts);
await ctx.watch();
} else {
await esbuild.build(opts);
}

9
extension/metadata.json Normal file
View File

@@ -0,0 +1,9 @@
{
"uuid": "dashward@hangman-lab.top",
"name": "Dashward",
"description": "A dedicated rightmost workspace for custom widgets, in the spirit of macOS Dashboard.",
"shell-version": ["48"],
"url": "https://git.hangman-lab.top/hzhang/Dashward",
"version": 1,
"settings-schema": "org.gnome.shell.extensions.dashward"
}

16
extension/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "@dashward/extension",
"version": "0.0.0",
"private": true,
"description": "GNOME Shell extension that creates and guards the Dashward workspace.",
"type": "module",
"scripts": {
"build": "node esbuild.config.js",
"dev": "node esbuild.config.js --watch"
},
"devDependencies": {
"@girs/gnome-shell": "48.0.0-next.1",
"@girs/meta-16": "16.0.0-next.1",
"@girs/gtk-3.0": "3.24.0-next.1"
}
}

View File

@@ -0,0 +1,9 @@
// Stub: see design §7 — web container process.
// Spawn container via Gio.Subprocess; match its window by Wayland app_id;
// pin to dashboard workspace; restart with exponential backoff on crash.
export class ContainerSupervisor {
dispose(): void {
// graceful shutdown via DBus, then SIGTERM after 2s
}
}

View File

@@ -0,0 +1,8 @@
// Stub: see design §8.1 — Shell ↔ Container DBus.
// Bus: top.hangmanlab.Dashward.Shell, path /top/hangmanlab/Dashward.
export class DBusService {
dispose(): void {
// unown bus name, disconnect handlers
}
}

View File

@@ -0,0 +1,9 @@
// Stub: see design §13 — entry/exit UX.
// Three paths in: 4-finger swipe right past last workspace, Super+Grave,
// panel button. All converge on openDashboard().
export class EntryUX {
dispose(): void {
// remove gesture hook, keybinding, panel indicator
}
}

View File

@@ -0,0 +1,40 @@
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import { WorkspaceWarden } from './workspace-warden.js';
import { WindowGuard } from './window-guard.js';
import { ContainerSupervisor } from './container-supervisor.js';
import { EntryUX } from './entry-ux.js';
import { DBusService } from './dbus-service.js';
export default class DashwardExtension extends Extension {
private warden?: WorkspaceWarden;
private guard?: WindowGuard;
private container?: ContainerSupervisor;
private entry?: EntryUX;
private dbus?: DBusService;
enable(): void {
log('[Dashward] enable: P0 skeleton — components not wired yet');
this.warden = new WorkspaceWarden();
this.guard = new WindowGuard();
this.container = new ContainerSupervisor();
this.entry = new EntryUX();
this.dbus = new DBusService();
}
disable(): void {
log('[Dashward] disable');
this.dbus?.dispose();
this.entry?.dispose();
this.container?.dispose();
this.guard?.dispose();
this.warden?.dispose();
this.dbus = undefined;
this.entry = undefined;
this.container = undefined;
this.guard = undefined;
this.warden = undefined;
}
}
declare function log(msg: string): void;

View File

@@ -0,0 +1,9 @@
// Stub: see design §6 — window-defense rules.
// Layer 1: window-added bounce-back. Layer 2: overview drop refusal.
// Layer 3: hide from switcher strip. Layer 4: workspace switcher clamp.
export class WindowGuard {
dispose(): void {
// disconnect all signals and restore patched prototypes
}
}

View File

@@ -0,0 +1,9 @@
// Stub: see design §5 — workspace lifecycle.
// Responsibilities: snapshot gsettings, append the dashboard workspace, hold
// position invariant on every workspace mutation, restore on disable.
export class WorkspaceWarden {
dispose(): void {
// restore snapshotted settings, remove dashboard workspace
}
}

10
extension/tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"noEmit": true,
"types": []
},
"include": ["src/**/*"]
}

View File

@@ -0,0 +1,13 @@
{
"name": "@dashward/widget-clock",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js --external:@dashward/widget-sdk",
"dev": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js --external:@dashward/widget-sdk --watch"
},
"dependencies": {
"@dashward/widget-sdk": "workspace:*"
}
}

View File

@@ -0,0 +1,41 @@
import { defineWidget } from '@dashward/widget-sdk';
interface ClockConfig {
hour12: boolean;
showSeconds: boolean;
}
export default defineWidget<ClockConfig>({
id: 'clock',
defaultConfig: { hour12: false, showSeconds: true },
mount(host, { config, lifecycle, system }) {
const root = host.element.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>
:host { display: grid; place-items: center; }
.time { font: 600 clamp(2rem, 8vw, 5rem) ui-sans-serif, system-ui; }
:host([data-theme="dark"]) .time { color: #f0f0f0; }
</style>
<div class="time"></div>
`;
const el = root.querySelector('.time') as HTMLDivElement;
const render = () => {
const cfg = config.get();
el.textContent = new Date().toLocaleTimeString(undefined, {
hour12: cfg.hour12,
hour: '2-digit',
minute: '2-digit',
second: cfg.showSeconds ? '2-digit' : undefined,
});
};
const timer = setInterval(render, 1000);
render();
lifecycle.onUnmount(() => clearInterval(timer));
config.onChange(render);
system.onThemeChange(t => (host.element.dataset.theme = t));
},
});

View File

@@ -0,0 +1,10 @@
{
"id": "clock",
"name": "Clock",
"version": "0.0.0",
"entry": "dist/index.js",
"icon": "icon.svg",
"size": { "w": 2, "h": 2, "minW": 1, "minH": 1, "maxW": 4, "maxH": 4 },
"permissions": ["timer"],
"configSchema": { "hour12": "boolean", "showSeconds": "boolean" }
}