feat: ddcutil display brightness support #101
This commit is contained in:
+172
-57
@@ -15,14 +15,14 @@ global.HARDWARE = global.HARDWARE || {
|
|||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
status: {
|
status: {
|
||||||
value: null,
|
|
||||||
path: null,
|
path: null,
|
||||||
command: null,
|
command: null,
|
||||||
|
value: {},
|
||||||
},
|
},
|
||||||
brightness: {
|
brightness: {
|
||||||
value: null,
|
|
||||||
path: null,
|
path: null,
|
||||||
max: null,
|
command: null,
|
||||||
|
value: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
keyboard: {
|
keyboard: {
|
||||||
@@ -52,7 +52,8 @@ const init = async () => {
|
|||||||
HARDWARE.display.status.path = getDisplayStatusPath();
|
HARDWARE.display.status.path = getDisplayStatusPath();
|
||||||
HARDWARE.display.status.command = getDisplayStatusCommand();
|
HARDWARE.display.status.command = getDisplayStatusCommand();
|
||||||
HARDWARE.display.brightness.path = getDisplayBrightnessPath();
|
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.audio.device = getAudioDevice();
|
||||||
HARDWARE.support = checkSupport();
|
HARDWARE.support = checkSupport();
|
||||||
HARDWARE.initialized = true;
|
HARDWARE.initialized = true;
|
||||||
@@ -86,25 +87,35 @@ const init = async () => {
|
|||||||
// Show hardware infos
|
// Show hardware infos
|
||||||
process.stdout.write("\n");
|
process.stdout.write("\n");
|
||||||
const unsupported = "unsupported";
|
const unsupported = "unsupported";
|
||||||
|
const batteryLevel = `${getBatteryLevel()} (sysfs)`;
|
||||||
|
const batteryLevelInfo = HARDWARE.support.batteryLevel ? batteryLevel : unsupported;
|
||||||
console.info(
|
console.info(
|
||||||
`Battery Level [${HARDWARE.support.batteryLevel ? HARDWARE.battery.level.path : unsupported}]:`,
|
`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(
|
console.info(
|
||||||
`Display Status [${HARDWARE.support.displayStatus ? HARDWARE.display.status.path : unsupported}]:`,
|
`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(
|
console.info(
|
||||||
`Display Brightness [${HARDWARE.support.displayBrightness ? HARDWARE.display.brightness.path : unsupported}]:`,
|
`Display Brightness [${HARDWARE.support.displayBrightness ? HARDWARE.display.brightness.path || "ddc://vcp/feature/0x10" : unsupported}]:`,
|
||||||
HARDWARE.support.displayBrightness ? getDisplayBrightness() : unsupported,
|
displayBrightnessInfo,
|
||||||
);
|
);
|
||||||
|
const audioVolume = `${getAudioVolume()} (pactl)`;
|
||||||
|
const audioVolumeInfo = HARDWARE.support.audioVolume ? audioVolume : unsupported;
|
||||||
console.info(
|
console.info(
|
||||||
`Audio Volume [${HARDWARE.support.audioVolume ? HARDWARE.audio.device : unsupported}]:`,
|
`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(
|
console.info(
|
||||||
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "dbus://sm/puri/OSK0" : unsupported}]:`,
|
`Keyboard Visibility [${HARDWARE.support.keyboardVisibility ? "dbus://sm/puri/OSK0" : unsupported}]:`,
|
||||||
HARDWARE.support.keyboardVisibility ? `${getKeyboardVisibility()} (squeekboard)` : unsupported,
|
keyboardVisibilityInfo,
|
||||||
);
|
);
|
||||||
process.stdout.write("\n");
|
process.stdout.write("\n");
|
||||||
|
|
||||||
@@ -138,9 +149,9 @@ const init = async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for display changes
|
// Monitor display changes (1s)
|
||||||
setDisplayStatus("ON", () => {
|
setDisplayStatus("ON", () => {
|
||||||
interval(update, 1000);
|
interval(update, 1 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -154,23 +165,65 @@ const update = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display status has changed
|
// Check if display status has changed
|
||||||
if (HARDWARE.support.displayStatus) {
|
if (HARDWARE.support.displayStatus) {
|
||||||
const status = await readFile(`${HARDWARE.display.status.path}/dpms`, false);
|
let displayStatusChanged = false;
|
||||||
if (status !== null && status !== HARDWARE.display.status.value) {
|
|
||||||
HARDWARE.display.status.value = status;
|
// Use sysfs dpms path if available
|
||||||
console.info("Update Display Status:", getDisplayStatus());
|
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");
|
EVENTS.emit("updateDisplay");
|
||||||
|
console.info("Update Display Status:", getDisplayStatus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display brightness has changed
|
// Check if display brightness has changed
|
||||||
if (HARDWARE.support.displayBrightness) {
|
if (HARDWARE.support.displayBrightness) {
|
||||||
const brightness = await readFile(`${HARDWARE.display.brightness.path}/brightness`, false);
|
let displayBrightnessChanged = false;
|
||||||
if (brightness !== null && brightness !== HARDWARE.display.brightness.value) {
|
|
||||||
HARDWARE.display.brightness.value = brightness;
|
// Use cache brightness path if available
|
||||||
console.info("Update Display Brightness:", getDisplayBrightness());
|
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");
|
EVENTS.emit("updateDisplay");
|
||||||
|
console.info("Update Display Brightness:", getDisplayBrightness());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -233,22 +286,26 @@ const sessionDesktop = () => {
|
|||||||
* @returns {Object} Returns support object with boolean values.
|
* @returns {Object} Returns support object with boolean values.
|
||||||
*/
|
*/
|
||||||
const checkSupport = () => {
|
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 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 {
|
return {
|
||||||
batteryLevel: !!battery.path,
|
batteryLevel: batteryPath,
|
||||||
displayStatus: !!status.path && !!status.command,
|
displayStatus: statusPath && statusCommand,
|
||||||
displayBrightness: sudo && !!status.command && !!brightness.path && !!brightness.max,
|
displayBrightness: sudo && statusPath && statusCommand && (brightnessPath || brightnessCommand),
|
||||||
keyboardVisibility: keyboard,
|
keyboardVisibility: keyboard,
|
||||||
audioVolume: !!audio,
|
audioVolume: audioDevice,
|
||||||
sudoRights: sudo,
|
sudoRights: sudo,
|
||||||
appUpdate: service && sudo && deb,
|
appUpdate: service && sudo && release,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -484,7 +541,6 @@ const getDisplayStatusCommand = () => {
|
|||||||
return map.command;
|
return map.command;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.warn("No display command available");
|
|
||||||
return null;
|
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 = () => {
|
const getDisplayBrightnessCommand = () => {
|
||||||
if (!HARDWARE.display.brightness.path) {
|
const type = HARDWARE.session.type;
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const max = readFile(`${HARDWARE.display.brightness.path}/max_brightness`);
|
|
||||||
if (max) {
|
|
||||||
return parseInt(max, 10);
|
|
||||||
}
|
}
|
||||||
return null;
|
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.
|
* @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) {
|
if (!HARDWARE.support.displayBrightness) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const brightness = readFile(`${HARDWARE.display.brightness.path}/brightness`);
|
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) {
|
if (brightness) {
|
||||||
const max = HARDWARE.display.brightness.max || 1;
|
const max = HARDWARE.display.brightness.value.max || 1;
|
||||||
return Math.max(1, Math.min(Math.round((parseInt(brightness, 10) / max) * 100), 100));
|
return Math.max(1, Math.min(Math.round((parseInt(brightness, 10) / max) * 100), 100));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return null;
|
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,
|
* This function takes a brightness value between 1 to 100 percent,
|
||||||
* maps it to the proper range and writes it to the system.
|
* 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");
|
if (typeof callback === "function") callback(null, "Invalid brightness");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const max = HARDWARE.display.brightness.max || 1;
|
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 value = Math.max(1, Math.min(Math.round((brightness / 100) * max), max));
|
||||||
const proc = execAsyncCommand("sudo", ["tee", `${HARDWARE.display.brightness.path}/brightness`], callback);
|
const proc = execAsyncCommand("sudo", ["tee", path.join(HARDWARE.display.brightness.path, "brightness")], callback);
|
||||||
proc.stdin.write(value.toString());
|
proc.stdin.write(value.toString());
|
||||||
proc.stdin.end();
|
proc.stdin.end();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1035,15 +1153,12 @@ const dbusCall = (path, method, values, callback = null) => {
|
|||||||
*/
|
*/
|
||||||
const readFile = (path, sync = true) => {
|
const readFile = (path, sync = true) => {
|
||||||
if (!sync) {
|
if (!sync) {
|
||||||
return new Promise((resolve) => {
|
return fsp
|
||||||
const reader = fsp.readFile(path, "utf8");
|
.readFile(path, "utf8")
|
||||||
reader.then((content) => {
|
.then((content) => content.trim())
|
||||||
resolve(content.trim());
|
.catch((error) => {
|
||||||
});
|
|
||||||
reader.catch((error) => {
|
|
||||||
console.error(`Read File ${path} Async:`, error.message);
|
console.error(`Read File ${path} Async:`, error.message);
|
||||||
resolve(null);
|
return null;
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user