🔧 chore: use retrozinndev/gnim-utils for extra function in utils module
also started developing the universal compositor implementation again
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "gnim-utils"]
|
||||
path = src/utils
|
||||
url = https://github.com/retrozinndev/gnim-utils.git
|
||||
+1
-1
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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> | JSX.Element | number | string | boolean | null | undefined;
|
||||
|
||||
export const decoder = new TextDecoder("utf-8"),
|
||||
encoder = new TextEncoder();
|
||||
@@ -107,16 +115,6 @@ export function pickObjectKeys<ObjT = object>(obj: ObjT, keys: Array<keyof ObjT>
|
||||
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 {
|
||||
switch(true) {
|
||||
case (/^[/]/).test(path):
|
||||
@@ -130,44 +128,6 @@ export function pathToURI(path: string): string {
|
||||
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 {
|
||||
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<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)}
|
||||
value={createBinding(player, "position").as(Math.floor)}
|
||||
onChangeValue={(_, type, value) => {
|
||||
if(type === undefined || type === null)
|
||||
return;
|
||||
if(type == null) return;
|
||||
|
||||
if(!dragTimer) {
|
||||
dragTimer = setTimeout(() =>
|
||||
|
||||
@@ -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<AstalWp.Endpoint>(
|
||||
createBinding(AstalWp.get_default(), "defaultSpeaker"),
|
||||
|
||||
Reference in New Issue
Block a user