diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8da9261 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "gnim-utils"] + path = src/utils + url = https://github.com/retrozinndev/gnim-utils.git diff --git a/src/modules/auth.ts b/src/modules/auth.ts index 42307fd..ec1cb2d 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -18,7 +18,7 @@ export class Auth extends PolkitAgent.Listener { constructor() { 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.#handle = this.register( diff --git a/src/modules/compositors/hyprland.ts b/src/modules/compositors/hyprland.ts new file mode 100644 index 0000000..c2a3e08 --- /dev/null +++ b/src/modules/compositors/hyprland.ts @@ -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 + } + ); + }); + } +} diff --git a/src/modules/compositors/index.ts b/src/modules/compositors/index.ts new file mode 100644 index 0000000..d5b6e44 --- /dev/null +++ b/src/modules/compositors/index.ts @@ -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)) + 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 = []; + protected _focusedClient: Client|null = null; + + @getter(Array) + get workspaces() { return this._workspaces; } + + @getter(gtype(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! :)`); + } + } +} diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 2852d6f..2a65287 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -1,16 +1,24 @@ import { createPoll } from "ags/time"; import { exec, execAsync } from "ags/process"; -import { Accessor, For, getScope, With } from "ags"; import { Astal, Gtk } from "ags/gtk4"; import { getSymbolicIcon } from "./apps"; import GLib from "gi://GLib?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 | number | string | boolean | null | undefined; export const decoder = new TextDecoder("utf-8"), encoder = new TextEncoder(); @@ -107,16 +115,6 @@ export function pickObjectKeys(obj: ObjT, keys: Array return finalObject; } -export function variableToBoolean(variable: any|Array|Accessor|any>): boolean|Accessor { - return (variable instanceof Accessor) ? - variable.as(v => Array.isArray(v) ? - (v as Array).length > 0 - : Boolean(v)) - : Array.isArray(variable) ? - variable.length > 0 - : Boolean(variable); -} - export function pathToURI(path: string): string { switch(true) { case (/^[/]/).test(path): @@ -130,44 +128,6 @@ export function pathToURI(path: string): string { return path; } -export function transform, RType = any>( - v: Accessor|ValueType, fn: (v: ValueType) => RType -): RType|Accessor { - - return (v instanceof Accessor) ? - v.as(fn) - : fn(v); -} - -export function transformWidget( - v: Accessor>|ValueType|Array, - fn: (v: ValueType, i?: Accessor|number) => JSX.Element -): WidgetNodeType { - - return (v instanceof Accessor) ? - Array.isArray(v.get()) ? - For({ - each: v as Accessor>, - children: (cval, i) => fn(cval, i) - }) - : With({ - value: v as Accessor, - children: fn - }) - : (Array.isArray(v) ? - v.map(val => fn(val)) - : fn(v)); -} - -export function filter( - v: Accessor>|Array, - fn: (v: ValueType, i: number, array: Array) => FilterReturnType -): Array|Accessor> { - 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 { execAsync([ "mkdir", "-p", dir ]); } @@ -215,154 +175,3 @@ export function addSliderMarksFromMinMax(slider: Astal.Slider, amountOfMarks: nu return slider; } - -/** initialize and sub class properties with accessors */ -export function construct(klass: Class, props: Record>): 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> = 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(accessor: Accessor, 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 { - const get = () => gobj ? gobj[prop] : defaultValue; - - return new Accessor( - 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, - prop: Prop, - defaultValue: Default -): Accessor { - 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( - () => 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; -} diff --git a/src/utils b/src/utils new file mode 160000 index 0000000..bedadf7 --- /dev/null +++ b/src/utils @@ -0,0 +1 @@ +Subproject commit bedadf7a2283310416cf186483fad852061cc3ee diff --git a/src/window/center-window/widgets/BigMedia.tsx b/src/window/center-window/widgets/BigMedia.tsx index 2155cf4..e610cae 100644 --- a/src/window/center-window/widgets/BigMedia.tsx +++ b/src/window/center-window/widgets/BigMedia.tsx @@ -95,8 +95,7 @@ class PlayerWidget extends Gtk.Box { { - if(type === undefined || type === null) - return; + if(type == null) return; if(!dragTimer) { dragTimer = setTimeout(() => diff --git a/src/window/osd/index.tsx b/src/window/osd/index.tsx index c90e154..fdf3269 100644 --- a/src/window/osd/index.tsx +++ b/src/window/osd/index.tsx @@ -3,7 +3,7 @@ import { createBinding, createState, With } from "ags"; import { Wireplumber } from "../../modules/volume"; import { Windows } from "../../windows"; 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 GLib from "gi://GLib?version=2.0"; @@ -13,7 +13,7 @@ import OSDMode from "./modules/osdmode"; export const OSDModes = { sink: new OSDMode({ - available: secureBinding(AstalWp.get_default(), "defaultSpeaker", false).as((sink) => + available: createBinding(AstalWp.get_default(), "defaultSpeaker").as((sink) => Boolean(sink)), icon: secureBaseBinding( createBinding(AstalWp.get_default(), "defaultSpeaker"),