summaryrefslogtreecommitdiff
path: root/lang/gjs/src/binding.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lang/gjs/src/binding.ts')
-rw-r--r--lang/gjs/src/binding.ts89
1 files changed, 89 insertions, 0 deletions
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<T = unknown> {
+ subscribe(callback: (value: T) => void): () => void
+ get(): T
+ [key: string]: any
+}
+
+export interface Connectable {
+ connect(signal: string, callback: (...args: any[]) => unknown): number
+ disconnect(id: number): void
+ [key: string]: any
+}
+
+export default class Binding<Value> {
+ private transformFn = (v: any) => v
+
+ #emitter: Subscribable<Value> | Connectable
+ #prop?: string
+
+ static bind<
+ T extends Connectable,
+ P extends keyof T,
+ >(object: T, property: P): Binding<T[P]>
+
+ static bind<T>(object: Subscribable<T>): Binding<T>
+
+ static bind(emitter: Connectable | Subscribable, prop?: string) {
+ return new Binding(emitter, prop)
+ }
+
+ private constructor(emitter: Connectable | Subscribable<Value>, prop?: string) {
+ this.#emitter = emitter
+ this.#prop = prop && kebabify(prop)
+ }
+
+ toString() {
+ return `Binding<${this.#emitter}${this.#prop ? `, "${this.#prop}"` : ""}>`
+ }
+
+ as<T>(fn: (v: Value) => T): Binding<T> {
+ const bind = new Binding(this.#emitter, this.#prop)
+ bind.transformFn = (v: Value) => fn(this.transformFn(v))
+ return bind as unknown as Binding<T>
+ }
+
+ get(): Value {
+ if (typeof this.#emitter.get === "function")
+ return this.transformFn(this.#emitter.get())
+
+ if (typeof this.#prop === "string") {
+ const getter = `get_${snakeify(this.#prop)}`
+ if (typeof this.#emitter[getter] === "function")
+ return this.transformFn(this.#emitter[getter]())
+
+ return this.transformFn(this.#emitter[this.#prop])
+ }
+
+ throw Error("can not get value of binding")
+ }
+
+ subscribe(callback: (value: Value) => void): () => void {
+ if (typeof this.#emitter.subscribe === "function") {
+ return this.#emitter.subscribe(() => {
+ callback(this.get())
+ })
+ }
+ else if (typeof this.#emitter.connect === "function") {
+ const signal = `notify::${this.#prop}`
+ const id = this.#emitter.connect(signal, () => {
+ callback(this.get())
+ })
+ return () => {
+ (this.#emitter.disconnect as Connectable["disconnect"])(id)
+ }
+ }
+ throw Error(`${this.#emitter} is not bindable`)
+ }
+}
+
+export const { bind } = Binding