From e84f5f0bdf4fc27b5ffeb5e228ebfc1c21256a77 Mon Sep 17 00:00:00 2001 From: retrozinndev Date: Tue, 26 Aug 2025 16:16:58 -0300 Subject: [PATCH] :sparkles: feat: add backlight support --- src/modules/backlight.ts | 175 ++++++++++++++++++++++++++ src/modules/brightness.ts | 42 ------- src/widget/bar/FocusedClient.tsx | 5 +- src/widget/control-center/Sliders.tsx | 22 ++++ 4 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 src/modules/backlight.ts delete mode 100644 src/modules/brightness.ts diff --git a/src/modules/backlight.ts b/src/modules/backlight.ts new file mode 100644 index 0000000..8054bc5 --- /dev/null +++ b/src/modules/backlight.ts @@ -0,0 +1,175 @@ +import { monitorFile, readFile, writeFile } from "ags/file"; +import GObject, { getter, ParamSpec, register, setter, signal } from "ags/gobject"; +import Gio from "gi://Gio?version=2.0"; + + +export { Backlight }; +@register({ GTypeName: "Backlight" }) +class Backlight extends GObject.Object { + + private static _backlights: Array = []; + public static get backlights() { + return this._backlights; + }; + + private static default: Backlight; + + readonly #name: string; + #path: string; + #maxBrightness: number; + #brightness: number; + #available: boolean = true; + + @signal(Number) brightnessChanged(_: number): void {}; + + @getter(String) + get path() { return this.#path; } + + @getter(GObject.Object as unknown as ParamSpec) + get default() { return Backlight.default; } + + @getter(Boolean) + get isDefault() { return this.path === this.default?.path; } + + @getter(Number) + get brightness() { return this.#brightness; }; + @setter(Number) + set brightness(level: number) { + if(!this.writeBrightness(level)) return; + + this.#brightness = level; + this.notify("brightness"); + this.emit("brightness-changed", level); + } + + @getter(Number) + get maxBrightness() { return this.#maxBrightness;}; + + @getter(Boolean) + get available() { return this.#available; } + + + declare $signals: GObject.Object.SignalSignatures & { + "brightness-changed": (value: number) => void + }; + + public static setDefault(backlight: Backlight): void { + const prev = this.default; + this.default = backlight; + + prev && prev.notify("is-default"); + backlight.notify("is-default"); + this.backlights.forEach(bk => bk.notify("default")); + } + + public static amount(): number { + const dir = Gio.File.new_for_path(`/sys/class/backlight`); + let num: number = 0, + fileEnum: Gio.FileEnumerator; + + try { + fileEnum = dir.enumerate_children("standard::*", Gio.FileQueryInfoFlags.NONE, null); + + for(const _ of fileEnum) + num++; + } catch(_) { + return num; + } + + return num; + } + + public static scan(): Array { + const dir = Gio.File.new_for_path(`/sys/class/backlight`), + backlights: Array = []; + + let fileEnum: Gio.FileEnumerator; + + try { + fileEnum = dir.enumerate_children("standard::*", Gio.FileQueryInfoFlags.NONE, null); + for(const backlight of fileEnum) { + try { + backlights.push(new Backlight(backlight.get_name())); + } catch(_) {} + } + } catch(_) { + return []; + } + + Backlight._backlights = backlights; + return backlights; + } + + // intel_backlight is mostly the default on laptops + constructor(name: string = "intel_backlight") { + super(); + + // check if backlight exists + if(!Gio.File.new_for_path(`/sys/class/backlight/${name}/brightness`).query_exists(null)) { + this.#available = false; + this.notify("available"); + throw new Error(`Brightness: Couldn't find brightness for "${name}"`); + } + + this.#name = name; + this.#path = `/sys/class/backlight/${name}`; + this.notify("path"); + this.#maxBrightness = Number.parseInt(readFile(`${this.#path}/max_brightness`)); + this.notify("max-brightness"); + this.#brightness = Number.parseInt(readFile(`${this.#path}/brightness`)) + + + monitorFile(`/sys/class/backlight/${name}/brightness`, () => { + this.#brightness = this.readBrightness(); + this.notify("brightness"); + this.emit("brightness-changed", this.brightness); + }); + } + + private readBrightness(): number { + try { + const brightness = Number.parseInt(readFile(`${this.#path}/brightness`)); + return brightness; + } catch(e) { + console.error(`Backlight: An error occurred while reading brightness from "${this.#name}"`); + } + + return this.#brightness ?? this.#maxBrightness ?? 0; + } + + private writeBrightness(level: number): boolean { + try { + writeFile(`${this.#path}/brightness`, level.toString()); + return true; + } catch(e) { + console.error(`Backlight: Couldn't set brightness for "${this.#name}". Stderr: ${e}`); + } + + return false; + } + + public static getDefault(): Backlight|null { + if(this.default) + return this.default; + + if(this.backlights.length < 1) + this.scan(); + + const first = this.backlights[0]; + if(first) { + try { + this.default = first; + return this.default; + } catch(_) {} + } + + return null; + } + + public emit( + signal: Signal, + ...args: Parameters<(typeof this.$signals)[Signal]> + ): void { + super.emit(signal, ...args); + } +} diff --git a/src/modules/brightness.ts b/src/modules/brightness.ts deleted file mode 100644 index b98067d..0000000 --- a/src/modules/brightness.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { exec, execAsync, GObject, monitorFile, readFileAsync, register, signal } from "astal"; -import { Connectable } from "astal/binding"; - - -/** !!TODO!! Needs more work and testing - * I(retrozinndev) don't have a monitor that has software-controlled brightness :( - */ -@register({ GTypeName: "Brightness" }) -class Brightness extends GObject.Object implements Connectable { - private readonly backlight: string|undefined; - private max: number; - private brightness: number; - - @signal(Number) - declare brightnessChanged: (value: number) => void; - - constructor(backlightDevice?: string) { - super(); - this.backlight = backlightDevice || "intel_backlight"; - this.max = Number.parseInt(exec(`brightnessctl -d ${backlightDevice} max`)) - this.brightness = Number.parseInt(exec(`brightnessctl -d ${backlightDevice} get`)) - - readFileAsync(`/sys/class/backlight/${backlightDevice}/brightness`).catch(() => { - throw new Error(`Couldn't find backlight ${backlightDevice}`); - }); - - monitorFile(`/sys/class/backlight/${backlightDevice}/brightness`, async () => { - this.brightness = Number.parseInt(await execAsync(`brightnessctl -d ${backlightDevice} get`)); - this.max = Number.parseInt(await execAsync(`brightnessctl -d ${backlightDevice} max`)); - - this.emit("brightness-changed", this.brightness); - }); - } - - public setBrightness(newBrightness: number): void { - execAsync(`brightnessctl -d ${this.backlight} set ${newBrightness || this.brightness}`).catch(() => { - throw new Error(`Couldn't set brightness of backlight ${this.backlight}`); - }); - - this.emit("brightness-changed", newBrightness); - } -} diff --git a/src/widget/bar/FocusedClient.tsx b/src/widget/bar/FocusedClient.tsx index e9574d8..a459b35 100644 --- a/src/widget/bar/FocusedClient.tsx +++ b/src/widget/bar/FocusedClient.tsx @@ -10,13 +10,12 @@ import AstalHyprland from "gi://AstalHyprland"; const hyprland = AstalHyprland.get_default(); // Fix empty focused-client on opening a window on an empty workspace -hyprland.connect("client-added", () => hyprland.notify("focused-client")); +hyprland.connect("notify::clients", () => hyprland.notify("focused-client")); export const FocusedClient = () => { const focusedClient = createBinding(hyprland, "focusedClient"); - return + return {(focusedClient) => focusedClient?.class && diff --git a/src/widget/control-center/Sliders.tsx b/src/widget/control-center/Sliders.tsx index 4b18ace..99e73e6 100644 --- a/src/widget/control-center/Sliders.tsx +++ b/src/widget/control-center/Sliders.tsx @@ -4,6 +4,7 @@ import { Pages } from "./Pages"; import { PageSound } from "./pages/Sound"; import { PageMicrophone } from "./pages/Microphone"; import { createBinding, With } from "ags"; +import { Backlight } from "../../modules/backlight"; import AstalWp from "gi://AstalWp"; @@ -48,6 +49,27 @@ export function Sliders() { slidersPages?.toggle(PageMicrophone)} /> } + + {Backlight.getDefault() && + + {(bklight: Backlight) => bklight && + + { + bklight.brightness = bklight.maxBrightness + }} iconName={"display-brightness-symbolic"} + /> + + { + Backlight.getDefault()!.brightness = value + }} + /> + + } + + } + slidersPages = self} /> }