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>
This commit is contained in:
@@ -6,7 +6,28 @@
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Visible without JS. runtime.js removes it on mount; the error
|
||||
handler below replaces its content if JS throws. -->
|
||||
<div id="boot">dashboard.html loaded — waiting for runtime.js</div>
|
||||
<main id="grid"></main>
|
||||
<script type="module" src="runtime.js"></script>
|
||||
|
||||
<script>
|
||||
// Tiny inline diagnostic so a runtime.js syntax/runtime error is
|
||||
// visible on screen (and forwarded to the host) instead of failing
|
||||
// silently.
|
||||
window.addEventListener('error', function (e) {
|
||||
var b = document.getElementById('boot');
|
||||
if (b) b.textContent = 'JS ERROR: ' + (e.message || '?') +
|
||||
(e.filename ? ' @ ' + e.filename + ':' + e.lineno : '');
|
||||
});
|
||||
window.addEventListener('unhandledrejection', function (e) {
|
||||
var b = document.getElementById('boot');
|
||||
if (b) b.textContent = 'PROMISE REJECT: ' + String(e.reason);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- bundle is self-contained; classic script avoids file:// module
|
||||
loader quirks in WebKitGTK. -->
|
||||
<script src="runtime.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,28 +1,94 @@
|
||||
// Page-side runtime (design §11). For P3 the grid is just decoration —
|
||||
// it renders a "no widgets yet" placeholder so the dashboard workspace
|
||||
// has visible content. Real widget loading / edit mode / layout
|
||||
// persistence come in P5+.
|
||||
// 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__?: {
|
||||
call(method: string, args?: unknown): Promise<unknown>;
|
||||
};
|
||||
__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">P3 — empty dashboard.</p>
|
||||
<p class="meta">No widgets installed yet. Widget SDK + edit mode arrive in P5.</p>
|
||||
<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] P3 placeholder mounted');
|
||||
console.info('[dashward-runtime] P4 bridge mounted');
|
||||
|
||||
export {};
|
||||
|
||||
@@ -33,6 +33,19 @@ body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#boot {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 80, 80, 0.85);
|
||||
color: white;
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user