From a1e74829e8943209f6286b42173a6ea3d9f8c85b Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 20 May 2026 15:23:01 -0400 Subject: [PATCH] fix(display): auto-detect displays for mirror/extend via displayplacer list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handler was a stub that required an explicit configStr — the Svelte caller never provides one, so mirror/extend silently failed every launch. Now auto-detects connected displays: - mirror: parses displayplacer list output, adds mirror: to secondary display(s); removes any existing mirror key first. - extend: re-applies current layout if already extended; otherwise strips mirror keys and computes side-by-side origins from each display's width. - configStr still takes priority when provided (manual per-device override). Co-Authored-By: Claude Sonnet 4.6 --- src/main/system_handlers.ts | 74 +++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/src/main/system_handlers.ts b/src/main/system_handlers.ts index 4e267e4..13cf5ce 100644 --- a/src/main/system_handlers.ts +++ b/src/main/system_handlers.ts @@ -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:. + 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