UNPKG

gnim

Version:

Library which brings JSX and reactivity to GNOME JavaScript.

497 lines (435 loc) 15.5 kB
/** * In the future I would like to make type declaration in decorators optional * and infer it from typescript types at transpile time. Currently, we could * either use stage 2 decorators with the "emitDecoratorMetadata" and * "experimentalDecorators" tsconfig options. However, metadata is not supported * by esbuild which is what I'm mostly targeting as the bundler for performance * reasons. https://github.com/evanw/esbuild/issues/257 * However, I believe that we should not use stage 2 anymore, * so I'm waiting for a better alternative. */ import GObject from "gi://GObject" import GLib from "gi://GLib" import { definePropertyGetter, kebabify } from "./util.js" const priv = Symbol("gobject private") const { defineProperty, fromEntries, entries } = Object const { Object: GObj, registerClass } = GObject export { GObject as default } export { GObj as Object } export const SignalFlags = GObject.SignalFlags export type SignalFlags = GObject.SignalFlags export const AccumulatorType = GObject.AccumulatorType export type AccumulatorType = GObject.AccumulatorType export type ParamSpec<T = unknown> = GObject.ParamSpec<T> export const ParamSpec = GObject.ParamSpec export type ParamFlags = GObject.ParamFlags export const ParamFlags = GObject.ParamFlags export type GType<T = unknown> = GObject.GType<T> type GObj = GObject.Object interface GObjPrivate extends GObj { [priv]: Record<string, any> } type Meta = { properties?: { [fieldName: string]: { flags: ParamFlags type: PropertyTypeDeclaration<unknown> } } signals?: { [key: string]: { default?: boolean flags?: SignalFlags accumulator?: AccumulatorType return_type?: GType param_types?: Array<GType> method: (...arg: any[]) => unknown } } } type Context = { private: false; static: false; name: string } type PropertyContext<T> = ClassFieldDecoratorContext<GObj, T> & Context type GetterContext<T> = ClassGetterDecoratorContext<GObj, T> & Context type SetterContext<T> = ClassSetterDecoratorContext<GObj, T> & Context type SignalContext<T extends () => any> = ClassMethodDecoratorContext<GObj, T> & Context type SignalOptions = { default?: boolean flags?: SignalFlags accumulator?: AccumulatorType } type PropertyTypeDeclaration<T> = | ((name: string, flags: ParamFlags) => ParamSpec<T>) | ParamSpec<T> | { $gtype: GType<T> } function assertField( ctx: ClassFieldDecoratorContext | ClassGetterDecoratorContext | ClassSetterDecoratorContext, ): string { if (ctx.private) throw Error("private fields are not supported") if (ctx.static) throw Error("static fields are not supported") if (typeof ctx.name !== "string") { throw Error("only strings can be gobject property keys") } return ctx.name } /** * Defines a readable *and* writeable property to be registered when using the {@link register} decorator. * * Example: * ```ts * class { * \@property(String) myProp = "" * } * ``` */ export function property<T>(typeDeclaration: PropertyTypeDeclaration<T>) { return function ( _: void, ctx: PropertyContext<T>, options?: { metaOnly: true }, ): (this: GObj, init: T) => any { const fieldName = assertField(ctx) const key = kebabify(fieldName) const meta: Partial<Meta> = ctx.metadata! meta.properties ??= {} meta.properties[fieldName] = { flags: ParamFlags.READWRITE, type: typeDeclaration } ctx.addInitializer(function () { definePropertyGetter(this, fieldName as Extract<keyof GObj, string>) if (options && options.metaOnly) return defineProperty(this, fieldName, { enumerable: true, configurable: false, set(v: T) { if (this[priv][key] !== v) { this[priv][key] = v this.notify(key) } }, get(): T { return this[priv][key] }, } satisfies ThisType<GObjPrivate>) }) return function (init: T) { const dict = ((this as GObjPrivate)[priv] ??= {}) dict[key] = init return init } } } /** * Defines a read-only property to be registered when using the {@link register} decorator. * If the getter has a setter pair decorated with the {@link setter} decorator the property will be readable *and* writeable. * * Example: * ```ts * class { * \@setter(String) * set myProp(value: string) { * // * } * * \@getter(String) * get myProp(): string { * return "" * } * } * ``` */ export function getter<T>(typeDeclaration: PropertyTypeDeclaration<T>) { return function (get: (this: GObj) => any, ctx: GetterContext<T>) { const fieldName = assertField(ctx) const meta: Partial<Meta> = ctx.metadata! const props = (meta.properties ??= {}) if (fieldName in props) { const { flags, type } = props[fieldName] props[fieldName] = { flags: flags | ParamFlags.READABLE, type } } else { props[fieldName] = { flags: ParamFlags.READABLE, type: typeDeclaration } } return get } } /** * Defines a write-only property to be registered when using the {@link register} decorator. * If the setter has a getter pair decorated with the {@link getter} decorator the property will be writeable *and* readable. * * Example: * ```ts * class { * \@setter(String) * set myProp(value: string) { * // * } * * \@getter(String) * get myProp(): string { * return "" * } * } * ``` */ export function setter<T>(typeDeclaration: PropertyTypeDeclaration<T>) { return function (set: (this: GObj, value: any) => void, ctx: SetterContext<T>) { const fieldName = assertField(ctx) const meta: Partial<Meta> = ctx.metadata! const props = (meta.properties ??= {}) if (fieldName in props) { const { flags, type } = props[fieldName] props[fieldName] = { flags: flags | ParamFlags.WRITABLE, type } } else { props[fieldName] = { flags: ParamFlags.WRITABLE, type: typeDeclaration } } return set } } type ParamType<P> = P extends { $gtype: GType<infer T> } ? T : P extends GType<infer T> ? T : never type ParamTypes<Params> = { [K in keyof Params]: ParamType<Params[K]> } /** * Defines a signal to be registered when using the {@link register} decorator. * * Example: * ```ts * class { * \@signal([String, Number], Boolean, { * accumulator: AccumulatorType.FIRST_WINS * }) * mySignal(str: string, n: number): boolean { * // default handler * return false * } * } * ``` */ export function signal< const Params extends Array<{ $gtype: GType } | GType>, Return extends { $gtype: GType } | GType, >( params: Params, returnType: Return, options?: SignalOptions, ): ( method: (this: GObj, ...args: any) => ParamType<Return>, ctx: SignalContext<typeof method>, ) => (this: GObj, ...args: ParamTypes<Params>) => any /** * Defines a signal to be registered when using the {@link register} decorator. * * Example: * ```ts * class { * \@signal(String, Number) * mySignal(str: string, n: number): void { * // default handler * } * } * ``` */ export function signal<Params extends Array<{ $gtype: GType } | GType>>( ...params: Params ): ( method: (this: GObject.Object, ...args: any) => void, ctx: SignalContext<typeof method>, ) => (this: GObject.Object, ...args: ParamTypes<Params>) => void export function signal< Params extends Array<{ $gtype: GType } | GType>, Return extends { $gtype: GType } | GType, >( ...args: Params | [params: Params, returnType?: Return, options?: SignalOptions] ): ( method: (this: GObj, ...args: ParamTypes<Params>) => ParamType<Return> | void, ctx: SignalContext<typeof method>, ) => typeof method { return function (method, ctx) { if (ctx.private) throw Error("private fields are not supported") if (ctx.static) throw Error("static fields are not supported") if (typeof ctx.name !== "string") { throw Error("only strings can be gobject signals") } const signalName = kebabify(ctx.name) const meta: Partial<Meta> = ctx.metadata! const signals = (meta.signals ??= {}) if (Array.isArray(args[0])) { const [params, returnType, options] = args as [ params: Params, returnType?: Return, options?: SignalOptions, ] signals[signalName] = { method, default: options?.default ?? true, param_types: params.map((i) => ("$gtype" in i ? i.$gtype : i)), ...(returnType && { return_type: "$gtype" in returnType ? returnType.$gtype : returnType, }), ...(options?.flags && { flags: options.flags, }), ...(typeof options?.accumulator === "number" && { accumulator: options.accumulator, }), } } else { const params = args as Params signals[signalName] = { method, default: true, param_types: params.map((i) => ("$gtype" in i ? i.$gtype : i)), } } return function (...params) { return this.emit(signalName, ...params) as ParamType<Return> } } } const MAXINT = 2 ** 31 - 1 const MININT = -(2 ** 31) const MAXUINT = 2 ** 32 - 1 const MAXFLOAT = 3.4028235e38 const MINFLOAT = -3.4028235e38 const MININT64 = Number.MIN_SAFE_INTEGER const MAXINT64 = Number.MAX_SAFE_INTEGER function pspecFromGType(type: GType<unknown>, name: string, flags: ParamFlags) { switch (type) { case GObject.TYPE_BOOLEAN: return ParamSpec.boolean(name, "", "", flags, false) case GObject.TYPE_STRING: return ParamSpec.string(name, "", "", flags, "") case GObject.TYPE_INT: return ParamSpec.int(name, "", "", flags, MININT, MAXINT, 0) case GObject.TYPE_UINT: return ParamSpec.uint(name, "", "", flags, 0, MAXUINT, 0) case GObject.TYPE_INT64: return ParamSpec.int64(name, "", "", flags, MININT64, MAXINT64, 0) case GObject.TYPE_UINT64: return ParamSpec.uint64(name, "", "", flags, 0, Number.MAX_SAFE_INTEGER, 0) case GObject.TYPE_FLOAT: return ParamSpec.float(name, "", "", flags, MINFLOAT, MAXFLOAT, 0) case GObject.TYPE_DOUBLE: return ParamSpec.double(name, "", "", flags, Number.MIN_VALUE, Number.MIN_VALUE, 0) case GObject.TYPE_JSOBJECT: return ParamSpec.jsobject(name, "", "", flags) case GObject.TYPE_VARIANT: return ParamSpec.object(name, "", "", flags as any, GLib.Variant) case GObject.TYPE_ENUM: case GObject.TYPE_INTERFACE: case GObject.TYPE_BOXED: case GObject.TYPE_POINTER: case GObject.TYPE_PARAM: case GObject.type_from_name("GType"): throw Error(`cannot guess ParamSpec from GType "${type}"`) case GObject.TYPE_OBJECT: default: return ParamSpec.object(name, "", "", flags as any, type) } } function pspec(name: string, flags: ParamFlags, declaration: PropertyTypeDeclaration<unknown>) { if (declaration instanceof ParamSpec) return declaration if (declaration === Object || declaration === Function || declaration === Array) { return ParamSpec.jsobject(name, "", "", flags) } if (declaration === String) { return ParamSpec.string(name, "", "", flags, "") } if (declaration === Number) { return ParamSpec.double(name, "", "", flags, -Number.MAX_VALUE, Number.MAX_VALUE, 0) } if (declaration === Boolean) { return ParamSpec.boolean(name, "", "", flags, false) } if ("$gtype" in declaration) { return pspecFromGType(declaration.$gtype, name, flags) } if (typeof declaration === "function") { return declaration(name, flags) } throw Error("invalid PropertyTypeDeclaration") } type MetaInfo = GObject.MetaInfo<never, Array<{ $gtype: GType<unknown> }>, never> /** * Replacement for {@link GObject.registerClass} * This decorator consumes metadata needed to register types where the provided decorators are used: * - {@link signal} * - {@link property} * - {@link getter} * - {@link setter} * * Example: * ```ts * \@register({ GTypeName: "MyClass" }) * class MyClass extends GObject.Object { } * ``` */ export function register<Cls extends { new (...args: any): GObj }>(options: MetaInfo = {}) { return function (cls: Cls, ctx: ClassDecoratorContext<Cls>) { const t = options.Template if (typeof t === "string" && !t.startsWith("resource://") && !t.startsWith("file://")) { options.Template = new TextEncoder().encode(t) } const meta = ctx.metadata! as Meta const props: Record<string, ParamSpec<unknown>> = fromEntries( entries(meta.properties ?? {}).map(([fieldName, { flags, type }]) => { const key = kebabify(fieldName) const spec = pspec(key, flags, type) return [key, spec] }), ) const signals = fromEntries( entries(meta.signals ?? {}).map(([signalName, { default: def, method, ...signal }]) => { if (def) { defineProperty(cls.prototype, `on_${signalName.replaceAll("-", "_")}`, { enumerable: false, configurable: false, value: method, }) } return [signalName, signal] }), ) delete meta.properties delete meta.signals registerClass({ ...options, Properties: props, Signals: signals }, cls) } } /** * @experimental * Asserts a gtype in cases where the type is too loose/strict. * * Example: * ```ts * type Tuple = [number, number] * const Tuple = gtype<Tuple>(Array) * * class { * \@property(Tuple) value = [1, 2] as Tuple * } * ``` */ export function gtype<Assert>(type: GType<any> | { $gtype: GType<any> }): { $gtype: GType<Assert> } { return "$gtype" in type ? type : { $gtype: type } } declare global { interface FunctionConstructor { $gtype: GType<(...args: any[]) => any> } interface ArrayConstructor { $gtype: GType<any[]> } interface DateConstructor { $gtype: GType<Date> } interface MapConstructor { $gtype: GType<Map<any, any>> } interface SetConstructor { $gtype: GType<Set<any>> } } Function.$gtype = Object.$gtype as FunctionConstructor["$gtype"] Array.$gtype = Object.$gtype as ArrayConstructor["$gtype"] Date.$gtype = Object.$gtype as DateConstructor["$gtype"] Map.$gtype = Object.$gtype as MapConstructor["$gtype"] Set.$gtype = Object.$gtype as SetConstructor["$gtype"]