feat(displays): add display detection and management (sway) with daemon APIs and UI controls

This commit is contained in:
2026-01-09 18:14:26 +00:00
parent ee631c21c4
commit 06cea4bb37
8 changed files with 239 additions and 3 deletions

View File

@@ -5,6 +5,7 @@
*/
import { runCommand } from '../utils/command.ts';
import type { DisplayInfo } from './system-info.ts';
export interface SwayConfig {
runtimeDir: string;
@@ -306,6 +307,95 @@ for_window [app_id="chromium-browser"] fullscreen enable
}
}
/**
* Get connected displays via swaymsg
*/
async getDisplays(config: { runtimeDir: string; waylandDisplay: string }): Promise<DisplayInfo[]> {
const env: Record<string, string> = {
XDG_RUNTIME_DIR: config.runtimeDir,
WAYLAND_DISPLAY: config.waylandDisplay,
};
const envString = Object.entries(env)
.map(([k, v]) => `${k}=${v}`)
.join(' ');
const cmd = new Deno.Command('runuser', {
args: ['-u', this.user, '--', 'sh', '-c', `${envString} swaymsg -t get_outputs`],
stdout: 'piped',
stderr: 'piped',
});
try {
const result = await cmd.output();
if (!result.success) {
console.error('[displays] Failed to get outputs');
return [];
}
const outputs = JSON.parse(new TextDecoder().decode(result.stdout));
return outputs.map((output: {
name: string;
make: string;
model: string;
serial: string;
active: boolean;
current_mode?: { width: number; height: number; refresh: number };
focused: boolean;
}) => ({
name: output.name,
make: output.make || 'Unknown',
model: output.model || 'Unknown',
serial: output.serial || '',
active: output.active,
width: output.current_mode?.width || 0,
height: output.current_mode?.height || 0,
refreshRate: Math.round((output.current_mode?.refresh || 0) / 1000),
isPrimary: output.focused,
}));
} catch (error) {
console.error(`[displays] Error: ${error}`);
return [];
}
}
/**
* Enable or disable a display
*/
async setDisplayEnabled(
config: { runtimeDir: string; waylandDisplay: string },
name: string,
enabled: boolean
): Promise<boolean> {
const command = `output ${name} ${enabled ? 'enable' : 'disable'}`;
console.log(`[displays] ${command}`);
return this.swaymsg(config, command);
}
/**
* Move the kiosk browser to a specific display
*/
async setKioskDisplay(
config: { runtimeDir: string; waylandDisplay: string },
name: string
): Promise<boolean> {
console.log(`[displays] Setting primary display to ${name}`);
// Focus the chromium window and move it to the target output
const commands = [
`[app_id="chromium-browser"] focus`,
`move container to output ${name}`,
`focus output ${name}`,
`[app_id="chromium-browser"] fullscreen enable`,
];
for (const cmd of commands) {
await this.swaymsg(config, cmd);
}
return true;
}
private async pipeOutput(
process: Deno.ChildProcess,
name: string