summaryrefslogtreecommitdiff
path: root/lang/gjs/lib
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-10-15 01:26:32 +0200
committerAylur <[email protected]>2024-10-15 01:26:32 +0200
commit2f71cd4c08bb4514efe43533e6a5d03535204c29 (patch)
treefc991a12e159ad645187862c90f40731794d6e47 /lang/gjs/lib
parent9fab13452a26ed55c01047d4225f699f43bba20d (diff)
refactor lua and gjs lib
Diffstat (limited to 'lang/gjs/lib')
-rw-r--r--lang/gjs/lib/binding.ts89
-rw-r--r--lang/gjs/lib/file.ts45
-rw-r--r--lang/gjs/lib/gobject.ts180
-rw-r--r--lang/gjs/lib/process.ts68
-rw-r--r--lang/gjs/lib/time.ts13
-rw-r--r--lang/gjs/lib/variable.ts230
6 files changed, 625 insertions, 0 deletions
diff --git a/lang/gjs/lib/binding.ts b/lang/gjs/lib/binding.ts
new file mode 100644
index 0000000..95d905f
--- /dev/null
+++ b/lang/gjs/lib/binding.ts
@@ -0,0 +1,89 @@
+export const snakeify = (str: string) => str
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
+ .replaceAll("-", "_")
+ .toLowerCase()
+
+export const kebabify = (str: string) => str
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
+ .replaceAll("_", "-")
+ .toLowerCase()
+
+export interface Subscribable<T = unknown> {
+ subscribe(callback: (value: T) => void): () => void
+ get(): T
+ [key: string]: any
+}
+
+export interface Connectable {
+ connect(signal: string, callback: (...args: any[]) => unknown): number
+ disconnect(id: number): void
+ [key: string]: any
+}
+
+export default class Binding<Value> {
+ private transformFn = (v: any) => v
+
+ #emitter: Subscribable<Value> | Connectable
+ #prop?: string
+
+ static bind<
+ T extends Connectable,
+ P extends keyof T,
+ >(object: T, property: P): Binding<T[P]>
+
+ static bind<T>(object: Subscribable<T>): Binding<T>
+
+ static bind(emitter: Connectable | Subscribable, prop?: string) {
+ return new Binding(emitter, prop)
+ }
+
+ private constructor(emitter: Connectable | Subscribable<Value>, prop?: string) {
+ this.#emitter = emitter
+ this.#prop = prop && kebabify(prop)
+ }
+
+ toString() {
+ return `Binding<${this.#emitter}${this.#prop ? `, "${this.#prop}"` : ""}>`
+ }
+
+ as<T>(fn: (v: Value) => T): Binding<T> {
+ const bind = new Binding(this.#emitter, this.#prop)
+ bind.transformFn = (v: Value) => fn(this.transformFn(v))
+ return bind as unknown as Binding<T>
+ }
+
+ get(): Value {
+ if (typeof this.#emitter.get === "function")
+ return this.transformFn(this.#emitter.get())
+
+ if (typeof this.#prop === "string") {
+ const getter = `get_${snakeify(this.#prop)}`
+ if (typeof this.#emitter[getter] === "function")
+ return this.transformFn(this.#emitter[getter]())
+
+ return this.transformFn(this.#emitter[this.#prop])
+ }
+
+ throw Error("can not get value of binding")
+ }
+
+ subscribe(callback: (value: Value) => void): () => void {
+ if (typeof this.#emitter.subscribe === "function") {
+ return this.#emitter.subscribe(() => {
+ callback(this.get())
+ })
+ }
+ else if (typeof this.#emitter.connect === "function") {
+ const signal = `notify::${this.#prop}`
+ const id = this.#emitter.connect(signal, () => {
+ callback(this.get())
+ })
+ return () => {
+ (this.#emitter.disconnect as Connectable["disconnect"])(id)
+ }
+ }
+ throw Error(`${this.#emitter} is not bindable`)
+ }
+}
+
+export const { bind } = Binding
diff --git a/lang/gjs/lib/file.ts b/lang/gjs/lib/file.ts
new file mode 100644
index 0000000..7b9de3a
--- /dev/null
+++ b/lang/gjs/lib/file.ts
@@ -0,0 +1,45 @@
+import Astal from "gi://AstalIO"
+import Gio from "gi://Gio"
+
+export function readFile(path: string): string {
+ return Astal.read_file(path) || ""
+}
+
+export function readFileAsync(path: string): Promise<string> {
+ return new Promise((resolve, reject) => {
+ Astal.read_file_async(path, (_, res) => {
+ try {
+ resolve(Astal.read_file_finish(res) || "")
+ }
+ catch (error) {
+ reject(error)
+ }
+ })
+ })
+}
+
+export function writeFile(path: string, content: string): void {
+ Astal.write_file(path, content)
+}
+
+export function writeFileAsync(path: string, content: string): Promise<void> {
+ return new Promise((resolve, reject) => {
+ Astal.write_file_async(path, content, (_, res) => {
+ try {
+ resolve(Astal.write_file_finish(res))
+ }
+ catch (error) {
+ reject(error)
+ }
+ })
+ })
+}
+
+export function monitorFile(
+ path: string,
+ callback: (file: string, event: Gio.FileMonitorEvent) => void,
+): Gio.FileMonitor {
+ return Astal.monitor_file(path, (file: string, event: Gio.FileMonitorEvent) => {
+ callback(file, event)
+ })!
+}
diff --git a/lang/gjs/lib/gobject.ts b/lang/gjs/lib/gobject.ts
new file mode 100644
index 0000000..4740764
--- /dev/null
+++ b/lang/gjs/lib/gobject.ts
@@ -0,0 +1,180 @@
+export { default as GObject, default as default } from "gi://GObject"
+export { default as Gio } from "gi://Gio"
+export { default as GLib } from "gi://GLib"
+
+import GObject from "gi://GObject"
+const meta = Symbol("meta")
+
+const { ParamSpec, ParamFlags } = GObject
+
+const kebabify = (str: string) => str
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
+ .replaceAll("_", "-")
+ .toLowerCase()
+
+type SignalDeclaration = {
+ flags?: GObject.SignalFlags
+ accumulator?: GObject.AccumulatorType
+ return_type?: GObject.GType
+ param_types?: Array<GObject.GType>
+}
+
+type PropertyDeclaration =
+ | InstanceType<typeof GObject.ParamSpec>
+ | { $gtype: GObject.GType }
+ | typeof String
+ | typeof Number
+ | typeof Boolean
+ | typeof Object
+
+type GObjectConstructor = {
+ [meta]?: {
+ Properties?: { [key: string]: GObject.ParamSpec }
+ Signals?: { [key: string]: GObject.SignalDefinition }
+ }
+ new(...args: any[]): any
+}
+
+type MetaInfo = GObject.MetaInfo<never, Array<{ $gtype: GObject.GType }>, never>
+
+export function register(options: MetaInfo = {}) {
+ return function (cls: GObjectConstructor) {
+ GObject.registerClass({
+ Signals: { ...cls[meta]?.Signals },
+ Properties: { ...cls[meta]?.Properties },
+ ...options,
+ }, cls)
+ }
+}
+
+export function property(declaration: PropertyDeclaration = Object) {
+ return function (target: any, prop: any, desc?: PropertyDescriptor) {
+ target.constructor[meta] ??= {}
+ target.constructor[meta].Properties ??= {}
+
+ const name = kebabify(prop)
+
+ if (!desc) {
+ let value = defaultValue(declaration)
+
+ Object.defineProperty(target, prop, {
+ get() {
+ return value
+ },
+ set(v) {
+ if (v !== value) {
+ value = v
+ this.notify(name)
+ }
+ },
+ })
+
+ Object.defineProperty(target, `set_${name.replace("-", "_")}`, {
+ value: function (v: any) {
+ this[prop] = v
+ },
+ })
+
+ Object.defineProperty(target, `get_${name.replace("-", "_")}`, {
+ value: function () {
+ return this[prop]
+ },
+ })
+
+ target.constructor[meta].Properties[kebabify(prop)] = pspec(name, ParamFlags.READWRITE, declaration)
+ }
+
+ else {
+ let flags = 0
+ if (desc.get) flags |= ParamFlags.READABLE
+ if (desc.set) flags |= ParamFlags.WRITABLE
+
+ target.constructor[meta].Properties[kebabify(prop)] = pspec(name, flags, declaration)
+ }
+ }
+}
+
+export function signal(...params: Array<{ $gtype: GObject.GType } | typeof Object>):
+(target: any, signal: any, desc?: PropertyDescriptor) => void
+
+export function signal(declaration?: SignalDeclaration):
+(target: any, signal: any, desc?: PropertyDescriptor) => void
+
+export function signal(
+ declaration?: SignalDeclaration | { $gtype: GObject.GType } | typeof Object,
+ ...params: Array<{ $gtype: GObject.GType } | typeof Object>
+) {
+ return function (target: any, signal: any, desc?: PropertyDescriptor) {
+ target.constructor[meta] ??= {}
+ target.constructor[meta].Signals ??= {}
+
+ const name = kebabify(signal)
+
+ if (declaration || params.length > 0) {
+ // @ts-expect-error TODO: type assert
+ const arr = [declaration, ...params].map(v => v.$gtype)
+ target.constructor[meta].Signals[name] = {
+ param_types: arr,
+ }
+ }
+ else {
+ target.constructor[meta].Signals[name] = declaration
+ }
+
+ if (!desc) {
+ Object.defineProperty(target, signal, {
+ value: function (...args: any[]) {
+ this.emit(name, ...args)
+ },
+ })
+ }
+ else {
+ const og: ((...args: any[]) => void) = desc.value
+ desc.value = function (...args: any[]) {
+ // @ts-expect-error not typed
+ this.emit(name, ...args)
+ }
+ Object.defineProperty(target, `on_${name.replace("-", "_")}`, {
+ value: function (...args: any[]) {
+ return og(...args)
+ },
+ })
+ }
+ }
+}
+
+function pspec(name: string, flags: number, declaration: PropertyDeclaration) {
+ if (declaration instanceof ParamSpec)
+ return declaration
+
+ switch (declaration) {
+ case String:
+ return ParamSpec.string(name, "", "", flags, "")
+ case Number:
+ return ParamSpec.double(name, "", "", flags, -Number.MAX_VALUE, Number.MAX_VALUE, 0)
+ case Boolean:
+ return ParamSpec.boolean(name, "", "", flags, false)
+ case Object:
+ return ParamSpec.jsobject(name, "", "", flags)
+ default:
+ // @ts-expect-error misstyped
+ return ParamSpec.object(name, "", "", flags, declaration.$gtype)
+ }
+}
+
+function defaultValue(declaration: PropertyDeclaration) {
+ if (declaration instanceof ParamSpec)
+ return declaration.get_default_value()
+
+ switch (declaration) {
+ case String:
+ return "default-string"
+ case Number:
+ return 0
+ case Boolean:
+ return false
+ case Object:
+ default:
+ return null
+ }
+}
diff --git a/lang/gjs/lib/process.ts b/lang/gjs/lib/process.ts
new file mode 100644
index 0000000..2f7816b
--- /dev/null
+++ b/lang/gjs/lib/process.ts
@@ -0,0 +1,68 @@
+import Astal from "gi://AstalIO"
+
+type Args = {
+ cmd: string | string[]
+ out?: (stdout: string) => void
+ err?: (stderr: string) => void
+}
+
+export function subprocess(args: Args): Astal.Process
+
+export function subprocess(
+ cmd: string | string[],
+ onOut?: (stdout: string) => void,
+ onErr?: (stderr: string) => void,
+): Astal.Process
+
+export function subprocess(
+ argsOrCmd: Args | string | string[],
+ onOut: (stdout: string) => void = print,
+ onErr: (stderr: string) => void = printerr,
+) {
+ const args = Array.isArray(argsOrCmd) || typeof argsOrCmd === "string"
+ const { cmd, err, out } = {
+ cmd: args ? argsOrCmd : argsOrCmd.cmd,
+ err: args ? onErr : argsOrCmd.err || onErr,
+ out: args ? onOut : argsOrCmd.out || onOut,
+ }
+
+ const proc = Array.isArray(cmd)
+ ? Astal.Process.subprocessv(cmd)
+ : Astal.Process.subprocess(cmd)
+
+ proc.connect("stdout", (_, stdout: string) => out(stdout))
+ proc.connect("stderr", (_, stderr: string) => err(stderr))
+ return proc
+}
+
+/** @throws {GLib.Error} Throws stderr */
+export function exec(cmd: string | string[]) {
+ return Array.isArray(cmd)
+ ? Astal.Process.execv(cmd)
+ : Astal.Process.exec(cmd)
+}
+
+export function execAsync(cmd: string | string[]): Promise<string> {
+ return new Promise((resolve, reject) => {
+ if (Array.isArray(cmd)) {
+ Astal.Process.exec_asyncv(cmd, (_, res) => {
+ try {
+ resolve(Astal.Process.exec_asyncv_finish(res))
+ }
+ catch (error) {
+ reject(error)
+ }
+ })
+ }
+ else {
+ Astal.Process.exec_async(cmd, (_, res) => {
+ try {
+ resolve(Astal.Process.exec_finish(res))
+ }
+ catch (error) {
+ reject(error)
+ }
+ })
+ }
+ })
+}
diff --git a/lang/gjs/lib/time.ts b/lang/gjs/lib/time.ts
new file mode 100644
index 0000000..a7e1e61
--- /dev/null
+++ b/lang/gjs/lib/time.ts
@@ -0,0 +1,13 @@
+import Astal from "gi://AstalIO"
+
+export function interval(interval: number, callback?: () => void) {
+ return Astal.Time.interval(interval, () => void callback?.())
+}
+
+export function timeout(timeout: number, callback?: () => void) {
+ return Astal.Time.timeout(timeout, () => void callback?.())
+}
+
+export function idle(callback?: () => void) {
+ return Astal.Time.idle(() => void callback?.())
+}
diff --git a/lang/gjs/lib/variable.ts b/lang/gjs/lib/variable.ts
new file mode 100644
index 0000000..9b3d3d2
--- /dev/null
+++ b/lang/gjs/lib/variable.ts
@@ -0,0 +1,230 @@
+import Astal from "gi://AstalIO"
+import Binding, { type Connectable, type Subscribable } from "./binding.js"
+import { interval } from "./time.js"
+import { execAsync, subprocess } from "./process.js"
+
+class VariableWrapper<T> extends Function {
+ private variable!: Astal.VariableBase
+ private errHandler? = console.error
+
+ private _value: T
+ private _poll?: Astal.Time
+ private _watch?: Astal.Process
+
+ private pollInterval = 1000
+ private pollExec?: string[] | string
+ private pollTransform?: (stdout: string, prev: T) => T
+ private pollFn?: (prev: T) => T | Promise<T>
+
+ private watchTransform?: (stdout: string, prev: T) => T
+ private watchExec?: string[] | string
+
+ constructor(init: T) {
+ super()
+ this._value = init
+ this.variable = new Astal.VariableBase()
+ this.variable.connect("dropped", () => {
+ this.stopWatch()
+ this.stopPoll()
+ })
+ this.variable.connect("error", (_, err) => this.errHandler?.(err))
+ return new Proxy(this, {
+ apply: (target, _, args) => target._call(args[0]),
+ })
+ }
+
+ private _call<R = T>(transform?: (value: T) => R): Binding<R> {
+ const b = Binding.bind(this)
+ return transform ? b.as(transform) : b as unknown as Binding<R>
+ }
+
+ toString() {
+ return String(`Variable<${this.get()}>`)
+ }
+
+ get(): T { return this._value }
+ set(value: T) {
+ if (value !== this._value) {
+ this._value = value
+ this.variable.emit("changed")
+ }
+ }
+
+ startPoll() {
+ if (this._poll)
+ return
+
+ if (this.pollFn) {
+ this._poll = interval(this.pollInterval, () => {
+ const v = this.pollFn!(this.get())
+ if (v instanceof Promise) {
+ v.then(v => this.set(v))
+ .catch(err => this.variable.emit("error", err))
+ }
+ else {
+ this.set(v)
+ }
+ })
+ }
+ else if (this.pollExec) {
+ this._poll = interval(this.pollInterval, () => {
+ execAsync(this.pollExec!)
+ .then(v => this.set(this.pollTransform!(v, this.get())))
+ .catch(err => this.variable.emit("error", err))
+ })
+ }
+ }
+
+ startWatch() {
+ if (this._watch)
+ return
+
+ this._watch = subprocess({
+ cmd: this.watchExec!,
+ out: out => this.set(this.watchTransform!(out, this.get())),
+ err: err => this.variable.emit("error", err),
+ })
+ }
+
+ stopPoll() {
+ this._poll?.cancel()
+ delete this._poll
+ }
+
+ stopWatch() {
+ this._watch?.kill()
+ delete this._watch
+ }
+
+ isPolling() { return !!this._poll }
+ isWatching() { return !!this._watch }
+
+ drop() {
+ this.variable.emit("dropped")
+ }
+
+ onDropped(callback: () => void) {
+ this.variable.connect("dropped", callback)
+ return this as unknown as Variable<T>
+ }
+
+ onError(callback: (err: string) => void) {
+ delete this.errHandler
+ this.variable.connect("error", (_, err) => callback(err))
+ return this as unknown as Variable<T>
+ }
+
+ subscribe(callback: (value: T) => void) {
+ const id = this.variable.connect("changed", () => {
+ callback(this.get())
+ })
+ return () => this.variable.disconnect(id)
+ }
+
+ poll(
+ interval: number,
+ exec: string | string[],
+ transform?: (stdout: string, prev: T) => T
+ ): Variable<T>
+
+ poll(
+ interval: number,
+ callback: (prev: T) => T | Promise<T>
+ ): Variable<T>
+
+ poll(
+ interval: number,
+ exec: string | string[] | ((prev: T) => T | Promise<T>),
+ transform: (stdout: string, prev: T) => T = out => out as T,
+ ) {
+ this.stopPoll()
+ this.pollInterval = interval
+ this.pollTransform = transform
+ if (typeof exec === "function") {
+ this.pollFn = exec
+ delete this.pollExec
+ }
+ else {
+ this.pollExec = exec
+ delete this.pollFn
+ }
+ this.startPoll()
+ return this as unknown as Variable<T>
+ }
+
+ watch(
+ exec: string | string[],
+ transform: (stdout: string, prev: T) => T = out => out as T,
+ ) {
+ this.stopWatch()
+ this.watchExec = exec
+ this.watchTransform = transform
+ this.startWatch()
+ return this as unknown as Variable<T>
+ }
+
+ observe(
+ objs: Array<[obj: Connectable, signal: string]>,
+ callback: (...args: any[]) => T,
+ ): Variable<T>
+
+ observe(
+ obj: Connectable,
+ signal: string,
+ callback: (...args: any[]) => T,
+ ): Variable<T>
+
+ observe(
+ objs: Connectable | Array<[obj: Connectable, signal: string]>,
+ sigOrFn: string | ((obj: Connectable, ...args: any[]) => T),
+ callback?: (obj: Connectable, ...args: any[]) => T,
+ ) {
+ const f = typeof sigOrFn === "function" ? sigOrFn : callback ?? (() => this.get())
+ const set = (obj: Connectable, ...args: any[]) => this.set(f(obj, ...args))
+
+ if (Array.isArray(objs)) {
+ for (const obj of objs) {
+ const [o, s] = obj
+ const id = o.connect(s, set)
+ this.onDropped(() => o.disconnect(id))
+ }
+ }
+ else {
+ if (typeof sigOrFn === "string") {
+ const id = objs.connect(sigOrFn, set)
+ this.onDropped(() => objs.disconnect(id))
+ }
+ }
+
+ return this as unknown as Variable<T>
+ }
+
+ static derive<
+ const Deps extends Array<Subscribable<any>>,
+ Args extends {
+ [K in keyof Deps]: Deps[K] extends Subscribable<infer T> ? T : never
+ },
+ V = Args,
+ >(deps: Deps, fn: (...args: Args) => V = (...args) => args as unknown as V) {
+ const update = () => fn(...deps.map(d => d.get()) as Args)
+ const derived = new Variable(update())
+ const unsubs = deps.map(dep => dep.subscribe(() => derived.set(update())))
+ derived.onDropped(() => unsubs.map(unsub => unsub()))
+ return derived
+ }
+}
+
+export interface Variable<T> extends Omit<VariableWrapper<T>, "bind"> {
+ <R>(transform: (value: T) => R): Binding<R>
+ (): Binding<T>
+}
+
+export const Variable = new Proxy(VariableWrapper as any, {
+ apply: (_t, _a, args) => new VariableWrapper(args[0]),
+}) as {
+ derive: typeof VariableWrapper["derive"]
+ <T>(init: T): Variable<T>
+ new<T>(init: T): Variable<T>
+}
+
+export default Variable