From 1f90c819a0376895906303f2001b2e66671265f7 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 12 May 2026 13:36:38 -0400 Subject: [PATCH] docs(bridge): update types + docs for set_display_layout configStr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add set_display_layout (+ other missing system handler methods) to AetherNativeBridge interface in types.ts - README: clarify configStr source (event_device.data_json) and no-op behaviour when absent - TODO_AGENTS: correct 7z→bsdtar throughout; mark Electron-side set_display_layout items done; remove completed bsdtar doc item Co-Authored-By: Claude Sonnet 4.6 --- README.md | 2 +- documentation/TODO_AGENTS.md | 61 +++++++++++++++++++++++++++++++++--- src/shared/types.ts | 16 ++++++++-- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fc3cfe3..7148080 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ to change. | `window_control({action, value?})` | Electron window: maximize, minimize, restore, close, fullscreen, kiosk, devtools, reload. | | `set_wallpaper({path})` | Sets desktop wallpaper. macOS (AppleScript) + Linux (gsettings/Gnome). | | `power_control({action})` | Shutdown, reboot, or sleep. macOS + Linux. Requires sudo for shutdown/reboot. | -| `set_display_layout({mode, configStr?})` | Mirror/extend displays via bundled `displayplacer` binary. macOS only. | +| `set_display_layout({mode, configStr?})` | Mirror/extend displays via bundled `displayplacer` binary. macOS only. `configStr` is the output of `displayplacer list` for that machine, stored in `event_device.data_json.displayplacer_config_mirror` / `displayplacer_config_extend`. Required — silently no-ops without it. | | `manage_recording({action, options?})` | Screen recording via bundled `aperture` binary. macOS only. | | `update_app(args)` | **Stub.** Downloads update package but does not install. Not functional. | | `list_tools()` | Returns a self-describing manifest of all available bridge functions. | diff --git a/documentation/TODO_AGENTS.md b/documentation/TODO_AGENTS.md index 252ba0b..2975188 100644 --- a/documentation/TODO_AGENTS.md +++ b/documentation/TODO_AGENTS.md @@ -15,7 +15,7 @@ - Upgraded Electron from 34.x to 42.0.1. - Replaced deprecated `electron-packager` with `@electron/packager` 20.0.0. - Added a `package:linux` smoke test path so packaging failures can be isolated from macOS-specific behavior. -- **Fixed packaging hang on Node 26:** `yauzl` 2.10.0 (used by `extract-zip` in `@electron/packager`) emits no `data` events on Node 26 streams, causing zip extraction to hang indefinitely. Fix: patched `@electron/packager/dist/unzip.js` to use `7z` (system binary) instead of `extract-zip`. Patch is re-applied on every `npm install` via the `postinstall` script at `scripts/patch-packager-unzip.js`. +- **Fixed packaging hang on Node 26:** `yauzl` 2.10.0 (used by `extract-zip` in `@electron/packager`) emits no `data` events on Node 26 streams, causing zip extraction to hang indefinitely. Fix: patched `@electron/packager/dist/unzip.js` to use `bsdtar` (libarchive) instead of `extract-zip`. `bsdtar` was chosen over `7z` because `7z` refuses macOS `.app` bundles with chained symlinks inside framework bundles. Patch is re-applied on every `npm install` via the `postinstall` script at `scripts/patch-packager-unzip.js`. ## Verified So Far - `npm run dev` works once the Electron binary is present locally. @@ -31,14 +31,13 @@ ## Remaining Items 1. Test that the packaged Linux binary runs end-to-end against the dev API. -2. Document that `bsdtar` (libarchive) must be present on the build host — new build-time dependency. On Arch: `sudo pacman -S libarchive`. ## Root Cause Summary (Packaging Hang) - **Tool chain:** Node 26.1.0 + `@electron/packager` 20.0.0 + `extract-zip` 2.0.1 + `yauzl` 2.10.0 - **Symptom:** `npm run package:linux` exits 0 but produces no output. Debug log shows it starts extraction but never finishes. - **Root cause:** `yauzl` opens a read stream for the first zip entry, but on Node 26, no `data` events are ever emitted on that stream. The `pipeline(readStream, writeStream)` call in `extract-zip` blocks forever. -- **Fix:** Replace the one-liner `extractElectronZip` function in `node_modules/@electron/packager/dist/unzip.js` with a `child_process.execSync` call to `7z x`. A `postinstall` npm script re-applies this patch after each `npm install`. -- **Build-time dependency:** `p7zip` (provides `/usr/bin/7z`) must be installed on the build host. On Arch: `pacman -S p7zip`. +- **Fix:** Replace the one-liner `extractElectronZip` function in `node_modules/@electron/packager/dist/unzip.js` with a `child_process.execSync` call to `bsdtar -xf`. `bsdtar` was chosen over `7z` because `7z` refuses macOS `.app` bundles with chained symlinks (e.g. `Electron Framework.framework/Libraries → Versions/Current/Libraries`). A `postinstall` npm script re-applies this patch after each `npm install`. +- **Build-time dependency:** `libarchive` (provides `bsdtar`) must be installed on the build host. On Arch: `pacman -S libarchive`; macOS: included in Xcode CLT or `brew install libarchive`; Ubuntu/Debian: `apt install libarchive-tools`. ## References - Electron 42 release notes: https://www.electronjs.org/blog/electron-42-0 @@ -50,3 +49,57 @@ - Was on Electron 34. - The problem is not the backend API keys or the frontend site bootstrap flow. - The packaging fix is a node_modules patch, not upstream. If `@electron/packager` or `extract-zip` releases a Node 26-compatible version, the `postinstall` script should be removed. + +--- + +## Pending Feature: set_display_layout — displayplacer Per-Device Config + +**Background (added 2026-05-12, from Svelte-side LaunchProfile work):** + +The Svelte Events Launcher (`launcher_file_cont.svelte`) now resolves a `LaunchProfile` per file extension and calls `native.set_display_layout({ mode })` before opening a presentation. The underlying handler in `src/main/system_handlers.ts` uses a bundled `displayplacer` macOS binary. The wiring is complete on both ends — but it **silently no-ops on every device** because `displayplacer` requires a per-machine `configStr` (the output of `displayplacer list` on *that specific Mac*) to identify the exact display UUIDs and pixel positions. Without it, `displayplacer` cannot apply any layout. + +**What's needed:** + +1. **Capture `configStr` per room Mac.** On each presentation Mac, run: + ``` + displayplacer list + ``` + This prints the current display configuration. Copy the full output for both the "extend" and "mirror" layouts (they differ in which display is primary and how they're positioned). + +2. **Store configs in the API.** The Svelte side reads device config from `event_device.data_json`. Store the captured strings there: + ```json + { + "displayplacer_config_extend": "", + "displayplacer_config_mirror": "" + } + ``` + Admin UI to set these values already exists via the normal event_device edit flow. You can also set them directly via the V3 CRUD API (`PATCH /v3/crud/event_device/{id}/`). + +3. ✅ **`set_display_layout` in `src/main/system_handlers.ts` already accepts `configStr`.** Handler signature is `{ mode, configStr }` and uses it correctly — no change needed. + +4. ✅ **Electron relay (`src/preload/index.ts`) already forwards `configStr`** — args passed as-is. `AetherNativeBridge` type in `src/shared/types.ts` updated to include `set_display_layout` with correct signature (2026-05-12). + +5. **Pass `configStr` from Svelte.** The Svelte call site in `launcher_file_cont.svelte` (Step 3 in `handle_open_file`) currently calls: + ```ts + await native.set_display_layout({ mode: profile.display_mode }).catch(() => {}); + ``` + It needs to be updated to thread `configStr` from `$ae_loc.native_device.data_json`: + ```ts + const cfg_key = profile.display_mode === 'mirror' + ? 'displayplacer_config_mirror' + : 'displayplacer_config_extend'; + const configStr = ($ae_loc as any).native_device?.data_json?.[cfg_key] ?? null; + await native.set_display_layout({ mode: profile.display_mode, configStr }).catch(() => {}); + ``` + +**Contract already in place (Svelte side — no action needed):** +- `$ae_loc.native_device` is the `event_device` object loaded during native app bootstrap +- `data_json` is an open JSON field on that object +- `handle_open_file()` already calls `set_display_layout` at the right point — it just needs the configStr threaded through + +**Resources:** +- displayplacer GitHub (usage + examples): https://github.com/jakehilborn/displayplacer +- `displayplacer list` — prints current layout as a re-runnable config string +- `displayplacer ` — applies layout; the configStr from `list` is what you pass back +- Current Electron handler: `src/main/system_handlers.ts` — find the `set_display_layout` IPC handler +- Svelte call site: `src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte` — Step 3 comment in `handle_open_file()` diff --git a/src/shared/types.ts b/src/shared/types.ts index 46ec0bd..698eaed 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -23,12 +23,22 @@ export interface AetherNativeBridge { // File/Cache Handlers check_cache: (args: {cache_root: string, hash: string, hash_prefix_length?: number}) => Promise; download_to_cache: (args: {url: string, cache_root: string, hash: string, api_key: string, account_id?: string, hash_prefix_length?: number}) => Promise<{success: boolean, error?: string}>; - launch_from_cache: (args: {cache_root: string, hash: string, temp_root: string, filename: string, hash_prefix_length?: number}) => Promise<{success: boolean, error?: string}>; - + copy_from_cache_to_temp: (args: {cache_root: string, hash: string, temp_root: string, filename: string, hash_prefix_length?: number}) => Promise<{success: boolean, path?: string, error?: string}>; + launch_from_cache: (args: {cache_root: string, hash: string, temp_root: string, filename: string, hash_prefix_length?: number, script_template?: string}) => Promise<{success: boolean, error?: string}>; + // Specialized Presentation Handlers (Phase 5) launch_presentation: (args: {path: string, app?: string}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>; control_presentation: (args: {app: 'powerpoint' | 'keynote', action: 'next' | 'prev' | 'start' | 'stop'}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>; - + + // System Handlers (Phase 5) + window_control: (args: {action: 'maximize' | 'unmaximize' | 'minimize' | 'restore' | 'close' | 'devtools' | 'kiosk' | 'fullscreen' | 'reload', value?: boolean}) => Promise<{success: boolean, error?: string}>; + set_wallpaper: (args: {path: string}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>; + power_control: (args: {action: 'shutdown' | 'reboot' | 'sleep'}) => Promise<{success: boolean, error?: string}>; + open_external: (args: {url: string, app?: 'chrome' | 'firefox'}) => Promise<{success: boolean, error?: string}>; + manage_recording: (args: {action: 'start' | 'stop' | 'status', options?: {fps?: number, audioDeviceId?: string, output?: string}}) => Promise<{success: boolean, isRecording?: boolean, pid?: number, error?: string}>; + set_display_layout: (args: {mode: 'mirror' | 'extend', configStr?: string | null}) => Promise<{success: boolean, error?: string, stdout?: string, stderr?: string}>; + update_app: (args: {source: 'url' | 'file', url?: string, path?: string}) => Promise<{success: boolean, message?: string, downloadedPath?: string, error?: string}>; + // Self-Documentation list_tools: () => Promise>; }