import Gio from 'gi://Gio'; import { LOG_PREFIX, DDCUTIL_SLEEP_MULTIPLIER } from './constants.js'; /** A display as parsed from `ddcutil detect`, before any state is attached. */ export interface ParsedDisplay { bus: string; name: string; connector: string; } // Run a command asynchronously, capturing stdout. The callback always fires: // with stdout on success, or an empty string on failure. export function runCommandAsync(args: string[], callback: (stdout: string) => void): void { console.log(`${LOG_PREFIX} Running: ${args.join(' ')}`); try { const subprocess = new Gio.Subprocess({ argv: args, flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE, }); subprocess.init(null); subprocess.communicate_utf8_async(null, null, (proc: Gio.Subprocess | null, result: Gio.AsyncResult) => { try { const [, stdout] = (proc ?? subprocess).communicate_utf8_finish(result); console.log(`${LOG_PREFIX} Done: ${stdout.trim().substring(0, 80)}`); callback(stdout); } catch (e) { console.error(`${LOG_PREFIX} Command failed: ${e}`); callback(''); } }); } catch (e) { console.error(`${LOG_PREFIX} Failed to spawn: ${e}`); } } // Detect connected DDC displays and return them parsed. export function detectDisplays(callback: (displays: ParsedDisplay[]) => void): void { runCommandAsync(['ddcutil', 'detect', '--brief'], (stdout: string) => { callback(parseDisplays(stdout)); }); } // Parse the output of `ddcutil detect --brief` into a list of displays. export function parseDisplays(stdout: string): ParsedDisplay[] { const displays: ParsedDisplay[] = []; const blocks = stdout.split(/^Display\s+\d+/m); for (const block of blocks) { if (!block.trim()) continue; const busMatch = block.match(/I2C bus:\s+\/dev\/i2c-(\d+)/); const monitorMatch = block.match(/Monitor:\s+(.+)/); const connectorMatch = block.match(/DRM connector:\s+card\d+-(\S+)/); if (busMatch) { const bus = busMatch[1]; const name = monitorMatch ? monitorMatch[1].trim() : `Display (bus ${bus})`; const connector = connectorMatch ? connectorMatch[1] : ''; displays.push({ bus, name, connector }); } } return displays; } // Read the current brightness (0-100) for a bus, or null if unreadable. export function readBrightness(bus: string, vcpCode: string, callback: (value: number | null) => void): void { runCommandAsync( ['ddcutil', 'getvcp', vcpCode, '--bus', bus, '--brief', '--sleep-multiplier', DDCUTIL_SLEEP_MULTIPLIER], (stdout: string) => { const match = stdout.match(/VCP\s+\w+\s+\w+\s+(\d+)\s+(\d+)/); callback(match ? parseInt(match[1]) : null); }, ); } // Set the brightness (0-100) for a bus. `callback` fires when the write // completes (success or failure), so callers can chain the next write. // `--noverify` skips ddcutil's post-write read-back for speed. export function setBrightness(bus: string, vcpCode: string, value: number, callback: () => void): void { runCommandAsync( ['ddcutil', 'setvcp', vcpCode, String(value), '--bus', bus, '--noverify', '--sleep-multiplier', DDCUTIL_SLEEP_MULTIPLIER], () => callback(), ); }