🔧 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() {
|
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(
|
||||||
|
|||||||
@@ -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 { 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(() =>
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
Reference in New Issue
Block a user