fix(display): auto-detect displays for mirror/extend via displayplacer list
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:<primary_id> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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