Files
Dashward/container/runtime/runtime.ts
hzhang 5ed4170e69 feat: P4 — DBus bridge + page round-trip + system theme tracking
Three new wires from page to shell and back, proving the bridge end to
end. The dashboard placeholder now follows the system color-scheme via
the full chain: page-side dashShell.call('getTheme') → WebKit script
message handler → DBus method on the shell-owned service → gsettings
read → return value back up. ThemeChanged proves the reverse direction:
the shell watches color-scheme and emits a DBus signal that the
container forwards to the page as a CustomEvent.

extension/src/dbus-service.ts
  Owns top.hangmanlab.Dashward.Shell on the session bus; exports
  GetTheme (reads org.gnome.desktop.interface::color-scheme, maps
  prefer-dark→dark / else→light); emits ThemeChanged on the gsettings
  changed signal. Bus acquisition is async, so the constructor takes an
  onReady callback that fires from the bus_acquired handler.

extension/src/extension.ts
  Sequencing: warden → guard → dbus, and only after dbus_acquired
  callback fires do we spawn ContainerSupervisor. Disable order is
  reversed (container first so its DBus proxy stops calling the service
  before we unown the bus name).

container/src/dbus-client.ts
  Thin GJS proxy wrapper around the Shell interface; exposes a typed
  getTheme() / onThemeChange(cb) API.

container/src/bridge.ts
  Registers a `shellCall` UCM script-message handler; parses
  {id, method, args} JSON from the page, dispatches to invoke(), and
  feeds the result back via evaluate_javascript. Shell signals are
  forwarded to the page via window.__dashShell__._onSignal.

container/runtime/runtime.ts
  Installs window.__dashShell__ at script start, calls getTheme() on
  load and applies the result to <html data-theme>, listens for the
  ThemeChanged CustomEvent.

container/runtime/dashboard.html + esbuild.config.js
  Switched the runtime bundle from `format: 'esm'` to IIFE so it loads
  as a classic <script> -- WebKitGTK silently refuses `<script
  type="module">` over file:// for ES module resolution. Added an
  inline error catcher that surfaces JS errors in the visible boot
  diagnostic div instead of failing silently.

container/src/main.ts
  Construct ShellClient + Bridge before load_uri so the very first
  page-side dashShell.call() has a handler waiting. Added load-changed
  / load-failed signal logging for future diagnosis.

Verified on ubuntu2504-test VM: enable produces clean log chain through
"container window arrived", page renders with "P4 — shell bridge
online" placeholder and the correct system theme on first paint, manual
`gsettings set ... color-scheme prefer-dark` flips the placeholder
background live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:54:19 +01:00

95 lines
2.7 KiB
TypeScript

// Page-side runtime (design §11). P4 establishes the shell-bridge and
// uses it to follow the system color scheme. Real widget loading / edit
// mode / layout persistence arrive in P5+.
interface PendingCall {
resolve(value: unknown): void;
reject(err: Error): void;
}
interface DashShellInternal {
call<T = unknown>(method: string, args?: unknown): Promise<T>;
_resolve(id: number, value: unknown): void;
_reject(id: number, error: string): void;
_onSignal(name: string, payload: unknown): void;
}
declare global {
interface Window {
__dashShell__?: DashShellInternal;
webkit?: { messageHandlers: { shellCall: { postMessage(data: string): void } } };
}
}
const pending = new Map<number, PendingCall>();
let nextId = 1;
const dashShell: DashShellInternal = {
call<T>(method: string, args?: unknown): Promise<T> {
return new Promise<T>((resolve, reject) => {
const id = nextId++;
pending.set(id, { resolve: resolve as (v: unknown) => void, reject });
try {
window.webkit!.messageHandlers.shellCall.postMessage(
JSON.stringify({ id, method, args }),
);
} catch (e) {
pending.delete(id);
reject(e instanceof Error ? e : new Error(String(e)));
}
});
},
_resolve(id, value) {
const p = pending.get(id);
if (!p) return;
pending.delete(id);
p.resolve(value);
},
_reject(id, err) {
const p = pending.get(id);
if (!p) return;
pending.delete(id);
p.reject(new Error(err));
},
_onSignal(name, payload) {
window.dispatchEvent(new CustomEvent(`dashward:${name}`, { detail: payload }));
},
};
window.__dashShell__ = dashShell;
// Theme: ask once on load, then follow ThemeChanged from the shell.
function applyTheme(theme: unknown): void {
if (typeof theme !== 'string') return;
document.documentElement.dataset.theme = theme;
}
dashShell
.call<string>('getTheme')
.then(applyTheme)
.catch(e => console.warn('[dashward] getTheme failed:', e));
window.addEventListener('dashward:ThemeChanged', evt => {
applyTheme((evt as CustomEvent<unknown>).detail);
});
// Remove the boot diagnostic now that runtime.js is executing.
document.getElementById('boot')?.remove();
// Placeholder card so the dashboard workspace is visually distinct.
const grid = document.getElementById('grid');
if (grid) {
const placeholder = document.createElement('section');
placeholder.className = 'placeholder';
placeholder.innerHTML = `
<h1>Dashward</h1>
<p class="hint">P4 — shell bridge online.</p>
<p class="meta">Theme follows system. Widgets land in P5.</p>
`;
grid.appendChild(placeholder);
}
console.info('[dashward-runtime] P4 bridge mounted');
export {};