15 Commits

Author SHA1 Message Date
Scott Idem
99e0ebb7c3 docs: update TODO_AGENTS — current display_control status + future ideas section 2026-05-20 18:43:14 -04:00
Scott Idem
51db51d991 docs/build: update README + compiled dist for list-modes/set-mode features
README:
  - set_display_layout: note idempotent behavior (no flicker if already in state)
  - add list_display_modes and set_display_mode rows to bridge table
  - add list-modes and set-mode to Option B test commands

dist/: compiled output from tsc (system_handlers, preload, index)
2026-05-20 18:27:58 -04:00
Scott Idem
6cddd69891 fix(display): skip mirror/extend if already in requested state
mirror_displays() now checks CGDisplayMirrorsDisplay() for each secondary
before calling CGBeginDisplayConfiguration — avoids a needless display
reconfiguration (and potential flicker) when already mirrored.
extend_displays() already had this check; no change needed there.
2026-05-20 18:24:33 -04:00
Scott Idem
58060aea8a build: rebuild display_control binary (list-modes + set-mode) 2026-05-20 18:17:17 -04:00
Scott Idem
54308c6d4a feat(display): add list-modes and set-mode commands
display_control.m:
  list-modes  — JSON array of all online displays with every usable mode
                (width, height, refresh, pixel_width, pixel_height, hidpi, is_current)
  set-mode <display_index> <width> <height> [--refresh <hz>] [--hidpi] [--no-hidpi]
              — picks best matching CGDisplayMode; auto-prefers HiDPI on
                built-in and non-HiDPI on externals; highest refresh wins on ties

system_handlers.ts:
  native:list-display-modes  — runs binary, parses JSON, returns displays[]
  native:set-display-mode    — runs binary with supplied args

preload/index.ts + shared/types.ts:
  list_display_modes() / set_display_mode() exposed through bridge with full types
2026-05-20 18:16:43 -04:00
Scott Idem
b2cd0736df fix: use window.maximize() instead of explicit workArea dimensions
Electron screen.workArea is unreliable on KDE — returns full bounds,
causing the window to extend behind the taskbar. Delegating to the native
WM via mainWindow.maximize() works correctly on KDE, macOS, and Linux.
2026-05-20 18:14:26 -04:00
Scott Idem
a230ff09de fix: use workArea instead of bounds for initial window size
Respects taskbar on Linux (KDE etc.) and menu bar on macOS.
bounds caused the window to extend behind the taskbar on KDE.
workArea gives {x, y, width, height} of the usable screen region.
2026-05-20 17:54:05 -04:00
Scott Idem
7199d45719 feat: single instance lock; auto-size window to primary display bounds
- requestSingleInstanceLock(): second launch focuses existing window and quits.
- Window width/height/x/y set from screen.getPrimaryDisplay().bounds so the
  app fills the built-in display exactly on launch, regardless of resolution.
2026-05-20 17:48:09 -04:00
Scott Idem
48e24af84e build: recompile dist after display_control fail-fast fix 2026-05-20 17:45:01 -04:00
Scott Idem
cd1cd02bc2 build: commit display_control universal binary; fix packaging to extract to Resources/bin
- resources/bin/display_control: universal Mach-O (x86_64 + arm64),
  built via scripts/remote-build-display-control.sh on laptop 01.
- package:mac: add --extra-resource=resources/bin so the binary lands at
  [AppName].app/Contents/Resources/bin/ (outside app.asar) and is
  executable at runtime via process.resourcesPath.
2026-05-20 17:32:08 -04:00
Scott Idem
0ae0d644a9 build: add display_control binary (universal x86_64+arm64) 2026-05-20 17:28:39 -04:00
Scott Idem
ce2832a584 fix(build): replace scp with ssh+cat to handle space in username
scp's own user@host:path parser splits on the space in 'speaker ready'
even when the shell argument is quoted. ssh already handles it correctly.
Upload: ssh cat < file; Download: ssh cat > file.
2026-05-20 17:22:34 -04:00
Scott Idem
b6b902ad4a feat(build): add remote-build-display-control.sh for workstation → Mac builds
Copies display_control.m to laptop 01 via SSH, compiles universal binary
(x86_64 + arm64) on the remote Mac, pulls binary back to resources/bin/.
Defaults to 192.168.32.101; accepts optional IP override as $1.

README: restructure display_control build section — Option A (remote from
workstation, preferred) and Option B (direct on Mac).
2026-05-20 17:16:25 -04:00
Scott Idem
3da3b187ec build(display): universal binary — compile x86_64 + arm64 and lipo into fat binary
Compiles both arches separately then links with lipo -create.
Prevents silent failure when a binary built on Apple Silicon is deployed
to Intel venue Macs (MacBook Air 2018 fleet).
2026-05-20 17:10:56 -04:00
Scott Idem
86ea73bfbd fix(display): fail fast on invalid mode in display_control path; update docs
- Primary display_control path now validates mode explicitly ('mirror'|'extend')
  and returns an error instead of silently defaulting to extend.
  Passes mode directly to binary (simpler, avoids redundant ternary).
- README: update set_display_layout bridge table row to correctly describe
  display_control as primary and displayplacer as fallback.
- README: add one-time 'Build display_control Binary' section in Development
  with xcode-select, build, test, and git commit steps.
2026-05-20 17:04:39 -04:00
17 changed files with 662 additions and 65 deletions

View File

@@ -296,7 +296,9 @@ to change.
| `window_control({action, value?})` | Electron window: maximize, minimize, restore, close, fullscreen, kiosk, devtools, reload. |
| `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` | Sets desktop wallpaper. Accepts a local `path` or downloads from `url` (cached to `~/Library/Caches/OSIT/wallpaper/`). `url_external` sets a separate image on the projector/second display. `display`: `'all'` (default) \| `'primary'` \| `'external'`. macOS only in production; Linux returns a dev-mode preview payload without applying. |
| `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. Auto-detects connected displays via `displayplacer list` when no `configStr` is given — no manual config needed. `configStr` is an optional manual override (the full `displayplacer` config string) stored in `event_device.data_json` if per-device tuning is needed. |
| `set_display_layout({mode, configStr?})` | Mirror/extend displays. macOS only. **Idempotent** — returns success immediately if the displays are already in the requested state (no flicker). **Primary path:** bundled `display_control` binary (native CoreGraphics, no Homebrew). **Fallback:** `displayplacer` — used when the binary is absent or when a `configStr` override is set. `configStr` is a full `displayplacer` string stored in `event_device.data_json` for per-device tuning. Build `display_control` via `scripts/build-display-control.sh` on a Mac and commit `resources/bin/display_control`. |
| `list_display_modes()` | Returns all online displays with every usable `CGDisplayMode` per display: logical size, pixel size, refresh rate, HiDPI flag, and which mode is currently active. macOS only. JSON is parsed for you — result is `{ success, displays: DisplayInfo[] }`. Use this to populate a resolution picker in the UI. |
| `set_display_mode({display_index, width, height, refresh_rate?, hidpi?})` | Sets the resolution/refresh of one display via `CGConfigureDisplayWithDisplayMode`. `display_index` matches the `index` from `list_display_modes`. `hidpi`: `true` = force HiDPI (Retina scaling), `false` = force non-HiDPI, `null`/omit = auto (prefers HiDPI on built-in, non-HiDPI on externals). Picks the highest available refresh rate when multiple modes match. macOS only. |
| `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. |
@@ -352,6 +354,60 @@ See `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` Section 8
- **Handlers:** OS-level logic in `src/main/shell_handlers.ts`, `src/main/file_handlers.ts`, and `src/main/system_handlers.ts`.
- **Types:** Shared TypeScript interfaces in `src/shared/types.ts`.
### One-Time: Build the `display_control` Binary (macOS)
The `display_control` binary provides native CoreGraphics display mirror/extend without any Homebrew dependency. It must be compiled on a Mac (Xcode CLT required) and committed to the repo so it is bundled into every packaged app. Produces a universal binary (x86_64 + arm64) that runs on both Intel and Apple Silicon Macs.
Rebuild only if `scripts/display_control.m` changes.
#### Option A — From the Linux workstation (preferred)
Laptop 01 (`192.168.32.101`) is the designated build Mac — Xcode CLT is installed there.
The remote build script copies the source over SSH, compiles on that Mac, and pulls the binary back.
```bash
# From the repo root on the workstation:
./scripts/remote-build-display-control.sh
# Override the build Mac IP if needed:
./scripts/remote-build-display-control.sh 192.168.32.102
```
The script prints `file resources/bin/display_control` at the end. Confirm both arches appear:
```
resources/bin/display_control: Mach-O universal binary with 2 architectures: [x86_64] [arm64]
```
Then commit:
```bash
git add resources/bin/display_control
git commit -m "build: update display_control binary (universal)"
```
#### Option B — Directly on a Mac
If you have repo access on the Mac itself (Xcode CLT required — `xcode-select --install`):
```bash
./scripts/build-display-control.sh
# Expected last output line: x86_64 arm64
# Test with a second display connected:
./resources/bin/display_control status
./resources/bin/display_control extend
./resources/bin/display_control mirror
./resources/bin/display_control list-modes
./resources/bin/display_control set-mode 0 1920 1080
./resources/bin/display_control set-mode 1 1920 1080 --refresh 60 --no-hidpi
git add resources/bin/display_control
git commit -m "build: update display_control binary (universal)"
```
Once committed, `brew install displayplacer` is no longer required on any venue Mac. The `displayplacer` fallback remains in the handler only for `configStr` per-device overrides.
---
### Build Requirements (System Dependencies)
`bsdtar` (libarchive) must be present on the build host. It is used by the `postinstall` patch

39
dist/main/index.js vendored
View File

@@ -50,8 +50,8 @@ async function createWindow() {
cachedFullConfig = await (0, api_client_1.fetchFullConfig)(cachedSeed);
}
mainWindow = new electron_1.BrowserWindow({
width: 1600,
height: 900,
width: 1280,
height: 800,
title: 'OSIT Aether Launcher (Native)',
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
@@ -59,6 +59,9 @@ async function createWindow() {
nodeIntegration: false,
},
});
// Let the native window manager maximize — more reliable than using screen.workArea,
// which Electron under-reports on KDE and other Linux DEs.
mainWindow.maximize();
let targetUrl = 'http://demo.localhost:5173';
if (cachedFullConfig && cachedFullConfig.native_device) {
const device = cachedFullConfig.native_device;
@@ -82,15 +85,29 @@ async function createWindow() {
(0, shell_handlers_1.registerShellHandlers)();
(0, file_handlers_1.registerFileHandlers)();
(0, system_handlers_1.registerSystemHandlers)();
electron_1.app.on('ready', createWindow);
electron_1.app.on('window-all-closed', () => {
if (process.platform !== 'darwin')
electron_1.app.quit();
});
electron_1.app.on('activate', () => {
if (mainWindow === null)
createWindow();
});
// Single instance lock — if another instance is already running, focus it and quit.
const gotTheLock = electron_1.app.requestSingleInstanceLock();
if (!gotTheLock) {
electron_1.app.quit();
}
else {
electron_1.app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized())
mainWindow.restore();
mainWindow.focus();
}
});
electron_1.app.on('ready', createWindow);
electron_1.app.on('window-all-closed', () => {
if (process.platform !== 'darwin')
electron_1.app.quit();
});
electron_1.app.on('activate', () => {
if (mainWindow === null)
createWindow();
});
}
electron_1.ipcMain.handle('get-seed-config', async () => cachedSeed || await (0, config_loader_1.loadSeedConfig)());
electron_1.ipcMain.handle('get-device-config', async () => cachedFullConfig);
electron_1.ipcMain.handle('get-jwt', async () => null);

View File

@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/main/index.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/main/index.ts"],"names":[],"mappings":"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"}

View File

@@ -360,32 +360,46 @@ function registerSystemHandlers() {
}
return { success: false, error: 'Unknown action' };
});
// 6. Set Display Layout (Displayplacer)
// Auto-detects connected displays via `displayplacer list` when no explicit configStr is given.
// mirror: secondary display(s) mirror the primary.
// extend: displays shown side-by-side; un-mirrors if currently mirrored.
// 6. Set Display Layout
// Primary path: display_control (native CoreGraphics, no external deps).
// Build from scripts/display_control.m via scripts/build-display-control.sh on a Mac.
// Commit the resulting resources/bin/display_control binary to the repo.
// Fallback: displayplacer (requires: brew install displayplacer on each venue Mac).
// Also supports per-device configStr override (displayplacer format).
electron_1.ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
if (os.platform() !== 'darwin')
return { success: false, error: 'Display control only supported on macOS' };
// Try bundled binary first; fall back to common Homebrew/system locations.
// Install on a dev/venue Mac via: brew install displayplacer
const _bin_candidates = electron_1.app.isPackaged
// Primary: display_control — native CoreGraphics, no Homebrew dependency.
// Derived from OSIT MasterKey app (LegacyUtilities.m). No configStr support needed —
// CoreGraphics auto-detects all connected displays.
const dc_bin = electron_1.app.isPackaged
? path.join(process.resourcesPath, 'bin', 'display_control')
: path.join(__dirname, '../../resources/bin/display_control');
if (fs.existsSync(dc_bin) && !configStr) {
if (mode !== 'mirror' && mode !== 'extend') {
return { success: false, error: `Unsupported display mode: ${mode}` };
}
return await runExec(`"${dc_bin}" ${mode}`);
}
// Fallback: displayplacer — required when display_control binary is not built yet,
// or when a per-device configStr override is set (displayplacer-format string from event_device.data_json).
// Install: brew install displayplacer
const _dp_candidates = electron_1.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.
const dpPath = _dp_candidates.find(p => fs.existsSync(p)) ?? _dp_candidates[0];
// Explicit configStr takes priority — allows manual per-device override.
if (configStr) {
return await runExec(`"${binPath}" ${configStr}`);
return await runExec(`"${dpPath}" ${configStr}`);
}
// Auto-detect: `displayplacer list` emits a ready-to-run command line at the bottom.
// We parse the quoted display strings from that line and modify them for the requested mode.
const list_result = await runExec(`"${binPath}" list`);
// Auto-detect via `displayplacer list`.
const list_result = await runExec(`"${dpPath}" list`);
if (!list_result.success || !list_result.stdout) {
return { success: false, error: `displayplacer list failed: ${list_result.error ?? 'no output'}` };
return { success: false, error: `displayplacer not available. Build display_control from scripts/build-display-control.sh or run: brew install displayplacer` };
}
// The command line looks like: displayplacer "id:xxx res:... origin:(0,0) ..." "id:yyy ..."
const cmd_line = list_result.stdout.split('\n').find(l => l.trim().startsWith('displayplacer "'));
@@ -402,22 +416,19 @@ function registerSystemHandlers() {
return { success: false, error: 'Could not parse primary display ID from displayplacer output' };
}
const primary_id = primary_id_match[1];
// Primary display unchanged; secondary display(s) get mirror_of_display:<primary_id>.
const mirror_args = display_strings.map((s, i) => {
if (i === 0)
return `"${s}"`;
const without_existing_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
return `"${without_existing_mirror} mirror_of_display:${primary_id}"`;
}).join(' ');
return await runExec(`"${binPath}" ${mirror_args}`);
return await runExec(`"${dpPath}" ${mirror_args}`);
}
if (mode === 'extend') {
const any_mirrored = display_strings.some(s => /\bmirror_of_display:\S+/.test(s));
if (!any_mirrored) {
// Already extended — re-apply current layout to ensure it is active.
return await runExec(`"${binPath}" ${display_strings.map(s => `"${s}"`).join(' ')}`);
return await runExec(`"${dpPath}" ${display_strings.map(s => `"${s}"`).join(' ')}`);
}
// Remove mirror keys and compute side-by-side origins from each display's resolution.
let x_offset = 0;
const extend_args = display_strings.map((s) => {
const without_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
@@ -427,10 +438,51 @@ function registerSystemHandlers() {
x_offset += width;
return `"${updated}"`;
}).join(' ');
return await runExec(`"${binPath}" ${extend_args}`);
return await runExec(`"${dpPath}" ${extend_args}`);
}
return { success: false, error: `Unsupported display mode: ${mode}` };
});
// 6b. List Display Modes
electron_1.ipcMain.handle('native:list-display-modes', async () => {
if (os.platform() !== 'darwin')
return { success: false, error: 'Display control only supported on macOS' };
const dc_bin = electron_1.app.isPackaged
? path.join(process.resourcesPath, 'bin', 'display_control')
: path.join(__dirname, '../../resources/bin/display_control');
if (!fs.existsSync(dc_bin)) {
return { success: false, error: 'display_control binary not found. Build via scripts/build-display-control.sh.' };
}
const result = await runExec(`"${dc_bin}" list-modes`);
if (!result.success || !result.stdout) {
return { success: false, error: result.error ?? 'list-modes returned no output' };
}
try {
const displays = JSON.parse(result.stdout);
return { success: true, displays };
}
catch (e) {
return { success: false, error: `Failed to parse list-modes output: ${e.message}`, raw: result.stdout };
}
});
// 6c. Set Display Mode
electron_1.ipcMain.handle('native:set-display-mode', async (event, { display_index, width, height, refresh_rate, hidpi }) => {
if (os.platform() !== 'darwin')
return { success: false, error: 'Display control only supported on macOS' };
const dc_bin = electron_1.app.isPackaged
? path.join(process.resourcesPath, 'bin', 'display_control')
: path.join(__dirname, '../../resources/bin/display_control');
if (!fs.existsSync(dc_bin)) {
return { success: false, error: 'display_control binary not found. Build via scripts/build-display-control.sh.' };
}
let cmd = `"${dc_bin}" set-mode ${display_index} ${width} ${height}`;
if (refresh_rate)
cmd += ` --refresh ${refresh_rate}`;
if (hidpi === true)
cmd += ' --hidpi';
if (hidpi === false)
cmd += ' --no-hidpi';
return await runExec(cmd);
});
// 7. Update App
electron_1.ipcMain.handle('native:update-app', async (event, { source, url, path: localPath }) => {
// 1. Determine Source File

File diff suppressed because one or more lines are too long

View File

@@ -25,6 +25,8 @@ electron_1.contextBridge.exposeInMainWorld('aetherNative', {
window_control: (args) => electron_1.ipcRenderer.invoke('native:window-control', args),
manage_recording: (args) => electron_1.ipcRenderer.invoke('native:manage-recording', args),
set_display_layout: (args) => electron_1.ipcRenderer.invoke('native:set-display-layout', args),
list_display_modes: () => electron_1.ipcRenderer.invoke('native:list-display-modes'),
set_display_mode: (args) => electron_1.ipcRenderer.invoke('native:set-display-mode', args),
power_control: (args) => electron_1.ipcRenderer.invoke('native:power-control', args),
open_external: (args) => electron_1.ipcRenderer.invoke('native:open-external', args),
});

View File

@@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/preload/index.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/preload/index.ts"],"names":[],"mappings":"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"}

View File

@@ -82,27 +82,35 @@
**Current state (2026-05-20):**
- ✅ Correct `mirror_of_display:<uuid>` syntax used in displayplacer fallback (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
- `display_control` binary not yet built — must be compiled on a Mac and committed
- ✅ Correct `mirror_of_display:<uuid>` syntax used in displayplacer fallback
- ✅ Failures logged to Electron console (`[Launcher] set_display_layout:`)
- ✅ Display Mode toggle in Launcher config (Native OS section) — always visible
- `display_control` binary built (universal x86_64 + arm64), committed to repo
-**Idempotency**`mirror` and `extend` both no-op with a clean message if already in the requested state (no display flicker)
-**`list-modes`** — JSON array of all online displays + every usable `CGDisplayMode` (width, height, refresh, pixel size, HiDPI flag, is_current)
-**`set-mode`** — sets resolution/refresh via `CGConfigureDisplayWithDisplayMode`; supports `--refresh`, `--hidpi`, `--no-hidpi`; auto-prefers HiDPI on built-in, non-HiDPI on externals
- ✅ IPC handlers `native:list-display-modes` + `native:set-display-mode` wired through full bridge stack (system_handlers → preload → types → electron_relay)
- ✅ Remote build script (`scripts/remote-build-display-control.sh`) — compiles on laptop-01 via SSH from Linux workstation; uses `ssh cat` pipe pattern (avoids scp space-in-username bug)
**To build `display_control` (do this on a Mac):**
**To rebuild `display_control` after source changes:**
```bash
# One-time: install Xcode Command Line Tools if not already installed
xcode-select --install
# From repo root on workstation (laptop-01 must be reachable):
./scripts/remote-build-display-control.sh
# Then:
# Or directly on a Mac:
./scripts/build-display-control.sh
# Test it with a second display connected:
# Test with a second display connected:
./resources/bin/display_control status
./resources/bin/display_control extend
./resources/bin/display_control mirror
./resources/bin/display_control list-modes
./resources/bin/display_control set-mode 0 1920 1080
./resources/bin/display_control set-mode 1 1920 1080 --refresh 60 --no-hidpi
# Commit the binary:
# Commit:
git add resources/bin/display_control
git commit -m "build: add display_control binary (macOS CoreGraphics)"
git commit -m "build: update display_control binary (universal)"
```
**Optional per-device override (displayplacer format, for edge cases):**
@@ -114,3 +122,27 @@ For rooms where auto-detection produces the wrong result, store the raw configSt
}
```
`configStr` is passed from the Svelte call site and uses the displayplacer fallback path directly.
---
## Future Ideas
Capabilities worth adding as the Launcher matures. Roughly ordered by venue-day impact.
### 1. Display reconfiguration events (push IPC)
`CGDisplayRegisterReconfigurationCallback` fires when a display is connected or removed. Wrapping this in a `webContents.send('native:display-changed', payload)` push event would let the Svelte UI auto-mirror the moment a projector cable lands — eliminating the most common operator action during show setup. Currently the UI must poll `status` or the operator presses Mirror manually.
### 2. Audio output routing
When mirroring to a projector the audio output should follow. CoreAudio (`AudioObjectSetPropertyData` on `kAudioHardwarePropertyDefaultOutputDevice`) can switch the default output device programmatically. Candidate bridge method: `set_audio_output({device_name?, prefer_hdmi?})`. Could be called automatically as part of the mirror flow, or exposed as a standalone control.
### 3. Battery / power status in telemetry
`get_device_info` returns CPU and RAM but nothing about power. On venue MacBook Airs this matters operationally. IOKit (`IOPSCopyPowerSourcesInfo` / `IOPSGetPowerSourceDescription`) can surface: charge %, is-charging, time-remaining, health. Low-cost addition to the existing telemetry handler.
### 4. Presentation state feedback
`control_presentation` is fire-and-forget. AppleScript can query the current slide index and total slide count from both PowerPoint (`current slide index of active presentation`) and Keynote (`slide number of current slide of front document`). A `get_presentation_state()` bridge method returning `{ app, slide, total, presenting }` would let the Launcher UI show "Slide 7 of 42" — useful for operators monitoring multiple rooms.
### 5. Push event channel (IPC renderer notifications)
All bridge calls are currently request-response. Adding a `webContents.send` channel for unsolicited Electron → renderer events would unlock: display plug/unplug (#1 above), file download progress, network state changes, "presentation ended" detection. A thin `ipcMain.on('native:subscribe', ...)` registration pattern on the Electron side and a corresponding `ipcRenderer.on` listener in the preload would cover all use cases without breaking the existing handler structure.
### 6. Kiosk / accidental-quit hardening
A speaker or operator can accidentally Cmd+Q the launcher mid-presentation. `app.on('before-quit')` with either a confirmation dialog or an API-controlled lock flag (`event_device.data_json.kiosk_locked: true`) would prevent this. Can be toggled remotely — lock before the show, unlock after.

View File

@@ -10,7 +10,7 @@
"build": "tsc",
"watch": "tsc -w",
"package:linux": "tsc && electron-packager . aether_launcher --platform=linux --arch=x64 --out=builds --overwrite --prune=true",
"package:mac": "tsc && electron-packager . aether_launcher --platform=darwin --arch=x64,arm64 --out=builds --overwrite --prune=true --icon=resources/img/osit_logo.icns"
"package:mac": "tsc && electron-packager . aether_launcher --platform=darwin --arch=x64,arm64 --out=builds --overwrite --prune=true --icon=resources/img/osit_logo.icns --extra-resource=resources/bin"
},
"devDependencies": {
"@types/node": "^22.19.0",

BIN
resources/bin/display_control Executable file

Binary file not shown.

View File

@@ -24,13 +24,26 @@ fi
mkdir -p "$OUT_DIR"
echo "Building display_control..."
clang -framework Cocoa -framework Carbon \
-o "$OUT_BIN" "$SRC"
TMP_X86="$OUT_DIR/display_control_x86_64"
TMP_ARM="$OUT_DIR/display_control_arm64"
echo "Building display_control (x86_64)..."
clang -arch x86_64 -framework Cocoa -framework Carbon \
-o "$TMP_X86" "$SRC"
echo "Building display_control (arm64)..."
clang -arch arm64 -framework Cocoa -framework Carbon \
-o "$TMP_ARM" "$SRC"
echo "Linking universal binary..."
lipo -create -output "$OUT_BIN" "$TMP_X86" "$TMP_ARM"
rm "$TMP_X86" "$TMP_ARM"
chmod +x "$OUT_BIN"
echo "Built: $OUT_BIN"
echo ""
echo "Built universal binary: $OUT_BIN"
lipo -archs "$OUT_BIN"
echo ""
echo "Test it:"
echo " $OUT_BIN status"

View File

@@ -11,8 +11,12 @@
#import <Cocoa/Cocoa.h>
#include <Carbon/Carbon.h>
#include <math.h>
#define MAX_DISPLAYS 8
#define MAX_MODES 256
typedef struct { long src_index; size_t w, h, pw, ph; double refresh; } ModeEntry;
static int mirror_displays(void) {
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
@@ -27,6 +31,17 @@ static int mirror_displays(void) {
CGDirectDisplayID mainID = CGMainDisplayID();
// Idempotency: if every secondary is already mirroring mainID, nothing to do.
CGDisplayCount alreadyMirrored = 0;
for (CGDisplayCount i = 0; i < numOnline; i++) {
if (onlineDspys[i] != mainID && CGDisplayMirrorsDisplay(onlineDspys[i]) == mainID)
alreadyMirrored++;
}
if (alreadyMirrored == numOnline - 1) {
printf("Displays already mirrored.\n");
return 0;
}
CGDisplayConfigRef config;
CGError err = CGBeginDisplayConfiguration(&config);
if (err != kCGErrorSuccess) {
@@ -125,10 +140,200 @@ static void print_status(void) {
(numOnline > 1 && numActive < numOnline) ? "mirrored" : "extended");
}
// list-modes
// Outputs a JSON array describing every online display and its available modes.
static void list_modes(void) {
CGDirectDisplayID displays[MAX_DISPLAYS];
CGDisplayCount count = 0;
CGGetOnlineDisplayList(MAX_DISPLAYS, displays, &count);
CGDirectDisplayID mainID = CGMainDisplayID();
printf("[\n");
for (CGDisplayCount d = 0; d < count; d++) {
CGDirectDisplayID dID = displays[d];
CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(dID);
size_t curW = CGDisplayModeGetWidth(currentMode);
size_t curH = CGDisplayModeGetHeight(currentMode);
double curR = CGDisplayModeGetRefreshRate(currentMode);
size_t curPW = CGDisplayModeGetPixelWidth(currentMode);
size_t curPH = CGDisplayModeGetPixelHeight(currentMode);
// Include HiDPI duplicate entries so scaled modes are visible.
CFStringRef optKeys[] = { kCGDisplayShowDuplicateLowResolutionModes };
CFBooleanRef optVals[] = { kCFBooleanTrue };
CFDictionaryRef opts = CFDictionaryCreate(NULL,
(const void **)optKeys, (const void **)optVals, 1,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(dID, opts);
CFRelease(opts);
CFIndex total = CFArrayGetCount(allModes);
// Collect usable modes first so we know the count for comma handling.
ModeEntry usable[MAX_MODES];
int usable_count = 0;
for (CFIndex m = 0; m < total && usable_count < MAX_MODES; m++) {
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, m);
if (!CGDisplayModeIsUsableForDesktopGUI(mode)) continue;
usable[usable_count++] = (ModeEntry){
.src_index = (long)m,
.w = CGDisplayModeGetWidth(mode),
.h = CGDisplayModeGetHeight(mode),
.pw = CGDisplayModeGetPixelWidth(mode),
.ph = CGDisplayModeGetPixelHeight(mode),
.refresh = CGDisplayModeGetRefreshRate(mode)
};
}
printf(" {\n");
printf(" \"index\": %u,\n", d);
printf(" \"id\": %u,\n", (unsigned int)dID);
printf(" \"is_main\": %s,\n", dID == mainID ? "true" : "false");
printf(" \"current_width\": %zu,\n", curW);
printf(" \"current_height\": %zu,\n", curH);
printf(" \"current_refresh\": %.2f,\n", curR);
printf(" \"current_pixel_width\": %zu,\n", curPW);
printf(" \"current_pixel_height\": %zu,\n", curPH);
printf(" \"modes\": [\n");
for (int i = 0; i < usable_count; i++) {
ModeEntry *e = &usable[i];
int isCurrent = (e->w == curW && e->h == curH &&
e->pw == curPW && e->ph == curPH &&
fabs(e->refresh - curR) < 0.5);
printf(" {\"index\":%ld,\"width\":%zu,\"height\":%zu,"
"\"refresh\":%.2f,\"pixel_width\":%zu,\"pixel_height\":%zu,"
"\"hidpi\":%s,\"is_current\":%s}%s\n",
e->src_index, e->w, e->h, e->refresh, e->pw, e->ph,
(e->pw > e->w || e->ph > e->h) ? "true" : "false",
isCurrent ? "true" : "false",
(i < usable_count - 1) ? "," : "");
}
printf(" ]\n");
printf(" }%s\n", (d < count - 1) ? "," : "");
CGDisplayModeRelease(currentMode);
CFRelease(allModes);
}
printf("]\n");
}
// set-mode
// display_idx : index from list-modes (0 = primary, 1 = first external, ...)
// req_w/h : logical width × height (what macOS calls "looks like X×Y")
// req_refresh : 0 = pick highest available; >0 = must be within 1 Hz
// force_hidpi : 1 = HiDPI only; -1 = non-HiDPI only; 0 = auto
// auto prefers HiDPI on the built-in, non-HiDPI on externals
static int set_mode(int display_idx, size_t req_w, size_t req_h,
double req_refresh, int force_hidpi) {
CGDirectDisplayID displays[MAX_DISPLAYS];
CGDisplayCount count = 0;
CGGetOnlineDisplayList(MAX_DISPLAYS, displays, &count);
if (display_idx < 0 || (CGDisplayCount)display_idx >= count) {
fprintf(stderr, "Display index %d out of range (0..%u).\n",
display_idx, count > 0 ? count - 1 : 0);
return 1;
}
CGDirectDisplayID dID = displays[display_idx];
int isMain = (dID == CGMainDisplayID());
CFStringRef optKeys[] = { kCGDisplayShowDuplicateLowResolutionModes };
CFBooleanRef optVals[] = { kCFBooleanTrue };
CFDictionaryRef opts = CFDictionaryCreate(NULL,
(const void **)optKeys, (const void **)optVals, 1,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFArrayRef allModes = CGDisplayCopyAllDisplayModes(dID, opts);
CFRelease(opts);
CFIndex total = CFArrayGetCount(allModes);
CGDisplayModeRef bestMode = NULL;
double bestRefresh = -1.0;
int bestScore = -1; // higher = more preferred
for (CFIndex m = 0; m < total; m++) {
CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, m);
if (!CGDisplayModeIsUsableForDesktopGUI(mode)) continue;
size_t w = CGDisplayModeGetWidth(mode);
size_t h = CGDisplayModeGetHeight(mode);
if (w != req_w || h != req_h) continue;
double refresh = CGDisplayModeGetRefreshRate(mode);
if (req_refresh > 0.0 && fabs(refresh - req_refresh) > 1.0) continue;
size_t pw = CGDisplayModeGetPixelWidth(mode);
int isHiDPI = (pw > w);
if (force_hidpi == 1 && !isHiDPI) continue;
if (force_hidpi == -1 && isHiDPI) continue;
// Score: prefer HiDPI on main display, non-HiDPI on external.
int score = (force_hidpi == 0)
? ((isMain && isHiDPI) || (!isMain && !isHiDPI)) ? 1 : 0
: 0;
if (bestMode == NULL || score > bestScore ||
(score == bestScore && refresh > bestRefresh)) {
bestMode = mode;
bestRefresh = refresh;
bestScore = score;
}
}
if (!bestMode) {
fprintf(stderr, "No matching mode for display %d: %zux%zu",
display_idx, req_w, req_h);
if (req_refresh > 0.0) fprintf(stderr, " @%.0fHz", req_refresh);
if (force_hidpi == 1) fprintf(stderr, " [HiDPI required]");
if (force_hidpi == -1) fprintf(stderr, " [non-HiDPI required]");
fprintf(stderr, ".\nRun: display_control list-modes\n");
CFRelease(allModes);
return 1;
}
size_t setW = CGDisplayModeGetWidth(bestMode);
size_t setH = CGDisplayModeGetHeight(bestMode);
double setR = CGDisplayModeGetRefreshRate(bestMode);
size_t setPW = CGDisplayModeGetPixelWidth(bestMode);
size_t setPH = CGDisplayModeGetPixelHeight(bestMode);
CGDisplayConfigRef config;
CGError err = CGBeginDisplayConfiguration(&config);
if (err != kCGErrorSuccess) {
fprintf(stderr, "CGBeginDisplayConfiguration failed: %d\n", err);
CFRelease(allModes);
return 1;
}
err = CGConfigureDisplayWithDisplayMode(config, dID, bestMode, NULL);
if (err != kCGErrorSuccess) {
fprintf(stderr, "CGConfigureDisplayWithDisplayMode failed: %d\n", err);
CGCancelDisplayConfiguration(config);
CFRelease(allModes);
return 1;
}
err = CGCompleteDisplayConfiguration(config, kCGConfigurePermanently);
if (err != kCGErrorSuccess) {
fprintf(stderr, "CGCompleteDisplayConfiguration failed: %d\n", err);
CFRelease(allModes);
return 1;
}
printf("Set display %d to %zux%zu @%.0fHz (pixel %zux%zu).\n",
display_idx, setW, setH, setR, setPW, setPH);
CFRelease(allModes);
return 0;
}
// main
int main(int argc, const char * argv[]) {
@autoreleasepool {
if (argc < 2) {
fprintf(stderr, "Usage: display_control <mirror|extend|status>\n");
fprintf(stderr, "Usage: display_control <mirror|extend|status|list-modes|set-mode>\n");
return 1;
}
@@ -141,8 +346,31 @@ int main(int argc, const char * argv[]) {
} else if (strcmp(cmd, "status") == 0) {
print_status();
return 0;
} else if (strcmp(cmd, "list-modes") == 0) {
list_modes();
return 0;
} else if (strcmp(cmd, "set-mode") == 0) {
if (argc < 5) {
fprintf(stderr, "Usage: display_control set-mode <display_index> <width> <height> [--refresh <hz>] [--hidpi] [--no-hidpi]\n");
return 1;
}
int display_idx = atoi(argv[2]);
size_t req_w = (size_t)atol(argv[3]);
size_t req_h = (size_t)atol(argv[4]);
double req_refresh = 0.0;
int force_hidpi = 0;
for (int i = 5; i < argc; i++) {
if (strcmp(argv[i], "--refresh") == 0 && i + 1 < argc) {
req_refresh = atof(argv[++i]);
} else if (strcmp(argv[i], "--hidpi") == 0) {
force_hidpi = 1;
} else if (strcmp(argv[i], "--no-hidpi") == 0) {
force_hidpi = -1;
}
}
return set_mode(display_idx, req_w, req_h, req_refresh, force_hidpi);
} else {
fprintf(stderr, "Unknown command: %s\nUsage: display_control <mirror|extend|status>\n", cmd);
fprintf(stderr, "Unknown command: %s\nUsage: display_control <mirror|extend|status|list-modes|set-mode>\n", cmd);
return 1;
}
}

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# scripts/remote-build-display-control.sh
# Build the display_control universal binary on a remote Mac via SSH,
# then pull the result back to resources/bin/display_control.
#
# Use this from the Linux workstation when you don't have a Mac locally.
# The target Mac must have Xcode Command Line Tools installed.
#
# USAGE:
# ./scripts/remote-build-display-control.sh # uses default Mac (laptop 01)
# ./scripts/remote-build-display-control.sh 192.168.32.102
#
# The default IP is laptop 01 — the designated build Mac.
# Change DEFAULT_IP below if Xcode moves to a different laptop.
#
# SSH key must already be installed on the target:
# ssh-copy-id "speaker ready"@192.168.32.101
set -eo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SRC="$SCRIPT_DIR/display_control.m"
OUT_BIN="$REPO_ROOT/resources/bin/display_control"
SSH_USER="speaker ready"
DEFAULT_IP="192.168.32.101"
IP="${1:-$DEFAULT_IP}"
REMOTE_TMP="/tmp/ae_display_control_build"
echo "═══════════════════════════════════════════════"
echo " Remote build: display_control"
echo " Build Mac: $SSH_USER @ $IP"
echo "═══════════════════════════════════════════════"
# ── Step 1: Copy source to remote ────────────────────────────────────────────
echo ""
echo "Step 1/4 — Copying source to $IP..."
ssh "$SSH_USER@$IP" "mkdir -p $REMOTE_TMP"
ssh "$SSH_USER@$IP" "cat > $REMOTE_TMP/display_control.m" < "$SRC"
# ── Step 2: Build universal binary on remote ──────────────────────────────────
echo ""
echo "Step 2/4 — Building on $IP..."
ssh "$SSH_USER@$IP" bash << 'ENDSSH'
set -e
REMOTE_TMP="/tmp/ae_display_control_build"
SRC="$REMOTE_TMP/display_control.m"
TMP_X86="$REMOTE_TMP/display_control_x86_64"
TMP_ARM="$REMOTE_TMP/display_control_arm64"
OUT="$REMOTE_TMP/display_control"
echo " Compiling x86_64..."
clang -arch x86_64 -framework Cocoa -framework Carbon -o "$TMP_X86" "$SRC"
echo " Compiling arm64..."
clang -arch arm64 -framework Cocoa -framework Carbon -o "$TMP_ARM" "$SRC"
echo " Linking universal binary..."
lipo -create -output "$OUT" "$TMP_X86" "$TMP_ARM"
rm "$TMP_X86" "$TMP_ARM"
chmod +x "$OUT"
echo " Archs: $(lipo -archs "$OUT")"
ENDSSH
# ── Step 3: Pull binary back ──────────────────────────────────────────────────
echo ""
echo "Step 3/4 — Pulling binary to resources/bin/..."
mkdir -p "$(dirname "$OUT_BIN")"
ssh "$SSH_USER@$IP" "cat $REMOTE_TMP/display_control" > "$OUT_BIN"
chmod +x "$OUT_BIN"
# ── Step 4: Clean up remote ───────────────────────────────────────────────────
echo ""
echo "Step 4/4 — Cleaning up remote..."
ssh "$SSH_USER@$IP" "rm -rf $REMOTE_TMP"
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════"
echo " Built: $OUT_BIN"
file "$OUT_BIN"
echo ""
echo " If both archs shown above, you're good:"
echo " git add resources/bin/display_control"
echo " git commit -m \"build: update display_control binary (universal)\""
echo "═══════════════════════════════════════════════"

View File

@@ -19,8 +19,8 @@ async function createWindow() {
}
mainWindow = new BrowserWindow({
width: 1600,
height: 900,
width: 1280,
height: 800,
title: 'OSIT Aether Launcher (Native)',
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
@@ -29,6 +29,10 @@ async function createWindow() {
},
});
// Let the native window manager maximize — more reliable than using screen.workArea,
// which Electron under-reports on KDE and other Linux DEs.
mainWindow.maximize();
let targetUrl = 'http://demo.localhost:5173';
if (cachedFullConfig && cachedFullConfig.native_device) {
const device = cachedFullConfig.native_device;
@@ -55,15 +59,28 @@ registerShellHandlers();
registerFileHandlers();
registerSystemHandlers();
app.on('ready', createWindow);
// Single instance lock — if another instance is already running, focus it and quit.
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('ready', createWindow);
app.on('activate', () => {
if (mainWindow === null) createWindow();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (mainWindow === null) createWindow();
});
}
ipcMain.handle('get-seed-config', async () => cachedSeed || await loadSeedConfig());
ipcMain.handle('get-device-config', async () => cachedFullConfig);

View File

@@ -335,8 +335,10 @@ export function registerSystemHandlers() {
: path.join(__dirname, '../../resources/bin/display_control');
if (fs.existsSync(dc_bin) && !configStr) {
const dc_cmd = mode === 'mirror' ? 'mirror' : 'extend';
return await runExec(`"${dc_bin}" ${dc_cmd}`);
if (mode !== 'mirror' && mode !== 'extend') {
return { success: false, error: `Unsupported display mode: ${mode}` };
}
return await runExec(`"${dc_bin}" ${mode}`);
}
// Fallback: displayplacer — required when display_control binary is not built yet,
@@ -411,6 +413,63 @@ export function registerSystemHandlers() {
return { success: false, error: `Unsupported display mode: ${mode}` };
});
// 6b. List Display Modes
ipcMain.handle('native:list-display-modes', async () => {
if (os.platform() !== 'darwin') return { success: false, error: 'Display control only supported on macOS' };
const dc_bin = app.isPackaged
? path.join(process.resourcesPath, 'bin', 'display_control')
: path.join(__dirname, '../../resources/bin/display_control');
if (!fs.existsSync(dc_bin)) {
return { success: false, error: 'display_control binary not found. Build via scripts/build-display-control.sh.' };
}
const result = await runExec(`"${dc_bin}" list-modes`);
if (!result.success || !result.stdout) {
return { success: false, error: result.error ?? 'list-modes returned no output' };
}
try {
const displays = JSON.parse(result.stdout);
return { success: true, displays };
} catch (e: any) {
return { success: false, error: `Failed to parse list-modes output: ${e.message}`, raw: result.stdout };
}
});
// 6c. Set Display Mode
ipcMain.handle('native:set-display-mode', async (event, {
display_index,
width,
height,
refresh_rate,
hidpi
}: {
display_index: number;
width: number;
height: number;
refresh_rate?: number;
hidpi?: boolean | null;
}) => {
if (os.platform() !== 'darwin') return { success: false, error: 'Display control only supported on macOS' };
const dc_bin = app.isPackaged
? path.join(process.resourcesPath, 'bin', 'display_control')
: path.join(__dirname, '../../resources/bin/display_control');
if (!fs.existsSync(dc_bin)) {
return { success: false, error: 'display_control binary not found. Build via scripts/build-display-control.sh.' };
}
let cmd = `"${dc_bin}" set-mode ${display_index} ${width} ${height}`;
if (refresh_rate) cmd += ` --refresh ${refresh_rate}`;
if (hidpi === true) cmd += ' --hidpi';
if (hidpi === false) cmd += ' --no-hidpi';
return await runExec(cmd);
});
// 7. Update App
ipcMain.handle('native:update-app', async (event, { source, url, path: localPath }) => {
// 1. Determine Source File

View File

@@ -5,7 +5,7 @@ contextBridge.exposeInMainWorld('aetherNative', {
get_device_config: () => ipcRenderer.invoke('get-device-config'),
get_jwt: () => ipcRenderer.invoke('get-jwt'),
get_device_info: () => ipcRenderer.invoke('get-device-info'),
open_folder: (path: string) => ipcRenderer.invoke('native:open-folder', path),
run_cmd: (args: any) => ipcRenderer.invoke('native:run-cmd', args),
run_cmd_sync: (args: any) => ipcRenderer.invoke('native:run-cmd-sync', args),
@@ -27,6 +27,8 @@ contextBridge.exposeInMainWorld('aetherNative', {
window_control: (args: any) => ipcRenderer.invoke('native:window-control', args),
manage_recording: (args: any) => ipcRenderer.invoke('native:manage-recording', args),
set_display_layout: (args: any) => ipcRenderer.invoke('native:set-display-layout', args),
list_display_modes: () => ipcRenderer.invoke('native:list-display-modes'),
set_display_mode: (args: any) => ipcRenderer.invoke('native:set-display-mode', args),
power_control: (args: any) => ipcRenderer.invoke('native:power-control', args),
open_external: (args: any) => ipcRenderer.invoke('native:open-external', args),
});

View File

@@ -1,3 +1,26 @@
export interface DisplayMode {
index: number;
width: number;
height: number;
refresh: number;
pixel_width: number;
pixel_height: number;
hidpi: boolean;
is_current: boolean;
}
export interface DisplayInfo {
index: number;
id: number;
is_main: boolean;
current_width: number;
current_height: number;
current_refresh: number;
current_pixel_width: number;
current_pixel_height: number;
modes: DisplayMode[];
}
export interface SeedConfig {
event_device_id: string;
primary_api_base_url: string;
@@ -37,6 +60,8 @@ export interface AetherNativeBridge {
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}>;
list_display_modes: () => Promise<{success: boolean, displays?: DisplayInfo[], error?: string, raw?: string}>;
set_display_mode: (args: {display_index: number, width: number, height: number, refresh_rate?: number, hidpi?: boolean | null}) => Promise<{success: boolean, stdout?: string, stderr?: string, error?: string}>;
update_app: (args: {source: 'url' | 'file', url?: string, path?: string}) => Promise<{success: boolean, message?: string, downloadedPath?: string, error?: string}>;
// Self-Documentation