summaryrefslogtreecommitdiff
path: root/js/src
diff options
context:
space:
mode:
Diffstat (limited to 'js/src')
-rw-r--r--js/src/application.ts28
-rw-r--r--js/src/astalify.ts213
-rw-r--r--js/src/binding.ts78
-rw-r--r--js/src/process.ts83
-rw-r--r--js/src/time.ts27
-rw-r--r--js/src/variable.ts250
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