diff options
Diffstat (limited to 'core/gjs/src')
-rw-r--r-- | core/gjs/src/application.ts | 105 | ||||
-rw-r--r-- | core/gjs/src/astalify.ts | 331 | ||||
-rw-r--r-- | core/gjs/src/binding.ts | 88 | ||||
-rw-r--r-- | core/gjs/src/file.ts | 44 | ||||
-rw-r--r-- | core/gjs/src/imports.ts | 10 | ||||
-rw-r--r-- | core/gjs/src/jsx/jsx-runtime.ts | 87 | ||||
-rw-r--r-- | core/gjs/src/process.ts | 69 | ||||
-rw-r--r-- | core/gjs/src/time.ts | 13 | ||||
-rw-r--r-- | core/gjs/src/variable.ts | 227 | ||||
-rw-r--r-- | core/gjs/src/widgets.ts | 109 |
10 files changed, 1083 insertions, 0 deletions
diff --git a/core/gjs/src/application.ts b/core/gjs/src/application.ts new file mode 100644 index 0000000..0ba247e --- /dev/null +++ b/core/gjs/src/application.ts @@ -0,0 +1,105 @@ +import { Astal, GObject, Gio, GLib } from "./imports.js" + +type RequestHandler = { + (request: string, res: (response: any) => void): void +} + +type Config = Partial<{ + icons: string + instanceName: string + gtkTheme: string + iconTheme: string + cursorTheme: string + css: string + requestHandler: RequestHandler + main(...args: string[]): void + client(message: (msg: string) => string, ...args: string[]): void + hold: boolean +}> + +// @ts-expect-error missing types +// https://github.com/gjsify/ts-for-gir/issues/164 +import { setConsoleLogDomain } from "console" +import { exit, programArgs } from "system" + +class AstalJS extends Astal.Application { + static { GObject.registerClass(this) } + + eval(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) + } + }) + } + + requestHandler?: RequestHandler + + vfunc_request(msg: string, conn: Gio.SocketConnection): void { + if (typeof this.requestHandler === "function") { + this.requestHandler(msg, (response) => { + Astal.write_sock(conn, String(response), (_, res) => + Astal.write_sock_finish(res), + ) + }) + } + else { + super.vfunc_request(msg, conn) + } + } + + apply_css(style: string, reset = false) { + super.apply_css(style, reset) + } + + quit(code?: number): void { + super.quit() + exit(code ?? 0) + } + + start({ requestHandler, css, hold, main, client, icons, ...cfg }: Config = {}) { + client ??= () => { + print(`Astal instance "${this.instanceName}" already running`) + exit(1) + } + + Object.assign(this, cfg) + setConsoleLogDomain(this.instanceName) + + this.requestHandler = requestHandler + this.connect("activate", () => { + const path: string[] = import.meta.url.split("/").slice(3) + const file = path.at(-1)!.replace(".js", ".css") + const css = `/${path.slice(0, -1).join("/")}/${file}` + if (file.endsWith(".css") && GLib.file_test(css, GLib.FileTest.EXISTS)) + this.apply_css(css, false) + + main?.(...programArgs) + }) + + if (!this.acquire_socket()) + return client(msg => Astal.Application.send_message(this.instanceName, msg)!, ...programArgs) + + if (css) + this.apply_css(css, false) + + if (icons) + this.add_icons(icons) + + hold ??= true + if (hold) + this.hold() + + this.runAsync([]) + } +} + +export default new AstalJS() diff --git a/core/gjs/src/astalify.ts b/core/gjs/src/astalify.ts new file mode 100644 index 0000000..be395ee --- /dev/null +++ b/core/gjs/src/astalify.ts @@ -0,0 +1,331 @@ +import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "./binding.js" +import { Astal, Gtk, Gdk } from "./imports.js" +import { execAsync } from "./process.js" +import Variable from "./variable.js" + +Object.defineProperty(Astal.Box.prototype, "children", { + get() { return this.get_children() }, + set(v) { this.set_children(v) }, +}) + +function setChildren(parent: Gtk.Widget, children: Gtk.Widget[]) { + children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) + + // remove + if (parent instanceof Gtk.Bin) { + const ch = parent.get_child() + if (ch) + parent.remove(ch) + } + + // FIXME: add rest of the edge cases like Stack + if (parent instanceof Astal.Box) { + parent.set_children(children) + } + + else if (parent instanceof Astal.CenterBox) { + parent.startWidget = children[0] + parent.centerWidget = children[1] + parent.endWidget = children[2] + } + + else if (parent instanceof Astal.Overlay) { + const [child, ...overlays] = children + parent.set_child(child) + parent.set_overlays(overlays) + } + + else if (parent instanceof Gtk.Container) { + for (const ch of children) + parent.add(ch) + } +} + +function mergeBindings(array: any[]) { + function getValues(...args: any[]) { + let i = 0 + return array.map(value => value instanceof Binding + ? args[i++] + : value, + ) + } + + const bindings = array.filter(i => i instanceof Binding) + + if (bindings.length === 0) + return array + + if (bindings.length === 1) + return bindings[0].as(getValues) + + return Variable.derive(bindings, getValues)() +} + +function setProp(obj: any, prop: string, value: any) { + try { + const setter = `set_${snakeify(prop)}` + if (typeof obj[setter] === "function") + return obj[setter](value) + + if (Object.hasOwn(obj, prop)) + return (obj[prop] = value) + } + catch (error) { + console.error(`could not set property "${prop}" on ${obj}:`, error) + } + + console.error(`could not set property "${prop}" on ${obj}`) +} + +export type Widget<C extends InstanceType<typeof Gtk.Widget>> = C & { + className: string + css: string + cursor: Cursor + clickThrough: boolean + toggleClassName(name: string, on?: boolean): void + 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> +} + +function hook( + self: Gtk.Widget, + object: Connectable | Subscribable, + signalOrCallback: string | ((self: Gtk.Widget, ...args: any[]) => void), + callback?: (self: Gtk.Widget, ...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 ctor(self: any, config: any = {}, children: any = []) { + const { setup, ...props } = config + props.visible ??= true + + const bindings = Object.keys(props).reduce((acc: any, prop) => { + if (props[prop] instanceof Binding) { + const binding = props[prop] + setProp(self, prop, binding.get()) + delete props[prop] + return [...acc, [prop, binding]] + } + 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 = props[key] + delete props[key] + return [...acc, [sig, handler]] + } + return acc + }, []) + + Object.assign(self, props) + + for (const [signal, callback] of onHandlers) { + if (typeof callback === "function") { + self.connect(signal, callback) + } + else { + self.connect(signal, () => execAsync(callback) + .then(print).catch(console.error)) + } + } + + for (const [prop, bind] of bindings) { + if (prop === "child" || prop === "children") { + self.connect("destroy", bind.subscribe((v: any) => { + setChildren(self, v) + })) + } + self.connect("destroy", bind.subscribe((v: any) => { + setProp(self, prop, v) + })) + } + + children = mergeBindings(children.flat(Infinity)) + if (children instanceof Binding) { + setChildren(self, children.get()) + self.connect("destroy", children.subscribe((v) => { + setChildren(self, v) + })) + } + else { + if (children.length > 0) + setChildren(self, children) + } + + setup?.(self) + return self +} + +function proxify< + C extends typeof Gtk.Widget, +>(klass: C) { + Object.defineProperty(klass.prototype, "className", { + get() { return Astal.widget_get_class_names(this).join(" ") }, + set(v) { Astal.widget_set_class_names(this, v.split(/\s+/)) }, + }) + + Object.defineProperty(klass.prototype, "css", { + get() { return Astal.widget_get_css(this) }, + set(v) { Astal.widget_set_css(this, v) }, + }) + + Object.defineProperty(klass.prototype, "cursor", { + get() { return Astal.widget_get_cursor(this) }, + set(v) { Astal.widget_set_cursor(this, v) }, + }) + + Object.defineProperty(klass.prototype, "clickThrough", { + get() { return Astal.widget_get_click_through(this) }, + set(v) { Astal.widget_set_click_through(this, v) }, + }) + + Object.assign(klass.prototype, { + hook: function (obj: any, sig: any, callback: any) { + return hook(this as InstanceType<C>, obj, sig, callback) + }, + toggleClassName: function name(cn: string, cond = true) { + Astal.widget_toggle_class_name(this as InstanceType<C>, cn, cond) + }, + set_class_name: function (name: string) { + // @ts-expect-error unknown key + this.className = name + }, + set_css: function (css: string) { + // @ts-expect-error unknown key + this.css = css + }, + set_cursor: function (cursor: string) { + // @ts-expect-error unknown key + this.cursor = cursor + }, + set_click_through: function (clickThrough: boolean) { + // @ts-expect-error unknown key + this.clickThrough = clickThrough + }, + }) + + const proxy = new Proxy(klass, { + construct(_, [conf, ...children]) { + // @ts-expect-error abstract class + return ctor(new klass(), conf, children) + }, + apply(_t, _a, [conf, ...children]) { + // @ts-expect-error abstract class + return ctor(new klass(), conf, children) + }, + }) + + return proxy +} + +export default function astalify< + C extends typeof Gtk.Widget, + P extends Record<string, any>, + N extends string = "Widget", +>(klass: C) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type Astal<N> = Omit<C, "new"> & { + new(props?: P, ...children: Gtk.Widget[]): Widget<InstanceType<C>> + (props?: P, ...children: Gtk.Widget[]): Widget<InstanceType<C>> + } + + return proxify(klass) as unknown as Astal<N> +} + +type BindableProps<T> = { + [K in keyof T]: Binding<T[K]> | T[K]; +} + +type SigHandler< + W extends InstanceType<typeof Gtk.Widget>, + Args extends Array<unknown>, +> = ((self: Widget<W>, ...args: Args) => unknown) | string | string[] + +export type ConstructProps< + Self extends InstanceType<typeof Gtk.Widget>, + Props extends Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array<unknown>> = Record<`on${string}`, any[]>, +> = Partial<{ + // @ts-expect-error can't assign to unknown, but it works as expected though + [S in keyof Signals]: SigHandler<Self, Signals[S]> +}> & Partial<{ + [Key in `on${string}`]: SigHandler<Self, any[]> +}> & BindableProps<Partial<Props> & { + className?: string + css?: string + cursor?: string + clickThrough?: boolean +}> & { + onDestroy?: (self: Widget<Self>) => unknown + onDraw?: (self: Widget<Self>) => unknown + onKeyPressEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown + onKeyReleaseEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown + onButtonPressEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown + onButtonReleaseEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown + onRealize?: (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/core/gjs/src/binding.ts b/core/gjs/src/binding.ts new file mode 100644 index 0000000..feec6fc --- /dev/null +++ b/core/gjs/src/binding.ts @@ -0,0 +1,88 @@ +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 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") { + 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/core/gjs/src/file.ts b/core/gjs/src/file.ts new file mode 100644 index 0000000..90b33a1 --- /dev/null +++ b/core/gjs/src/file.ts @@ -0,0 +1,44 @@ +import { Astal, Gio } from "./imports.js" + +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/core/gjs/src/imports.ts b/core/gjs/src/imports.ts new file mode 100644 index 0000000..cbed004 --- /dev/null +++ b/core/gjs/src/imports.ts @@ -0,0 +1,10 @@ +// this file's purpose is to have glib versions in one place +// this is only really needed for Gtk/Astal because +// ts-gir might generate gtk4 versions too + +export { default as Astal } from "gi://Astal?version=0.1" +export { default as GObject } from "gi://GObject?version=2.0" +export { default as Gio } from "gi://Gio?version=2.0" +export { default as Gtk } from "gi://Gtk?version=3.0" +export { default as Gdk } from "gi://Gdk?version=3.0" +export { default as GLib } from "gi://GLib?version=2.0" diff --git a/core/gjs/src/jsx/jsx-runtime.ts b/core/gjs/src/jsx/jsx-runtime.ts new file mode 100644 index 0000000..70f098f --- /dev/null +++ b/core/gjs/src/jsx/jsx-runtime.ts @@ -0,0 +1,87 @@ +import { Gtk } from "../imports.js" +import * as Widget from "../widgets.js" + +function isArrowFunction(func: any): func is (args: any) => any { + return !Object.hasOwn(func, "prototype") +} + +export function jsx( + ctor: keyof typeof ctors | typeof Gtk.Widget, + { children, ...props }: any, +) { + children ??= [] + + if (!Array.isArray(children)) + children = [children] + + children = children.filter(Boolean) + + if (typeof ctor === "string") + return (ctors as any)[ctor](props, children) + + if (children.length === 1) + props.child = children[0] + else if (children.length > 1) + props.children = children + + if (isArrowFunction(ctor)) + return ctor(props) + + // @ts-expect-error can be class or function + return new ctor(props) +} + +const ctors = { + box: Widget.Box, + button: Widget.Button, + centerbox: Widget.CenterBox, + // TODO: circularprogress + drawingarea: Widget.DrawingArea, + entry: Widget.Entry, + eventbox: Widget.EventBox, + // TODO: fixed + // TODO: flowbox + icon: Widget.Icon, + label: Widget.Label, + levelbar: Widget.LevelBar, + // TODO: listbox + overlay: Widget.Overlay, + revealer: Widget.Revealer, + scrollable: Widget.Scrollable, + slider: Widget.Slider, + // TODO: stack + switch: Widget.Switch, + window: Widget.Window, +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + type Element = Gtk.Widget + type ElementClass = Gtk.Widget + interface IntrinsicElements { + box: Widget.BoxProps + button: Widget.ButtonProps + centerbox: Widget.CenterBoxProps + // TODO: circularprogress + drawingarea: Widget.DrawingAreaProps + entry: Widget.EntryProps + eventbox: Widget.EventBoxProps + // TODO: fixed + // TODO: flowbox + icon: Widget.IconProps + label: Widget.LabelProps + levelbar: Widget.LevelBarProps + // TODO: listbox + overlay: Widget.OverlayProps + revealer: Widget.RevealerProps + scrollable: Widget.ScrollableProps + slider: Widget.SliderProps + // TODO: stack + switch: Widget.SwitchProps + window: Widget.WindowProps + } + } +} + +export const jsxs = jsx diff --git a/core/gjs/src/process.ts b/core/gjs/src/process.ts new file mode 100644 index 0000000..c5329e2 --- /dev/null +++ b/core/gjs/src/process.ts @@ -0,0 +1,69 @@ +import { Astal } from "./imports.js" + +type Args<Out = void, Err = void> = { + cmd: string | string[] + out?: (stdout: string) => Out + err?: (stderr: string) => Err +} + +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, + } +} + +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 { cmd, err, out } = args(argsOrCmd, onOut, onErr) + 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/core/gjs/src/time.ts b/core/gjs/src/time.ts new file mode 100644 index 0000000..4e28ad0 --- /dev/null +++ b/core/gjs/src/time.ts @@ -0,0 +1,13 @@ +import { Astal } from "./imports.js" + +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/core/gjs/src/variable.ts b/core/gjs/src/variable.ts new file mode 100644 index 0000000..d583ab1 --- /dev/null +++ b/core/gjs/src/variable.ts @@ -0,0 +1,227 @@ +import Binding, { type Connectable } from "./binding.js" +import { Astal } from "./imports.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") + 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 | ((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 + o.connect(s, set) + } + } + else { + if (typeof sigOrFn === "string") + objs.connect(sigOrFn, set) + } + + return this as unknown as Variable<T> + } + + static derive< + 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 + }, + 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 diff --git a/core/gjs/src/widgets.ts b/core/gjs/src/widgets.ts new file mode 100644 index 0000000..82d4708 --- /dev/null +++ b/core/gjs/src/widgets.ts @@ -0,0 +1,109 @@ +/* eslint-disable max-len */ +import { Astal, Gtk } from "./imports.js" +import astalify, { type ConstructProps, type Widget } from "./astalify.js" + +export { astalify, ConstructProps } + +// Box +export type Box = Widget<Astal.Box> +export const Box = astalify<typeof Astal.Box, BoxProps, "Box">(Astal.Box) +export type BoxProps = ConstructProps<Astal.Box, Astal.Box.ConstructorProps> + +// Button +export type Button = Widget<Astal.Button> +export const Button = astalify<typeof Astal.Button, ButtonProps, "Button">(Astal.Button) +export type ButtonProps = ConstructProps<Astal.Button, Astal.Button.ConstructorProps, { + onClicked: [] + onClick: [event: Astal.ClickEvent] + onClickRelease: [event: Astal.ClickEvent] + onHover: [event: Astal.HoverEvent] + onHoverLost: [event: Astal.HoverEvent] + onScroll: [event: Astal.ScrollEvent] +}> + +// CenterBox +export type CenterBox = Widget<Astal.CenterBox> +export const CenterBox = astalify<typeof Astal.CenterBox, CenterBoxProps, "CenterBox">(Astal.CenterBox) +export type CenterBoxProps = ConstructProps<Astal.CenterBox, Astal.CenterBox.ConstructorProps> + +// TODO: CircularProgress + +// DrawingArea +export type DrawingArea = Widget<Gtk.DrawingArea> +export const DrawingArea = astalify<typeof Gtk.DrawingArea, DrawingAreaProps, "DrawingArea">(Gtk.DrawingArea) +export type DrawingAreaProps = ConstructProps<Gtk.DrawingArea, Gtk.DrawingArea.ConstructorProps, { + onDraw: [cr: any] // TODO: cairo types +}> + +// Entry +export type Entry = Widget<Gtk.Entry> +export const Entry = astalify<typeof Gtk.Entry, EntryProps, "Entry">(Gtk.Entry) +export type EntryProps = ConstructProps<Gtk.Entry, Gtk.Entry.ConstructorProps, { + onChanged: [] + onActivate: [] +}> + +// EventBox +export type EventBox = Widget<Astal.EventBox> +export const EventBox = astalify<typeof Astal.EventBox, EventBoxProps, "EventBox">(Astal.EventBox) +export type EventBoxProps = ConstructProps<Astal.EventBox, Astal.EventBox.ConstructorProps, { + onClick: [event: Astal.ClickEvent] + onClickRelease: [event: Astal.ClickEvent] + onHover: [event: Astal.HoverEvent] + onHoverLost: [event: Astal.HoverEvent] + onScroll: [event: Astal.ScrollEvent] +}> + +// TODO: Fixed +// TODO: FlowBox + +// Icon +export type Icon = Widget<Astal.Icon> +export const Icon = astalify<typeof Astal.Icon, IconProps, "Icon">(Astal.Icon) +export type IconProps = ConstructProps<Astal.Icon, Astal.Icon.ConstructorProps> + +// Label +export type Label = Widget<Astal.Label> +export const Label = astalify<typeof Astal.Label, LabelProps, "Label">(Astal.Label) +export type LabelProps = ConstructProps<Astal.Label, Astal.Label.ConstructorProps> + +// LevelBar +export type LevelBar = Widget<Astal.LevelBar> +export const LevelBar = astalify<typeof Astal.LevelBar, LevelBarProps, "LevelBar">(Astal.LevelBar) +export type LevelBarProps = ConstructProps<Astal.LevelBar, Astal.LevelBar.ConstructorProps> + +// TODO: ListBox + +// Overlay +export type Overlay = Widget<Astal.Overlay> +export const Overlay = astalify<typeof Astal.Overlay, OverlayProps, "Overlay">(Astal.Overlay) +export type OverlayProps = ConstructProps<Astal.Overlay, Astal.Overlay.ConstructorProps> + +// Revealer +export type Revealer = Widget<Gtk.Revealer> +export const Revealer = astalify<typeof Gtk.Revealer, RevealerProps, "Revealer">(Gtk.Revealer) +export type RevealerProps = ConstructProps<Gtk.Revealer, Gtk.Revealer.ConstructorProps> + +// Scrollable +export type Scrollable = Widget<Astal.Scrollable> +export const Scrollable = astalify<typeof Astal.Scrollable, ScrollableProps, "Scrollable">(Astal.Scrollable) +export type ScrollableProps = ConstructProps<Astal.Scrollable, Astal.Scrollable.ConstructorProps> + +// Slider +export type Slider = Widget<Astal.Slider> +export const Slider = astalify<typeof Astal.Slider, SliderProps, "Slider">(Astal.Slider) +export type SliderProps = ConstructProps<Astal.Slider, Astal.Slider.ConstructorProps, { + onDragged: [] +}> + +// TODO: Stack + +// Switch +export type Switch = Widget<Gtk.Switch> +export const Switch = astalify<typeof Gtk.Switch, SwitchProps, "Switch">(Gtk.Switch) +export type SwitchProps = ConstructProps<Gtk.Switch, Gtk.Switch.ConstructorProps> + +// Window +export type Window = Widget<Astal.Window> +export const Window = astalify<typeof Astal.Window, WindowProps, "Window">(Astal.Window) +export type WindowProps = ConstructProps<Astal.Window, Astal.Window.ConstructorProps> |