feat(displays): add display detection and management (sway) with daemon APIs and UI controls
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user