Compare commits
3 Commits
2c7b609295
...
2bf4d7c141
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bf4d7c141 | ||
|
|
e37fd1ddbb | ||
|
|
a1e74829e8 |
@@ -284,7 +284,7 @@ to change.
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `launch_presentation({path, app?, os?})` | Platform-aware launcher. Resolves `[home]`/`[tmp]` placeholders. **Note:** uses legacy `-e` flag for AppleScript; prefer `copy_from_cache_to_temp` + `run_osascript` for new flows. |
|
||||
| `launch_presentation({path, app?, os?})` | Platform-aware launcher. Resolves `[home]`/`[tmp]` placeholders. Hardened (2026-05-11): AppleScript written to temp `.scpt` file, same as `run_osascript`. For new flows prefer `copy_from_cache_to_temp` + `run_osascript` for full control. |
|
||||
| `control_presentation({app, action})` | Slide navigation (`next`/`prev`/`start`/`stop`) for PowerPoint or Keynote via AppleScript. macOS only. |
|
||||
|
||||
### System Management (Phase 5)
|
||||
@@ -294,9 +294,9 @@ to change.
|
||||
| `get_device_config()` | Returns hydrated device config injected at startup from `seed.json` + API. |
|
||||
| `get_device_info()` | Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths. |
|
||||
| `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). |
|
||||
| `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. `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
||||
98
dist/main/shell_handlers.js
vendored
98
dist/main/shell_handlers.js
vendored
@@ -224,65 +224,115 @@ end tell
|
||||
});
|
||||
electron_1.ipcMain.handle('native:list-tools', async () => {
|
||||
return [
|
||||
// --- Config & Info ---
|
||||
{
|
||||
name: 'get_device_config',
|
||||
description: 'Returns hydrated device config injected at startup from seed.json + API.',
|
||||
params: {}
|
||||
},
|
||||
{
|
||||
name: 'get_device_info',
|
||||
description: 'Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths.',
|
||||
params: {}
|
||||
},
|
||||
// --- File Cache ---
|
||||
{
|
||||
name: 'check_cache',
|
||||
description: 'Checks if a file exists in the hashed cache. verify_hash:true re-hashes to confirm integrity.',
|
||||
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number (optional, default 2)', verify_hash: 'boolean (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'download_to_cache',
|
||||
description: 'Streams a file from the API into the hashed cache with SHA-256 integrity check. Cleans stale .tmp files older than 5 min.',
|
||||
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string', hash_prefix_length: 'number (optional, default 2)' }
|
||||
},
|
||||
{
|
||||
name: 'copy_from_cache_to_temp',
|
||||
description: 'Preferred primitive. Copies a cached file to temp with its original filename. Returns { success, path }. Caller decides what to do next.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string', hash_prefix_length: 'number (optional, default 2)' }
|
||||
},
|
||||
{
|
||||
name: 'launch_from_cache',
|
||||
description: 'Combines copy_from_cache_to_temp + execute. Runs native_template after copying — AppleScript string with {{path}} placeholder, or "shell:<cmd>" prefix. Returns error if native_template is null.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string', hash_prefix_length: 'number (optional)', native_template: 'string | null' }
|
||||
},
|
||||
// --- Shell & OS ---
|
||||
{
|
||||
name: 'open_folder',
|
||||
description: 'Opens a directory in the OS file explorer (Finder/Files/Explorer).',
|
||||
description: 'Opens a directory in the OS file explorer (Finder on macOS).',
|
||||
params: { path: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'run_cmd',
|
||||
description: 'Executes an asynchronous shell command with a timeout.',
|
||||
params: { cmd: 'string', timeout: 'number (optional)' }
|
||||
description: 'Async shell command execution with timeout.',
|
||||
params: { cmd: 'string', timeout: 'number (optional, default 30000ms)' }
|
||||
},
|
||||
{
|
||||
name: 'run_cmd_sync',
|
||||
description: 'Executes a synchronous shell command.',
|
||||
description: 'Synchronous shell command execution.',
|
||||
params: { cmd: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'run_osascript',
|
||||
description: 'Executes a raw AppleScript string (macOS only).',
|
||||
description: 'Hardened AppleScript executor — writes to temp .scpt file, handles multi-line scripts and paths with special characters. macOS only.',
|
||||
params: { script: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'kill_processes',
|
||||
description: 'Forcefully terminates processes by name.',
|
||||
description: 'Terminates processes by name. macOS/Linux: pkill -f. Windows: taskkill /F.',
|
||||
params: { process_name_li: 'string[]' }
|
||||
},
|
||||
{
|
||||
name: 'open_local_file_v2',
|
||||
description: 'Opens a local file using the default OS handler.',
|
||||
params: { filePath: 'string' }
|
||||
description: 'Opens a file with its default OS application via shell.openPath.',
|
||||
params: { path: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'open_external',
|
||||
description: 'Opens a URL in Chrome, Firefox, or the system default browser.',
|
||||
params: { url: 'string', app: 'chrome | firefox | default (optional)' }
|
||||
},
|
||||
// --- Presentations ---
|
||||
{
|
||||
name: 'launch_presentation',
|
||||
description: 'Phase 5: Specialized launcher for PowerPoint, Keynote, and LibreOffice with auto-focus.',
|
||||
params: { path: 'string', app: 'default|powerpoint|keynote' }
|
||||
description: 'Platform-aware launcher for PowerPoint, Keynote, LibreOffice. Resolves [home]/[tmp] placeholders. Hardened AppleScript (2026-05-11). Prefer copy_from_cache_to_temp + run_osascript for new flows.',
|
||||
params: { path: 'string', app: 'default | powerpoint | keynote (optional)', os_platform: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'control_presentation',
|
||||
description: 'Phase 5: Remote navigation for active slideshows.',
|
||||
params: { app: 'powerpoint|keynote', action: 'next|prev|start|stop' }
|
||||
description: 'Slide navigation for active PowerPoint or Keynote via AppleScript. macOS only.',
|
||||
params: { app: 'powerpoint | keynote', action: 'next | prev | start | stop' }
|
||||
},
|
||||
// --- System Management ---
|
||||
{
|
||||
name: 'set_wallpaper',
|
||||
description: 'Sets desktop wallpaper. Downloads from url (cached to ~/Library/Caches/OSIT/wallpaper/) or applies local path. url_external targets projector/second display separately. macOS only in production.',
|
||||
params: { path: 'string (optional)', url: 'string (optional)', url_external: 'string (optional)', display: 'all | primary | external (optional, default all)', api_key: 'string (optional)', account_id: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'check_cache',
|
||||
description: 'Checks if a file exists in the local organized cache.',
|
||||
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number' }
|
||||
name: 'set_display_layout',
|
||||
description: 'Mirror or extend displays via bundled displayplacer. macOS only. Auto-detects displays when no configStr given; configStr is an optional manual override.',
|
||||
params: { mode: 'mirror | extend', configStr: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'download_to_cache',
|
||||
description: 'Downloads a file from the API directly into the native cache.',
|
||||
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string' }
|
||||
name: 'window_control',
|
||||
description: 'Electron window management.',
|
||||
params: { action: 'maximize | unmaximize | minimize | restore | close | fullscreen | kiosk | devtools | reload', value: 'boolean (optional, used by fullscreen/kiosk/devtools)' }
|
||||
},
|
||||
{
|
||||
name: 'launch_from_cache',
|
||||
description: 'Atomic operation: Copies file from cache to temp with original name and launches via specialized handler.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string' }
|
||||
name: 'power_control',
|
||||
description: 'Shutdown, reboot, or sleep the host machine. macOS + Linux. May require sudo for shutdown/reboot.',
|
||||
params: { action: 'shutdown | reboot | sleep' }
|
||||
},
|
||||
{
|
||||
name: 'get_device_info',
|
||||
description: 'Returns hardware and OS metadata (CPUs, RAM, IP addresses, Hostname).',
|
||||
params: {}
|
||||
name: 'manage_recording',
|
||||
description: 'Screen recording via bundled aperture binary. macOS only.',
|
||||
params: { action: 'start | stop | status', options: '{ fps?, audioDeviceId?, output? } (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'update_app',
|
||||
description: 'STUB: Downloads update package but does not install. Not functional.',
|
||||
params: { source: 'url | file', url: 'string (optional)', path: 'string (optional)' }
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
2
dist/main/shell_handlers.js.map
vendored
2
dist/main/shell_handlers.js.map
vendored
File diff suppressed because one or more lines are too long
68
dist/main/system_handlers.js
vendored
68
dist/main/system_handlers.js
vendored
@@ -361,32 +361,68 @@ 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.
|
||||
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' };
|
||||
const binPath = electron_1.app.isPackaged
|
||||
? path.join(process.resourcesPath, 'bin', 'displayplacer')
|
||||
: path.join(__dirname, '../../resources/bin/displayplacer');
|
||||
let cmd = '';
|
||||
// Explicit config string always takes priority — allows manual override per device.
|
||||
if (configStr) {
|
||||
return await runExec(`"${binPath}" ${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`);
|
||||
if (!list_result.success || !list_result.stdout) {
|
||||
return { success: false, error: `displayplacer list failed: ${list_result.error ?? 'no output'}` };
|
||||
}
|
||||
// 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 "'));
|
||||
if (!cmd_line) {
|
||||
return { success: false, error: 'Only one display connected or displayplacer list output unrecognised' };
|
||||
}
|
||||
const display_strings = [...cmd_line.matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
||||
if (display_strings.length < 2) {
|
||||
return { success: false, error: 'Only one display found; cannot change layout' };
|
||||
}
|
||||
if (mode === 'mirror') {
|
||||
// This usually requires querying current IDs, which is complex.
|
||||
// If configStr is provided (output of 'displayplacer list'), use it.
|
||||
if (configStr) {
|
||||
cmd = `"${binPath}" ${configStr}`;
|
||||
const primary_id_match = display_strings[0].match(/\bid:([^\s]+)/);
|
||||
if (!primary_id_match) {
|
||||
return { success: false, error: 'Could not parse primary display ID from displayplacer output' };
|
||||
}
|
||||
else {
|
||||
return { success: false, error: 'Config string required for now' };
|
||||
const primary_id = primary_id_match[1];
|
||||
// Primary display unchanged; secondary display(s) get mirror:<primary_id>.
|
||||
const mirror_args = display_strings.map((s, i) => {
|
||||
if (i === 0)
|
||||
return `"${s}"`;
|
||||
const without_existing_mirror = s.replace(/\s*mirror:\S+/g, '').trim();
|
||||
return `"${without_existing_mirror} mirror:${primary_id}"`;
|
||||
}).join(' ');
|
||||
return await runExec(`"${binPath}" ${mirror_args}`);
|
||||
}
|
||||
if (mode === 'extend') {
|
||||
const any_mirrored = display_strings.some(s => /\bmirror:\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(' ')}`);
|
||||
}
|
||||
// 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:\S+/g, '').trim();
|
||||
const res_match = without_mirror.match(/\bres:(\d+)x\d+/);
|
||||
const width = res_match ? parseInt(res_match[1]) : 1920;
|
||||
const updated = without_mirror.replace(/\borigin:\([^)]+\)/, `origin:(${x_offset},0)`);
|
||||
x_offset += width;
|
||||
return `"${updated}"`;
|
||||
}).join(' ');
|
||||
return await runExec(`"${binPath}" ${extend_args}`);
|
||||
}
|
||||
else if (mode === 'extend') {
|
||||
if (configStr) {
|
||||
cmd = `"${binPath}" ${configStr}`;
|
||||
}
|
||||
}
|
||||
if (cmd) {
|
||||
return await runExec(cmd);
|
||||
}
|
||||
return { success: false, error: 'Invalid mode or missing config' };
|
||||
return { success: false, error: `Unsupported display mode: ${mode}` };
|
||||
});
|
||||
// 7. Update App
|
||||
electron_1.ipcMain.handle('native:update-app', async (event, { source, url, path: localPath }) => {
|
||||
|
||||
2
dist/main/system_handlers.js.map
vendored
2
dist/main/system_handlers.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -171,65 +171,115 @@ end tell
|
||||
|
||||
ipcMain.handle('native:list-tools', async () => {
|
||||
return [
|
||||
// --- Config & Info ---
|
||||
{
|
||||
name: 'get_device_config',
|
||||
description: 'Returns hydrated device config injected at startup from seed.json + API.',
|
||||
params: {}
|
||||
},
|
||||
{
|
||||
name: 'get_device_info',
|
||||
description: 'Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths.',
|
||||
params: {}
|
||||
},
|
||||
// --- File Cache ---
|
||||
{
|
||||
name: 'check_cache',
|
||||
description: 'Checks if a file exists in the hashed cache. verify_hash:true re-hashes to confirm integrity.',
|
||||
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number (optional, default 2)', verify_hash: 'boolean (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'download_to_cache',
|
||||
description: 'Streams a file from the API into the hashed cache with SHA-256 integrity check. Cleans stale .tmp files older than 5 min.',
|
||||
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string', hash_prefix_length: 'number (optional, default 2)' }
|
||||
},
|
||||
{
|
||||
name: 'copy_from_cache_to_temp',
|
||||
description: 'Preferred primitive. Copies a cached file to temp with its original filename. Returns { success, path }. Caller decides what to do next.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string', hash_prefix_length: 'number (optional, default 2)' }
|
||||
},
|
||||
{
|
||||
name: 'launch_from_cache',
|
||||
description: 'Combines copy_from_cache_to_temp + execute. Runs native_template after copying — AppleScript string with {{path}} placeholder, or "shell:<cmd>" prefix. Returns error if native_template is null.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string', hash_prefix_length: 'number (optional)', native_template: 'string | null' }
|
||||
},
|
||||
// --- Shell & OS ---
|
||||
{
|
||||
name: 'open_folder',
|
||||
description: 'Opens a directory in the OS file explorer (Finder/Files/Explorer).',
|
||||
description: 'Opens a directory in the OS file explorer (Finder on macOS).',
|
||||
params: { path: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'run_cmd',
|
||||
description: 'Executes an asynchronous shell command with a timeout.',
|
||||
params: { cmd: 'string', timeout: 'number (optional)' }
|
||||
description: 'Async shell command execution with timeout.',
|
||||
params: { cmd: 'string', timeout: 'number (optional, default 30000ms)' }
|
||||
},
|
||||
{
|
||||
name: 'run_cmd_sync',
|
||||
description: 'Executes a synchronous shell command.',
|
||||
description: 'Synchronous shell command execution.',
|
||||
params: { cmd: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'run_osascript',
|
||||
description: 'Executes a raw AppleScript string (macOS only).',
|
||||
description: 'Hardened AppleScript executor — writes to temp .scpt file, handles multi-line scripts and paths with special characters. macOS only.',
|
||||
params: { script: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'kill_processes',
|
||||
description: 'Forcefully terminates processes by name.',
|
||||
description: 'Terminates processes by name. macOS/Linux: pkill -f. Windows: taskkill /F.',
|
||||
params: { process_name_li: 'string[]' }
|
||||
},
|
||||
{
|
||||
name: 'open_local_file_v2',
|
||||
description: 'Opens a local file using the default OS handler.',
|
||||
params: { filePath: 'string' }
|
||||
description: 'Opens a file with its default OS application via shell.openPath.',
|
||||
params: { path: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'open_external',
|
||||
description: 'Opens a URL in Chrome, Firefox, or the system default browser.',
|
||||
params: { url: 'string', app: 'chrome | firefox | default (optional)' }
|
||||
},
|
||||
// --- Presentations ---
|
||||
{
|
||||
name: 'launch_presentation',
|
||||
description: 'Phase 5: Specialized launcher for PowerPoint, Keynote, and LibreOffice with auto-focus.',
|
||||
params: { path: 'string', app: 'default|powerpoint|keynote' }
|
||||
description: 'Platform-aware launcher for PowerPoint, Keynote, LibreOffice. Resolves [home]/[tmp] placeholders. Hardened AppleScript (2026-05-11). Prefer copy_from_cache_to_temp + run_osascript for new flows.',
|
||||
params: { path: 'string', app: 'default | powerpoint | keynote (optional)', os_platform: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'control_presentation',
|
||||
description: 'Phase 5: Remote navigation for active slideshows.',
|
||||
params: { app: 'powerpoint|keynote', action: 'next|prev|start|stop' }
|
||||
description: 'Slide navigation for active PowerPoint or Keynote via AppleScript. macOS only.',
|
||||
params: { app: 'powerpoint | keynote', action: 'next | prev | start | stop' }
|
||||
},
|
||||
// --- System Management ---
|
||||
{
|
||||
name: 'set_wallpaper',
|
||||
description: 'Sets desktop wallpaper. Downloads from url (cached to ~/Library/Caches/OSIT/wallpaper/) or applies local path. url_external targets projector/second display separately. macOS only in production.',
|
||||
params: { path: 'string (optional)', url: 'string (optional)', url_external: 'string (optional)', display: 'all | primary | external (optional, default all)', api_key: 'string (optional)', account_id: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'check_cache',
|
||||
description: 'Checks if a file exists in the local organized cache.',
|
||||
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number' }
|
||||
name: 'set_display_layout',
|
||||
description: 'Mirror or extend displays via bundled displayplacer. macOS only. Auto-detects displays when no configStr given; configStr is an optional manual override.',
|
||||
params: { mode: 'mirror | extend', configStr: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'download_to_cache',
|
||||
description: 'Downloads a file from the API directly into the native cache.',
|
||||
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string' }
|
||||
name: 'window_control',
|
||||
description: 'Electron window management.',
|
||||
params: { action: 'maximize | unmaximize | minimize | restore | close | fullscreen | kiosk | devtools | reload', value: 'boolean (optional, used by fullscreen/kiosk/devtools)' }
|
||||
},
|
||||
{
|
||||
name: 'launch_from_cache',
|
||||
description: 'Atomic operation: Copies file from cache to temp with original name and launches via specialized handler.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string' }
|
||||
name: 'power_control',
|
||||
description: 'Shutdown, reboot, or sleep the host machine. macOS + Linux. May require sudo for shutdown/reboot.',
|
||||
params: { action: 'shutdown | reboot | sleep' }
|
||||
},
|
||||
{
|
||||
name: 'get_device_info',
|
||||
description: 'Returns hardware and OS metadata (CPUs, RAM, IP addresses, Hostname).',
|
||||
params: {}
|
||||
name: 'manage_recording',
|
||||
description: 'Screen recording via bundled aperture binary. macOS only.',
|
||||
params: { action: 'start | stop | status', options: '{ fps?, audioDeviceId?, output? } (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'update_app',
|
||||
description: 'STUB: Downloads update package but does not install. Not functional.',
|
||||
params: { source: 'url | file', url: 'string (optional)', path: 'string (optional)' }
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
@@ -319,6 +319,9 @@ export function registerSystemHandlers() {
|
||||
});
|
||||
|
||||
// 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.
|
||||
ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
|
||||
if (os.platform() !== 'darwin') return { success: false, error: 'Display control only supported on macOS' };
|
||||
|
||||
@@ -326,27 +329,68 @@ export function registerSystemHandlers() {
|
||||
? path.join(process.resourcesPath, 'bin', 'displayplacer')
|
||||
: path.join(__dirname, '../../resources/bin/displayplacer');
|
||||
|
||||
let cmd = '';
|
||||
// Explicit config string always takes priority — allows manual override per device.
|
||||
if (configStr) {
|
||||
return await runExec(`"${binPath}" ${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`);
|
||||
if (!list_result.success || !list_result.stdout) {
|
||||
return { success: false, error: `displayplacer list failed: ${list_result.error ?? 'no output'}` };
|
||||
}
|
||||
|
||||
// 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 "'));
|
||||
if (!cmd_line) {
|
||||
return { success: false, error: 'Only one display connected or displayplacer list output unrecognised' };
|
||||
}
|
||||
|
||||
const display_strings = [...cmd_line.matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
||||
if (display_strings.length < 2) {
|
||||
return { success: false, error: 'Only one display found; cannot change layout' };
|
||||
}
|
||||
|
||||
if (mode === 'mirror') {
|
||||
// This usually requires querying current IDs, which is complex.
|
||||
// If configStr is provided (output of 'displayplacer list'), use it.
|
||||
if (configStr) {
|
||||
cmd = `"${binPath}" ${configStr}`;
|
||||
} else {
|
||||
return { success: false, error: 'Config string required for now' };
|
||||
}
|
||||
} else if (mode === 'extend') {
|
||||
if (configStr) {
|
||||
cmd = `"${binPath}" ${configStr}`;
|
||||
}
|
||||
const primary_id_match = display_strings[0].match(/\bid:([^\s]+)/);
|
||||
if (!primary_id_match) {
|
||||
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:<primary_id>.
|
||||
const mirror_args = display_strings.map((s, i) => {
|
||||
if (i === 0) return `"${s}"`;
|
||||
const without_existing_mirror = s.replace(/\s*mirror:\S+/g, '').trim();
|
||||
return `"${without_existing_mirror} mirror:${primary_id}"`;
|
||||
}).join(' ');
|
||||
|
||||
return await runExec(`"${binPath}" ${mirror_args}`);
|
||||
}
|
||||
|
||||
if (cmd) {
|
||||
return await runExec(cmd);
|
||||
if (mode === 'extend') {
|
||||
const any_mirrored = display_strings.some(s => /\bmirror:\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(' ')}`);
|
||||
}
|
||||
|
||||
// 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:\S+/g, '').trim();
|
||||
const res_match = without_mirror.match(/\bres:(\d+)x\d+/);
|
||||
const width = res_match ? parseInt(res_match[1]) : 1920;
|
||||
const updated = without_mirror.replace(/\borigin:\([^)]+\)/, `origin:(${x_offset},0)`);
|
||||
x_offset += width;
|
||||
return `"${updated}"`;
|
||||
}).join(' ');
|
||||
|
||||
return await runExec(`"${binPath}" ${extend_args}`);
|
||||
}
|
||||
|
||||
return { success: false, error: 'Invalid mode or missing config' };
|
||||
return { success: false, error: `Unsupported display mode: ${mode}` };
|
||||
});
|
||||
|
||||
// 7. Update App
|
||||
|
||||
Reference in New Issue
Block a user