Files
ddc-brightness-control/src/ddcutil.ts
T

87 lines
3.4 KiB
TypeScript

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(),
);
}