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:
@@ -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()`
|
|
||||||
|
|||||||
@@ -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)`);
|
||||||
|
|||||||
Reference in New Issue
Block a user