fix(display): fix mirror_of_display syntax, add Homebrew fallback, update docs

- `mirror:` was wrong; correct displayplacer param is `mirror_of_display:<uuid>`
- Same fix applied to the strip regex (extend path) and mirror detection check
- Binary lookup now tries resources/bin → /opt/homebrew/bin → /usr/local/bin
  so dev/venue Macs with `brew install displayplacer` work without bundling
- Updated TODO_AGENTS.md: marks auto-detection complete, documents one-time
  brew install step, bundling path, and optional per-device configStr override

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-20 16:33:25 -04:00
parent 2bf4d7c141
commit 9df9d884e5
2 changed files with 47 additions and 52 deletions

View File

@@ -66,54 +66,42 @@
--- ---
## Pending Feature: set_display_layout — displayplacer Per-Device Config ## set_display_layout — displayplacer Setup & Status (updated 2026-05-20)
**Background (added 2026-05-12, from Svelte-side LaunchProfile work):** **Reference:** [jakehilborn/displayplacer](https://github.com/jakehilborn/displayplacer)
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. **Current state (2026-05-20):**
**What's needed:** - ✅ Handler auto-detects displays via `displayplacer list` — no per-device `configStr` required for normal use
- ✅ Binary lookup order: `resources/bin/displayplacer` (bundled) → `/opt/homebrew/bin/displayplacer` (Apple Silicon Homebrew) → `/usr/local/bin/displayplacer` (Intel Homebrew)
- ✅ Correct `mirror_of_display:<uuid>` syntax used (was `mirror:` — wrong, now fixed)
- ✅ Failures logged to Electron console (`[Launcher] set_display_layout:`) instead of silently swallowed
-**Display Mode toggle** added to Launcher config (Native OS section) — Extend/Mirror buttons always visible, no Technical Mode required
1. **Capture `configStr` per room Mac.** On each presentation Mac, run: **One-time setup on each venue Mac:**
``` ```bash
displayplacer list brew install displayplacer
``` ```
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). The binary is not bundled in the Electron build yet. Homebrew installs it to `/opt/homebrew/bin/` (Apple Silicon) or `/usr/local/bin/` (Intel) — both are in the fallback lookup chain.
2. **Store configs in the API.** The Svelte side reads device config from `event_device.data_json`. Store the captured strings there: **Bundling the binary (future):**
```json To ship displayplacer inside the `.app` bundle without requiring Homebrew on venue Macs:
{ 1. Copy the `displayplacer` binary to `resources/bin/displayplacer`
"displayplacer_config_extend": "<full configStr for extended layout>", 2. Mark it executable: `chmod +x resources/bin/displayplacer`
"displayplacer_config_mirror": "<full configStr for mirrored layout>" 3. Add to `package.json` `extraResources` so `@electron/packager` includes it
}
```
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. **Optional per-device override (manual tuning):**
For rooms where auto-detection produces the wrong result, store the raw configStr in `event_device.data_json`:
```json
{
"displayplacer_config_extend": "<output of displayplacer list in extended layout>",
"displayplacer_config_mirror": "<output of displayplacer list in mirrored layout>"
}
```
The handler accepts `configStr` and uses it directly, bypassing auto-detection. Pass it from the Svelte call site if 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). **displayplacer quick reference:**
- `displayplacer list` — prints current display info and a ready-to-run config string at the bottom
5. **Pass `configStr` from Svelte.** The Svelte call site in `launcher_file_cont.svelte` (Step 3 in `handle_open_file`) currently calls: - `displayplacer "<display_string>" "<display_string>"` — applies layout
```ts - Mirror syntax: add `mirror_of_display:<primary_uuid>` to the secondary display string
await native.set_display_layout({ mode: profile.display_mode }).catch(() => {}); - Extend syntax: set `origin:(<x>,0)` with non-overlapping x offsets per display
```
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 <configStr>` — 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()`

View File

@@ -325,9 +325,16 @@ export function registerSystemHandlers() {
ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => { ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
if (os.platform() !== 'darwin') return { success: false, error: 'Display control only supported on macOS' }; if (os.platform() !== 'darwin') return { success: false, error: 'Display control only supported on macOS' };
const binPath = app.isPackaged // Try bundled binary first; fall back to common Homebrew/system locations.
? path.join(process.resourcesPath, 'bin', 'displayplacer') // Install on a dev/venue Mac via: brew install displayplacer
: path.join(__dirname, '../../resources/bin/displayplacer'); const _bin_candidates = app.isPackaged
? [path.join(process.resourcesPath, 'bin', 'displayplacer')]
: [
path.join(__dirname, '../../resources/bin/displayplacer'),
'/opt/homebrew/bin/displayplacer', // Apple Silicon Homebrew
'/usr/local/bin/displayplacer', // Intel Homebrew
];
const binPath = _bin_candidates.find(p => fs.existsSync(p)) ?? _bin_candidates[0];
// Explicit config string always takes priority — allows manual override per device. // Explicit config string always takes priority — allows manual override per device.
if (configStr) { if (configStr) {
@@ -359,18 +366,18 @@ export function registerSystemHandlers() {
} }
const primary_id = primary_id_match[1]; const primary_id = primary_id_match[1];
// Primary display unchanged; secondary display(s) get mirror:<primary_id>. // Primary display unchanged; secondary display(s) get mirror_of_display:<primary_id>.
const mirror_args = display_strings.map((s, i) => { const mirror_args = display_strings.map((s, i) => {
if (i === 0) return `"${s}"`; if (i === 0) return `"${s}"`;
const without_existing_mirror = s.replace(/\s*mirror:\S+/g, '').trim(); const without_existing_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
return `"${without_existing_mirror} mirror:${primary_id}"`; return `"${without_existing_mirror} mirror_of_display:${primary_id}"`;
}).join(' '); }).join(' ');
return await runExec(`"${binPath}" ${mirror_args}`); return await runExec(`"${binPath}" ${mirror_args}`);
} }
if (mode === 'extend') { if (mode === 'extend') {
const any_mirrored = display_strings.some(s => /\bmirror:\S+/.test(s)); const any_mirrored = display_strings.some(s => /\bmirror_of_display:\S+/.test(s));
if (!any_mirrored) { if (!any_mirrored) {
// Already extended — re-apply current layout to ensure it is active. // Already extended — re-apply current layout to ensure it is active.
return await runExec(`"${binPath}" ${display_strings.map(s => `"${s}"`).join(' ')}`); return await runExec(`"${binPath}" ${display_strings.map(s => `"${s}"`).join(' ')}`);
@@ -379,7 +386,7 @@ export function registerSystemHandlers() {
// Remove mirror keys and compute side-by-side origins from each display's resolution. // Remove mirror keys and compute side-by-side origins from each display's resolution.
let x_offset = 0; let x_offset = 0;
const extend_args = display_strings.map((s) => { const extend_args = display_strings.map((s) => {
const without_mirror = s.replace(/\s*mirror:\S+/g, '').trim(); const without_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
const res_match = without_mirror.match(/\bres:(\d+)x\d+/); const res_match = without_mirror.match(/\bres:(\d+)x\d+/);
const width = res_match ? parseInt(res_match[1]) : 1920; const width = res_match ? parseInt(res_match[1]) : 1920;
const updated = without_mirror.replace(/\borigin:\([^)]+\)/, `origin:(${x_offset},0)`); const updated = without_mirror.replace(/\borigin:\([^)]+\)/, `origin:(${x_offset},0)`);