feat(users/Profpatsch): dbus service for changing monitor brightness

This simple dbus service will use the ddcci interfaces to change
brightness for both the internal and external monitor (roughly in
sync).

Currently in the alacritty-change-color-scheme script because I’m lazy
and still experimenting.

Change-Id: Ib2c4323699ed9d19ee398f84680b755df4b25798
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12891
Tested-by: BuildkiteCI
Reviewed-by: Profpatsch <mail@profpatsch.de>
This commit is contained in:
Profpatsch 2024-12-13 20:18:56 +01:00
parent fe9692fabf
commit e1fda52bed

View file

@ -18,7 +18,9 @@ const { AsyncHooksContextManager } = require('@opentelemetry/context-async-hooks
const { EventEmitter } = require('stream');
const { setTimeout } = require('node:timers/promises');
const { promisify } = require('util');
const { randomUUID, randomBytes, pseudoRandomBytes } = require('crypto');
const { pseudoRandomBytes } = require('crypto');
const { execFile } = require('node:child_process');
const { readdir } = require('fs/promises');
// NB: this code is like 80% copilot generated, and seriously missing error handling.
// It might break at any time, but for now it seems to work lol.
@ -712,8 +714,183 @@ class Tracer {
}
}
const execFileP = promisify(execFile);
/** actual brightness roughly (but not quite arghhhh) logarithmic on a ThinkPad T14s with effin ARM crap
/* @type {number[]}
*/
const brightnessMappingInternal = [0, 2, 4, 6, 9, 14, 20, 35, 50, 84, 120, 147, 200, 255];
const brightnessMappingExternal = [0, 0, 0, 0, 12, 24, 36, 48, 60, 72, 84, 100, 100, 100];
const maxBrightnessVal = brightnessMappingInternal.length - 1;
async function getInitialInternalMonitorBrightness() {
const { stdout } = await execFileP(`blight`, [`get`, 'brightness'], {});
const brightness = parseInt(stdout, 10);
const brightnessVal = brightnessMappingInternal.findIndex(value => value >= brightness);
return brightnessVal;
}
/**
* @param {number} brightnessVal number between 0 and `maxBrightnessVal`
* (the amount of steps we want) where 0 is turn off if possible and `maxBrightnessVal` is full brightness
* The steps should be roughly aligned (by human eye) between the internal and external monitor.
*/
async function SetBrightnessAllMonitors(brightnessVal) {
return Promise.all([
(async () => {
const internalMonitorBrightness = brightnessMappingInternal[brightnessVal] ?? '255';
// set the brightness for internal monitor with the `blight` command line util
console.log(`Setting internal monitor brightness to ${internalMonitorBrightness}`);
await execFileP(`blight`, [`set`, `${internalMonitorBrightness}`], {});
})(),
(async () => {
// find external monitors by listing all directories in /dev/bus/ddcci and reading their numbers
// external monitor brightness (ddc) is between 0 and 100
const externalBrightness = brightnessMappingExternal[brightnessVal] ?? '100';
ddcutilNewBrightness(externalBrightness);
})(),
]);
}
const ddcutilEmitter = new EventEmitter();
/**
* @param {number} brightness
*/
function ddcutilNewBrightness(brightness) {
ddcutilEmitter.emit('new-brightness', brightness);
}
/**
* @type {number | null}
*/
let nextBrightness = null;
/** Set the brightness of the external monitor using ddcutil,
* but only if nothing else is already trying to set it. In that case schedule to set it later.
* Also debounce the brightness setting to avoid setting it multiple times in a short time.
*
* @param {number} externalBrightness
*/
async function onDdcutilBrightnessChange(externalBrightness) {
if (nextBrightness === externalBrightness) return;
if (nextBrightness !== null) {
nextBrightness = externalBrightness;
console.log(`Already running, will set brightness to ${externalBrightness} later`);
return;
}
nextBrightness = externalBrightness;
// debounce for some ms if anything happens to nextBrightness in the meantime, use that value
await setTimeout(250);
if (nextBrightness !== externalBrightness) {
console.log(
`Newer brightness value ${nextBrightness} was requested, ignoring ${externalBrightness}`,
);
const brightness = nextBrightness;
nextBrightness = null;
await onDdcutilBrightnessChange(brightness);
return;
}
const ddcciDevices = await readdir('/dev/bus/ddcci');
console.log(`Found ddcci devices: ${ddcciDevices}`);
for (const device of ddcciDevices) {
console.log(
`Setting external monitor brightness to ${externalBrightness} for device ${device}`,
);
await execFileP('ddcutil', [
`--bus`,
device,
`--sleep-multiplier`,
'0.20',
`setvcp`,
`0x10`,
`${externalBrightness}`,
]).catch(err => {
/** @type {string} */
let stdout = err.stdout;
// err.stdout contains "No monitor detected on bus"
if (stdout.includes('No monitor detected on bus')) {
console.log(`External monitor on bus ${device} is gone, ignoring.`);
return;
}
console.warn(`Error setting brightness with ddcutil for device ${device}`, err);
});
}
if (nextBrightness !== externalBrightness) {
const brightness = nextBrightness;
nextBrightness = null;
await onDdcutilBrightnessChange(brightness);
} else {
nextBrightness = null;
console.log(`Finished setting external monitor brightness to ${externalBrightness}`);
}
}
async function exportDisplayBrightnessDbusInterface() {
console.log(
'Exporting display brightness interface de.profpatsch.alacritty.DisplayBrightness',
);
const ifaceName = 'de.profpatsch.alacritty.DisplayBrightness';
const iface = {
name: 'de.profpatsch.alacritty.DisplayBrightness',
methods: {
// between 0 and 10
SetBrightnessAllMonitors: ['d', ''],
// either '+' or '-'
SetBrightnessAllMonitorsRelative: ['s', ''],
},
};
let currentBrightness = await getInitialInternalMonitorBrightness();
console.log(`Current brightness: ${currentBrightness}`);
ddcutilEmitter.on('new-brightness', onDdcutilBrightnessChange);
const ifaceImpl = {
/** @type {function(number): void} */
SetBrightnessAllMonitors: function (brightness) {
console.log(`SetBrightnessAllMonitors called with ${brightness}`);
// set the brightness
SetBrightnessAllMonitors(brightness);
},
/** @type {function(string): void} */
SetBrightnessAllMonitorsRelative: function (direction) {
console.log(`SetBrightnessAllMonitorsRelative called with ${direction}`);
switch (direction) {
case '+':
currentBrightness = Math.min(maxBrightnessVal, currentBrightness + 1);
break;
case '-':
currentBrightness = Math.max(0, currentBrightness - 1);
break;
default:
console.warn(`Invalid direction ${direction}`);
return;
}
ifaceImpl.SetBrightnessAllMonitors(currentBrightness);
},
};
try {
const retCode = await bus.requestName(ifaceName, 0);
console.log(
`Request name returned ${retCode} for interface de.profpatsch.alacritty.DisplayBrightness`,
);
bus.exportInterface(ifaceImpl, '/de/profpatsch/alacritty/DisplayBrightness', iface);
console.log('Exported interface de.profpatsch.alacritty.DisplayBrightness');
} catch (err) {
console.log('Error exporting interface de.profpatsch.alacritty.DisplayBrightness');
console.error(err);
}
}
async function main() {
await exportOtelInterface();
await exportDisplayBrightnessDbusInterface();
const tracer = await Tracer.setup('hello');
await tracer.withSpan(