diff options
Diffstat (limited to 'js/src')
-rw-r--r-- | js/src/application.ts | 28 | ||||
-rw-r--r-- | js/src/astalify.ts | 213 | ||||
-rw-r--r-- | js/src/binding.ts | 78 | ||||
-rw-r--r-- | js/src/process.ts | 83 | ||||
-rw-r--r-- | js/src/time.ts | 27 | ||||
-rw-r--r-- | js/src/variable.ts | 250 |
6 files changed, 679 insertions, 0 deletions
diff --git a/js/src/application.ts b/js/src/application.ts new file mode 100644 index 0000000..10f840e --- /dev/null +++ b/js/src/application.ts @@ -0,0 +1,28 @@ +export type RequestHandler = { + (request: string, res: (response: string) => void): void +} + +export type Config = Partial<{ + instanceName: string + gtkTheme: string + iconTheme: string + cursorTheme: string + css: string + requestHandler: RequestHandler + hold: boolean +}> + +export function runJS(body: string): Promise<any> { + return new Promise((res, rej) => { + try { + const fn = Function(`return (async function() { + ${body.includes(";") ? body : `return ${body};`} + })`) + fn()() + .then(res) + .catch(rej) + } catch (error) { + rej(error) + } + }) +} diff --git a/js/src/astalify.ts b/js/src/astalify.ts new file mode 100644 index 0000000..bc24db6 --- /dev/null +++ b/js/src/astalify.ts @@ -0,0 +1,213 @@ +import Binding, { kebabify, type Connectable, type Subscribable } from "./binding.js" + +export type Widget<C extends { new(...args: any): any }> = InstanceType<C> & { + className: string + css: string + cursor: Cursor + hook( + object: Connectable, + signal: string, + callback: (self: Widget<C>, ...args: any[]) => void, + ): Widget<C> + hook( + object: Subscribable, + callback: (self: Widget<C>, ...args: any[]) => void, + ): Widget<C> +} + +export default function <G extends { Bin: any, Container: any, Widget: any }>( + Gtk: G, + setter: (prop: string) => `set${string}`, + Astal: { + cssSetter: (w: any, css: string) => void, + cssGetter: (w: any) => string | null, + classSetter: (w: any, name: string[]) => void, + classGetter: (w: any) => string[], + cursorSetter: (w: any, cursor: string) => void, + cursorGetter: (w: any) => string | null, + }, +) { + function hook( + self: any, + object: Connectable | Subscribable, + signalOrCallback: string | ((self: any, ...args: any[]) => void), + callback?: (self: any, ...args: any[]) => void, + ) { + if (typeof object.connect === "function" && callback) { + const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { + callback(self, ...args) + }) + self.connect("destroy", () => { + (object.disconnect as Connectable["disconnect"])(id) + }) + } + + else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { + const unsub = object.subscribe((...args: unknown[]) => { + signalOrCallback(self, ...args) + }) + self.connect("destroy", unsub) + } + + return self + } + + function setChild(parent: any, child: any) { + if (parent instanceof Gtk.Bin) { + if (parent.get_child()) + parent.remove(parent.get_child()!) + } + if (parent instanceof Gtk.Container) + parent.add(child) + } + + function ctor(self: any, config: any, ...children: any[]) { + const { setup, child, ...props } = config + props.visible ??= true + + const bindings = Object.keys(props).reduce((acc: any, prop) => { + if (props[prop] instanceof Binding) { + const bind = [prop, props[prop]] + prop === "child" + ? setChild(self, props[prop].get()) + : self[setter(prop)](props[prop].get()) + + delete props[prop] + return [...acc, bind] + } + return acc + }, []) + + const onHandlers = Object.keys(props).reduce((acc: any, key) => { + if (key.startsWith("on")) { + const sig = kebabify(key).split("-").slice(1).join("-") + const handler = [sig, props[key]] + delete props[key] + return [...acc, handler] + } + return acc + }, []) + + Object.assign(self, props) + Object.assign(self, { + hook(obj: any, sig: any, callback: any) { + return hook(self, obj, sig, callback) + }, + }) + + if (child instanceof Binding) { + setChild(self, child.get()) + self.connect("destroy", child.subscribe(v => { + setChild(self, v) + })) + } else if (self instanceof Gtk.Container && child instanceof Gtk.Widget) { + self.add(child) + } + + for (const [signal, callback] of onHandlers) + self.connect(signal, callback) + + if (self instanceof Gtk.Container && children) { + for (const child of children) + self.add(child) + } + + for (const [prop, bind] of bindings) { + self.connect("destroy", bind.subscribe((v: any) => { + self[`${setter(prop)}`](v) + })) + } + + setup?.(self) + return self + } + + return function proxify< + C extends { new(...args: any[]): any }, + >(klass: C) { + Object.defineProperty(klass.prototype, "className", { + get() { return Astal.classGetter(this).join(" ") }, + set(v) { Astal.classSetter(this, v.split(/\s+/)) }, + }) + + Object.defineProperty(klass.prototype, "css", { + get() { return Astal.cssGetter(this) }, + set(v) { Astal.cssSetter(this, v) }, + }) + + Object.defineProperty(klass.prototype, "cursor", { + get() { return Astal.cursorGetter(this) }, + set(v) { Astal.cursorSetter(this, v) }, + }) + + const proxy = new Proxy(klass, { + construct(_, [conf, ...children]) { + const self = new klass + return ctor(self, conf, ...children) + }, + apply(_t, _a, [conf, ...children]) { + const self = new klass + return ctor(self, conf, ...children) + }, + }) + + return proxy + } +} + + +type BindableProps<T> = { + [K in keyof T]: Binding<NonNullable<T[K]>> | T[K]; +} + +export type ConstructProps< + Self extends { new(...args: any[]): any }, + Props = unknown, + Signals = unknown +> = { + [Key in `on${string}`]: (self: Widget<Self>) => unknown +} & Partial<Signals> & BindableProps<Props & { + className?: string + css?: string + cursor?: string +}> & { + onDestroy?: (self: Widget<Self>) => unknown + onDraw?: (self: Widget<Self>) => unknown + setup?: (self: Widget<Self>) => void +} + +type Cursor = + | "default" + | "help" + | "pointer" + | "context-menu" + | "progress" + | "wait" + | "cell" + | "crosshair" + | "text" + | "vertical-text" + | "alias" + | "copy" + | "no-drop" + | "move" + | "not-allowed" + | "grab" + | "grabbing" + | "all-scroll" + | "col-resize" + | "row-resize" + | "n-resize" + | "e-resize" + | "s-resize" + | "w-resize" + | "ne-resize" + | "nw-resize" + | "sw-resize" + | "se-resize" + | "ew-resize" + | "ns-resize" + | "nesw-resize" + | "nwse-resize" + | "zoom-in" + | "zoom-out" diff --git a/js/src/binding.ts b/js/src/binding.ts new file mode 100644 index 0000000..a8b6d55 --- /dev/null +++ b/js/src/binding.ts @@ -0,0 +1,78 @@ +export const kebabify = (str: string) => str + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replaceAll("_", "-") + .toLowerCase() + +export interface Subscribable<T = unknown> { + subscribe(callback: () => 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 emitter: Subscribable<Value> | Connectable + private prop?: string + private transformFn = (v: any) => v + + 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") + 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/js/src/process.ts b/js/src/process.ts new file mode 100644 index 0000000..537aba8 --- /dev/null +++ b/js/src/process.ts @@ -0,0 +1,83 @@ +type Proc = { + connect(sig: "stdout" | "stderr", fn: (_: any, out: string) => void): number +} + +type Config<P extends Proc> = { + defaultOut(stdout: string): void + defaultErr(stdout: string): void + subprocess(cmd: string): P + subprocessv(cmd: string[]): P + exec(cmd: string): string | null + execv(cmd: string[]): string | null + execAsync(cmd: string): P + execAsyncv(cmd: string[]): P +} + +type Args<Out = void, Err = void> = { + cmd: string | string[], + out?: (stdout: string) => Out, + err?: (stderr: string) => Err, +} + +export default function <P extends Proc>(config: Config<P>) { + function args<O, E>(argsOrCmd: Args | string | string[], onOut: O, onErr: E) { + const params = Array.isArray(argsOrCmd) || typeof argsOrCmd === "string" + return { + cmd: params ? argsOrCmd : argsOrCmd.cmd, + err: params ? onErr : argsOrCmd.err || onErr, + out: params ? onOut : argsOrCmd.out || onOut, + } + } + + function subprocess(args: Args): P + function subprocess( + cmd: string | string[], + onOut?: (stdout: string) => void, + onErr?: (stderr: string) => void, + ): P + function subprocess( + argsOrCmd: Args | string | string[], + onOut: (stdout: string) => void = config.defaultOut, + onErr: (stderr: string) => void = config.defaultErr, + ) { + const { cmd, err, out } = args(argsOrCmd, onOut, onErr) + const proc = Array.isArray(cmd) + ? config.subprocessv(cmd) + : config.subprocess(cmd) + + proc.connect("stdout", (_, stdout: string) => out(stdout)) + proc.connect("stderr", (_, stderr: string) => err(stderr)) + return proc + } + + function exec<Out = string, Err = string>( + args: Args<Out, Err> + ): Out | Err + function exec<Out = string, Err = string>( + cmd: string | string[], + onOut?: (stdout: string) => Out, + onErr?: (stderr: string) => Err, + ): Out | Err + function exec<Out = string, Err = string>( + argsOrCmd: Args<Out, Err> | string | string[], + onOut: (stdout: string) => Out = out => out as Out, + onErr: (stderr: string) => Err = out => out as Err, + ): Out | Err { + const { cmd, err, out } = args(argsOrCmd, onOut, onErr) + return Array.isArray(cmd) + ? out(config.execv(cmd)!) as Out + : err(config.exec(cmd)!) as Err + } + + function execAsync(cmd: string | string[]): Promise<string> { + const proc = Array.isArray(cmd) + ? config.execAsyncv(cmd) + : config.execAsync(cmd) + return new Promise((resolve, reject) => { + proc.connect("stdout", (_, out: string) => resolve(out)) + proc.connect("stderr", (_, err: string) => reject(err)) + }) + } + + return { subprocess, exec, execAsync } +} diff --git a/js/src/time.ts b/js/src/time.ts new file mode 100644 index 0000000..ce98c9a --- /dev/null +++ b/js/src/time.ts @@ -0,0 +1,27 @@ +interface Time { + connect(sig: "now", fn: () => void): number + cancel(): void +} + +export default function Time<T extends Time>(Time: { + interval(interval: number, closure: any): T + timeout(timeout: number, closure: any): T + idle(closure: any): T +}) { + function interval(interval: number, callback: () => void) { + const t = Time.interval(interval, null) + t.connect("now", callback) + return t + } + function timeout(timeout: number, callback: () => void) { + const t = Time.timeout(timeout, null) + t.connect("now", callback) + return t + } + function idle(callback: () => void) { + const t = Time.idle(null) + t.connect("now", callback) + return t + } + return { interval, timeout, idle } +} diff --git a/js/src/variable.ts b/js/src/variable.ts new file mode 100644 index 0000000..1d4e07f --- /dev/null +++ b/js/src/variable.ts @@ -0,0 +1,250 @@ +import Binding, { type Connectable } from "./binding.js" + +type VariableBase = { + emit(sig: "error" | "changed" | "dropped", ...args: any[]): void + connect(sig: "error" | "changed" | "dropped", fn: (...args: any[]) => void): number + disconnect(id: number): void + runDispose?(): void // node, deno + run_dispose?(): void // gjs +} + +type VariableBaseCtor = { + new(): VariableBase +} + +type Time = any +type Process = any + +type Config = { + defaultErrHandler(err: any): void + VariableBase: VariableBaseCtor + interval(n: number, fn: () => void): Time + execAsync(cmd: string | string[]): Promise<string> + subprocess(args: { + cmd: string | string[], + out: (s: string) => void, + err: (s: string) => void + }): Process +} + +// @ts-expect-error missing values +export const config: Config = {} + +class VariableWrapper<T> extends Function { + private variable!: VariableBase + private errHandler? = config.defaultErrHandler + + private _value: T + private _poll?: Time + private _watch?: 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 config.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 = config.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 = config.interval(this.pollInterval, () => { + config.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 = config.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") + this.variable.runDispose?.() + this.variable.run_dispose?.() + } + + 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 | ((...args: any[]) => T), + callback?: (...args: any[]) => T, + ) { + const f = typeof sigOrFn === "function" ? sigOrFn : callback ?? (() => this.get()) + const set = (_: Connectable, ...args: any[]) => this.set(f(...args)) + + if (Array.isArray(objs)) { + for (const obj of objs) { + const [o, s] = obj + o.connect(s, set) + } + } else { + if (typeof sigOrFn === "string") + objs.connect(sigOrFn, set) + } + + return this as unknown as Variable<T> + } + + static derive<V, + const Deps extends Array<Variable<any> | Binding<any>>, + Args extends { + [K in keyof Deps]: Deps[K] extends Variable<infer T> + ? T : Deps[K] extends Binding<infer T> ? T : never + }, + >(deps: Deps, fn: (...args: Args) => 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 |