summaryrefslogtreecommitdiff
path: root/core/gjs/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/gjs/src')
-rw-r--r--core/gjs/src/application.ts105
-rw-r--r--core/gjs/src/astalify.ts331
-rw-r--r--core/gjs/src/binding.ts88
-rw-r--r--core/gjs/src/file.ts44
-rw-r--r--core/gjs/src/imports.ts10
-rw-r--r--core/gjs/src/jsx/jsx-runtime.ts87
-rw-r--r--core/gjs/src/process.ts69
-rw-r--r--core/gjs/src/time.ts13
-rw-r--r--core/gjs/src/variable.ts227
-rw-r--r--core/gjs/src/widgets.ts109
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>