Compare commits
8 Commits
48e24af84e
...
99e0ebb7c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99e0ebb7c3 | ||
|
|
51db51d991 | ||
|
|
6cddd69891 | ||
|
|
58060aea8a | ||
|
|
54308c6d4a | ||
|
|
b2cd0736df | ||
|
|
a230ff09de | ||
|
|
7199d45719 |
@@ -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. macOS only. **Primary path:** bundled `display_control` binary (native CoreGraphics, no Homebrew). **Fallback:** `displayplacer` — used when `display_control` binary is absent, or when a `configStr` override is set. `configStr` is an optional manual override (full `displayplacer` config string) stored in `event_device.data_json` for per-device tuning. Build `display_control` once via `scripts/build-display-control.sh` on a Mac and commit `resources/bin/display_control`. |
|
||||
| `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. |
|
||||
@@ -394,6 +396,9 @@ If you have repo access on the Mac itself (Xcode CLT required — `xcode-select
|
||||
./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)"
|
||||
|
||||
39
dist/main/index.js
vendored
39
dist/main/index.js
vendored
@@ -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);
|
||||
|
||||
2
dist/main/index.js.map
vendored
2
dist/main/index.js.map
vendored
@@ -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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"}
|
||||
41
dist/main/system_handlers.js
vendored
41
dist/main/system_handlers.js
vendored
@@ -442,6 +442,47 @@ function registerSystemHandlers() {
|
||||
}
|
||||
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
|
||||
|
||||
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
2
dist/preload/index.js
vendored
2
dist/preload/index.js
vendored
@@ -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),
|
||||
});
|
||||
|
||||
2
dist/preload/index.js.map
vendored
2
dist/preload/index.js.map
vendored
@@ -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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"}
|
||||
@@ -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.
|
||||
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -413,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
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user