feat: ddcutil display brightness support #101
This commit is contained in:
+178
-63
@@ -15,14 +15,14 @@ global.HARDWARE = global.HARDWARE || {
|
||||
},
|
||||
display: {
|
||||
status: {
|
||||
value: null,
|
||||
path: null,
|
||||
command: null,
|
||||
value: {},
|
||||
},
|
||||
brightness: {
|
||||
value: null,
|
||||
path: null,
|
||||
max: null,
|
||||
command: null,
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
keyboard: {
|
||||
@@ -52,7 +52,8 @@ const init = async () => {
|
||||
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||
HARDWARE.display.status.command = getDisplayStatusCommand();
|
||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
||||
HARDWARE.display.brightness.max = getDisplayBrightnessMax();
|
||||
HARDWARE.display.brightness.command = getDisplayBrightnessCommand();
|
||||
HARDWARE.display.brightness.value.max = getDisplayBrightnessMax();
|
||||
HARDWARE.audio.device = getAudioDevice();
|
||||
HARDWARE.support = checkSupport();
|
||||
HARDWARE.initialized = true;
|
||||
@@ -86,25 +87,35 @@ const init = async () => {
|
||||
// Show hardware infos
|
||||
process.stdout.write("\n");
|
||||
const unsupported = "unsupported";
|
||||
const batteryLevel = `${getBatteryLevel()} (sysfs)`;
|
||||
const batteryLevelInfo = HARDWARE.support.batteryLevel ? batteryLevel : unsupported;
|
||||
console.info(
|
||||
`Battery Level [${HARDWARE.support.batteryLevel ? HARDWARE.battery.level.path : unsupported}]:`,
|
||||
HARDWARE.support.batteryLevel ? getBatteryLevel() : unsupported,
|
||||
batteryLevelInfo,
|
||||
);
|
||||
const displayStatus = `${getDisplayStatus()} (${HARDWARE.display.status.command})`;
|
||||
const displayStatusInfo = HARDWARE.support.displayStatus ? displayStatus : unsupported;
|
||||
console.info(
|
||||
`Display Status [${HARDWARE.support.displayStatus ? HARDWARE.display.status.path : unsupported}]:`,
|
||||
HARDWARE.support.displayStatus ? `${getDisplayStatus()} (${HARDWARE.display.status.command})` : unsupported,
|
||||
displayStatusInfo,
|
||||
);
|
||||
const displayBrightness = `${getDisplayBrightness()} (${HARDWARE.display.brightness.command || "sysfs"})`;
|
||||
const displayBrightnessInfo = HARDWARE.support.displayBrightness ? displayBrightness : unsupported;
|
||||
console.info(
|
||||
`Display Brightness [${HARDWARE.support.displayBrightness ? HARDWARE.display.brightness.path : unsupported}]:`,
|
||||
HARDWARE.support.displayBrightness ? getDisplayBrightness() : unsupported,
|
||||
`Display Brightness [${HARDWARE.support.displayBrightness ? HARDWARE.display.brightness.path || "ddc://vcp/feature/0x10" : unsupported}]:`,
|
||||
displayBrightnessInfo,
|
||||
);
|
||||
const audioVolume = `${getAudioVolume()} (pactl)`;
|
||||
const audioVolumeInfo = HARDWARE.support.audioVolume ? audioVolume : unsupported;
|
||||
console.info(
|
||||
`Audio Volume [${HARDWARE.support.audioVolume ? HARDWARE.audio.device : unsupported}]:`,
|
||||
HARDWARE.support.audioVolume ? getAudioVolume() : unsupported,
|
||||
audioVolumeInfo,
|
||||
);
|
||||
const keyboardVisibility = `${getKeyboardVisibility()} (squeekboard)`;
|
||||
const keyboardVisibilityInfo = HARDWARE.support.keyboardVisibility ? keyboardVisibility : unsupported;
|
||||
console.info(
|
||||
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "dbus://sm/puri/OSK0" : unsupported}]:`,
|
||||
HARDWARE.support.keyboardVisibility ? `${getKeyboardVisibility()} (squeekboard)` : unsupported,
|
||||
keyboardVisibilityInfo,
|
||||
);
|
||||
process.stdout.write("\n");
|
||||
|
||||
@@ -138,9 +149,9 @@ const init = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Check for display changes
|
||||
// Monitor display changes (1s)
|
||||
setDisplayStatus("ON", () => {
|
||||
interval(update, 1000);
|
||||
interval(update, 1 * 1000);
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -154,23 +165,65 @@ const update = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display status has changed
|
||||
// Check if display status has changed
|
||||
if (HARDWARE.support.displayStatus) {
|
||||
const status = await readFile(`${HARDWARE.display.status.path}/dpms`, false);
|
||||
if (status !== null && status !== HARDWARE.display.status.value) {
|
||||
HARDWARE.display.status.value = status;
|
||||
console.info("Update Display Status:", getDisplayStatus());
|
||||
let displayStatusChanged = false;
|
||||
|
||||
// Use sysfs dpms path if available
|
||||
if (HARDWARE.display.status.path) {
|
||||
const power = await readFile(path.join(HARDWARE.display.status.path, "dpms"), false);
|
||||
const powerChanged = !!power && power !== HARDWARE.display.status.value.power;
|
||||
|
||||
// Update internal status values
|
||||
HARDWARE.display.status.value.power = power;
|
||||
displayStatusChanged |= powerChanged;
|
||||
}
|
||||
|
||||
// Use sysfs status path if available
|
||||
if (HARDWARE.display.status.path) {
|
||||
const connection = await readFile(path.join(HARDWARE.display.status.path, "status"), false);
|
||||
const connectionChanged = !!connection && connection !== HARDWARE.display.status.value.connection;
|
||||
|
||||
// Update internal status values
|
||||
HARDWARE.display.status.value.connection = connection;
|
||||
displayStatusChanged |= connectionChanged;
|
||||
}
|
||||
|
||||
// Emit display event if changed
|
||||
if (displayStatusChanged) {
|
||||
EVENTS.emit("updateDisplay");
|
||||
console.info("Update Display Status:", getDisplayStatus());
|
||||
}
|
||||
}
|
||||
|
||||
// Display brightness has changed
|
||||
// Check if display brightness has changed
|
||||
if (HARDWARE.support.displayBrightness) {
|
||||
const brightness = await readFile(`${HARDWARE.display.brightness.path}/brightness`, false);
|
||||
if (brightness !== null && brightness !== HARDWARE.display.brightness.value) {
|
||||
HARDWARE.display.brightness.value = brightness;
|
||||
console.info("Update Display Brightness:", getDisplayBrightness());
|
||||
let displayBrightnessChanged = false;
|
||||
|
||||
// Use cache brightness path if available
|
||||
if (HARDWARE.display.brightness.command) {
|
||||
const brightness = await readFile(path.join(APP.cache, "Brightness.vcp"), false);
|
||||
const brightnessChanged = !!brightness && brightness !== HARDWARE.display.brightness.value.brightness;
|
||||
|
||||
// Update internal brightness values
|
||||
HARDWARE.display.brightness.value.brightness = brightness;
|
||||
displayBrightnessChanged |= brightnessChanged;
|
||||
}
|
||||
|
||||
// Use sysfs brightness path if available
|
||||
if (HARDWARE.display.brightness.path) {
|
||||
const brightness = await readFile(path.join(HARDWARE.display.brightness.path, "brightness"), false);
|
||||
const brightnessChanged = !!brightness && brightness !== HARDWARE.display.brightness.value.brightness;
|
||||
|
||||
// Update internal brightness values
|
||||
HARDWARE.display.brightness.value.brightness = brightness;
|
||||
displayBrightnessChanged |= brightnessChanged;
|
||||
}
|
||||
|
||||
// Emit display event if changed
|
||||
if (displayBrightnessChanged) {
|
||||
EVENTS.emit("updateDisplay");
|
||||
console.info("Update Display Brightness:", getDisplayBrightness());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -233,22 +286,26 @@ const sessionDesktop = () => {
|
||||
* @returns {Object} Returns support object with boolean values.
|
||||
*/
|
||||
const checkSupport = () => {
|
||||
const battery = HARDWARE.battery.level;
|
||||
const status = HARDWARE.display.status;
|
||||
const brightness = HARDWARE.display.brightness;
|
||||
const audio = HARDWARE.audio.device;
|
||||
const keyboard = processRuns("squeekboard");
|
||||
const service = serviceRuns(APP.name);
|
||||
const sudo = sudoRights();
|
||||
const deb = APP.build.maker === "deb";
|
||||
const service = serviceRuns(APP.name);
|
||||
const keyboard = processRuns("squeekboard");
|
||||
const release = APP.build.maker === "deb";
|
||||
|
||||
const audioDevice = !!HARDWARE.audio.device;
|
||||
const batteryPath = !!HARDWARE.battery.level.path;
|
||||
const statusPath = !!HARDWARE.display.status.path;
|
||||
const statusCommand = !!HARDWARE.display.status.command;
|
||||
const brightnessPath = !!HARDWARE.display.brightness.path && !!HARDWARE.display.brightness.value.max;
|
||||
const brightnessCommand = !!HARDWARE.display.brightness.command && !!HARDWARE.display.brightness.value.max;
|
||||
|
||||
return {
|
||||
batteryLevel: !!battery.path,
|
||||
displayStatus: !!status.path && !!status.command,
|
||||
displayBrightness: sudo && !!status.command && !!brightness.path && !!brightness.max,
|
||||
batteryLevel: batteryPath,
|
||||
displayStatus: statusPath && statusCommand,
|
||||
displayBrightness: sudo && statusPath && statusCommand && (brightnessPath || brightnessCommand),
|
||||
keyboardVisibility: keyboard,
|
||||
audioVolume: !!audio,
|
||||
audioVolume: audioDevice,
|
||||
sudoRights: sudo,
|
||||
appUpdate: service && sudo && deb,
|
||||
appUpdate: service && sudo && release,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -484,7 +541,6 @@ const getDisplayStatusCommand = () => {
|
||||
return map.command;
|
||||
}
|
||||
}
|
||||
console.warn("No display command available");
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -573,23 +629,62 @@ const getDisplayBrightnessPath = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the maximum display brightness value using `/sys/class/backlight/.../max_brightness`.
|
||||
* Gets the available display brightness command checking for `ddcutil`.
|
||||
*
|
||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||
* @returns {string|null} The display brightness command or null if nothing was found.
|
||||
*/
|
||||
const getDisplayBrightnessMax = () => {
|
||||
if (!HARDWARE.display.brightness.path) {
|
||||
return null;
|
||||
}
|
||||
const max = readFile(`${HARDWARE.display.brightness.path}/max_brightness`);
|
||||
if (max) {
|
||||
return parseInt(max, 10);
|
||||
const getDisplayBrightnessCommand = () => {
|
||||
const type = HARDWARE.session.type;
|
||||
const desktop = HARDWARE.session.desktop;
|
||||
const mapping = {
|
||||
wayland: [{ command: "ddcutil", desktops: ["*"] }],
|
||||
x11: [{ command: "ddcutil", desktops: ["*"] }],
|
||||
}[type];
|
||||
for (const map of mapping || []) {
|
||||
if (sudoRights() && commandExists(map.command) && map.desktops.some((d) => d === "*" || desktop.includes(d))) {
|
||||
HARDWARE.display.brightness.path = null;
|
||||
switch (map.command) {
|
||||
case "ddcutil":
|
||||
const output = execSyncCommand("sudo", ["ddcutil", "capabilities"]);
|
||||
if (output && output.includes("Feature: 10")) {
|
||||
fs.writeFileSync(path.join(APP.cache, "Brightness.vcp"), "");
|
||||
return map.command;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return map.command;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display brightness level using `/sys/class/backlight/.../brightness`.
|
||||
* Gets the maximum display brightness value using `/sys/class/backlight/.../max_brightness` or `ddcutil getvcp 10`.
|
||||
*
|
||||
* @returns {number|null} The brightness maximum value or null if an error occurs.
|
||||
*/
|
||||
const getDisplayBrightnessMax = () => {
|
||||
switch (HARDWARE.display.brightness.command) {
|
||||
case "ddcutil":
|
||||
const output = execSyncCommand("sudo", ["ddcutil", "getvcp", "10", "--brief"]);
|
||||
const match = output ? output.match(/VCP 10 C (\d+) (\d+)/) : null;
|
||||
if (match) {
|
||||
return parseInt(match[2], 10);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (HARDWARE.display.brightness.path) {
|
||||
const max = readFile(path.join(HARDWARE.display.brightness.path, "max_brightness"));
|
||||
if (max) {
|
||||
return parseInt(max, 10);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current display brightness level using `/sys/class/backlight/.../brightness` or `ddcutil getvcp 10`.
|
||||
*
|
||||
* @returns {number|null} The brightness level as a percentage or null if an error occurs.
|
||||
*/
|
||||
@@ -597,16 +692,27 @@ const getDisplayBrightness = () => {
|
||||
if (!HARDWARE.support.displayBrightness) {
|
||||
return null;
|
||||
}
|
||||
const brightness = readFile(`${HARDWARE.display.brightness.path}/brightness`);
|
||||
if (brightness) {
|
||||
const max = HARDWARE.display.brightness.max || 1;
|
||||
return Math.max(1, Math.min(Math.round((parseInt(brightness, 10) / max) * 100), 100));
|
||||
switch (HARDWARE.display.brightness.command) {
|
||||
case "ddcutil":
|
||||
const output = execSyncCommand("sudo", ["ddcutil", "getvcp", "10", "--brief"]);
|
||||
const match = output ? output.match(/VCP 10 C (\d+) (\d+)/) : null;
|
||||
if (match) {
|
||||
return parseInt(match[1], 10);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (HARDWARE.display.brightness.path) {
|
||||
const brightness = readFile(path.join(HARDWARE.display.brightness.path, "brightness"));
|
||||
if (brightness) {
|
||||
const max = HARDWARE.display.brightness.value.max || 1;
|
||||
return Math.max(1, Math.min(Math.round((parseInt(brightness, 10) / max) * 100), 100));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the display brightness level using `/sys/class/backlight/.../brightness`.
|
||||
* Sets the display brightness level using `/sys/class/backlight/.../brightness` or `ddcutil setvcp 10`.
|
||||
*
|
||||
* This function takes a brightness value between 1 to 100 percent,
|
||||
* maps it to the proper range and writes it to the system.
|
||||
@@ -624,11 +730,23 @@ const setDisplayBrightness = (brightness, callback = null) => {
|
||||
if (typeof callback === "function") callback(null, "Invalid brightness");
|
||||
return;
|
||||
}
|
||||
const max = HARDWARE.display.brightness.max || 1;
|
||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
||||
proc.stdin.write(value.toString());
|
||||
proc.stdin.end();
|
||||
switch (HARDWARE.display.brightness.command) {
|
||||
case "ddcutil":
|
||||
execAsyncCommand("sudo", ["ddcutil", "setvcp", "10", `${brightness}`], (reply, error) => {
|
||||
if (!error) {
|
||||
fs.writeFileSync(path.join(APP.cache, "Brightness.vcp"), `${brightness}`);
|
||||
}
|
||||
if (typeof callback === "function") callback(reply, error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (HARDWARE.display.brightness.path) {
|
||||
const max = HARDWARE.display.brightness.value.max || 1;
|
||||
const value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||
const proc = execAsyncCommand("sudo", ["tee", path.join(HARDWARE.display.brightness.path, "brightness")], callback);
|
||||
proc.stdin.write(value.toString());
|
||||
proc.stdin.end();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1035,16 +1153,13 @@ const dbusCall = (path, method, values, callback = null) => {
|
||||
*/
|
||||
const readFile = (path, sync = true) => {
|
||||
if (!sync) {
|
||||
return new Promise((resolve) => {
|
||||
const reader = fsp.readFile(path, "utf8");
|
||||
reader.then((content) => {
|
||||
resolve(content.trim());
|
||||
});
|
||||
reader.catch((error) => {
|
||||
return fsp
|
||||
.readFile(path, "utf8")
|
||||
.then((content) => content.trim())
|
||||
.catch((error) => {
|
||||
console.error(`Read File ${path} Async:`, error.message);
|
||||
resolve(null);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
try {
|
||||
return fs.readFileSync(path, "utf8").trim();
|
||||
|
||||
Reference in New Issue
Block a user