summaryrefslogtreecommitdiff
path: root/lang/gjs/src/_astal.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lang/gjs/src/_astal.ts')
-rw-r--r--lang/gjs/src/_astal.ts188
1 files changed, 188 insertions, 0 deletions
diff --git a/lang/gjs/src/_astal.ts b/lang/gjs/src/_astal.ts
new file mode 100644
index 0000000..6f3285b
--- /dev/null
+++ b/lang/gjs/src/_astal.ts
@@ -0,0 +1,188 @@
+import Variable from "./variable.js"
+import { execAsync } from "./process.js"
+import Binding, { Connectable, kebabify, snakeify, Subscribable } from "./binding.js"
+
+export const noImplicitDestroy = Symbol("no no implicit destroy")
+export const setChildren = Symbol("children setter method")
+
+export 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)()
+}
+
+export function setProp(obj: any, prop: string, value: any) {
+ try {
+ const setter = `set_${snakeify(prop)}`
+ if (typeof obj[setter] === "function")
+ return obj[setter](value)
+
+ return (obj[prop] = value)
+ } catch (error) {
+ console.error(`could not set property "${prop}" on ${obj}:`, error)
+ }
+}
+
+export type BindableProps<T> = {
+ [K in keyof T]: Binding<T[K]> | T[K];
+}
+
+export function hook<Widget extends Connectable>(
+ widget: Widget,
+ object: Connectable | Subscribable,
+ signalOrCallback: string | ((self: Widget, ...args: any[]) => void),
+ callback?: (self: Widget, ...args: any[]) => void,
+) {
+ if (typeof object.connect === "function" && callback) {
+ const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => {
+ callback(widget, ...args)
+ })
+ widget.connect("destroy", () => {
+ (object.disconnect as Connectable["disconnect"])(id)
+ })
+ } else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") {
+ const unsub = object.subscribe((...args: unknown[]) => {
+ signalOrCallback(widget, ...args)
+ })
+ widget.connect("destroy", unsub)
+ }
+}
+
+export function construct<Widget extends Connectable & { [setChildren]: (children: any[]) => void }>(widget: Widget, config: any) {
+ const { setup, child, children = [], ...props } = config
+
+ if (child) {
+ children.unshift(child)
+ }
+
+ // remove undefined values
+ for (const [key, value] of Object.entries(props)) {
+ if (value === undefined) {
+ delete props[key]
+ }
+ }
+
+ // collect bindings
+ const bindings: Array<[string, Binding<any>]> = Object
+ .keys(props)
+ .reduce((acc: any, prop) => {
+ if (props[prop] instanceof Binding) {
+ const binding = props[prop]
+ delete props[prop]
+ return [...acc, [prop, binding]]
+ }
+ return acc
+ }, [])
+
+ // collect signal handlers
+ const onHandlers: Array<[string, string | (() => unknown)]> = 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
+ }, [])
+
+ // set children
+ const mergedChildren = mergeBindings(children.flat(Infinity))
+ if (mergedChildren instanceof Binding) {
+ widget[setChildren](mergedChildren.get())
+ widget.connect("destroy", mergedChildren.subscribe((v) => {
+ widget[setChildren](v)
+ }))
+ } else {
+ if (mergedChildren.length > 0) {
+ widget[setChildren](mergedChildren)
+ }
+ }
+
+ // setup signal handlers
+ for (const [signal, callback] of onHandlers) {
+ const sig = signal.startsWith("notify")
+ ? signal.replace("-", "::")
+ : signal
+
+ if (typeof callback === "function") {
+ widget.connect(sig, callback)
+ } else {
+ widget.connect(sig, () => execAsync(callback)
+ .then(print).catch(console.error))
+ }
+ }
+
+ // setup bindings handlers
+ for (const [prop, binding] of bindings) {
+ if (prop === "child" || prop === "children") {
+ widget.connect("destroy", binding.subscribe((v: any) => {
+ widget[setChildren](v)
+ }))
+ }
+ widget.connect("destroy", binding.subscribe((v: any) => {
+ setProp(widget, prop, v)
+ }))
+ setProp(widget, prop, binding.get())
+ }
+
+ // filter undefined values
+ for (const [key, value] of Object.entries(props)) {
+ if (value === undefined) {
+ delete props[key]
+ }
+ }
+
+ Object.assign(widget, props)
+ setup?.(widget)
+ return widget
+}
+
+function isArrowFunction(func: any): func is (args: any) => any {
+ return !Object.hasOwn(func, "prototype")
+}
+
+export function jsx(
+ ctors: Record<string, { new(props: any): any } | ((props: any) => any)>,
+ ctor: string | ((props: any) => any) | { new(props: any): any },
+ { children, ...props }: any,
+) {
+ children ??= []
+
+ if (!Array.isArray(children))
+ children = [children]
+
+ children = children.filter(Boolean)
+
+ if (children.length === 1)
+ props.child = children[0]
+ else if (children.length > 1)
+ props.children = children
+
+ if (typeof ctor === "string") {
+ if (isArrowFunction(ctors[ctor]))
+ return ctors[ctor](props)
+
+ return new ctors[ctor](props)
+ }
+
+ if (isArrowFunction(ctor))
+ return ctor(props)
+
+ return new ctor(props)
+}