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 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 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(transform?: (value: T) => R): Binding { const b = Binding.bind(this) return transform ? b.as(transform) : b as unknown as Binding } 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 } onError(callback: (err: string) => void) { delete this.errHandler this.variable.connect("error", (_, err) => callback(err)) return this as unknown as Variable } 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 poll( interval: number, callback: (prev: T) => T | Promise ): Variable poll( interval: number, exec: string | string[] | ((prev: T) => T | Promise), 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 } 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 } observe( objs: Array<[obj: Connectable, signal: string]>, callback: (...args: any[]) => T, ): Variable observe( obj: Connectable, signal: string, callback: (...args: any[]) => T, ): Variable 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 } static derive< const Deps extends Array>, Args extends { [K in keyof Deps]: Deps[K] extends Subscribable ? 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 extends Omit, "bind"> { (transform: (value: T) => R): Binding (): Binding } export const Variable = new Proxy(VariableWrapper as any, { apply: (_t, _a, args) => new VariableWrapper(args[0]), }) as { derive: typeof VariableWrapper["derive"] (init: T): Variable new(init: T): Variable } export const { derive } = Variable export default Variable