From bafd48d3df9b43a1d49ec015eff30619d595468b Mon Sep 17 00:00:00 2001 From: Aylur Date: Tue, 15 Oct 2024 13:25:45 +0000 Subject: update lua and gjs layout installing the gjs package through meson or npm now results in the same exposed structure lua: fix rockspec docs: aur package --- docs/guide/getting-started/installation.md | 4 +- lang/gjs/gtk3/app.ts | 105 ---------- lang/gjs/gtk3/astalify.ts | 325 ----------------------------- lang/gjs/gtk3/index.ts | 9 - lang/gjs/gtk3/jsx-runtime.ts | 96 --------- lang/gjs/gtk3/widget.ts | 154 -------------- lang/gjs/gtk4/app.ts | 1 - lang/gjs/gtk4/astalify.ts | 1 - lang/gjs/gtk4/index.ts | 1 - lang/gjs/gtk4/jsx-runtime.ts | 1 - lang/gjs/index.ts | 7 +- lang/gjs/lib/binding.ts | 89 -------- lang/gjs/lib/file.ts | 45 ---- lang/gjs/lib/gobject.ts | 180 ---------------- lang/gjs/lib/process.ts | 68 ------ lang/gjs/lib/time.ts | 13 -- lang/gjs/lib/variable.ts | 230 -------------------- lang/gjs/meson.build | 25 ++- lang/gjs/package.json | 16 +- lang/gjs/src/binding.ts | 89 ++++++++ lang/gjs/src/file.ts | 45 ++++ lang/gjs/src/gobject.ts | 180 ++++++++++++++++ lang/gjs/src/gtk3/app.ts | 105 ++++++++++ lang/gjs/src/gtk3/astalify.ts | 325 +++++++++++++++++++++++++++++ lang/gjs/src/gtk3/index.ts | 9 + lang/gjs/src/gtk3/jsx-runtime.ts | 96 +++++++++ lang/gjs/src/gtk3/widget.ts | 154 ++++++++++++++ lang/gjs/src/gtk4/app.ts | 1 + lang/gjs/src/gtk4/astalify.ts | 1 + lang/gjs/src/gtk4/index.ts | 1 + lang/gjs/src/gtk4/jsx-runtime.ts | 1 + lang/gjs/src/index.ts | 6 + lang/gjs/src/process.ts | 68 ++++++ lang/gjs/src/time.ts | 13 ++ lang/gjs/src/variable.ts | 230 ++++++++++++++++++++ lang/gjs/tsconfig.json | 4 +- lang/lua/astal-dev-1.rockspec | 22 +- lang/lua/astal/binding.lua | 71 +++++++ lang/lua/astal/file.lua | 45 ++++ lang/lua/astal/gtk3/app.lua | 96 +++++++++ lang/lua/astal/gtk3/astalify.lua | 236 +++++++++++++++++++++ lang/lua/astal/gtk3/init.lua | 5 + lang/lua/astal/gtk3/widget.lua | 90 ++++++++ lang/lua/astal/init.lua | 27 +++ lang/lua/astal/process.lua | 78 +++++++ lang/lua/astal/time.lua | 27 +++ lang/lua/astal/variable.lua | 276 ++++++++++++++++++++++++ lang/lua/gtk3/app.lua | 96 --------- lang/lua/gtk3/astalify.lua | 236 --------------------- lang/lua/gtk3/widget.lua | 90 -------- lang/lua/init.lua | 27 --- lang/lua/lib/binding.lua | 71 ------- lang/lua/lib/file.lua | 45 ---- lang/lua/lib/process.lua | 78 ------- lang/lua/lib/time.lua | 27 --- lang/lua/lib/variable.lua | 276 ------------------------ 56 files changed, 2319 insertions(+), 2298 deletions(-) delete mode 100644 lang/gjs/gtk3/app.ts delete mode 100644 lang/gjs/gtk3/astalify.ts delete mode 100644 lang/gjs/gtk3/index.ts delete mode 100644 lang/gjs/gtk3/jsx-runtime.ts delete mode 100644 lang/gjs/gtk3/widget.ts delete mode 100644 lang/gjs/gtk4/app.ts delete mode 100644 lang/gjs/gtk4/astalify.ts delete mode 100644 lang/gjs/gtk4/index.ts delete mode 100644 lang/gjs/gtk4/jsx-runtime.ts delete mode 100644 lang/gjs/lib/binding.ts delete mode 100644 lang/gjs/lib/file.ts delete mode 100644 lang/gjs/lib/gobject.ts delete mode 100644 lang/gjs/lib/process.ts delete mode 100644 lang/gjs/lib/time.ts delete mode 100644 lang/gjs/lib/variable.ts create mode 100644 lang/gjs/src/binding.ts create mode 100644 lang/gjs/src/file.ts create mode 100644 lang/gjs/src/gobject.ts create mode 100644 lang/gjs/src/gtk3/app.ts create mode 100644 lang/gjs/src/gtk3/astalify.ts create mode 100644 lang/gjs/src/gtk3/index.ts create mode 100644 lang/gjs/src/gtk3/jsx-runtime.ts create mode 100644 lang/gjs/src/gtk3/widget.ts create mode 100644 lang/gjs/src/gtk4/app.ts create mode 100644 lang/gjs/src/gtk4/astalify.ts create mode 100644 lang/gjs/src/gtk4/index.ts create mode 100644 lang/gjs/src/gtk4/jsx-runtime.ts create mode 100644 lang/gjs/src/index.ts create mode 100644 lang/gjs/src/process.ts create mode 100644 lang/gjs/src/time.ts create mode 100644 lang/gjs/src/variable.ts create mode 100644 lang/lua/astal/binding.lua create mode 100644 lang/lua/astal/file.lua create mode 100644 lang/lua/astal/gtk3/app.lua create mode 100644 lang/lua/astal/gtk3/astalify.lua create mode 100644 lang/lua/astal/gtk3/init.lua create mode 100644 lang/lua/astal/gtk3/widget.lua create mode 100644 lang/lua/astal/init.lua create mode 100644 lang/lua/astal/process.lua create mode 100644 lang/lua/astal/time.lua create mode 100644 lang/lua/astal/variable.lua delete mode 100644 lang/lua/gtk3/app.lua delete mode 100644 lang/lua/gtk3/astalify.lua delete mode 100644 lang/lua/gtk3/widget.lua delete mode 100644 lang/lua/init.lua delete mode 100644 lang/lua/lib/binding.lua delete mode 100644 lang/lua/lib/file.lua delete mode 100644 lang/lua/lib/process.lua delete mode 100644 lang/lua/lib/time.lua delete mode 100644 lang/lua/lib/variable.lua diff --git a/docs/guide/getting-started/installation.md b/docs/guide/getting-started/installation.md index 96cbdfa..aaa6d0d 100644 --- a/docs/guide/getting-started/installation.md +++ b/docs/guide/getting-started/installation.md @@ -10,12 +10,10 @@ Read more about it on the [nix page](./nix#astal) maintainer: [@kotontrion](https://github.com/kotontrion) - - :::code-group ```sh [Core Library] -yay -S libastal-git +yay -S libastal-io-git libastal-git ``` ```sh [Every Library] diff --git a/lang/gjs/gtk3/app.ts b/lang/gjs/gtk3/app.ts deleted file mode 100644 index 1191dc4..0000000 --- a/lang/gjs/gtk3/app.ts +++ /dev/null @@ -1,105 +0,0 @@ -import IO from "gi://AstalIO" -import GObject from "gi://GObject" -import Astal from "gi://Astal?version=3.0" -import Gio from "gi://Gio?version=2.0" -import Gtk from "gi://Gtk?version=3.0" - -Gtk.init(null) - -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 -}> - -import { setConsoleLogDomain } from "console" -import { exit, programArgs } from "system" - -export default new (class AstalJS extends Astal.Application { - static { GObject.registerClass({ GTypeName: "AstalJS" }, this) } - - eval(body: string): Promise { - 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) => { - IO.write_sock(conn, String(response), (_, res) => - IO.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", () => { - main?.(...programArgs) - }) - - try { - this.acquire_socket() - } - catch (error) { - return client(msg => IO.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([]) - } -}) diff --git a/lang/gjs/gtk3/astalify.ts b/lang/gjs/gtk3/astalify.ts deleted file mode 100644 index d31046c..0000000 --- a/lang/gjs/gtk3/astalify.ts +++ /dev/null @@ -1,325 +0,0 @@ -import Astal from "gi://Astal?version=3.0" -import Gtk from "gi://Gtk?version=3.0" -import Gdk from "gi://Gdk?version=3.0" -import GObject from "gi://GObject" -import { execAsync } from "../lib/process.js" -import Variable from "../lib/variable.js" -import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "../lib/binding.js" - -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)() -} - -function setProp(obj: any, prop: string, value: any) { - try { - // the setter method has to be used because - // array like properties are not bound correctly as props - 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 default function astalify< - C extends { new(...args: any[]): Gtk.Widget }, ->(cls: C) { - class Widget extends cls { - get css(): string { return Astal.widget_get_css(this) } - set css(css: string) { Astal.widget_set_css(this, css) } - get_css(): string { return this.css } - set_css(css: string) { this.css = css } - - get className(): string { return Astal.widget_get_class_names(this).join(" ") } - set className(className: string) { Astal.widget_set_class_names(this, className.split(/\s+/)) } - get_class_name(): string { return this.className } - set_class_name(className: string) { this.className = className } - - get cursor(): Cursor { return Astal.widget_get_cursor(this) as Cursor } - set cursor(cursor: Cursor) { Astal.widget_set_cursor(this, cursor) } - get_cursor(): Cursor { return this.cursor } - set_cursor(cursor: Cursor) { this.cursor = cursor } - - get clickThrough(): boolean { return Astal.widget_get_click_through(this) } - set clickThrough(clickThrough: boolean) { Astal.widget_set_click_through(this, clickThrough) } - get_click_through(): boolean { return this.clickThrough } - set_click_through(clickThrough: boolean) { this.clickThrough = clickThrough } - - declare __no_implicit_destroy: boolean - get noImplicitDestroy(): boolean { return this.__no_implicit_destroy } - set noImplicitDestroy(value: boolean) { this.__no_implicit_destroy = value } - - _setChildren(children: Gtk.Widget[]) { - children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget - ? ch - : new Gtk.Label({ visible: true, label: String(ch) })) - - // remove - if (this instanceof Gtk.Bin) { - const ch = this.get_child() - if (ch) - this.remove(ch) - if (ch && !children.includes(ch) && !this.noImplicitDestroy) - ch?.destroy() - } - else if (this instanceof Gtk.Container) { - for (const ch of this.get_children()) { - this.remove(ch) - if (!children.includes(ch) && !this.noImplicitDestroy) - ch?.destroy() - } - } - - // TODO: add more container types - if (this instanceof Astal.Box) { - this.set_children(children) - } - - else if (this instanceof Astal.Stack) { - this.set_children(children) - } - - else if (this instanceof Astal.CenterBox) { - this.startWidget = children[0] - this.centerWidget = children[1] - this.endWidget = children[2] - } - - else if (this instanceof Astal.Overlay) { - const [child, ...overlays] = children - this.set_child(child) - this.set_overlays(overlays) - } - - else if (this instanceof Gtk.Container) { - for (const ch of children) - this.add(ch) - } - } - - toggleClassName(cn: string, cond = true) { - Astal.widget_toggle_class_name(this, cn, cond) - } - - hook( - object: Connectable, - signal: string, - callback: (self: this, ...args: any[]) => void, - ): this - hook( - object: Subscribable, - callback: (self: this, ...args: any[]) => void, - ): this - hook( - object: Connectable | Subscribable, - signalOrCallback: string | ((self: this, ...args: any[]) => void), - callback?: (self: this, ...args: any[]) => void, - ) { - if (typeof object.connect === "function" && callback) { - const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { - callback(this, ...args) - }) - this.connect("destroy", () => { - (object.disconnect as Connectable["disconnect"])(id) - }) - } - - else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { - const unsub = object.subscribe((...args: unknown[]) => { - signalOrCallback(this, ...args) - }) - this.connect("destroy", unsub) - } - - return this - } - - constructor(...params: any[]) { - super() - const [config] = params - - const { setup, child, children = [], ...props } = config - props.visible ??= true - - if (child) - children.unshift(child) - - // collect bindings - const bindings = 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 = 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) { - this._setChildren(mergedChildren.get()) - this.connect("destroy", mergedChildren.subscribe((v) => { - this._setChildren(v) - })) - } - else { - if (mergedChildren.length > 0) { - this._setChildren(mergedChildren) - } - } - - // setup signal handlers - for (const [signal, callback] of onHandlers) { - if (typeof callback === "function") { - this.connect(signal, callback) - } - else { - this.connect(signal, () => execAsync(callback) - .then(print).catch(console.error)) - } - } - - // setup bindings handlers - for (const [prop, binding] of bindings) { - if (prop === "child" || prop === "children") { - this.connect("destroy", binding.subscribe((v: any) => { - this._setChildren(v) - })) - } - this.connect("destroy", binding.subscribe((v: any) => { - setProp(this, prop, v) - })) - setProp(this, prop, binding.get()) - } - - Object.assign(this, props) - setup?.(this) - } - } - - GObject.registerClass({ - GTypeName: `Astal_${cls.name}`, - Properties: { - "class-name": GObject.ParamSpec.string( - "class-name", "", "", GObject.ParamFlags.READWRITE, "", - ), - "css": GObject.ParamSpec.string( - "css", "", "", GObject.ParamFlags.READWRITE, "", - ), - "cursor": GObject.ParamSpec.string( - "cursor", "", "", GObject.ParamFlags.READWRITE, "default", - ), - "click-through": GObject.ParamSpec.boolean( - "click-through", "", "", GObject.ParamFlags.READWRITE, false, - ), - "no-implicit-destroy": GObject.ParamSpec.boolean( - "no-implicit-destroy", "", "", GObject.ParamFlags.READWRITE, false, - ), - }, - }, Widget) - - return Widget -} - -type BindableProps = { - [K in keyof T]: Binding | T[K]; -} - -type SigHandler< - W extends InstanceType, - Args extends Array, -> = ((self: W, ...args: Args) => unknown) | string | string[] - -export type ConstructProps< - Self extends InstanceType, - Props extends Gtk.Widget.ConstructorProps, - Signals extends Record<`on${string}`, Array> = Record<`on${string}`, any[]>, -> = Partial<{ - // @ts-expect-error can't assign to unknown, but it works as expected though - [S in keyof Signals]: SigHandler -}> & Partial<{ - [Key in `on${string}`]: SigHandler -}> & BindableProps & { - className?: string - css?: string - cursor?: string - clickThrough?: boolean -}> & { - onDestroy?: (self: Self) => unknown - onDraw?: (self: Self) => unknown - onKeyPressEvent?: (self: Self, event: Gdk.Event) => unknown - onKeyReleaseEvent?: (self: Self, event: Gdk.Event) => unknown - onButtonPressEvent?: (self: Self, event: Gdk.Event) => unknown - onButtonReleaseEvent?: (self: Self, event: Gdk.Event) => unknown - onRealize?: (self: Self) => unknown - setup?: (self: Self) => void -} - -export type BindableChild = Gtk.Widget | Binding - -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/lang/gjs/gtk3/index.ts b/lang/gjs/gtk3/index.ts deleted file mode 100644 index cfafbda..0000000 --- a/lang/gjs/gtk3/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Astal from "gi://Astal?version=3.0" -import Gtk from "gi://Gtk?version=3.0" -import Gdk from "gi://Gdk?version=3.0" -import astalify, { type ConstructProps } from "./astalify.js" - -export { Astal, Gtk, Gdk } -export { default as App } from "./app.js" -export { astalify, ConstructProps } -export * as Widget from "./widget.js" diff --git a/lang/gjs/gtk3/jsx-runtime.ts b/lang/gjs/gtk3/jsx-runtime.ts deleted file mode 100644 index 22dc424..0000000 --- a/lang/gjs/gtk3/jsx-runtime.ts +++ /dev/null @@ -1,96 +0,0 @@ -import Gtk from "gi://Gtk?version=3.0" -import { mergeBindings, type BindableChild } from "./astalify.js" -import * as Widget from "./widget.js" - -function isArrowFunction(func: any): func is (args: any) => any { - return !Object.hasOwn(func, "prototype") -} - -export function Fragment({ children = [], child }: { - child?: BindableChild - children?: Array -}) { - return mergeBindings([...children, child]) -} - -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 (children.length === 1) - props.child = children[0] - else if (children.length > 1) - props.children = children - - if (typeof ctor === "string") { - return new ctors[ctor](props) - } - - 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, - circularprogress: Widget.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, - stack: Widget.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 - circularprogress: Widget.CircularProgressProps - 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 - stack: Widget.StackProps - switch: Widget.SwitchProps - window: Widget.WindowProps - } - } -} - -export const jsxs = jsx diff --git a/lang/gjs/gtk3/widget.ts b/lang/gjs/gtk3/widget.ts deleted file mode 100644 index fd70ed6..0000000 --- a/lang/gjs/gtk3/widget.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* eslint-disable max-len */ -import Astal from "gi://Astal?version=3.0" -import Gtk from "gi://Gtk?version=3.0" -import GObject from "gi://GObject" -import astalify, { type ConstructProps, type BindableChild } from "./astalify.js" - -// Box -Object.defineProperty(Astal.Box.prototype, "children", { - get() { return this.get_children() }, - set(v) { this.set_children(v) }, -}) - -export type BoxProps = ConstructProps -export class Box extends astalify(Astal.Box) { - static { GObject.registerClass({ GTypeName: "Box" }, this) } - constructor(props?: BoxProps, ...children: Array) { super({ children, ...props } as any) } -} - -// Button -export type ButtonProps = ConstructProps -export class Button extends astalify(Astal.Button) { - static { GObject.registerClass({ GTypeName: "Button" }, this) } - constructor(props?: ButtonProps, child?: BindableChild) { super({ child, ...props } as any) } -} - -// CenterBox -export type CenterBoxProps = ConstructProps -export class CenterBox extends astalify(Astal.CenterBox) { - static { GObject.registerClass({ GTypeName: "CenterBox" }, this) } - constructor(props?: CenterBoxProps, ...children: Array) { super({ children, ...props } as any) } -} - -// CircularProgress -export type CircularProgressProps = ConstructProps -export class CircularProgress extends astalify(Astal.CircularProgress) { - static { GObject.registerClass({ GTypeName: "CircularProgress" }, this) } - constructor(props?: CircularProgressProps, child?: BindableChild) { super({ child, ...props } as any) } -} - -// DrawingArea -export type DrawingAreaProps = ConstructProps -export class DrawingArea extends astalify(Gtk.DrawingArea) { - static { GObject.registerClass({ GTypeName: "DrawingArea" }, this) } - constructor(props?: DrawingAreaProps) { super(props as any) } -} - -// Entry -export type EntryProps = ConstructProps -export class Entry extends astalify(Gtk.Entry) { - static { GObject.registerClass({ GTypeName: "Entry" }, this) } - constructor(props?: EntryProps) { super(props as any) } -} - -// EventBox -export type EventBoxProps = ConstructProps -export class EventBox extends astalify(Astal.EventBox) { - static { GObject.registerClass({ GTypeName: "EventBox" }, this) } - constructor(props?: EventBoxProps, child?: BindableChild) { super({ child, ...props } as any) } -} - -// // TODO: Fixed -// // TODO: FlowBox -// -// Icon -export type IconProps = ConstructProps -export class Icon extends astalify(Astal.Icon) { - static { GObject.registerClass({ GTypeName: "Icon" }, this) } - constructor(props?: IconProps) { super(props as any) } -} - -// Label -export type LabelProps = ConstructProps -export class Label extends astalify(Astal.Label) { - static { GObject.registerClass({ GTypeName: "Label" }, this) } - constructor(props?: LabelProps) { super(props as any) } -} - -// LevelBar -export type LevelBarProps = ConstructProps -export class LevelBar extends astalify(Astal.LevelBar) { - static { GObject.registerClass({ GTypeName: "LevelBar" }, this) } - constructor(props?: LevelBarProps) { super(props as any) } -} - -// TODO: ListBox - -// Overlay -export type OverlayProps = ConstructProps -export class Overlay extends astalify(Astal.Overlay) { - static { GObject.registerClass({ GTypeName: "Overlay" }, this) } - constructor(props?: OverlayProps, ...children: Array) { super({ children, ...props } as any) } -} - -// Revealer -export type RevealerProps = ConstructProps -export class Revealer extends astalify(Gtk.Revealer) { - static { GObject.registerClass({ GTypeName: "Revealer" }, this) } - constructor(props?: RevealerProps, child?: BindableChild) { super({ child, ...props } as any) } -} - -// Scrollable -export type ScrollableProps = ConstructProps -export class Scrollable extends astalify(Astal.Scrollable) { - static { GObject.registerClass({ GTypeName: "Scrollable" }, this) } - constructor(props?: ScrollableProps, child?: BindableChild) { super({ child, ...props } as any) } -} - -// Slider -export type SliderProps = ConstructProps -export class Slider extends astalify(Astal.Slider) { - static { GObject.registerClass({ GTypeName: "Slider" }, this) } - constructor(props?: SliderProps) { super(props as any) } -} - -// Stack -export type StackProps = ConstructProps -export class Stack extends astalify(Astal.Stack) { - static { GObject.registerClass({ GTypeName: "Stack" }, this) } - constructor(props?: StackProps, ...children: Array) { super({ children, ...props } as any) } -} - -// Switch -export type SwitchProps = ConstructProps -export class Switch extends astalify(Gtk.Switch) { - static { GObject.registerClass({ GTypeName: "Switch" }, this) } - constructor(props?: SwitchProps) { super(props as any) } -} - -// Window -export type WindowProps = ConstructProps -export class Window extends astalify(Astal.Window) { - static { GObject.registerClass({ GTypeName: "Window" }, this) } - constructor(props?: WindowProps, child?: BindableChild) { super({ child, ...props } as any) } -} diff --git a/lang/gjs/gtk4/app.ts b/lang/gjs/gtk4/app.ts deleted file mode 100644 index d931f73..0000000 --- a/lang/gjs/gtk4/app.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO: gtk4 diff --git a/lang/gjs/gtk4/astalify.ts b/lang/gjs/gtk4/astalify.ts deleted file mode 100644 index d931f73..0000000 --- a/lang/gjs/gtk4/astalify.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO: gtk4 diff --git a/lang/gjs/gtk4/index.ts b/lang/gjs/gtk4/index.ts deleted file mode 100644 index d931f73..0000000 --- a/lang/gjs/gtk4/index.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO: gtk4 diff --git a/lang/gjs/gtk4/jsx-runtime.ts b/lang/gjs/gtk4/jsx-runtime.ts deleted file mode 100644 index d931f73..0000000 --- a/lang/gjs/gtk4/jsx-runtime.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO: gtk4 diff --git a/lang/gjs/index.ts b/lang/gjs/index.ts index 4f52259..46e72b1 100644 --- a/lang/gjs/index.ts +++ b/lang/gjs/index.ts @@ -1,6 +1 @@ -export * from "./lib/process.js" -export * from "./lib/time.js" -export * from "./lib/file.js" -export * from "./lib/gobject.js" -export { bind, default as Binding } from "./lib/binding.js" -export { Variable } from "./lib/variable.js" +export * from "./src" diff --git a/lang/gjs/lib/binding.ts b/lang/gjs/lib/binding.ts deleted file mode 100644 index 95d905f..0000000 --- a/lang/gjs/lib/binding.ts +++ /dev/null @@ -1,89 +0,0 @@ -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 { - 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 { - private transformFn = (v: any) => v - - #emitter: Subscribable | Connectable - #prop?: string - - static bind< - T extends Connectable, - P extends keyof T, - >(object: T, property: P): Binding - - static bind(object: Subscribable): Binding - - static bind(emitter: Connectable | Subscribable, prop?: string) { - return new Binding(emitter, prop) - } - - private constructor(emitter: Connectable | Subscribable, prop?: string) { - this.#emitter = emitter - this.#prop = prop && kebabify(prop) - } - - toString() { - return `Binding<${this.#emitter}${this.#prop ? `, "${this.#prop}"` : ""}>` - } - - as(fn: (v: Value) => T): Binding { - const bind = new Binding(this.#emitter, this.#prop) - bind.transformFn = (v: Value) => fn(this.transformFn(v)) - return bind as unknown as Binding - } - - 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/lang/gjs/lib/file.ts b/lang/gjs/lib/file.ts deleted file mode 100644 index 7b9de3a..0000000 --- a/lang/gjs/lib/file.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Astal from "gi://AstalIO" -import Gio from "gi://Gio" - -export function readFile(path: string): string { - return Astal.read_file(path) || "" -} - -export function readFileAsync(path: string): Promise { - 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 { - 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/lang/gjs/lib/gobject.ts b/lang/gjs/lib/gobject.ts deleted file mode 100644 index 4740764..0000000 --- a/lang/gjs/lib/gobject.ts +++ /dev/null @@ -1,180 +0,0 @@ -export { default as GObject, default as default } from "gi://GObject" -export { default as Gio } from "gi://Gio" -export { default as GLib } from "gi://GLib" - -import GObject from "gi://GObject" -const meta = Symbol("meta") - -const { ParamSpec, ParamFlags } = GObject - -const kebabify = (str: string) => str - .replace(/([a-z])([A-Z])/g, "$1-$2") - .replaceAll("_", "-") - .toLowerCase() - -type SignalDeclaration = { - flags?: GObject.SignalFlags - accumulator?: GObject.AccumulatorType - return_type?: GObject.GType - param_types?: Array -} - -type PropertyDeclaration = - | InstanceType - | { $gtype: GObject.GType } - | typeof String - | typeof Number - | typeof Boolean - | typeof Object - -type GObjectConstructor = { - [meta]?: { - Properties?: { [key: string]: GObject.ParamSpec } - Signals?: { [key: string]: GObject.SignalDefinition } - } - new(...args: any[]): any -} - -type MetaInfo = GObject.MetaInfo, never> - -export function register(options: MetaInfo = {}) { - return function (cls: GObjectConstructor) { - GObject.registerClass({ - Signals: { ...cls[meta]?.Signals }, - Properties: { ...cls[meta]?.Properties }, - ...options, - }, cls) - } -} - -export function property(declaration: PropertyDeclaration = Object) { - return function (target: any, prop: any, desc?: PropertyDescriptor) { - target.constructor[meta] ??= {} - target.constructor[meta].Properties ??= {} - - const name = kebabify(prop) - - if (!desc) { - let value = defaultValue(declaration) - - Object.defineProperty(target, prop, { - get() { - return value - }, - set(v) { - if (v !== value) { - value = v - this.notify(name) - } - }, - }) - - Object.defineProperty(target, `set_${name.replace("-", "_")}`, { - value: function (v: any) { - this[prop] = v - }, - }) - - Object.defineProperty(target, `get_${name.replace("-", "_")}`, { - value: function () { - return this[prop] - }, - }) - - target.constructor[meta].Properties[kebabify(prop)] = pspec(name, ParamFlags.READWRITE, declaration) - } - - else { - let flags = 0 - if (desc.get) flags |= ParamFlags.READABLE - if (desc.set) flags |= ParamFlags.WRITABLE - - target.constructor[meta].Properties[kebabify(prop)] = pspec(name, flags, declaration) - } - } -} - -export function signal(...params: Array<{ $gtype: GObject.GType } | typeof Object>): -(target: any, signal: any, desc?: PropertyDescriptor) => void - -export function signal(declaration?: SignalDeclaration): -(target: any, signal: any, desc?: PropertyDescriptor) => void - -export function signal( - declaration?: SignalDeclaration | { $gtype: GObject.GType } | typeof Object, - ...params: Array<{ $gtype: GObject.GType } | typeof Object> -) { - return function (target: any, signal: any, desc?: PropertyDescriptor) { - target.constructor[meta] ??= {} - target.constructor[meta].Signals ??= {} - - const name = kebabify(signal) - - if (declaration || params.length > 0) { - // @ts-expect-error TODO: type assert - const arr = [declaration, ...params].map(v => v.$gtype) - target.constructor[meta].Signals[name] = { - param_types: arr, - } - } - else { - target.constructor[meta].Signals[name] = declaration - } - - if (!desc) { - Object.defineProperty(target, signal, { - value: function (...args: any[]) { - this.emit(name, ...args) - }, - }) - } - else { - const og: ((...args: any[]) => void) = desc.value - desc.value = function (...args: any[]) { - // @ts-expect-error not typed - this.emit(name, ...args) - } - Object.defineProperty(target, `on_${name.replace("-", "_")}`, { - value: function (...args: any[]) { - return og(...args) - }, - }) - } - } -} - -function pspec(name: string, flags: number, declaration: PropertyDeclaration) { - if (declaration instanceof ParamSpec) - return declaration - - switch (declaration) { - case String: - return ParamSpec.string(name, "", "", flags, "") - case Number: - return ParamSpec.double(name, "", "", flags, -Number.MAX_VALUE, Number.MAX_VALUE, 0) - case Boolean: - return ParamSpec.boolean(name, "", "", flags, false) - case Object: - return ParamSpec.jsobject(name, "", "", flags) - default: - // @ts-expect-error misstyped - return ParamSpec.object(name, "", "", flags, declaration.$gtype) - } -} - -function defaultValue(declaration: PropertyDeclaration) { - if (declaration instanceof ParamSpec) - return declaration.get_default_value() - - switch (declaration) { - case String: - return "default-string" - case Number: - return 0 - case Boolean: - return false - case Object: - default: - return null - } -} diff --git a/lang/gjs/lib/process.ts b/lang/gjs/lib/process.ts deleted file mode 100644 index 2f7816b..0000000 --- a/lang/gjs/lib/process.ts +++ /dev/null @@ -1,68 +0,0 @@ -import Astal from "gi://AstalIO" - -type Args = { - cmd: string | string[] - out?: (stdout: string) => void - err?: (stderr: string) => void -} - -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 args = Array.isArray(argsOrCmd) || typeof argsOrCmd === "string" - const { cmd, err, out } = { - cmd: args ? argsOrCmd : argsOrCmd.cmd, - err: args ? onErr : argsOrCmd.err || onErr, - out: args ? onOut : argsOrCmd.out || onOut, - } - - 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 { - 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/lang/gjs/lib/time.ts b/lang/gjs/lib/time.ts deleted file mode 100644 index a7e1e61..0000000 --- a/lang/gjs/lib/time.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Astal from "gi://AstalIO" - -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/lang/gjs/lib/variable.ts b/lang/gjs/lib/variable.ts deleted file mode 100644 index 9b3d3d2..0000000 --- a/lang/gjs/lib/variable.ts +++ /dev/null @@ -1,230 +0,0 @@ -import Astal from "gi://AstalIO" -import Binding, { type Connectable, type Subscribable } from "./binding.js" -import { interval } from "./time.js" -import { execAsync, subprocess } from "./process.js" - -class VariableWrapper 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 - - 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(transform?: (value: T) => R): Binding { - const b = Binding.bind(this) - return transform ? b.as(transform) : b as unknown as Binding - } - - 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") - } - - onDropped(callback: () => void) { - this.variable.connect("dropped", callback) - return this as unknown as Variable - } - - onError(callback: (err: string) => void) { - delete this.errHandler - this.variable.connect("error", (_, err) => callback(err)) - return this as unknown as Variable - } - - 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 - - poll( - interval: number, - callback: (prev: T) => T | Promise - ): Variable - - poll( - interval: number, - exec: string | string[] | ((prev: T) => T | Promise), - 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 - } - - 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 - } - - observe( - objs: Array<[obj: Connectable, signal: string]>, - callback: (...args: any[]) => T, - ): Variable - - observe( - obj: Connectable, - signal: string, - callback: (...args: any[]) => T, - ): Variable - - 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 - const id = o.connect(s, set) - this.onDropped(() => o.disconnect(id)) - } - } - else { - if (typeof sigOrFn === "string") { - const id = objs.connect(sigOrFn, set) - this.onDropped(() => objs.disconnect(id)) - } - } - - return this as unknown as Variable - } - - static derive< - const Deps extends Array>, - Args extends { - [K in keyof Deps]: Deps[K] extends Subscribable ? 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 extends Omit, "bind"> { - (transform: (value: T) => R): Binding - (): Binding -} - -export const Variable = new Proxy(VariableWrapper as any, { - apply: (_t, _a, args) => new VariableWrapper(args[0]), -}) as { - derive: typeof VariableWrapper["derive"] - (init: T): Variable - new(init: T): Variable -} - -export default Variable diff --git a/lang/gjs/meson.build b/lang/gjs/meson.build index 8f3058f..388b301 100644 --- a/lang/gjs/meson.build +++ b/lang/gjs/meson.build @@ -1,9 +1,22 @@ project('astal-gjs') -datadir = get_option('prefix') / get_option('datadir') -pkgdata = datadir / 'astal' / 'gjs' +dest = get_option('prefix') / get_option('datadir') / 'astal' / 'gjs' -install_data('index.ts', install_dir: pkgdata) -install_subdir('lib', install_dir: pkgdata) -install_subdir('gtk3', install_dir: pkgdata) -install_subdir('gtk4', install_dir: pkgdata) +dependency('astal-io-0.1') +dependency('astal-3.0') + +install_data( + [ + 'src/binding.ts', + 'src/file.ts', + 'src/gobject.ts', + 'src/index.ts', + 'src/process.ts', + 'src/time.ts', + 'src/variable.ts', + ], + install_dir: dest, +) + +install_subdir('src/gtk3', install_dir: dest) +# install_subdir('src/gtk4', install_dir: dest) diff --git a/lang/gjs/package.json b/lang/gjs/package.json index 447ddcd..9f44388 100644 --- a/lang/gjs/package.json +++ b/lang/gjs/package.json @@ -16,14 +16,14 @@ }, "exports": { ".": "./index.ts", - "./gtk3": "./gtk3/index.ts", - "./gtk4": "./gtk3/index.ts", - "./lib/binding": "./lib/binding.ts", - "./lib/file": "./lib/file.ts", - "./lib/gobject": "./lib/gobject.ts", - "./lib/process": "./lib/process.ts", - "./lib/time": "./lib/time.ts", - "./lib/variable": "./lib/variable.ts" + "./gtk3": "./src/gtk3/index.ts", + "./gtk4": "./src/gtk3/index.ts", + "./binding": "./src/binding.ts", + "./file": "./src/file.ts", + "./gobject": "./src/gobject.ts", + "./process": "./src/process.ts", + "./time": "./src/time.ts", + "./variable": "./src/variable.ts" }, "engines": { "gjs": ">=1.79.0" diff --git a/lang/gjs/src/binding.ts b/lang/gjs/src/binding.ts new file mode 100644 index 0000000..95d905f --- /dev/null +++ b/lang/gjs/src/binding.ts @@ -0,0 +1,89 @@ +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 { + 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 { + private transformFn = (v: any) => v + + #emitter: Subscribable | Connectable + #prop?: string + + static bind< + T extends Connectable, + P extends keyof T, + >(object: T, property: P): Binding + + static bind(object: Subscribable): Binding + + static bind(emitter: Connectable | Subscribable, prop?: string) { + return new Binding(emitter, prop) + } + + private constructor(emitter: Connectable | Subscribable, prop?: string) { + this.#emitter = emitter + this.#prop = prop && kebabify(prop) + } + + toString() { + return `Binding<${this.#emitter}${this.#prop ? `, "${this.#prop}"` : ""}>` + } + + as(fn: (v: Value) => T): Binding { + const bind = new Binding(this.#emitter, this.#prop) + bind.transformFn = (v: Value) => fn(this.transformFn(v)) + return bind as unknown as Binding + } + + 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/lang/gjs/src/file.ts b/lang/gjs/src/file.ts new file mode 100644 index 0000000..7b9de3a --- /dev/null +++ b/lang/gjs/src/file.ts @@ -0,0 +1,45 @@ +import Astal from "gi://AstalIO" +import Gio from "gi://Gio" + +export function readFile(path: string): string { + return Astal.read_file(path) || "" +} + +export function readFileAsync(path: string): Promise { + 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 { + 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/lang/gjs/src/gobject.ts b/lang/gjs/src/gobject.ts new file mode 100644 index 0000000..4740764 --- /dev/null +++ b/lang/gjs/src/gobject.ts @@ -0,0 +1,180 @@ +export { default as GObject, default as default } from "gi://GObject" +export { default as Gio } from "gi://Gio" +export { default as GLib } from "gi://GLib" + +import GObject from "gi://GObject" +const meta = Symbol("meta") + +const { ParamSpec, ParamFlags } = GObject + +const kebabify = (str: string) => str + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replaceAll("_", "-") + .toLowerCase() + +type SignalDeclaration = { + flags?: GObject.SignalFlags + accumulator?: GObject.AccumulatorType + return_type?: GObject.GType + param_types?: Array +} + +type PropertyDeclaration = + | InstanceType + | { $gtype: GObject.GType } + | typeof String + | typeof Number + | typeof Boolean + | typeof Object + +type GObjectConstructor = { + [meta]?: { + Properties?: { [key: string]: GObject.ParamSpec } + Signals?: { [key: string]: GObject.SignalDefinition } + } + new(...args: any[]): any +} + +type MetaInfo = GObject.MetaInfo, never> + +export function register(options: MetaInfo = {}) { + return function (cls: GObjectConstructor) { + GObject.registerClass({ + Signals: { ...cls[meta]?.Signals }, + Properties: { ...cls[meta]?.Properties }, + ...options, + }, cls) + } +} + +export function property(declaration: PropertyDeclaration = Object) { + return function (target: any, prop: any, desc?: PropertyDescriptor) { + target.constructor[meta] ??= {} + target.constructor[meta].Properties ??= {} + + const name = kebabify(prop) + + if (!desc) { + let value = defaultValue(declaration) + + Object.defineProperty(target, prop, { + get() { + return value + }, + set(v) { + if (v !== value) { + value = v + this.notify(name) + } + }, + }) + + Object.defineProperty(target, `set_${name.replace("-", "_")}`, { + value: function (v: any) { + this[prop] = v + }, + }) + + Object.defineProperty(target, `get_${name.replace("-", "_")}`, { + value: function () { + return this[prop] + }, + }) + + target.constructor[meta].Properties[kebabify(prop)] = pspec(name, ParamFlags.READWRITE, declaration) + } + + else { + let flags = 0 + if (desc.get) flags |= ParamFlags.READABLE + if (desc.set) flags |= ParamFlags.WRITABLE + + target.constructor[meta].Properties[kebabify(prop)] = pspec(name, flags, declaration) + } + } +} + +export function signal(...params: Array<{ $gtype: GObject.GType } | typeof Object>): +(target: any, signal: any, desc?: PropertyDescriptor) => void + +export function signal(declaration?: SignalDeclaration): +(target: any, signal: any, desc?: PropertyDescriptor) => void + +export function signal( + declaration?: SignalDeclaration | { $gtype: GObject.GType } | typeof Object, + ...params: Array<{ $gtype: GObject.GType } | typeof Object> +) { + return function (target: any, signal: any, desc?: PropertyDescriptor) { + target.constructor[meta] ??= {} + target.constructor[meta].Signals ??= {} + + const name = kebabify(signal) + + if (declaration || params.length > 0) { + // @ts-expect-error TODO: type assert + const arr = [declaration, ...params].map(v => v.$gtype) + target.constructor[meta].Signals[name] = { + param_types: arr, + } + } + else { + target.constructor[meta].Signals[name] = declaration + } + + if (!desc) { + Object.defineProperty(target, signal, { + value: function (...args: any[]) { + this.emit(name, ...args) + }, + }) + } + else { + const og: ((...args: any[]) => void) = desc.value + desc.value = function (...args: any[]) { + // @ts-expect-error not typed + this.emit(name, ...args) + } + Object.defineProperty(target, `on_${name.replace("-", "_")}`, { + value: function (...args: any[]) { + return og(...args) + }, + }) + } + } +} + +function pspec(name: string, flags: number, declaration: PropertyDeclaration) { + if (declaration instanceof ParamSpec) + return declaration + + switch (declaration) { + case String: + return ParamSpec.string(name, "", "", flags, "") + case Number: + return ParamSpec.double(name, "", "", flags, -Number.MAX_VALUE, Number.MAX_VALUE, 0) + case Boolean: + return ParamSpec.boolean(name, "", "", flags, false) + case Object: + return ParamSpec.jsobject(name, "", "", flags) + default: + // @ts-expect-error misstyped + return ParamSpec.object(name, "", "", flags, declaration.$gtype) + } +} + +function defaultValue(declaration: PropertyDeclaration) { + if (declaration instanceof ParamSpec) + return declaration.get_default_value() + + switch (declaration) { + case String: + return "default-string" + case Number: + return 0 + case Boolean: + return false + case Object: + default: + return null + } +} diff --git a/lang/gjs/src/gtk3/app.ts b/lang/gjs/src/gtk3/app.ts new file mode 100644 index 0000000..1191dc4 --- /dev/null +++ b/lang/gjs/src/gtk3/app.ts @@ -0,0 +1,105 @@ +import IO from "gi://AstalIO" +import GObject from "gi://GObject" +import Astal from "gi://Astal?version=3.0" +import Gio from "gi://Gio?version=2.0" +import Gtk from "gi://Gtk?version=3.0" + +Gtk.init(null) + +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 +}> + +import { setConsoleLogDomain } from "console" +import { exit, programArgs } from "system" + +export default new (class AstalJS extends Astal.Application { + static { GObject.registerClass({ GTypeName: "AstalJS" }, this) } + + eval(body: string): Promise { + 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) => { + IO.write_sock(conn, String(response), (_, res) => + IO.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", () => { + main?.(...programArgs) + }) + + try { + this.acquire_socket() + } + catch (error) { + return client(msg => IO.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([]) + } +}) diff --git a/lang/gjs/src/gtk3/astalify.ts b/lang/gjs/src/gtk3/astalify.ts new file mode 100644 index 0000000..2cd6984 --- /dev/null +++ b/lang/gjs/src/gtk3/astalify.ts @@ -0,0 +1,325 @@ +import Astal from "gi://Astal?version=3.0" +import Gtk from "gi://Gtk?version=3.0" +import Gdk from "gi://Gdk?version=3.0" +import GObject from "gi://GObject" +import { execAsync } from "../process.js" +import Variable from "../variable.js" +import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "../binding.js" + +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)() +} + +function setProp(obj: any, prop: string, value: any) { + try { + // the setter method has to be used because + // array like properties are not bound correctly as props + 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 default function astalify< + C extends { new(...args: any[]): Gtk.Widget }, +>(cls: C) { + class Widget extends cls { + get css(): string { return Astal.widget_get_css(this) } + set css(css: string) { Astal.widget_set_css(this, css) } + get_css(): string { return this.css } + set_css(css: string) { this.css = css } + + get className(): string { return Astal.widget_get_class_names(this).join(" ") } + set className(className: string) { Astal.widget_set_class_names(this, className.split(/\s+/)) } + get_class_name(): string { return this.className } + set_class_name(className: string) { this.className = className } + + get cursor(): Cursor { return Astal.widget_get_cursor(this) as Cursor } + set cursor(cursor: Cursor) { Astal.widget_set_cursor(this, cursor) } + get_cursor(): Cursor { return this.cursor } + set_cursor(cursor: Cursor) { this.cursor = cursor } + + get clickThrough(): boolean { return Astal.widget_get_click_through(this) } + set clickThrough(clickThrough: boolean) { Astal.widget_set_click_through(this, clickThrough) } + get_click_through(): boolean { return this.clickThrough } + set_click_through(clickThrough: boolean) { this.clickThrough = clickThrough } + + declare __no_implicit_destroy: boolean + get noImplicitDestroy(): boolean { return this.__no_implicit_destroy } + set noImplicitDestroy(value: boolean) { this.__no_implicit_destroy = value } + + _setChildren(children: Gtk.Widget[]) { + children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) + + // remove + if (this instanceof Gtk.Bin) { + const ch = this.get_child() + if (ch) + this.remove(ch) + if (ch && !children.includes(ch) && !this.noImplicitDestroy) + ch?.destroy() + } + else if (this instanceof Gtk.Container) { + for (const ch of this.get_children()) { + this.remove(ch) + if (!children.includes(ch) && !this.noImplicitDestroy) + ch?.destroy() + } + } + + // TODO: add more container types + if (this instanceof Astal.Box) { + this.set_children(children) + } + + else if (this instanceof Astal.Stack) { + this.set_children(children) + } + + else if (this instanceof Astal.CenterBox) { + this.startWidget = children[0] + this.centerWidget = children[1] + this.endWidget = children[2] + } + + else if (this instanceof Astal.Overlay) { + const [child, ...overlays] = children + this.set_child(child) + this.set_overlays(overlays) + } + + else if (this instanceof Gtk.Container) { + for (const ch of children) + this.add(ch) + } + } + + toggleClassName(cn: string, cond = true) { + Astal.widget_toggle_class_name(this, cn, cond) + } + + hook( + object: Connectable, + signal: string, + callback: (self: this, ...args: any[]) => void, + ): this + hook( + object: Subscribable, + callback: (self: this, ...args: any[]) => void, + ): this + hook( + object: Connectable | Subscribable, + signalOrCallback: string | ((self: this, ...args: any[]) => void), + callback?: (self: this, ...args: any[]) => void, + ) { + if (typeof object.connect === "function" && callback) { + const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { + callback(this, ...args) + }) + this.connect("destroy", () => { + (object.disconnect as Connectable["disconnect"])(id) + }) + } + + else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { + const unsub = object.subscribe((...args: unknown[]) => { + signalOrCallback(this, ...args) + }) + this.connect("destroy", unsub) + } + + return this + } + + constructor(...params: any[]) { + super() + const [config] = params + + const { setup, child, children = [], ...props } = config + props.visible ??= true + + if (child) + children.unshift(child) + + // collect bindings + const bindings = 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 = 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) { + this._setChildren(mergedChildren.get()) + this.connect("destroy", mergedChildren.subscribe((v) => { + this._setChildren(v) + })) + } + else { + if (mergedChildren.length > 0) { + this._setChildren(mergedChildren) + } + } + + // setup signal handlers + for (const [signal, callback] of onHandlers) { + if (typeof callback === "function") { + this.connect(signal, callback) + } + else { + this.connect(signal, () => execAsync(callback) + .then(print).catch(console.error)) + } + } + + // setup bindings handlers + for (const [prop, binding] of bindings) { + if (prop === "child" || prop === "children") { + this.connect("destroy", binding.subscribe((v: any) => { + this._setChildren(v) + })) + } + this.connect("destroy", binding.subscribe((v: any) => { + setProp(this, prop, v) + })) + setProp(this, prop, binding.get()) + } + + Object.assign(this, props) + setup?.(this) + } + } + + GObject.registerClass({ + GTypeName: `Astal_${cls.name}`, + Properties: { + "class-name": GObject.ParamSpec.string( + "class-name", "", "", GObject.ParamFlags.READWRITE, "", + ), + "css": GObject.ParamSpec.string( + "css", "", "", GObject.ParamFlags.READWRITE, "", + ), + "cursor": GObject.ParamSpec.string( + "cursor", "", "", GObject.ParamFlags.READWRITE, "default", + ), + "click-through": GObject.ParamSpec.boolean( + "click-through", "", "", GObject.ParamFlags.READWRITE, false, + ), + "no-implicit-destroy": GObject.ParamSpec.boolean( + "no-implicit-destroy", "", "", GObject.ParamFlags.READWRITE, false, + ), + }, + }, Widget) + + return Widget +} + +type BindableProps = { + [K in keyof T]: Binding | T[K]; +} + +type SigHandler< + W extends InstanceType, + Args extends Array, +> = ((self: W, ...args: Args) => unknown) | string | string[] + +export type ConstructProps< + Self extends InstanceType, + Props extends Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array> = Record<`on${string}`, any[]>, +> = Partial<{ + // @ts-expect-error can't assign to unknown, but it works as expected though + [S in keyof Signals]: SigHandler +}> & Partial<{ + [Key in `on${string}`]: SigHandler +}> & BindableProps & { + className?: string + css?: string + cursor?: string + clickThrough?: boolean +}> & { + onDestroy?: (self: Self) => unknown + onDraw?: (self: Self) => unknown + onKeyPressEvent?: (self: Self, event: Gdk.Event) => unknown + onKeyReleaseEvent?: (self: Self, event: Gdk.Event) => unknown + onButtonPressEvent?: (self: Self, event: Gdk.Event) => unknown + onButtonReleaseEvent?: (self: Self, event: Gdk.Event) => unknown + onRealize?: (self: Self) => unknown + setup?: (self: Self) => void +} + +export type BindableChild = Gtk.Widget | Binding + +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/lang/gjs/src/gtk3/index.ts b/lang/gjs/src/gtk3/index.ts new file mode 100644 index 0000000..cfafbda --- /dev/null +++ b/lang/gjs/src/gtk3/index.ts @@ -0,0 +1,9 @@ +import Astal from "gi://Astal?version=3.0" +import Gtk from "gi://Gtk?version=3.0" +import Gdk from "gi://Gdk?version=3.0" +import astalify, { type ConstructProps } from "./astalify.js" + +export { Astal, Gtk, Gdk } +export { default as App } from "./app.js" +export { astalify, ConstructProps } +export * as Widget from "./widget.js" diff --git a/lang/gjs/src/gtk3/jsx-runtime.ts b/lang/gjs/src/gtk3/jsx-runtime.ts new file mode 100644 index 0000000..22dc424 --- /dev/null +++ b/lang/gjs/src/gtk3/jsx-runtime.ts @@ -0,0 +1,96 @@ +import Gtk from "gi://Gtk?version=3.0" +import { mergeBindings, type BindableChild } from "./astalify.js" +import * as Widget from "./widget.js" + +function isArrowFunction(func: any): func is (args: any) => any { + return !Object.hasOwn(func, "prototype") +} + +export function Fragment({ children = [], child }: { + child?: BindableChild + children?: Array +}) { + return mergeBindings([...children, child]) +} + +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 (children.length === 1) + props.child = children[0] + else if (children.length > 1) + props.children = children + + if (typeof ctor === "string") { + return new ctors[ctor](props) + } + + 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, + circularprogress: Widget.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, + stack: Widget.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 + circularprogress: Widget.CircularProgressProps + 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 + stack: Widget.StackProps + switch: Widget.SwitchProps + window: Widget.WindowProps + } + } +} + +export const jsxs = jsx diff --git a/lang/gjs/src/gtk3/widget.ts b/lang/gjs/src/gtk3/widget.ts new file mode 100644 index 0000000..fd70ed6 --- /dev/null +++ b/lang/gjs/src/gtk3/widget.ts @@ -0,0 +1,154 @@ +/* eslint-disable max-len */ +import Astal from "gi://Astal?version=3.0" +import Gtk from "gi://Gtk?version=3.0" +import GObject from "gi://GObject" +import astalify, { type ConstructProps, type BindableChild } from "./astalify.js" + +// Box +Object.defineProperty(Astal.Box.prototype, "children", { + get() { return this.get_children() }, + set(v) { this.set_children(v) }, +}) + +export type BoxProps = ConstructProps +export class Box extends astalify(Astal.Box) { + static { GObject.registerClass({ GTypeName: "Box" }, this) } + constructor(props?: BoxProps, ...children: Array) { super({ children, ...props } as any) } +} + +// Button +export type ButtonProps = ConstructProps +export class Button extends astalify(Astal.Button) { + static { GObject.registerClass({ GTypeName: "Button" }, this) } + constructor(props?: ButtonProps, child?: BindableChild) { super({ child, ...props } as any) } +} + +// CenterBox +export type CenterBoxProps = ConstructProps +export class CenterBox extends astalify(Astal.CenterBox) { + static { GObject.registerClass({ GTypeName: "CenterBox" }, this) } + constructor(props?: CenterBoxProps, ...children: Array) { super({ children, ...props } as any) } +} + +// CircularProgress +export type CircularProgressProps = ConstructProps +export class CircularProgress extends astalify(Astal.CircularProgress) { + static { GObject.registerClass({ GTypeName: "CircularProgress" }, this) } + constructor(props?: CircularProgressProps, child?: BindableChild) { super({ child, ...props } as any) } +} + +// DrawingArea +export type DrawingAreaProps = ConstructProps +export class DrawingArea extends astalify(Gtk.DrawingArea) { + static { GObject.registerClass({ GTypeName: "DrawingArea" }, this) } + constructor(props?: DrawingAreaProps) { super(props as any) } +} + +// Entry +export type EntryProps = ConstructProps +export class Entry extends astalify(Gtk.Entry) { + static { GObject.registerClass({ GTypeName: "Entry" }, this) } + constructor(props?: EntryProps) { super(props as any) } +} + +// EventBox +export type EventBoxProps = ConstructProps +export class EventBox extends astalify(Astal.EventBox) { + static { GObject.registerClass({ GTypeName: "EventBox" }, this) } + constructor(props?: EventBoxProps, child?: BindableChild) { super({ child, ...props } as any) } +} + +// // TODO: Fixed +// // TODO: FlowBox +// +// Icon +export type IconProps = ConstructProps +export class Icon extends astalify(Astal.Icon) { + static { GObject.registerClass({ GTypeName: "Icon" }, this) } + constructor(props?: IconProps) { super(props as any) } +} + +// Label +export type LabelProps = ConstructProps +export class Label extends astalify(Astal.Label) { + static { GObject.registerClass({ GTypeName: "Label" }, this) } + constructor(props?: LabelProps) { super(props as any) } +} + +// LevelBar +export type LevelBarProps = ConstructProps +export class LevelBar extends astalify(Astal.LevelBar) { + static { GObject.registerClass({ GTypeName: "LevelBar" }, this) } + constructor(props?: LevelBarProps) { super(props as any) } +} + +// TODO: ListBox + +// Overlay +export type OverlayProps = ConstructProps +export class Overlay extends astalify(Astal.Overlay) { + static { GObject.registerClass({ GTypeName: "Overlay" }, this) } + constructor(props?: OverlayProps, ...children: Array) { super({ children, ...props } as any) } +} + +// Revealer +export type RevealerProps = ConstructProps +export class Revealer extends astalify(Gtk.Revealer) { + static { GObject.registerClass({ GTypeName: "Revealer" }, this) } + constructor(props?: RevealerProps, child?: BindableChild) { super({ child, ...props } as any) } +} + +// Scrollable +export type ScrollableProps = ConstructProps +export class Scrollable extends astalify(Astal.Scrollable) { + static { GObject.registerClass({ GTypeName: "Scrollable" }, this) } + constructor(props?: ScrollableProps, child?: BindableChild) { super({ child, ...props } as any) } +} + +// Slider +export type SliderProps = ConstructProps +export class Slider extends astalify(Astal.Slider) { + static { GObject.registerClass({ GTypeName: "Slider" }, this) } + constructor(props?: SliderProps) { super(props as any) } +} + +// Stack +export type StackProps = ConstructProps +export class Stack extends astalify(Astal.Stack) { + static { GObject.registerClass({ GTypeName: "Stack" }, this) } + constructor(props?: StackProps, ...children: Array) { super({ children, ...props } as any) } +} + +// Switch +export type SwitchProps = ConstructProps +export class Switch extends astalify(Gtk.Switch) { + static { GObject.registerClass({ GTypeName: "Switch" }, this) } + constructor(props?: SwitchProps) { super(props as any) } +} + +// Window +export type WindowProps = ConstructProps +export class Window extends astalify(Astal.Window) { + static { GObject.registerClass({ GTypeName: "Window" }, this) } + constructor(props?: WindowProps, child?: BindableChild) { super({ child, ...props } as any) } +} diff --git a/lang/gjs/src/gtk4/app.ts b/lang/gjs/src/gtk4/app.ts new file mode 100644 index 0000000..d931f73 --- /dev/null +++ b/lang/gjs/src/gtk4/app.ts @@ -0,0 +1 @@ +// TODO: gtk4 diff --git a/lang/gjs/src/gtk4/astalify.ts b/lang/gjs/src/gtk4/astalify.ts new file mode 100644 index 0000000..d931f73 --- /dev/null +++ b/lang/gjs/src/gtk4/astalify.ts @@ -0,0 +1 @@ +// TODO: gtk4 diff --git a/lang/gjs/src/gtk4/index.ts b/lang/gjs/src/gtk4/index.ts new file mode 100644 index 0000000..d931f73 --- /dev/null +++ b/lang/gjs/src/gtk4/index.ts @@ -0,0 +1 @@ +// TODO: gtk4 diff --git a/lang/gjs/src/gtk4/jsx-runtime.ts b/lang/gjs/src/gtk4/jsx-runtime.ts new file mode 100644 index 0000000..d931f73 --- /dev/null +++ b/lang/gjs/src/gtk4/jsx-runtime.ts @@ -0,0 +1 @@ +// TODO: gtk4 diff --git a/lang/gjs/src/index.ts b/lang/gjs/src/index.ts new file mode 100644 index 0000000..161c369 --- /dev/null +++ b/lang/gjs/src/index.ts @@ -0,0 +1,6 @@ +export * from "./process.js" +export * from "./time.js" +export * from "./file.js" +export * from "./gobject.js" +export { bind, default as Binding } from "./binding.js" +export { Variable } from "./variable.js" diff --git a/lang/gjs/src/process.ts b/lang/gjs/src/process.ts new file mode 100644 index 0000000..2f7816b --- /dev/null +++ b/lang/gjs/src/process.ts @@ -0,0 +1,68 @@ +import Astal from "gi://AstalIO" + +type Args = { + cmd: string | string[] + out?: (stdout: string) => void + err?: (stderr: string) => void +} + +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 args = Array.isArray(argsOrCmd) || typeof argsOrCmd === "string" + const { cmd, err, out } = { + cmd: args ? argsOrCmd : argsOrCmd.cmd, + err: args ? onErr : argsOrCmd.err || onErr, + out: args ? onOut : argsOrCmd.out || onOut, + } + + 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 { + 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/lang/gjs/src/time.ts b/lang/gjs/src/time.ts new file mode 100644 index 0000000..a7e1e61 --- /dev/null +++ b/lang/gjs/src/time.ts @@ -0,0 +1,13 @@ +import Astal from "gi://AstalIO" + +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/lang/gjs/src/variable.ts b/lang/gjs/src/variable.ts new file mode 100644 index 0000000..9b3d3d2 --- /dev/null +++ b/lang/gjs/src/variable.ts @@ -0,0 +1,230 @@ +import Astal from "gi://AstalIO" +import Binding, { type Connectable, type Subscribable } from "./binding.js" +import { interval } from "./time.js" +import { execAsync, subprocess } from "./process.js" + +class VariableWrapper 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 + + 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(transform?: (value: T) => R): Binding { + const b = Binding.bind(this) + return transform ? b.as(transform) : b as unknown as Binding + } + + 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") + } + + onDropped(callback: () => void) { + this.variable.connect("dropped", callback) + return this as unknown as Variable + } + + onError(callback: (err: string) => void) { + delete this.errHandler + this.variable.connect("error", (_, err) => callback(err)) + return this as unknown as Variable + } + + 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 + + poll( + interval: number, + callback: (prev: T) => T | Promise + ): Variable + + poll( + interval: number, + exec: string | string[] | ((prev: T) => T | Promise), + 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 + } + + 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 + } + + observe( + objs: Array<[obj: Connectable, signal: string]>, + callback: (...args: any[]) => T, + ): Variable + + observe( + obj: Connectable, + signal: string, + callback: (...args: any[]) => T, + ): Variable + + 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 + const id = o.connect(s, set) + this.onDropped(() => o.disconnect(id)) + } + } + else { + if (typeof sigOrFn === "string") { + const id = objs.connect(sigOrFn, set) + this.onDropped(() => objs.disconnect(id)) + } + } + + return this as unknown as Variable + } + + static derive< + const Deps extends Array>, + Args extends { + [K in keyof Deps]: Deps[K] extends Subscribable ? 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 extends Omit, "bind"> { + (transform: (value: T) => R): Binding + (): Binding +} + +export const Variable = new Proxy(VariableWrapper as any, { + apply: (_t, _a, args) => new VariableWrapper(args[0]), +}) as { + derive: typeof VariableWrapper["derive"] + (init: T): Variable + new(init: T): Variable +} + +export default Variable diff --git a/lang/gjs/tsconfig.json b/lang/gjs/tsconfig.json index 71fd218..171e75b 100644 --- a/lang/gjs/tsconfig.json +++ b/lang/gjs/tsconfig.json @@ -10,9 +10,7 @@ }, "include": [ "@girs", - "lib/*", - // "gtk3/*", - // "gtk4/*", + "src/*.ts", "index.ts", ] } diff --git a/lang/lua/astal-dev-1.rockspec b/lang/lua/astal-dev-1.rockspec index d392a79..3970672 100644 --- a/lang/lua/astal-dev-1.rockspec +++ b/lang/lua/astal-dev-1.rockspec @@ -19,13 +19,19 @@ dependencies = { build = { type = "builtin", modules = { - ["astal.application"] = "lib/application.lua", - ["astal.binding"] = "lib/binding.lua", - ["astal.init"] = "lib/init.lua", - ["astal.process"] = "lib/process.lua", - ["astal.time"] = "lib/time.lua", - ["astal.variable"] = "lib/variable.lua", - ["astal.widget"] = "lib/widget.lua", - ["astal.file"] = "lib/file.lua", + ["astal.binding"] = "astal/binding.lua", + ["astal.file"] = "astal/file.lua", + ["astal.init"] = "astal/init.lua", + ["astal.process"] = "astal/process.lua", + ["astal.time"] = "astal/time.lua", + ["astal.variable"] = "astal/variable.lua", + ["astal.gtk3.app"] = "astal/gtk3/app.lua", + ["astal.gtk3.init"] = "astal/gtk3/init.lua", + ["astal.gtk3.astalify"] = "astal/gtk3/astalify.lua", + ["astal.gtk3.widget"] = "astal/gtk3/widget.lua", + -- ["astal.gtk4.app"] = "astal/gtk4/app.lua", + -- ["astal.gtk4.init"] = "astal/gtk4/init.lua", + -- ["astal.gtk4.astalify"] = "astal/gtk4/astalify.lua", + -- ["astal.gtk4.widget"] = "astal/gtk4/widget.lua", }, } diff --git a/lang/lua/astal/binding.lua b/lang/lua/astal/binding.lua new file mode 100644 index 0000000..ba1e6e4 --- /dev/null +++ b/lang/lua/astal/binding.lua @@ -0,0 +1,71 @@ +local lgi = require("lgi") +local GObject = lgi.require("GObject", "2.0") + +---@class Binding +---@field emitter table|Variable +---@field property? string +---@field transformFn function +local Binding = {} + +---@param emitter table +---@param property? string +---@return Binding +function Binding.new(emitter, property) + return setmetatable({ + emitter = emitter, + property = property, + transformFn = function(v) + return v + end, + }, Binding) +end + +function Binding:__tostring() + local str = "Binding<" .. tostring(self.emitter) + if self.property ~= nil then + str = str .. ", " .. self.property + end + return str .. ">" +end + +function Binding:get() + if self.property ~= nil and GObject.Object:is_type_of(self.emitter) then + return self.transformFn(self.emitter[self.property]) + end + if type(self.emitter.get) == "function" then + return self.transformFn(self.emitter:get()) + end + error("can not get: Not a GObject or a Variable " + self) +end + +---@param transform fun(value: any): any +---@return Binding +function Binding:as(transform) + local b = Binding.new(self.emitter, self.property) + b.transformFn = function(v) + return transform(self.transformFn(v)) + end + return b +end + +---@param callback fun(value: any) +---@return function +function Binding:subscribe(callback) + if self.property ~= nil and GObject.Object:is_type_of(self.emitter) then + local id = self.emitter.on_notify:connect(function() + callback(self:get()) + end, self.property, false) + return function() + GObject.signal_handler_disconnect(self.emitter, id) + end + end + if type(self.emitter.subscribe) == "function" then + return self.emitter:subscribe(function() + callback(self:get()) + end) + end + error("can not subscribe: Not a GObject or a Variable " + self) +end + +Binding.__index = Binding +return Binding diff --git a/lang/lua/astal/file.lua b/lang/lua/astal/file.lua new file mode 100644 index 0000000..e3be783 --- /dev/null +++ b/lang/lua/astal/file.lua @@ -0,0 +1,45 @@ +local lgi = require("lgi") +local Astal = lgi.require("AstalIO", "0.1") +local GObject = lgi.require("GObject", "2.0") + +local M = {} + +---@param path string +---@return string +function M.read_file(path) + return Astal.read_file(path) +end + +---@param path string +---@param callback fun(content: string, err: string): nil +function M.read_file_async(path, callback) + Astal.read_file_async(path, function(_, res) + local content, err = Astal.read_file_finish(res) + callback(content, err) + end) +end + +---@param path string +---@param content string +function M.write_file(path, content) + Astal.write_file(path, content) +end + +---@param path string +---@param content string +---@param callback? fun(err: string): nil +function M.write_file_async(path, content, callback) + Astal.write_file_async(path, content, function(_, res) + if type(callback) == "function" then + callback(Astal.write_file_finish(res)) + end + end) +end + +---@param path string +---@param callback fun(file: string, event: integer): nil +function M.monitor_file(path, callback) + return Astal.monitor_file(path, GObject.Closure(callback)) +end + +return M diff --git a/lang/lua/astal/gtk3/app.lua b/lang/lua/astal/gtk3/app.lua new file mode 100644 index 0000000..7895f69 --- /dev/null +++ b/lang/lua/astal/gtk3/app.lua @@ -0,0 +1,96 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "3.0") +local AstalIO = lgi.require("AstalIO", "0.1") + +local AstalLua = Astal.Application:derive("AstalLua") +local request_handler + +function AstalLua:do_request(msg, conn) + if type(request_handler) == "function" then + request_handler(msg, function(response) + AstalIO.write_sock(conn, tostring(response), function(_, res) + AstalIO.write_sock_finish(res) + end) + end) + else + Astal.Application.do_request(self, msg, conn) + end +end + +function AstalLua:quit(code) + Astal.Application.quit(self) + os.exit(code) +end + +local app = AstalLua() + +---@class StartConfig +---@field icons? string +---@field instance_name? string +---@field gtk_theme? string +---@field icon_theme? string +---@field cursor_theme? string +---@field css? string +---@field hold? boolean +---@field request_handler? fun(msg: string, response: fun(res: any)) +---@field main? fun(...): unknown +---@field client? fun(message: fun(msg: string): string, ...): unknown + +---@param config StartConfig | nil +function Astal.Application:start(config) + if config == nil then + config = {} + end + + if config.client == nil then + config.client = function() + print('Astal instance "' .. app.instance_name .. '" is already running') + os.exit(1) + end + end + + if config.hold == nil then + config.hold = true + end + + request_handler = config.request_handler + + if config.css then + self:apply_css(config.css) + end + if config.icons then + self:add_icons(config.icons) + end + if config.instance_name then + self.instance_name = config.instance_name + end + if config.gtk_theme then + self.gtk_theme = config.gtk_theme + end + if config.icon_theme then + self.icon_theme = config.icon_theme + end + if config.cursor_theme then + self.cursor_theme = config.cursor_theme + end + + app.on_activate = function() + if type(config.main) == "function" then + config.main(table.unpack(arg)) + end + if config.hold then + self:hold() + end + end + + local _, err = app:acquire_socket() + if err ~= nil then + return config.client(function(msg) + return AstalIO.send_message(self.instance_name, msg) + end, table.unpack(arg)) + end + + self:run(nil) +end + +return app diff --git a/lang/lua/astal/gtk3/astalify.lua b/lang/lua/astal/gtk3/astalify.lua new file mode 100644 index 0000000..065de40 --- /dev/null +++ b/lang/lua/astal/gtk3/astalify.lua @@ -0,0 +1,236 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "3.0") +local Gtk = lgi.require("Gtk", "3.0") +local GObject = lgi.require("GObject", "2.0") +local Binding = require("astal.lib.binding") +local Variable = require("astal.lib.variable") +local exec_async = require("astal.lib.process").exec_async + +local function filter(tbl, fn) + local copy = {} + for key, value in pairs(tbl) do + if fn(value, key) then + if type(key) == "number" then + table.insert(copy, value) + else + copy[key] = value + end + end + end + return copy +end + +local function map(tbl, fn) + local copy = {} + for key, value in pairs(tbl) do + copy[key] = fn(value) + end + return copy +end + +local flatten +flatten = function(tbl) + local copy = {} + for _, value in pairs(tbl) do + if type(value) == "table" and getmetatable(value) == nil then + for _, inner in pairs(flatten(value)) do + table.insert(copy, inner) + end + else + table.insert(copy, value) + end + end + return copy +end + +local function includes(tbl, elem) + for _, value in pairs(tbl) do + if value == elem then + return true + end + end + return false +end + +local function set_children(parent, children) + children = map(flatten(children), function(item) + if Gtk.Widget:is_type_of(item) then + return item + end + return Gtk.Label({ + visible = true, + label = tostring(item), + }) + end) + + -- remove + if Gtk.Bin:is_type_of(parent) then + local ch = parent:get_child() + if ch ~= nil then + parent:remove(ch) + end + if ch ~= nil and not includes(children, ch) and not parent.no_implicit_destroy then + ch:destroy() + end + elseif Gtk.Container:is_type_of(parent) then + for _, ch in ipairs(parent:get_children()) do + parent:remove(ch) + if ch ~= nil and not includes(children, ch) and not parent.no_implicit_destroy then + ch:destroy() + end + end + end + + -- TODO: add more container types + if Astal.Box:is_type_of(parent) then + parent:set_children(children) + elseif Astal.Stack:is_type_of(parent) then + parent:set_children(children) + elseif Astal.CenterBox:is_type_of(parent) then + parent.start_widget = children[1] + parent.center_widget = children[2] + parent.end_widget = children[3] + elseif Astal.Overlay:is_type_of(parent) then + parent:set_child(children[1]) + children[1] = nil + parent:set_overlays(children) + elseif Gtk.Container:is_type_of(parent) then + for _, child in pairs(children) do + if Gtk.Widget:is_type_of(child) then + parent:add(child) + end + end + end +end + +local function merge_bindings(array) + local function get_values(...) + local args = { ... } + local i = 0 + return map(array, function(value) + if getmetatable(value) == Binding then + i = i + 1 + return args[i] + else + return value + end + end) + end + + local bindings = filter(array, function(v) + return getmetatable(v) == Binding + end) + + if #bindings == 0 then + return array + end + + if #bindings == 1 then + return bindings[1]:as(get_values) + end + + return Variable.derive(bindings, get_values)() +end + +return function(ctor) + function ctor:hook(object, signalOrCallback, callback) + if GObject.Object:is_type_of(object) and type(signalOrCallback) == "string" then + local id + if string.sub(signalOrCallback, 1, 8) == "notify::" then + local prop = string.gsub(signalOrCallback, "notify::", "") + id = object.on_notify:connect(function() + callback(self, object[prop]) + end, prop, false) + else + id = object["on_" .. signalOrCallback]:connect(function(_, ...) + callback(self, ...) + end) + end + self.on_destroy = function() + GObject.signal_handler_disconnect(object, id) + end + elseif type(object.subscribe) == "function" then + local unsub = object.subscribe(function(...) + signalOrCallback(self, ...) + end) + self.on_destroy = unsub + else + error("can not hook: not gobject+signal or subscribable") + end + end + + function ctor:toggle_class_name(name, on) + Astal.widget_toggle_class_name(self, name, on) + end + + return function(tbl) + if tbl == nil then + tbl = {} + end + + local bindings = {} + local setup = tbl.setup + + -- collect children + local children = merge_bindings(flatten(filter(tbl, function(_, key) + return type(key) == "number" + end))) + + -- default visible to true + if type(tbl.visible) ~= "boolean" then + tbl.visible = true + end + + -- collect props + local props = filter(tbl, function(_, key) + return type(key) == "string" and key ~= "setup" + end) + + -- collect signal handlers + for prop, value in pairs(props) do + if string.sub(prop, 0, 2) == "on" and type(value) ~= "function" then + props[prop] = function() + exec_async(value, print) + end + end + end + + -- collect bindings + for prop, value in pairs(props) do + if getmetatable(value) == Binding then + bindings[prop] = value + props[prop] = value:get() + end + end + + -- construct, attach bindings, add children + local widget = ctor() + + if getmetatable(children) == Binding then + set_children(widget, children:get()) + widget.on_destroy = children:subscribe(function(v) + set_children(widget, v) + end) + else + if #children > 0 then + set_children(widget, children) + end + end + + for prop, binding in pairs(bindings) do + widget.on_destroy = binding:subscribe(function(v) + widget[prop] = v + end) + end + + for prop, value in pairs(props) do + widget[prop] = value + end + + if type(setup) == "function" then + setup(widget) + end + + return widget + end +end diff --git a/lang/lua/astal/gtk3/init.lua b/lang/lua/astal/gtk3/init.lua new file mode 100644 index 0000000..6fb5455 --- /dev/null +++ b/lang/lua/astal/gtk3/init.lua @@ -0,0 +1,5 @@ +return { + App = require("astal.gtk3.app"), + astalify = require("astal.gtk3.astalify"), + Widget = require("astal.gtk3.widget"), +} diff --git a/lang/lua/astal/gtk3/widget.lua b/lang/lua/astal/gtk3/widget.lua new file mode 100644 index 0000000..beaad6c --- /dev/null +++ b/lang/lua/astal/gtk3/widget.lua @@ -0,0 +1,90 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "3.0") +local Gtk = lgi.require("Gtk", "3.0") +local astalify = require("astal.gtk3.astalify") + +local Widget = { + astalify = astalify, + Box = astalify(Astal.Box), + Button = astalify(Astal.Button), + CenterBox = astalify(Astal.CenterBox), + CircularProgress = astalify(Astal.CircularProgress), + DrawingArea = astalify(Gtk.DrawingArea), + Entry = astalify(Gtk.Entry), + EventBox = astalify(Astal.EventBox), + -- TODO: Fixed + -- TODO: FlowBox + Icon = astalify(Astal.Icon), + Label = astalify(Gtk.Label), + LevelBar = astalify(Astal.LevelBar), + -- TODO: ListBox + Overlay = astalify(Astal.Overlay), + Revealer = astalify(Gtk.Revealer), + Scrollable = astalify(Astal.Scrollable), + Slider = astalify(Astal.Slider), + Stack = astalify(Astal.Stack), + Switch = astalify(Gtk.Switch), + Window = astalify(Astal.Window), +} + +Gtk.Widget._attribute.css = { + get = Astal.widget_get_css, + set = Astal.widget_set_css, +} + +Gtk.Widget._attribute.class_name = { + get = function(self) + local result = "" + local strings = Astal.widget_get_class_names(self) + for i, str in ipairs(strings) do + result = result .. str + if i < #strings then + result = result .. " " + end + end + return result + end, + set = function(self, class_name) + local names = {} + for word in class_name:gmatch("%S+") do + table.insert(names, word) + end + Astal.widget_set_class_names(self, names) + end, +} + +Gtk.Widget._attribute.cursor = { + get = Astal.widget_get_cursor, + set = Astal.widget_set_cursor, +} + +Gtk.Widget._attribute.click_through = { + get = Astal.widget_get_click_through, + set = Astal.widget_set_click_through, +} + +local no_implicit_destroy = {} +Gtk.Widget._attribute.no_implicit_destroy = { + get = function(self) + return no_implicit_destroy[self] or false + end, + set = function(self, v) + if no_implicit_destroy[self] == nil then + self.on_destroy = function() + no_implicit_destroy[self] = nil + end + end + no_implicit_destroy[self] = v + end, +} + +Astal.Box._attribute.children = { + get = Astal.Box.get_children, + set = Astal.Box.set_children, +} + +return setmetatable(Widget, { + __call = function(_, ctor) + return astalify(ctor) + end, +}) diff --git a/lang/lua/astal/init.lua b/lang/lua/astal/init.lua new file mode 100644 index 0000000..f442db0 --- /dev/null +++ b/lang/lua/astal/init.lua @@ -0,0 +1,27 @@ +local lgi = require("lgi") +local Binding = require("astal.binding") +local File = require("astal.file") +local Process = require("astal.proc") +local Time = require("astal.time") +local Variable = require("astal.variable") + +return { + Variable = Variable, + bind = Binding.new, + + interval = Time.interval, + timeout = Time.timeout, + idle = Time.idle, + + subprocess = Process.subprocess, + exec = Process.exec, + exec_async = Process.exec_async, + + read_file = File.read_file, + read_file_async = File.read_file_async, + write_file = File.write_file, + write_file_async = File.write_file_async, + monitor_file = File.monitor_file, + + require = lgi.require, +} diff --git a/lang/lua/astal/process.lua b/lang/lua/astal/process.lua new file mode 100644 index 0000000..b8b7436 --- /dev/null +++ b/lang/lua/astal/process.lua @@ -0,0 +1,78 @@ +local lgi = require("lgi") +local Astal = lgi.require("AstalIO", "0.1") + +local M = {} + +---@param commandline string | string[] +---@param on_stdout? fun(out: string): nil +---@param on_stderr? fun(err: string): nil +---@return { kill: function } | nil proc +function M.subprocess(commandline, on_stdout, on_stderr) + if on_stdout == nil then + on_stdout = function(out) + io.stdout:write(tostring(out) .. "\n") + end + end + + if on_stderr == nil then + on_stderr = function(err) + io.stderr:write(tostring(err) .. "\n") + end + end + + local proc, err + if type(commandline) == "table" then + proc, err = Astal.Process.subprocessv(commandline) + else + proc, err = Astal.Process.subprocess(commandline) + end + if err ~= nil then + err(err) + return nil + end + proc.on_stdout = function(_, stdoud) + on_stdout(stdoud) + end + proc.on_stderr = function(_, stderr) + on_stderr(stderr) + end + return proc +end + +---@param commandline string | string[] +---@return string, string +function M.exec(commandline) + if type(commandline) == "table" then + return Astal.Process.execv(commandline) + else + return Astal.Process.exec(commandline) + end +end + +---@param commandline string | string[] +---@param callback? fun(out: string, err: string): nil +function M.exec_async(commandline, callback) + if callback == nil then + callback = function(out, err) + if err ~= nil then + io.stdout:write(tostring(out) .. "\n") + else + io.stderr:write(tostring(err) .. "\n") + end + end + end + + if type(commandline) == "table" then + Astal.Process.exec_asyncv(commandline, function(_, res) + local out, err = Astal.Process.exec_asyncv_finish(res) + callback(out, err) + end) + else + Astal.Process.exec_async(commandline, function(_, res) + local out, err = Astal.Process.exec_finish(res) + callback(out, err) + end) + end +end + +return M diff --git a/lang/lua/astal/time.lua b/lang/lua/astal/time.lua new file mode 100644 index 0000000..7719da9 --- /dev/null +++ b/lang/lua/astal/time.lua @@ -0,0 +1,27 @@ +local lgi = require("lgi") +local Astal = lgi.require("AstalIO", "0.1") +local GObject = lgi.require("GObject", "2.0") + +local M = {} + +---@param interval number +---@param fn function +---@return { cancel: function, on_now: function } +function M.interval(interval, fn) + return Astal.Time.interval(interval, GObject.Closure(fn)) +end + +---@param timeout number +---@param fn function +---@return { cancel: function, on_now: function } +function M.timeout(timeout, fn) + return Astal.Time.timeout(timeout, GObject.Closure(fn)) +end + +---@param fn function +---@return { cancel: function, on_now: function } +function M.idle(fn) + return Astal.Time.idle(GObject.Closure(fn)) +end + +return M diff --git a/lang/lua/astal/variable.lua b/lang/lua/astal/variable.lua new file mode 100644 index 0000000..5a5e169 --- /dev/null +++ b/lang/lua/astal/variable.lua @@ -0,0 +1,276 @@ +local lgi = require("lgi") +local Astal = lgi.require("AstalIO", "0.1") +local GObject = lgi.require("GObject", "2.0") +local Binding = require("astal.binding") +local Time = require("astal.time") +local Process = require("astal.process") + +---@class Variable +---@field private variable table +---@field private err_handler? function +---@field private _value any +---@field private _poll? table +---@field private _watch? table +---@field private poll_interval number +---@field private poll_exec? string[] | string +---@field private poll_transform? fun(next: any, prev: any): any +---@field private poll_fn? function +---@field private watch_transform? fun(next: any, prev: any): any +---@field private watch_exec? string[] | string +local Variable = {} +Variable.__index = Variable + +---@param value any +---@return Variable +function Variable.new(value) + local v = Astal.VariableBase() + local variable = setmetatable({ + variable = v, + _value = value, + }, Variable) + v.on_dropped = function() + variable:stop_watch() + variable:stop_watch() + end + v.on_error = function(_, err) + if variable.err_handler then + variable.err_handler(err) + end + end + return variable +end + +---@param transform function +---@return Binding +function Variable:__call(transform) + if transform == nil then + transform = function(v) + return v + end + return Binding.new(self) + end + return Binding.new(self):as(transform) +end + +function Variable:__tostring() + return "Variable<" .. tostring(self:get()) .. ">" +end + +function Variable:get() + return self._value or nil +end + +function Variable:set(value) + if value ~= self:get() then + self._value = value + self.variable:emit_changed() + end +end + +function Variable:start_poll() + if self._poll ~= nil then + return + end + + if self.poll_fn then + self._poll = Time.interval(self.poll_interval, function() + self:set(self.poll_fn(self:get())) + end) + elseif self.poll_exec then + self._poll = Time.interval(self.poll_interval, function() + Process.exec_async(self.poll_exec, function(out, err) + if err ~= nil then + return self.variable.emit_error(err) + end + self:set(self.poll_transform(out, self:get())) + end) + end) + end +end + +function Variable:start_watch() + if self._watch then + return + end + + self._watch = Process.subprocess(self.watch_exec, function(out) + self:set(self.watch_transform(out, self:get())) + end, function(err) + self.variable.emit_error(err) + end) +end + +function Variable:stop_poll() + if self._poll then + self._poll.cancel() + end + self._poll = nil +end + +function Variable:stop_watch() + if self._watch then + self._watch.kill() + end + self._watch = nil +end + +function Variable:is_polling() + return self._poll ~= nil +end + +function Variable:is_watching() + return self._watch ~= nil +end + +function Variable:drop() + self.variable.emit_dropped() +end + +---@param callback function +---@return Variable +function Variable:on_dropped(callback) + self.variable.on_dropped = callback + return self +end + +---@param callback function +---@return Variable +function Variable:on_error(callback) + self.err_handler = nil + self.variable.on_eror = function(_, err) + callback(err) + end + return self +end + +---@param callback fun(value: any) +---@return function +function Variable:subscribe(callback) + local id = self.variable.on_changed:connect(function() + callback(self:get()) + end) + return function() + GObject.signal_handler_disconnect(self.variable, id) + end +end + +---@param interval number +---@param exec string | string[] | function +---@param transform? fun(next: any, prev: any): any +function Variable:poll(interval, exec, transform) + if transform == nil then + transform = function(next) + return next + end + end + self:stop_poll() + self.poll_interval = interval + self.poll_transform = transform + + if type(exec) == "function" then + self.poll_fn = exec + self.poll_exec = nil + else + self.poll_exec = exec + self.poll_fn = nil + end + self:start_poll() + return self +end + +---@param exec string | string[] +---@param transform? fun(next: any, prev: any): any +function Variable:watch(exec, transform) + if transform == nil then + transform = function(next) + return next + end + end + self:stop_poll() + self.watch_exec = exec + self.watch_transform = transform + self:start_watch() + return self +end + +---@param object table | table[] +---@param sigOrFn string | fun(...): any +---@param callback fun(...): any +---@return Variable +function Variable:observe(object, sigOrFn, callback) + local f + if type(sigOrFn) == "function" then + f = sigOrFn + elseif type(callback) == "function" then + f = callback + else + f = function() + return self:get() + end + end + local set = function(...) + self:set(f(...)) + end + + if type(sigOrFn) == "string" then + object["on_" .. sigOrFn]:connect(set) + else + for _, obj in ipairs(object) do + obj[1]["on_" .. obj[2]]:connect(set) + end + end + return self +end + +---@param deps Variable | (Binding | Variable)[] +---@param transform? fun(...): any +---@return Variable +function Variable.derive(deps, transform) + if type(transform) == "nil" then + transform = function(...) + return { ... } + end + end + + if getmetatable(deps) == Variable then + local var = Variable.new(transform(deps:get())) + deps:subscribe(function(v) + var:set(transform(v)) + end) + return var + end + + for i, var in ipairs(deps) do + if getmetatable(var) == Variable then + deps[i] = Binding.new(var) + end + end + + local update = function() + local params = {} + for i, binding in ipairs(deps) do + params[i] = binding:get() + end + return transform(table.unpack(params), 1, #deps) + end + + local var = Variable.new(update()) + + local unsubs = {} + for i, b in ipairs(deps) do + unsubs[i] = b:subscribe(update) + end + + var.variable.on_dropped = function() + for _, unsub in ipairs(unsubs) do + unsub() + end + end + return var +end + +return setmetatable(Variable, { + __call = function(_, v) + return Variable.new(v) + end, +}) diff --git a/lang/lua/gtk3/app.lua b/lang/lua/gtk3/app.lua deleted file mode 100644 index 7895f69..0000000 --- a/lang/lua/gtk3/app.lua +++ /dev/null @@ -1,96 +0,0 @@ -local lgi = require("lgi") -local Astal = lgi.require("Astal", "3.0") -local AstalIO = lgi.require("AstalIO", "0.1") - -local AstalLua = Astal.Application:derive("AstalLua") -local request_handler - -function AstalLua:do_request(msg, conn) - if type(request_handler) == "function" then - request_handler(msg, function(response) - AstalIO.write_sock(conn, tostring(response), function(_, res) - AstalIO.write_sock_finish(res) - end) - end) - else - Astal.Application.do_request(self, msg, conn) - end -end - -function AstalLua:quit(code) - Astal.Application.quit(self) - os.exit(code) -end - -local app = AstalLua() - ----@class StartConfig ----@field icons? string ----@field instance_name? string ----@field gtk_theme? string ----@field icon_theme? string ----@field cursor_theme? string ----@field css? string ----@field hold? boolean ----@field request_handler? fun(msg: string, response: fun(res: any)) ----@field main? fun(...): unknown ----@field client? fun(message: fun(msg: string): string, ...): unknown - ----@param config StartConfig | nil -function Astal.Application:start(config) - if config == nil then - config = {} - end - - if config.client == nil then - config.client = function() - print('Astal instance "' .. app.instance_name .. '" is already running') - os.exit(1) - end - end - - if config.hold == nil then - config.hold = true - end - - request_handler = config.request_handler - - if config.css then - self:apply_css(config.css) - end - if config.icons then - self:add_icons(config.icons) - end - if config.instance_name then - self.instance_name = config.instance_name - end - if config.gtk_theme then - self.gtk_theme = config.gtk_theme - end - if config.icon_theme then - self.icon_theme = config.icon_theme - end - if config.cursor_theme then - self.cursor_theme = config.cursor_theme - end - - app.on_activate = function() - if type(config.main) == "function" then - config.main(table.unpack(arg)) - end - if config.hold then - self:hold() - end - end - - local _, err = app:acquire_socket() - if err ~= nil then - return config.client(function(msg) - return AstalIO.send_message(self.instance_name, msg) - end, table.unpack(arg)) - end - - self:run(nil) -end - -return app diff --git a/lang/lua/gtk3/astalify.lua b/lang/lua/gtk3/astalify.lua deleted file mode 100644 index 065de40..0000000 --- a/lang/lua/gtk3/astalify.lua +++ /dev/null @@ -1,236 +0,0 @@ -local lgi = require("lgi") -local Astal = lgi.require("Astal", "3.0") -local Gtk = lgi.require("Gtk", "3.0") -local GObject = lgi.require("GObject", "2.0") -local Binding = require("astal.lib.binding") -local Variable = require("astal.lib.variable") -local exec_async = require("astal.lib.process").exec_async - -local function filter(tbl, fn) - local copy = {} - for key, value in pairs(tbl) do - if fn(value, key) then - if type(key) == "number" then - table.insert(copy, value) - else - copy[key] = value - end - end - end - return copy -end - -local function map(tbl, fn) - local copy = {} - for key, value in pairs(tbl) do - copy[key] = fn(value) - end - return copy -end - -local flatten -flatten = function(tbl) - local copy = {} - for _, value in pairs(tbl) do - if type(value) == "table" and getmetatable(value) == nil then - for _, inner in pairs(flatten(value)) do - table.insert(copy, inner) - end - else - table.insert(copy, value) - end - end - return copy -end - -local function includes(tbl, elem) - for _, value in pairs(tbl) do - if value == elem then - return true - end - end - return false -end - -local function set_children(parent, children) - children = map(flatten(children), function(item) - if Gtk.Widget:is_type_of(item) then - return item - end - return Gtk.Label({ - visible = true, - label = tostring(item), - }) - end) - - -- remove - if Gtk.Bin:is_type_of(parent) then - local ch = parent:get_child() - if ch ~= nil then - parent:remove(ch) - end - if ch ~= nil and not includes(children, ch) and not parent.no_implicit_destroy then - ch:destroy() - end - elseif Gtk.Container:is_type_of(parent) then - for _, ch in ipairs(parent:get_children()) do - parent:remove(ch) - if ch ~= nil and not includes(children, ch) and not parent.no_implicit_destroy then - ch:destroy() - end - end - end - - -- TODO: add more container types - if Astal.Box:is_type_of(parent) then - parent:set_children(children) - elseif Astal.Stack:is_type_of(parent) then - parent:set_children(children) - elseif Astal.CenterBox:is_type_of(parent) then - parent.start_widget = children[1] - parent.center_widget = children[2] - parent.end_widget = children[3] - elseif Astal.Overlay:is_type_of(parent) then - parent:set_child(children[1]) - children[1] = nil - parent:set_overlays(children) - elseif Gtk.Container:is_type_of(parent) then - for _, child in pairs(children) do - if Gtk.Widget:is_type_of(child) then - parent:add(child) - end - end - end -end - -local function merge_bindings(array) - local function get_values(...) - local args = { ... } - local i = 0 - return map(array, function(value) - if getmetatable(value) == Binding then - i = i + 1 - return args[i] - else - return value - end - end) - end - - local bindings = filter(array, function(v) - return getmetatable(v) == Binding - end) - - if #bindings == 0 then - return array - end - - if #bindings == 1 then - return bindings[1]:as(get_values) - end - - return Variable.derive(bindings, get_values)() -end - -return function(ctor) - function ctor:hook(object, signalOrCallback, callback) - if GObject.Object:is_type_of(object) and type(signalOrCallback) == "string" then - local id - if string.sub(signalOrCallback, 1, 8) == "notify::" then - local prop = string.gsub(signalOrCallback, "notify::", "") - id = object.on_notify:connect(function() - callback(self, object[prop]) - end, prop, false) - else - id = object["on_" .. signalOrCallback]:connect(function(_, ...) - callback(self, ...) - end) - end - self.on_destroy = function() - GObject.signal_handler_disconnect(object, id) - end - elseif type(object.subscribe) == "function" then - local unsub = object.subscribe(function(...) - signalOrCallback(self, ...) - end) - self.on_destroy = unsub - else - error("can not hook: not gobject+signal or subscribable") - end - end - - function ctor:toggle_class_name(name, on) - Astal.widget_toggle_class_name(self, name, on) - end - - return function(tbl) - if tbl == nil then - tbl = {} - end - - local bindings = {} - local setup = tbl.setup - - -- collect children - local children = merge_bindings(flatten(filter(tbl, function(_, key) - return type(key) == "number" - end))) - - -- default visible to true - if type(tbl.visible) ~= "boolean" then - tbl.visible = true - end - - -- collect props - local props = filter(tbl, function(_, key) - return type(key) == "string" and key ~= "setup" - end) - - -- collect signal handlers - for prop, value in pairs(props) do - if string.sub(prop, 0, 2) == "on" and type(value) ~= "function" then - props[prop] = function() - exec_async(value, print) - end - end - end - - -- collect bindings - for prop, value in pairs(props) do - if getmetatable(value) == Binding then - bindings[prop] = value - props[prop] = value:get() - end - end - - -- construct, attach bindings, add children - local widget = ctor() - - if getmetatable(children) == Binding then - set_children(widget, children:get()) - widget.on_destroy = children:subscribe(function(v) - set_children(widget, v) - end) - else - if #children > 0 then - set_children(widget, children) - end - end - - for prop, binding in pairs(bindings) do - widget.on_destroy = binding:subscribe(function(v) - widget[prop] = v - end) - end - - for prop, value in pairs(props) do - widget[prop] = value - end - - if type(setup) == "function" then - setup(widget) - end - - return widget - end -end diff --git a/lang/lua/gtk3/widget.lua b/lang/lua/gtk3/widget.lua deleted file mode 100644 index beaad6c..0000000 --- a/lang/lua/gtk3/widget.lua +++ /dev/null @@ -1,90 +0,0 @@ -local lgi = require("lgi") -local Astal = lgi.require("Astal", "3.0") -local Gtk = lgi.require("Gtk", "3.0") -local astalify = require("astal.gtk3.astalify") - -local Widget = { - astalify = astalify, - Box = astalify(Astal.Box), - Button = astalify(Astal.Button), - CenterBox = astalify(Astal.CenterBox), - CircularProgress = astalify(Astal.CircularProgress), - DrawingArea = astalify(Gtk.DrawingArea), - Entry = astalify(Gtk.Entry), - EventBox = astalify(Astal.EventBox), - -- TODO: Fixed - -- TODO: FlowBox - Icon = astalify(Astal.Icon), - Label = astalify(Gtk.Label), - LevelBar = astalify(Astal.LevelBar), - -- TODO: ListBox - Overlay = astalify(Astal.Overlay), - Revealer = astalify(Gtk.Revealer), - Scrollable = astalify(Astal.Scrollable), - Slider = astalify(Astal.Slider), - Stack = astalify(Astal.Stack), - Switch = astalify(Gtk.Switch), - Window = astalify(Astal.Window), -} - -Gtk.Widget._attribute.css = { - get = Astal.widget_get_css, - set = Astal.widget_set_css, -} - -Gtk.Widget._attribute.class_name = { - get = function(self) - local result = "" - local strings = Astal.widget_get_class_names(self) - for i, str in ipairs(strings) do - result = result .. str - if i < #strings then - result = result .. " " - end - end - return result - end, - set = function(self, class_name) - local names = {} - for word in class_name:gmatch("%S+") do - table.insert(names, word) - end - Astal.widget_set_class_names(self, names) - end, -} - -Gtk.Widget._attribute.cursor = { - get = Astal.widget_get_cursor, - set = Astal.widget_set_cursor, -} - -Gtk.Widget._attribute.click_through = { - get = Astal.widget_get_click_through, - set = Astal.widget_set_click_through, -} - -local no_implicit_destroy = {} -Gtk.Widget._attribute.no_implicit_destroy = { - get = function(self) - return no_implicit_destroy[self] or false - end, - set = function(self, v) - if no_implicit_destroy[self] == nil then - self.on_destroy = function() - no_implicit_destroy[self] = nil - end - end - no_implicit_destroy[self] = v - end, -} - -Astal.Box._attribute.children = { - get = Astal.Box.get_children, - set = Astal.Box.set_children, -} - -return setmetatable(Widget, { - __call = function(_, ctor) - return astalify(ctor) - end, -}) diff --git a/lang/lua/init.lua b/lang/lua/init.lua deleted file mode 100644 index b6ab30c..0000000 --- a/lang/lua/init.lua +++ /dev/null @@ -1,27 +0,0 @@ -local lgi = require("lgi") -local Binding = require("astal.lib.binding") -local File = require("astal.lib.file") -local Process = require("astal.lib.process") -local Time = require("astal.lib.time") -local Variable = require("astal.lib.variable") - -return { - Variable = Variable, - bind = Binding.new, - - interval = Time.interval, - timeout = Time.timeout, - idle = Time.idle, - - subprocess = Process.subprocess, - exec = Process.exec, - exec_async = Process.exec_async, - - read_file = File.read_file, - read_file_async = File.read_file_async, - write_file = File.write_file, - write_file_async = File.write_file_async, - monitor_file = File.monitor_file, - - require = lgi.require, -} diff --git a/lang/lua/lib/binding.lua b/lang/lua/lib/binding.lua deleted file mode 100644 index ba1e6e4..0000000 --- a/lang/lua/lib/binding.lua +++ /dev/null @@ -1,71 +0,0 @@ -local lgi = require("lgi") -local GObject = lgi.require("GObject", "2.0") - ----@class Binding ----@field emitter table|Variable ----@field property? string ----@field transformFn function -local Binding = {} - ----@param emitter table ----@param property? string ----@return Binding -function Binding.new(emitter, property) - return setmetatable({ - emitter = emitter, - property = property, - transformFn = function(v) - return v - end, - }, Binding) -end - -function Binding:__tostring() - local str = "Binding<" .. tostring(self.emitter) - if self.property ~= nil then - str = str .. ", " .. self.property - end - return str .. ">" -end - -function Binding:get() - if self.property ~= nil and GObject.Object:is_type_of(self.emitter) then - return self.transformFn(self.emitter[self.property]) - end - if type(self.emitter.get) == "function" then - return self.transformFn(self.emitter:get()) - end - error("can not get: Not a GObject or a Variable " + self) -end - ----@param transform fun(value: any): any ----@return Binding -function Binding:as(transform) - local b = Binding.new(self.emitter, self.property) - b.transformFn = function(v) - return transform(self.transformFn(v)) - end - return b -end - ----@param callback fun(value: any) ----@return function -function Binding:subscribe(callback) - if self.property ~= nil and GObject.Object:is_type_of(self.emitter) then - local id = self.emitter.on_notify:connect(function() - callback(self:get()) - end, self.property, false) - return function() - GObject.signal_handler_disconnect(self.emitter, id) - end - end - if type(self.emitter.subscribe) == "function" then - return self.emitter:subscribe(function() - callback(self:get()) - end) - end - error("can not subscribe: Not a GObject or a Variable " + self) -end - -Binding.__index = Binding -return Binding diff --git a/lang/lua/lib/file.lua b/lang/lua/lib/file.lua deleted file mode 100644 index e3be783..0000000 --- a/lang/lua/lib/file.lua +++ /dev/null @@ -1,45 +0,0 @@ -local lgi = require("lgi") -local Astal = lgi.require("AstalIO", "0.1") -local GObject = lgi.require("GObject", "2.0") - -local M = {} - ----@param path string ----@return string -function M.read_file(path) - return Astal.read_file(path) -end - ----@param path string ----@param callback fun(content: string, err: string): nil -function M.read_file_async(path, callback) - Astal.read_file_async(path, function(_, res) - local content, err = Astal.read_file_finish(res) - callback(content, err) - end) -end - ----@param path string ----@param content string -function M.write_file(path, content) - Astal.write_file(path, content) -end - ----@param path string ----@param content string ----@param callback? fun(err: string): nil -function M.write_file_async(path, content, callback) - Astal.write_file_async(path, content, function(_, res) - if type(callback) == "function" then - callback(Astal.write_file_finish(res)) - end - end) -end - ----@param path string ----@param callback fun(file: string, event: integer): nil -function M.monitor_file(path, callback) - return Astal.monitor_file(path, GObject.Closure(callback)) -end - -return M diff --git a/lang/lua/lib/process.lua b/lang/lua/lib/process.lua deleted file mode 100644 index b8b7436..0000000 --- a/lang/lua/lib/process.lua +++ /dev/null @@ -1,78 +0,0 @@ -local lgi = require("lgi") -local Astal = lgi.require("AstalIO", "0.1") - -local M = {} - ----@param commandline string | string[] ----@param on_stdout? fun(out: string): nil ----@param on_stderr? fun(err: string): nil ----@return { kill: function } | nil proc -function M.subprocess(commandline, on_stdout, on_stderr) - if on_stdout == nil then - on_stdout = function(out) - io.stdout:write(tostring(out) .. "\n") - end - end - - if on_stderr == nil then - on_stderr = function(err) - io.stderr:write(tostring(err) .. "\n") - end - end - - local proc, err - if type(commandline) == "table" then - proc, err = Astal.Process.subprocessv(commandline) - else - proc, err = Astal.Process.subprocess(commandline) - end - if err ~= nil then - err(err) - return nil - end - proc.on_stdout = function(_, stdoud) - on_stdout(stdoud) - end - proc.on_stderr = function(_, stderr) - on_stderr(stderr) - end - return proc -end - ----@param commandline string | string[] ----@return string, string -function M.exec(commandline) - if type(commandline) == "table" then - return Astal.Process.execv(commandline) - else - return Astal.Process.exec(commandline) - end -end - ----@param commandline string | string[] ----@param callback? fun(out: string, err: string): nil -function M.exec_async(commandline, callback) - if callback == nil then - callback = function(out, err) - if err ~= nil then - io.stdout:write(tostring(out) .. "\n") - else - io.stderr:write(tostring(err) .. "\n") - end - end - end - - if type(commandline) == "table" then - Astal.Process.exec_asyncv(commandline, function(_, res) - local out, err = Astal.Process.exec_asyncv_finish(res) - callback(out, err) - end) - else - Astal.Process.exec_async(commandline, function(_, res) - local out, err = Astal.Process.exec_finish(res) - callback(out, err) - end) - end -end - -return M diff --git a/lang/lua/lib/time.lua b/lang/lua/lib/time.lua deleted file mode 100644 index 7719da9..0000000 --- a/lang/lua/lib/time.lua +++ /dev/null @@ -1,27 +0,0 @@ -local lgi = require("lgi") -local Astal = lgi.require("AstalIO", "0.1") -local GObject = lgi.require("GObject", "2.0") - -local M = {} - ----@param interval number ----@param fn function ----@return { cancel: function, on_now: function } -function M.interval(interval, fn) - return Astal.Time.interval(interval, GObject.Closure(fn)) -end - ----@param timeout number ----@param fn function ----@return { cancel: function, on_now: function } -function M.timeout(timeout, fn) - return Astal.Time.timeout(timeout, GObject.Closure(fn)) -end - ----@param fn function ----@return { cancel: function, on_now: function } -function M.idle(fn) - return Astal.Time.idle(GObject.Closure(fn)) -end - -return M diff --git a/lang/lua/lib/variable.lua b/lang/lua/lib/variable.lua deleted file mode 100644 index c93d04d..0000000 --- a/lang/lua/lib/variable.lua +++ /dev/null @@ -1,276 +0,0 @@ -local lgi = require("lgi") -local Astal = lgi.require("AstalIO", "0.1") -local GObject = lgi.require("GObject", "2.0") -local Binding = require("astal.lib.binding") -local Time = require("astal.lib.time") -local Process = require("astal.lib.process") - ----@class Variable ----@field private variable table ----@field private err_handler? function ----@field private _value any ----@field private _poll? table ----@field private _watch? table ----@field private poll_interval number ----@field private poll_exec? string[] | string ----@field private poll_transform? fun(next: any, prev: any): any ----@field private poll_fn? function ----@field private watch_transform? fun(next: any, prev: any): any ----@field private watch_exec? string[] | string -local Variable = {} -Variable.__index = Variable - ----@param value any ----@return Variable -function Variable.new(value) - local v = Astal.VariableBase() - local variable = setmetatable({ - variable = v, - _value = value, - }, Variable) - v.on_dropped = function() - variable:stop_watch() - variable:stop_watch() - end - v.on_error = function(_, err) - if variable.err_handler then - variable.err_handler(err) - end - end - return variable -end - ----@param transform function ----@return Binding -function Variable:__call(transform) - if transform == nil then - transform = function(v) - return v - end - return Binding.new(self) - end - return Binding.new(self):as(transform) -end - -function Variable:__tostring() - return "Variable<" .. tostring(self:get()) .. ">" -end - -function Variable:get() - return self._value or nil -end - -function Variable:set(value) - if value ~= self:get() then - self._value = value - self.variable:emit_changed() - end -end - -function Variable:start_poll() - if self._poll ~= nil then - return - end - - if self.poll_fn then - self._poll = Time.interval(self.poll_interval, function() - self:set(self.poll_fn(self:get())) - end) - elseif self.poll_exec then - self._poll = Time.interval(self.poll_interval, function() - Process.exec_async(self.poll_exec, function(out, err) - if err ~= nil then - return self.variable.emit_error(err) - end - self:set(self.poll_transform(out, self:get())) - end) - end) - end -end - -function Variable:start_watch() - if self._watch then - return - end - - self._watch = Process.subprocess(self.watch_exec, function(out) - self:set(self.watch_transform(out, self:get())) - end, function(err) - self.variable.emit_error(err) - end) -end - -function Variable:stop_poll() - if self._poll then - self._poll.cancel() - end - self._poll = nil -end - -function Variable:stop_watch() - if self._watch then - self._watch.kill() - end - self._watch = nil -end - -function Variable:is_polling() - return self._poll ~= nil -end - -function Variable:is_watching() - return self._watch ~= nil -end - -function Variable:drop() - self.variable.emit_dropped() -end - ----@param callback function ----@return Variable -function Variable:on_dropped(callback) - self.variable.on_dropped = callback - return self -end - ----@param callback function ----@return Variable -function Variable:on_error(callback) - self.err_handler = nil - self.variable.on_eror = function(_, err) - callback(err) - end - return self -end - ----@param callback fun(value: any) ----@return function -function Variable:subscribe(callback) - local id = self.variable.on_changed:connect(function() - callback(self:get()) - end) - return function() - GObject.signal_handler_disconnect(self.variable, id) - end -end - ----@param interval number ----@param exec string | string[] | function ----@param transform? fun(next: any, prev: any): any -function Variable:poll(interval, exec, transform) - if transform == nil then - transform = function(next) - return next - end - end - self:stop_poll() - self.poll_interval = interval - self.poll_transform = transform - - if type(exec) == "function" then - self.poll_fn = exec - self.poll_exec = nil - else - self.poll_exec = exec - self.poll_fn = nil - end - self:start_poll() - return self -end - ----@param exec string | string[] ----@param transform? fun(next: any, prev: any): any -function Variable:watch(exec, transform) - if transform == nil then - transform = function(next) - return next - end - end - self:stop_poll() - self.watch_exec = exec - self.watch_transform = transform - self:start_watch() - return self -end - ----@param object table | table[] ----@param sigOrFn string | fun(...): any ----@param callback fun(...): any ----@return Variable -function Variable:observe(object, sigOrFn, callback) - local f - if type(sigOrFn) == "function" then - f = sigOrFn - elseif type(callback) == "function" then - f = callback - else - f = function() - return self:get() - end - end - local set = function(...) - self:set(f(...)) - end - - if type(sigOrFn) == "string" then - object["on_" .. sigOrFn]:connect(set) - else - for _, obj in ipairs(object) do - obj[1]["on_" .. obj[2]]:connect(set) - end - end - return self -end - ----@param deps Variable | (Binding | Variable)[] ----@param transform? fun(...): any ----@return Variable -function Variable.derive(deps, transform) - if type(transform) == "nil" then - transform = function(...) - return { ... } - end - end - - if getmetatable(deps) == Variable then - local var = Variable.new(transform(deps:get())) - deps:subscribe(function(v) - var:set(transform(v)) - end) - return var - end - - for i, var in ipairs(deps) do - if getmetatable(var) == Variable then - deps[i] = Binding.new(var) - end - end - - local update = function() - local params = {} - for i, binding in ipairs(deps) do - params[i] = binding:get() - end - return transform(table.unpack(params), 1, #deps) - end - - local var = Variable.new(update()) - - local unsubs = {} - for i, b in ipairs(deps) do - unsubs[i] = b:subscribe(update) - end - - var.variable.on_dropped = function() - for _, unsub in ipairs(unsubs) do - unsub() - end - end - return var -end - -return setmetatable(Variable, { - __call = function(_, v) - return Variable.new(v) - end, -}) -- cgit v1.2.3