feat(display): add display_control CoreGraphics binary, replace displayplacer as primary path
Derived from OSIT MasterKey app (LegacyUtilities.m, Ian Kohl 2019). Uses CGConfigureDisplayMirrorOfDisplay — native macOS API, no Homebrew dependency. Handler now tries display_control first; falls back to displayplacer for missing binary or per-device configStr overrides. Build the binary via scripts/build-display-control.sh on a Mac (requires Xcode CLT only), then commit resources/bin/display_control. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
23
dist/main/system_handlers.js
vendored
23
dist/main/system_handlers.js
vendored
@@ -367,9 +367,16 @@ function registerSystemHandlers() {
|
|||||||
electron_1.ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
|
electron_1.ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
|
||||||
if (os.platform() !== 'darwin')
|
if (os.platform() !== 'darwin')
|
||||||
return { success: false, error: 'Display control only supported on macOS' };
|
return { success: false, error: 'Display control only supported on macOS' };
|
||||||
const binPath = electron_1.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 = 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.
|
// Explicit config string always takes priority — allows manual override per device.
|
||||||
if (configStr) {
|
if (configStr) {
|
||||||
return await runExec(`"${binPath}" ${configStr}`);
|
return await runExec(`"${binPath}" ${configStr}`);
|
||||||
@@ -395,17 +402,17 @@ function registerSystemHandlers() {
|
|||||||
return { success: false, error: 'Could not parse primary display ID from displayplacer output' };
|
return { success: false, error: 'Could not parse primary display ID from displayplacer output' };
|
||||||
}
|
}
|
||||||
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)
|
if (i === 0)
|
||||||
return `"${s}"`;
|
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(' ')}`);
|
||||||
@@ -413,7 +420,7 @@ 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)`);
|
||||||
|
|||||||
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
@@ -66,31 +66,46 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## set_display_layout — displayplacer Setup & Status (updated 2026-05-20)
|
## set_display_layout — Setup & Status (updated 2026-05-20)
|
||||||
|
|
||||||
**Reference:** [jakehilborn/displayplacer](https://github.com/jakehilborn/displayplacer)
|
**Primary approach: `display_control` (native CoreGraphics — no Homebrew required)**
|
||||||
|
- Source: `scripts/display_control.m` (derived from OSIT MasterKey app, Ian Kohl 2019)
|
||||||
|
- Build: run `scripts/build-display-control.sh` on a Mac (requires Xcode CLT only)
|
||||||
|
- Output: `resources/bin/display_control` — commit this binary to the repo
|
||||||
|
- Uses `CGConfigureDisplayMirrorOfDisplay` — same CoreGraphics API macOS uses internally
|
||||||
|
- Supports 3+ displays; auto-detects all connected displays; no config string needed
|
||||||
|
|
||||||
|
**Fallback approach: `displayplacer` (requires `brew install displayplacer` on each venue Mac)**
|
||||||
|
- Reference: [jakehilborn/displayplacer](https://github.com/jakehilborn/displayplacer)
|
||||||
|
- Still used when `display_control` binary is not present
|
||||||
|
- Also used for per-device `configStr` overrides (displayplacer-format strings in `event_device.data_json`)
|
||||||
|
|
||||||
**Current state (2026-05-20):**
|
**Current state (2026-05-20):**
|
||||||
|
|
||||||
- ✅ Handler auto-detects displays via `displayplacer list` — no per-device `configStr` required for normal use
|
- ✅ Correct `mirror_of_display:<uuid>` syntax used in displayplacer fallback (was `mirror:` — wrong, now fixed)
|
||||||
- ✅ 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
|
- ✅ 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 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
|
||||||
|
|
||||||
**One-time setup on each venue Mac:**
|
**To build `display_control` (do this on a Mac):**
|
||||||
```bash
|
```bash
|
||||||
brew install displayplacer
|
# One-time: install Xcode Command Line Tools if not already installed
|
||||||
|
xcode-select --install
|
||||||
|
|
||||||
|
# Then:
|
||||||
|
./scripts/build-display-control.sh
|
||||||
|
|
||||||
|
# Test it with a second display connected:
|
||||||
|
./resources/bin/display_control status
|
||||||
|
./resources/bin/display_control extend
|
||||||
|
./resources/bin/display_control mirror
|
||||||
|
|
||||||
|
# Commit the binary:
|
||||||
|
git add resources/bin/display_control
|
||||||
|
git commit -m "build: add display_control binary (macOS CoreGraphics)"
|
||||||
```
|
```
|
||||||
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.
|
|
||||||
|
|
||||||
**Bundling the binary (future):**
|
**Optional per-device override (displayplacer format, for edge cases):**
|
||||||
To ship displayplacer inside the `.app` bundle without requiring Homebrew on venue Macs:
|
|
||||||
1. Copy the `displayplacer` binary to `resources/bin/displayplacer`
|
|
||||||
2. Mark it executable: `chmod +x resources/bin/displayplacer`
|
|
||||||
3. Add to `package.json` `extraResources` so `@electron/packager` includes it
|
|
||||||
|
|
||||||
**Optional per-device override (manual tuning):**
|
|
||||||
For rooms where auto-detection produces the wrong result, store the raw configStr in `event_device.data_json`:
|
For rooms where auto-detection produces the wrong result, store the raw configStr in `event_device.data_json`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -98,10 +113,4 @@ For rooms where auto-detection produces the wrong result, store the raw configSt
|
|||||||
"displayplacer_config_mirror": "<output of displayplacer list in mirrored 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.
|
`configStr` is passed from the Svelte call site and uses the displayplacer fallback path directly.
|
||||||
|
|
||||||
**displayplacer quick reference:**
|
|
||||||
- `displayplacer list` — prints current display info and a ready-to-run config string at the bottom
|
|
||||||
- `displayplacer "<display_string>" "<display_string>"` — applies layout
|
|
||||||
- Mirror syntax: add `mirror_of_display:<primary_uuid>` to the secondary display string
|
|
||||||
- Extend syntax: set `origin:(<x>,0)` with non-overlapping x offsets per display
|
|
||||||
|
|||||||
40
scripts/build-display-control.sh
Executable file
40
scripts/build-display-control.sh
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# scripts/build-display-control.sh
|
||||||
|
# Compile the display_control binary for macOS.
|
||||||
|
#
|
||||||
|
# Requirements: Xcode Command Line Tools
|
||||||
|
# xcode-select --install
|
||||||
|
#
|
||||||
|
# Run this on a Mac. Commit the resulting binary to resources/bin/
|
||||||
|
# so it is bundled into the packaged Electron app without any Homebrew dependency.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
SRC="$SCRIPT_DIR/display_control.m"
|
||||||
|
OUT_DIR="$REPO_ROOT/resources/bin"
|
||||||
|
OUT_BIN="$OUT_DIR/display_control"
|
||||||
|
|
||||||
|
if ! command -v clang &>/dev/null; then
|
||||||
|
echo "ERROR: clang not found."
|
||||||
|
echo "Install Xcode Command Line Tools: xcode-select --install"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
|
echo "Building display_control..."
|
||||||
|
clang -framework Cocoa -framework Carbon \
|
||||||
|
-o "$OUT_BIN" "$SRC"
|
||||||
|
|
||||||
|
chmod +x "$OUT_BIN"
|
||||||
|
|
||||||
|
echo "Built: $OUT_BIN"
|
||||||
|
echo ""
|
||||||
|
echo "Test it:"
|
||||||
|
echo " $OUT_BIN status"
|
||||||
|
echo " $OUT_BIN extend"
|
||||||
|
echo " $OUT_BIN mirror"
|
||||||
|
echo ""
|
||||||
|
echo "Once verified, commit resources/bin/display_control to the repo."
|
||||||
149
scripts/display_control.m
Normal file
149
scripts/display_control.m
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* display_control.m
|
||||||
|
* Native macOS CLI for programmatic display mirror/extend control.
|
||||||
|
*
|
||||||
|
* Derived from the OSIT MasterKey app (LegacyUtilities.m, Ian Kohl 2019).
|
||||||
|
* Uses CoreGraphics APIs — no external dependencies (no displayplacer/Homebrew required).
|
||||||
|
*
|
||||||
|
* Build: run scripts/build-display-control.sh on a Mac (requires Xcode Command Line Tools)
|
||||||
|
* Usage: display_control <mirror|extend|status>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#include <Carbon/Carbon.h>
|
||||||
|
|
||||||
|
#define MAX_DISPLAYS 8
|
||||||
|
|
||||||
|
static int mirror_displays(void) {
|
||||||
|
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||||||
|
CGDisplayCount numOnline = 0;
|
||||||
|
|
||||||
|
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||||||
|
|
||||||
|
if (numOnline < 2) {
|
||||||
|
fprintf(stderr, "No secondary display detected (%u online).\n", numOnline);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGDirectDisplayID mainID = CGMainDisplayID();
|
||||||
|
|
||||||
|
CGDisplayConfigRef config;
|
||||||
|
CGError err = CGBeginDisplayConfiguration(&config);
|
||||||
|
if (err != kCGErrorSuccess) {
|
||||||
|
fprintf(stderr, "CGBeginDisplayConfiguration failed: %d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL any_configured = NO;
|
||||||
|
for (CGDisplayCount i = 0; i < numOnline; i++) {
|
||||||
|
if (onlineDspys[i] != mainID) {
|
||||||
|
err = CGConfigureDisplayMirrorOfDisplay(config, onlineDspys[i], mainID);
|
||||||
|
if (err != kCGErrorSuccess) {
|
||||||
|
fprintf(stderr, "CGConfigureDisplayMirrorOfDisplay failed: %d\n", err);
|
||||||
|
CGCancelDisplayConfiguration(config);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
any_configured = YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any_configured) {
|
||||||
|
CGCancelDisplayConfiguration(config);
|
||||||
|
fprintf(stderr, "No secondary displays to mirror.\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = CGCompleteDisplayConfiguration(config, kCGConfigurePermanently);
|
||||||
|
if (err != kCGErrorSuccess) {
|
||||||
|
fprintf(stderr, "CGCompleteDisplayConfiguration failed: %d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Mirrored %u display(s).\n", numOnline - 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int extend_displays(void) {
|
||||||
|
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||||||
|
CGDirectDisplayID activeDspys[MAX_DISPLAYS] = {0};
|
||||||
|
CGDisplayCount numOnline = 0, numActive = 0;
|
||||||
|
|
||||||
|
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||||||
|
CGGetActiveDisplayList(MAX_DISPLAYS, activeDspys, &numActive);
|
||||||
|
|
||||||
|
if (numOnline < 2) {
|
||||||
|
fprintf(stderr, "No secondary display detected (%u online).\n", numOnline);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numActive >= numOnline) {
|
||||||
|
printf("Displays already extended.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGDirectDisplayID mainID = CGMainDisplayID();
|
||||||
|
|
||||||
|
CGDisplayConfigRef config;
|
||||||
|
CGError err = CGBeginDisplayConfiguration(&config);
|
||||||
|
if (err != kCGErrorSuccess) {
|
||||||
|
fprintf(stderr, "CGBeginDisplayConfiguration failed: %d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (CGDisplayCount i = 0; i < numOnline; i++) {
|
||||||
|
if (onlineDspys[i] != mainID) {
|
||||||
|
// kCGNullDirectDisplay as master = un-mirror (extend)
|
||||||
|
err = CGConfigureDisplayMirrorOfDisplay(config, onlineDspys[i], kCGNullDirectDisplay);
|
||||||
|
if (err != kCGErrorSuccess) {
|
||||||
|
fprintf(stderr, "CGConfigureDisplayMirrorOfDisplay(null) failed: %d\n", err);
|
||||||
|
CGCancelDisplayConfiguration(config);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = CGCompleteDisplayConfiguration(config, kCGConfigurePermanently);
|
||||||
|
if (err != kCGErrorSuccess) {
|
||||||
|
fprintf(stderr, "CGCompleteDisplayConfiguration failed: %d\n", err);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Displays extended.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void print_status(void) {
|
||||||
|
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||||||
|
CGDirectDisplayID activeDspys[MAX_DISPLAYS] = {0};
|
||||||
|
CGDisplayCount numOnline = 0, numActive = 0;
|
||||||
|
|
||||||
|
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||||||
|
CGGetActiveDisplayList(MAX_DISPLAYS, activeDspys, &numActive);
|
||||||
|
|
||||||
|
printf("online=%u active=%u %s\n",
|
||||||
|
numOnline, numActive,
|
||||||
|
(numOnline > 1 && numActive < numOnline) ? "mirrored" : "extended");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, const char * argv[]) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: display_control <mirror|extend|status>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *cmd = argv[1];
|
||||||
|
|
||||||
|
if (strcmp(cmd, "mirror") == 0) {
|
||||||
|
return mirror_displays();
|
||||||
|
} else if (strcmp(cmd, "extend") == 0) {
|
||||||
|
return extend_displays();
|
||||||
|
} else if (strcmp(cmd, "status") == 0) {
|
||||||
|
print_status();
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "Unknown command: %s\nUsage: display_control <mirror|extend|status>\n", cmd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -318,34 +318,48 @@ export function registerSystemHandlers() {
|
|||||||
return { success: false, error: 'Unknown action' };
|
return { success: false, error: 'Unknown action' };
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6. Set Display Layout (Displayplacer)
|
// 6. Set Display Layout
|
||||||
// Auto-detects connected displays via `displayplacer list` when no explicit configStr is given.
|
// Primary path: display_control (native CoreGraphics, no external deps).
|
||||||
// mirror: secondary display(s) mirror the primary.
|
// Build from scripts/display_control.m via scripts/build-display-control.sh on a Mac.
|
||||||
// extend: displays shown side-by-side; un-mirrors if currently mirrored.
|
// 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).
|
||||||
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' };
|
||||||
|
|
||||||
// Try bundled binary first; fall back to common Homebrew/system locations.
|
// Primary: display_control — native CoreGraphics, no Homebrew dependency.
|
||||||
// Install on a dev/venue Mac via: brew install displayplacer
|
// Derived from OSIT MasterKey app (LegacyUtilities.m). No configStr support needed —
|
||||||
const _bin_candidates = app.isPackaged
|
// CoreGraphics auto-detects all connected displays.
|
||||||
|
const dc_bin = app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, 'bin', 'display_control')
|
||||||
|
: 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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = app.isPackaged
|
||||||
? [path.join(process.resourcesPath, 'bin', 'displayplacer')]
|
? [path.join(process.resourcesPath, 'bin', 'displayplacer')]
|
||||||
: [
|
: [
|
||||||
path.join(__dirname, '../../resources/bin/displayplacer'),
|
path.join(__dirname, '../../resources/bin/displayplacer'),
|
||||||
'/opt/homebrew/bin/displayplacer', // Apple Silicon Homebrew
|
'/opt/homebrew/bin/displayplacer', // Apple Silicon Homebrew
|
||||||
'/usr/local/bin/displayplacer', // Intel Homebrew
|
'/usr/local/bin/displayplacer', // Intel Homebrew
|
||||||
];
|
];
|
||||||
const binPath = _bin_candidates.find(p => fs.existsSync(p)) ?? _bin_candidates[0];
|
const dpPath = _dp_candidates.find(p => fs.existsSync(p)) ?? _dp_candidates[0];
|
||||||
|
|
||||||
// Explicit config string always takes priority — allows manual override per device.
|
// Explicit configStr takes priority — allows manual per-device override.
|
||||||
if (configStr) {
|
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.
|
// Auto-detect via `displayplacer list`.
|
||||||
// We parse the quoted display strings from that line and modify them for the requested mode.
|
const list_result = await runExec(`"${dpPath}" list`);
|
||||||
const list_result = await runExec(`"${binPath}" list`);
|
|
||||||
if (!list_result.success || !list_result.stdout) {
|
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 ..."
|
// The command line looks like: displayplacer "id:xxx res:... origin:(0,0) ..." "id:yyy ..."
|
||||||
@@ -366,24 +380,21 @@ 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_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_of_display:\S+/g, '').trim();
|
const without_existing_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
|
||||||
return `"${without_existing_mirror} mirror_of_display:${primary_id}"`;
|
return `"${without_existing_mirror} mirror_of_display:${primary_id}"`;
|
||||||
}).join(' ');
|
}).join(' ');
|
||||||
|
|
||||||
return await runExec(`"${binPath}" ${mirror_args}`);
|
return await runExec(`"${dpPath}" ${mirror_args}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'extend') {
|
if (mode === 'extend') {
|
||||||
const any_mirrored = display_strings.some(s => /\bmirror_of_display:\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.
|
return await runExec(`"${dpPath}" ${display_strings.map(s => `"${s}"`).join(' ')}`);
|
||||||
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;
|
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_of_display:\S+/g, '').trim();
|
const without_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
|
||||||
@@ -394,7 +405,7 @@ export function registerSystemHandlers() {
|
|||||||
return `"${updated}"`;
|
return `"${updated}"`;
|
||||||
}).join(' ');
|
}).join(' ');
|
||||||
|
|
||||||
return await runExec(`"${binPath}" ${extend_args}`);
|
return await runExec(`"${dpPath}" ${extend_args}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: `Unsupported display mode: ${mode}` };
|
return { success: false, error: `Unsupported display mode: ${mode}` };
|
||||||
|
|||||||
Reference in New Issue
Block a user