🔧 chore(osd, modules/utils): better OSDMode implementation, introduce secureBinding

secureBinding is used to bind to an object's property, just like in createBinding, but with the possibility of adding a default value, for when errors occur, it returns that value
This commit is contained in:
retrozinndev
2025-09-27 18:18:37 -03:00
parent de3a1e2037
commit 8f73e01afb
3 changed files with 86 additions and 51 deletions
+27 -3
View File
@@ -6,7 +6,7 @@ import { getSymbolicIcon } from "./apps";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
import Gio from "gi://Gio?version=2.0"; import Gio from "gi://Gio?version=2.0";
import GObject from "gi://GObject?version=2.0"; import GObject from "ags/gobject";
/** gnim doesn't export this, so we need to do it again */ /** gnim doesn't export this, so we need to do it again */
@@ -230,8 +230,7 @@ export function construct<Class extends object>(klass: Class, props: Record<any,
subs.push(v.subscribe(() => { subs.push(v.subscribe(() => {
klass[k as keyof Class] = v.get() as Class[keyof Class]; klass[k as keyof Class] = v.get() as Class[keyof Class];
if(isGObject) if(isGObject)
klass.notify(k.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}` klass.notify(k.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`));
));
})); }));
klass[k as keyof Class] = v.get() as Class[keyof Class]; klass[k as keyof Class] = v.get() as Class[keyof Class];
@@ -277,3 +276,28 @@ export function createConnetions<
add(gobj, gobj.connect(sig as string, callback as never)); add(gobj, gobj.connect(sig as string, callback as never));
}); });
} }
export function secureBinding<
GObj extends GObject.Object,
Prop extends keyof GObj,
Returns extends unknown|undefined
>(
gobj: GObj,
prop: Prop,
defaultValue: Returns
): Accessor<GObj[Prop]|Returns> {
const get = () => gobj ? gobj[prop] : defaultValue;
return new Accessor<GObj[Prop]|Returns>(
get,
(notify) => {
const gobjectProp = (prop as string).replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`);
const id = gobj.connect(`notify::${gobjectProp}`, () => notify());
return () => {
try {
gobj.disconnect(id);
} catch(e) {}
}
}
);
}
+20 -48
View File
@@ -1,64 +1,35 @@
import { Astal, Gtk } from "ags/gtk4"; import { Astal, Gtk } from "ags/gtk4";
import { Accessor, createBinding, createState, With } from "ags"; import { createBinding, createState, With } from "ags";
import { Wireplumber } from "../../modules/volume"; import { Wireplumber } from "../../modules/volume";
import { Windows } from "../../windows"; import { Windows } from "../../windows";
import { Backlights } from "../../modules/backlight"; import { Backlights } from "../../modules/backlight";
import { construct, variableToBoolean } from "../../modules/utils"; import { secureBinding, variableToBoolean } from "../../modules/utils";
import GObject, { ParamSpec, property, register } from "ags/gobject";
import Pango from "gi://Pango?version=1.0"; import Pango from "gi://Pango?version=1.0";
import GLib from "gi://GLib?version=2.0"; import GLib from "gi://GLib?version=2.0";
import AstalWp from "gi://AstalWp?version=0.1";
import OSDMode from "./modules/osdmode";
@register({ GTypeName: "OSDMode" })
export class OSDMode extends GObject.Object {
readonly #subs: Array<() => void> = [];
@property(String)
icon: string = "image-missing";
@property(Number)
value: number = 0;
@property(Number)
max: number = 100;
@property(String as unknown as ParamSpec<string|null>)
text: string|null = null;
constructor(props: {
icon: string | Accessor<string>;
value: number | Accessor<number>;
max?: number | Accessor<number>;
text?: string | Accessor<string>;
}) {
super();
this.#subs = construct(this, props);
}
vfunc_dispose(): void {
this.#subs.forEach(s => s());
}
}
export const OSDModes = { export const OSDModes = {
SINK: () => new OSDMode({ sink: new OSDMode({
icon: createBinding(Wireplumber.getWireplumber().defaultSpeaker, "volumeIcon"), icon: secureBinding(AstalWp.get_default().defaultSpeaker, "volumeIcon",
value: createBinding(Wireplumber.getWireplumber().defaultSpeaker, "volume"), "audio-volume-high-symbolic"),
text: createBinding(Wireplumber.getWireplumber().defaultSpeaker, "description"), value: secureBinding(Wireplumber.getWireplumber().defaultSpeaker, "volume", 50),
text: secureBinding(Wireplumber.getWireplumber().defaultSpeaker, "description",
"Unknown Speaker"),
max: Wireplumber.getDefault().getMaxSinkVolume() / 100 max: Wireplumber.getDefault().getMaxSinkVolume() / 100
}), }),
BRIGHTNESS: () => Backlights.getDefault().available ? new OSDMode({ brightness: new OSDMode({
icon: "display-brightness-symbolic",
value: createBinding(Backlights.getDefault().default, "brightness"),
max: createBinding(Backlights.getDefault().default, "maxBrightness"),
text: createBinding(Backlights.getDefault().default, "name")
})
: new OSDMode({
icon: "display-brightness-symbolic", icon: "display-brightness-symbolic",
value: 100, value: secureBinding(Backlights.getDefault().default, "brightness", 100),
max: 100, max: secureBinding(Backlights.getDefault().default, "maxBrightness", 100),
text: "No Backlight found" text: secureBinding(Backlights.getDefault().default, "name", "Unknown Backlight"),
available: createBinding(Backlights.getDefault(), "available")
}) })
} }
const [osdMode, setOSDMode] = createState(OSDModes.SINK); const [osdMode, setOSDMode] = createState(OSDModes.sink);
let osdTimer: (GLib.Source|undefined), osdTimeout = 3500; let osdTimer: (GLib.Source|undefined), osdTimeout = 3500;
export const OSD = (mon: number) => export const OSD = (mon: number) =>
@@ -66,9 +37,10 @@ export const OSD = (mon: number) =>
anchor={Astal.WindowAnchor.BOTTOM} focusable={false} marginBottom={80} monitor={mon}> anchor={Astal.WindowAnchor.BOTTOM} focusable={false} marginBottom={80} monitor={mon}>
<Gtk.Box class={"osd"}> <Gtk.Box class={"osd"}>
<With value={osdMode(f => f)}> <With value={osdMode}>
{(_: () => OSDMode) => { {(mode: OSDMode) => {
const mode = _ as unknown as OSDMode; // for some reason, gnim runs this function :broken_heart: if(!mode.available) return;
return <Gtk.Box> return <Gtk.Box>
<Gtk.Image class={"icon"} iconName={ <Gtk.Image class={"icon"} iconName={
createBinding(mode, "icon") createBinding(mode, "icon")
+39
View File
@@ -0,0 +1,39 @@
import { Accessor } from "ags";
import { construct } from "../../../modules/utils";
import GObject, { gtype, property, register } from "ags/gobject";
@register({ GTypeName: "OSDMode" })
export default class OSDMode extends GObject.Object {
readonly #subs: Array<() => void> = [];
@property(String)
icon: string = "image-missing";
@property(Number)
value: number = 0;
@property(Number)
max: number = 100;
@property(gtype<string|null>(String))
text: string|null = null;
@property(Boolean)
available: boolean = true;
constructor(props: {
icon: string | Accessor<string>;
value: number | Accessor<number>;
max?: number | Accessor<number>;
text?: string | Accessor<string>;
available?: boolean | Accessor<boolean>;
}) {
super();
this.#subs = construct(this, props);
}
vfunc_dispose(): void {
this.#subs.forEach(s => s());
}
}