🔧 chore: use retrozinndev/gnim-utils for extra function in utils module

also started developing the universal compositor implementation again
This commit is contained in:
retrozinndev
2025-10-05 22:41:09 -03:00
parent b835de79ef
commit 76f90c4cc3
8 changed files with 218 additions and 208 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "gnim-utils"]
path = src/utils
url = https://github.com/retrozinndev/gnim-utils.git
+1 -1
View File
@@ -18,7 +18,7 @@ export class Auth extends PolkitAgent.Listener {
constructor() { constructor() {
super(); super();
this.#subject = Polkit.UnixSession.new(""); // TODO find how to get session id (for some reason, i can't find a session ID that works) this.#subject = Polkit.UnixSession.new("");
this.#pam = new AstalAuth.Pam(); this.#pam = new AstalAuth.Pam();
this.#handle = this.register( this.#handle = this.register(
+34
View File
@@ -0,0 +1,34 @@
import { register } from "ags/gobject";
import { Compositors } from ".";
import { createRoot } from "ags";
import { createScopedConnection } from "../utils";
import AstalHyprland from "gi://AstalHyprland";
@register({ GTypeName: "CompositorHyprland" })
export class CompositorHyprland extends Compositors.Compositor {
hyprland: AstalHyprland.Hyprland;
constructor() {
super();
try {
this.hyprland = AstalHyprland.get_default();
} catch(e) {
throw new Error(`Couldn't initialize CompositorHyprland: ${e}`);
}
createRoot(() => {
createScopedConnection(
this.hyprland, "workspace-added", (hws) => {
// check workspace existance
if(this._workspaces.filter(w => w.id === hws.id)[0])
return;
// TODO
}
);
});
}
}
+164
View File
@@ -0,0 +1,164 @@
import { CompositorHyprland } from "./hyprland";
import GObject, { getter, gtype, register } from "ags/gobject";
import GLib from "gi://GLib?version=2.0";
/** WIP modular implementation of a system that supports implementing
* a variety of Wayland Compositors
* @todo implement more general compositor info + a lot of stuff
* */
export namespace Compositors {
let compositor: Compositor|null = null;
@register({ GTypeName: "CompositorMonitor" })
export class Monitor extends GObject.Object {
#width: number;
#height: number;
#scaling: number;
@getter(Number)
get width() { return this.#width; }
@getter(Number)
get height() { return this.#height; }
@getter(Number)
get scaling() { return this.#scaling; }
constructor(width: number, height: number, scaling: number = 1) {
super();
this.#width = width;
this.#height = height;
this.#scaling = scaling;
}
}
@register({ GTypeName: "CompositorWorkspace" })
export class Workspace extends GObject.Object {
#id: number;
#monitor: Monitor;
@getter(Number)
get id() { return this.#id; }
@getter(Monitor)
get monitor() { return this.#monitor; }
constructor(monitor: Monitor, id: number = 0) {
super();
this.#monitor = monitor;
this.notify("monitor");
this.#id = id;
}
}
@register({ GTypeName: "CompositorClient" })
export class Client extends GObject.Object {
readonly #address: string|null = null;
#initialClass: string;
#class: string;
#title: string = "";
#mapped: boolean = true;
#position: [number, number] = [0, 0];
#xwayland: boolean = false;
@getter(gtype<string|null>(String))
get address() { return this.#address; }
@getter(String)
get title() { return this.#title; }
@getter(String)
get class() { return this.#class; }
@getter(String)
get initialClass() { return this.#initialClass; }
@getter(gtype<[number, number]>(Array))
get position() { return this.#position; }
@getter(Boolean)
get xwayland() { return this.#xwayland; }
@getter(Boolean)
get mapped() { return this.#mapped; }
constructor(props: {
address?: string;
title?: string;
mapped?: boolean;
class: string;
initialClass?: string;
/** [x, y] */
position?: [number, number];
}) {
super();
this.#class = props.class;
if(props.title !== undefined)
this.#title = props.title;
if(props.mapped !== undefined)
this.#mapped = props.mapped;
if(props.address !== undefined)
this.#address = props.address;
if(props.position !== undefined)
this.#position = props.position;
if(props.initialClass !== undefined)
this.#initialClass = props.initialClass;
else
this.#initialClass = props.class;
}
}
@register({ GTypeName: "Compositor" })
export class Compositor extends GObject.Object {
protected _workspaces: Array<Workspace> = [];
protected _focusedClient: Client|null = null;
@getter(Array<Workspace>)
get workspaces() { return this._workspaces; }
@getter(gtype<Client|null>(Client))
get focusedClient() { return this._focusedClient; }
constructor() {
super();
}
};
export function getDefault(): Compositor {
if(!compositor)
throw new Error("Compositors haven't been initialized correctly, please call `Compositors.init()` before calling any method in `Compositors`");
return compositor;
}
/** Uses the XDG_CURRENT_DESKTOP variable to detect running compositor's name.
* ---
* @returns running wayland compositor's name (lowercase) or `undefined` if variable's not set */
export function getName(): string|undefined {
return GLib.getenv("XDG_CURRENT_DESKTOP")?.toLowerCase() ?? undefined;
}
/** initialize colorshell's wayland compositor implementation abstraction.
* when called, and if it's implemented, sets the default compositor to an equivalent implementation for the current desktop(checks from XDG_CURRENT_DESKTOP) */
export function init(): void {
switch(Compositors.getName()) {
case "hyprland":
compositor = new CompositorHyprland();
break;
default:
console.error(`This compositor(${Compositors.getName()}) is not yet implemented to colorshell. Please contribute by implementing it if you can! :)`);
}
}
}
+12 -203
View File
@@ -1,16 +1,24 @@
import { createPoll } from "ags/time"; import { createPoll } from "ags/time";
import { exec, execAsync } from "ags/process"; import { exec, execAsync } from "ags/process";
import { Accessor, For, getScope, With } from "ags";
import { Astal, Gtk } from "ags/gtk4"; import { Astal, Gtk } from "ags/gtk4";
import { getSymbolicIcon } from "./apps"; 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";
export {
type JSXNode as WidgetNodeType,
toBoolean as variableToBoolean,
construct,
transform,
transformWidget,
createSubscription,
createAccessorBinding as baseBinding,
createScopedConnection,
createSecureBinding as secureBinding,
createSecureAccessorBinding as secureBaseBinding,
} from "../utils";
/** gnim doesn't export this, so we need to do it again */
export type WidgetNodeType = Array<JSX.Element> | JSX.Element | number | string | boolean | null | undefined;
export const decoder = new TextDecoder("utf-8"), export const decoder = new TextDecoder("utf-8"),
encoder = new TextEncoder(); encoder = new TextEncoder();
@@ -107,16 +115,6 @@ export function pickObjectKeys<ObjT = object>(obj: ObjT, keys: Array<keyof ObjT>
return finalObject; return finalObject;
} }
export function variableToBoolean(variable: any|Array<any>|Accessor<Array<any>|any>): boolean|Accessor<boolean> {
return (variable instanceof Accessor) ?
variable.as(v => Array.isArray(v) ?
(v as Array<any>).length > 0
: Boolean(v))
: Array.isArray(variable) ?
variable.length > 0
: Boolean(variable);
}
export function pathToURI(path: string): string { export function pathToURI(path: string): string {
switch(true) { switch(true) {
case (/^[/]/).test(path): case (/^[/]/).test(path):
@@ -130,44 +128,6 @@ export function pathToURI(path: string): string {
return path; return path;
} }
export function transform<ValueType = any|Array<any>, RType = any>(
v: Accessor<ValueType>|ValueType, fn: (v: ValueType) => RType
): RType|Accessor<RType> {
return (v instanceof Accessor) ?
v.as(fn)
: fn(v);
}
export function transformWidget<ValueType = unknown>(
v: Accessor<ValueType|Array<ValueType>>|ValueType|Array<ValueType>,
fn: (v: ValueType, i?: Accessor<number>|number) => JSX.Element
): WidgetNodeType {
return (v instanceof Accessor) ?
Array.isArray(v.get()) ?
For({
each: v as Accessor<Array<ValueType>>,
children: (cval, i) => fn(cval, i)
})
: With({
value: v as Accessor<ValueType>,
children: fn
})
: (Array.isArray(v) ?
v.map(val => fn(val))
: fn(v));
}
export function filter<ValueType = unknown, FilterReturnType = unknown>(
v: Accessor<Array<ValueType>>|Array<ValueType>,
fn: (v: ValueType, i: number, array: Array<ValueType>) => FilterReturnType
): Array<ValueType>|Accessor<Array<ValueType>> {
return ((v instanceof Accessor) ?
v(v => v.filter((it, i, arr) => fn(it, i, arr)))
: v.filter((it, i, arr) => fn(it, i, arr)));
}
export function makeDirectory(dir: string): void { export function makeDirectory(dir: string): void {
execAsync([ "mkdir", "-p", dir ]); execAsync([ "mkdir", "-p", dir ]);
} }
@@ -215,154 +175,3 @@ export function addSliderMarksFromMinMax(slider: Astal.Slider, amountOfMarks: nu
return slider; return slider;
} }
/** initialize and sub class properties with accessors */
export function construct<Class extends object>(klass: Class, props: Record<any, any|Accessor<any>>): Array<() => void> {
const subs: Array<() => void> = [];
const isGObject = klass instanceof GObject.Object;
Object.keys(props).forEach(k => {
const v = props[k as keyof typeof props];
if(v === undefined) return;
if(v instanceof Accessor) {
subs.push(v.subscribe(() => {
klass[k as keyof Class] = v.get() as Class[keyof Class];
if(isGObject)
klass.notify(k.replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`));
}));
klass[k as keyof Class] = v.get() as Class[keyof Class];
return;
}
klass[k as keyof Class] = v as Class[keyof Class];
});
return subs;
}
/** open connections to gobjects that are closed when the scope
* is disposed
* @experimental types don't work correctly yet
* */
export function createConnetions<
GObj extends GObject.Object,
Signals extends GObj["$signals"],
Signal extends keyof Signals,
Callback extends Signals[Signal]
>(...conns: Array<[GObj, Signal, Callback]>): void {
const scope = getScope();
const connections: Map<GObj, Array<number>> = new Map();
scope.onCleanup(() => connections.forEach((ids, gobj) =>
ids.forEach(id => gobj.disconnect(id))
));
function add(gobj: GObj, id: number): void {
if(connections.has(gobj)) {
connections.get(gobj)!.push(id);
return;
}
connections.set(gobj, [id]);
}
conns.forEach(([gobj, sig, callback]) => {
// type stuff
add(gobj, gobj.connect(sig as string, callback as never));
});
}
export function createSubscription<T = any>(accessor: Accessor<T>, callback: () => void): void {
const scope = getScope();
const unsub = accessor.subscribe(callback);
scope.onCleanup(unsub);
}
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) {}
}
}
);
}
/** securely bind to a property of a gobject accessor
* use this to securely bind to a property of a constantly
* updated variable that points to a gobject.
*
* It follows the same idea from secureBinding, it allows setting
* a default value to return when the base gobject is null.
*
* @param baseObject a binding to the constantly updated property
* that points to the gobject
* @param prop the property to bind
* @param defaultValue the value to return when the baseObject is
* null/undefined
*
* @returns a bind to the specified property of the constantly-updated
* object or the default value.
* */
export function secureBaseBinding<
T extends GObject.Object = GObject.Object,
Prop extends keyof T = keyof T,
Default = any
>(
baseObject: Accessor<T>,
prop: Prop,
defaultValue: Default
): Accessor<T[Prop]|Default> {
let gobj: T|undefined = baseObject.get();
let notify: () => void;
const baseSub = baseObject.subscribe(() => {
const newBase = baseObject.get();
if(!newBase) {
gobj = undefined;
notify!();
return;
}
});
const accessor = new Accessor<T[Prop]|Default>(
() => gobj ? gobj[prop] : defaultValue,
(notifyFun) => {
notify = notifyFun;
const id = gobj?.connect(
`notify::${(prop as string).replace(/[A-Z]/g, (s) => `-${s.toLowerCase()}`)}`,
() => notify()
);
return () => {
id && gobj?.disconnect(id);
baseSub();
}
}
);
return accessor;
}
Submodule
+1
Submodule src/utils added at bedadf7a22
@@ -95,8 +95,7 @@ class PlayerWidget extends Gtk.Box {
<Astal.Slider hexpand max={createBinding(player, "length").as(Math.floor)} <Astal.Slider hexpand max={createBinding(player, "length").as(Math.floor)}
value={createBinding(player, "position").as(Math.floor)} value={createBinding(player, "position").as(Math.floor)}
onChangeValue={(_, type, value) => { onChangeValue={(_, type, value) => {
if(type === undefined || type === null) if(type == null) return;
return;
if(!dragTimer) { if(!dragTimer) {
dragTimer = setTimeout(() => dragTimer = setTimeout(() =>
+2 -2
View File
@@ -3,7 +3,7 @@ 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 { secureBaseBinding, secureBinding, variableToBoolean } from "../../modules/utils"; import { secureBaseBinding, variableToBoolean } from "../../modules/utils";
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";
@@ -13,7 +13,7 @@ import OSDMode from "./modules/osdmode";
export const OSDModes = { export const OSDModes = {
sink: new OSDMode({ sink: new OSDMode({
available: secureBinding(AstalWp.get_default(), "defaultSpeaker", false).as((sink) => available: createBinding(AstalWp.get_default(), "defaultSpeaker").as((sink) =>
Boolean(sink)), Boolean(sink)),
icon: secureBaseBinding<AstalWp.Endpoint>( icon: secureBaseBinding<AstalWp.Endpoint>(
createBinding(AstalWp.get_default(), "defaultSpeaker"), createBinding(AstalWp.get_default(), "defaultSpeaker"),