GitHub

v1.3.0

Read the announcement: GTKX 1.3: Introducing @gtkx/animated.

New Features

  • @gtkx/animated brings React Spring to GTKuseSpring, useSprings, useTrail, useTransition, useChain and useSpringValue, the Spring, Trail and Transition components, and the animated(Component) wrapper with its animated.GtkLabel shorthand drive real GObject properties. The wrapper writes each frame straight onto the widget through its ref, so a running spring does not re-render the component, while a prop with no native setter, such as the accessible* props, falls back to a React render per frame. Frames come from the GTK frame clock, and useReducedMotion reads GTK's gtk-enable-animations and gtk-interface-reduced-motion settings.
  • A style prop on every widget — Every element that renders a Gtk.Widget takes a style object of CSS declarations, the way React DOM spells them. GTK4 has no inline styles, so the object compiles to a single rule in a Gtk.CssProvider that belongs to that widget alone, registered one step above STYLE_PROVIDER_PRIORITY_APPLICATION so style outranks anything a class in cssClasses sets. Changing the object rewrites that one rule, so the cost does not grow with the number of distinct values a component cycles through; setting the prop to undefined or null removes it again. A key starting with & nests a block under a selector derived from it, so "&:hover" styles the hover state, and a bare number gets px where CSS expects a length. The prop is typed as a curated list of the paint and typography properties GTK4 actually understands rather than the whole web set, so style={{ display: "flex" }} fails to compile instead of becoming a runtime warning — layout stays in the widget's own props. Style and StyleProperties are new public types.
  • Springs drive style@gtkx/animated writes style per frame the way it writes a GObject property, through the widget's ref and without re-rendering the component, which is what makes a color, a border-radius or a box-shadow animatable at all, none of which GTK exposes as a property. A spring can be the whole prop, style={level.to(declarations)}, or sit on a single declaration, style={{ color: styles.color }}, so the object a spring hook returns can be handed to style as it is, the way React Spring is written for the DOM. Only style is read that way: a spring nested inside any other object-valued prop is left alone.
  • Cairo ships as @gtkx/cairo — Contexts, surfaces, patterns, regions, matrices, font faces, font options, scaled fonts and devices are real classes. Surface, Pattern and FontFace are abstract, and instances arrive as the most specific class the package models, so surface instanceof ImageSurface and ctx.getSource() instanceof LinearPattern narrow, while kinds with no dedicated subclass — a solid or surface pattern, a group target — come back as the base class.
  • registerClass declares signals — A signals option creates GObject signals on the new type, each naming its paramTypes and optional returnType as numeric GTypes or as classes carrying one, its flags, and an accumulator of first-wins or true-handled. connect, emit, on, once, off and useSignal take the declared names, with progress_changed and progress-changed reaching the same signal, and emit converts each argument into a GValue of the declared type. SignalSpec and SignalGType are new public types.
  • GValue parameters take a plain JavaScript value — Every parameter the callee only reads is typed GObject.Value | JsValue and accepts the JavaScript value itself, with the GType inferred: string to gchararray, boolean to gboolean, a whole in-range number to gint, any other number to gdouble, bigint to gint64 or guint64, an array of strings to GStrv, a wrapper to the GType it carries, and null to a NULL gpointer. Arguments of an emitted signal infer the same way, and passing a GObject.Value built by hand still works wherever inference would guess something else. A signal handler still receives a real GObject.Value.
  • GType parameters take a class — Every parameter that takes a GType accepts the class registered under it alongside the numeric bigint, so Gio.ListStore.new(Gtk.Label) and GObject.typeName(Gtk.Label) work, as do signal arguments declared as GTypes. A class that never went through registerClass is rejected rather than resolving to its parent's type. Only the input direction widens: return values, out parameters and handler arguments still hand back the numeric GType.
  • Abstract registered typesabstract: true registers the type with G_TYPE_FLAG_ABSTRACT, the way the flag marks a C type. Registered subclasses instantiate as usual and inherit its vfunc overrides, while constructing the class itself throws, from JavaScript and from a native caller alike.
  • classInit and cssName on registerClassregisterClass takes a classInit hook and a cssName, and every generated *Class GTypeStruct wrapper is now paired with its class. GObject.ObjectClass.peek and Gtk.WidgetClass.peek hand back any type's class struct outside a classInit hook, so GObject.ObjectClass.peek(Gtk.Label).findProperty("label") works, backed by the new peekTypeClass and registerClassStruct runtime exports.
  • Property overrides with paramSpecOverrideparamSpecOverride(name, source) redeclares a property a parent class or an implemented interface already carries, the way g_param_spec_override does in C, giving the subclass its own storage and notify emission while the value type, flags and default stay the ones the overridden spec declares. The source is a wrapper class, an interface, or a raw GType, and the call throws when it declares no property under that name. newParamSpecOverride is the matching @gtkx/runtime export.
  • ParamSpec introspection — Every GObject.ParamSpec carries readonly name, nick, blurb, flags, valueType and ownerType getters, whether it comes from a paramSpec* constructor, a notify handler, or findProperty and listProperties. flags is the ParamFlags bitfield the spec was created with, valueType the GType of the values the property holds, and ownerType the GType the spec is installed on, TYPE_INVALID until it is installed on one. getParamSpecFlags, getParamSpecValueType and getParamSpecOwnerType back them from @gtkx/runtime.
  • fromVariant takes a variant alone and unpacks recursivelyfromVariant no longer requires a type string: it accepts a GLib.Variant on its own, and an options object with recursive: true unwraps every nested variant into the value it holds, all the way down. toVariant accepts Uint8Array or number[] for a byte array either way. FromVariantOptions, RecursiveFromVariantOptions, RecursiveVariantValue, VariantInput and ByteArray are new exported types.
  • Bare @gtkx/jsx imports — The generated JSX store gains an index module, so import { GtkLabel } from "@gtkx/jsx" reaches every namespace's components without the per-namespace subpath. The undeclared-library check in the CLI resolves and diagnoses the bare import, with its own message when the store has no index module.
  • v2ValueReturns, v2FinishResults and v2InoutReturns future flags — The future config block takes three new flags, all off by default and unconditional in 2.0. v2ValueReturns makes the bindings whose return or caller-allocated out parameter is a GValueGtk.DropTarget.getValue, Gtk.ConstantExpression.getValue, Gdk.Clipboard.readValueAsync, Gtk.Builder.valueFromStringType, Gtk.TreeModel.getValue and a handful more — hand back what the value holds, typed unknown. v2FinishResults drops the always-true leading success boolean from promisified async methods, so Gio.File.loadContentsAsync resolves to [number[], string | null], or [Uint8Array, string | null] with v2ByteArrays also enabled, and a call left with a single out parameter, such as replaceContentsAsync, resolves to that value directly. v2InoutReturns stops repeating a caller-allocated inout record in a method's result, because the callee mutates the instance you passed and the returned entry was always the same object you already hold, so Gsk.Path.getNext(point) becomes boolean rather than [boolean, PathPoint] and Pango.Matrix.transformRectangle(rect) becomes void; primitive inout parameters, which cannot be mutated in place, stay in the result either way. Flip them and run tsc: every site that needs attention is a type error.
  • New @gtkx/runtime exports — The GValue marshalling set fromValue, toValueHandle, tryToValueHandle, ValueMarshalError and JsValue is public, alongside coerceObjectProperty, registerWrapperClassResolver with its WrapperClassResolver type, matchRegex and matchAllRegex, which keep a regex subject's bytes alive alongside its MatchInfo, and trimFinish.

Breaking Changes

  • on<SignalName> methods become signal default handlersregisterClass installs every method matching /^on[A-Z]/ as the class-closure default handler for the kebab-cased signal it names, GJS-style, on every call, walking the whole prototype chain up to the generated wrapper class. This retroactively promotes helpers written before the feature existed: an onShow, onDestroy, onMap or onNotify method on a subclass now runs on every emission of that inherited signal. Audit every registered class for on-prefixed methods and rename any that were not meant to be handlers; a name matching no real signal is left alone.
  • Cairo moves into @gtkx/cairo, and @gtkx/gi/cairo is deprecated@gtkx/gi/cairo still resolves for all of 1.x and re-exports @gtkx/cairo, but a project that does not declare @gtkx/cairo gets the copy the cod...

Read more

v1.2.2

Changelog

New Features

Fixed-size array fields inside a record are writable

A field GIR declares as <array fixed-size="N"> lives inside its record's own allocation rather than behind a pointer, so writing one is a copy into memory the record already owns. Those fields now emit a setter alongside their getter, bounded by the declared length: coord.axes = [...] writes the elements in place, an array longer than the field drops the extra elements rather than running past it, and a shorter one leaves the remaining elements as they were. Nothing is allocated, so the record's own release is unchanged.

Bugfixes

  • Fixed a fixed-size array field inside a record reading as an empty array instead of its elements. Such a field is stored inline, but it was rendered with a pointer-style descriptor, so the getter loaded a pointer from the field's own offset.
  • Fixed a generated interface carrying its GType at runtime without declaring it. registerInterface tags the interface's prototype through the same path a class goes through, so Gtk.Orientable.prototype.__type__ held the interface's GType, but the generated typings never declared the member.

v1.2.1

Changelog

New Features

toVariant and fromVariant are public API on @gtkx/runtime

The GVariant codec behind useSetting was never exported, so calling a D-Bus method or building an a{sv} payload meant hand-assembling GLib.Variant.newArray, newDictEntry and newTuple and picking the right getter on the way back.

import { fromVariant, toVariant } from "@gtkx/runtime";
const args = toVariant("(a{sv})", [{ verbose: toVariant("b", true) }]);
const [names, count] = fromVariant("(asi)", reply);

A literal type string types both directions through VariantValue<S>: "as" an array of strings, "a{sv}" a record of GLib.Variant, "(si)" a [string, number], "x" and "t" a bigint; a string that is not a literal falls back to unknown. Unpacking is the inverse — a dictionary keyed by s, o or g comes back as a record and one keyed by anything else as a Map, a maybe as its value or null. A type string that is not exactly one complete GVariant type throws rather than half-reading it, and so does a value packed as "o" or "g" that is not a valid object path or signature.

Every deploy generates third-party notices

A package ships the Node.js runtime, the GTKX bundle and addon, and every npm package the bundle reaches, and nothing accounted for any of it: the deb's copyright file held the project's own terms alone, and no other target carried a notice file at all.

gtkx deploy now writes the deb's share/doc/<binaryName>/copyright in machine-readable copyright format 1.0, with a stanza per file the package carries, and gives every other target share/licenses/<binaryName>/THIRD-PARTY-NOTICES. Four sections: the bundled Node.js, whose LICENSE is the aggregate notice for V8, OpenSSL, ICU, libuv, zlib, brotli and llhttp; GTKX itself, with the MPL-2.0 notice, the MPL section 3.2(a) source pointer and the licenses of the Rust crates linked into gtkx.node; the bundled JavaScript dependencies, each license reproduced in full or its SPDX identifier listed; and the GTK, libadwaita, GtkSourceView and WebKitGTK the app declares, carrying what LGPL-2.1 section 6 asks of a work that uses a library.

The dependency list is what the build actually reached: gtkx build walks the module graph of dist/bundle.mjs and records every package into dist/gtkx-packages.json, which the deploy reads and keeps out of the staged tree. Flatpak source mode carries the notices inline to the same path. gtkx deploy --skip-build over a dist/ from an older gtkx build has no package list to read and fails asking for a build.

deploy.extraFiles takes a { source, mode } entry

An entry can now be { source: "tools/helper", mode: "755" } in place of a bare source path. mode is a three- or four-digit octal string, validated before anything is staged, and it is what nfpm records in the deb and the rpm, what the AppImage tree carries, and what the source-mode flatpak passes to install -D.

Breaking Changes

GLib criticals and addon failures raise an uncaught exception

A GLib CRITICAL, a GLib ERROR, a Rust panic caught at an FFI boundary and every internal failure the addon reports — a callback argument it could not decode, a boxed type it could not resolve, a timer it could not arm — produced one gtkx: line on stderr and nothing a JavaScript handler could see. Each now raises as a Node uncaughtException, marshalled to the install thread first when it came from another one. Records below CRITICAL are untouched, so GTK's theme-parser warnings stay warnings.

An app with no process.on("uncaughtException") handler now prints and exits non-zero at the first CRITICAL, so a g_return_if_fail violation that used to scroll past in a log ends the process. Install a handler and the app behaves as before: the handler runs, the call returns, the process stays up. gtkx dev already installs one. A Vitest run has no handler by default, so a suite that provoked criticals and passed anyway now fails — and the fix is the offending call, since GTK returned from it without doing what was asked.

deploy.flatpak.finishArgs and deploy.flatpak.cleanup merge with the defaults instead of replacing them

Setting either key threw the defaults away wholesale, so asking for one extra permission meant restating --share=ipc, --socket=wayland, --socket=fallback-x11 and --device=dri alongside it, and a config listing only --share=network produced an app that starts without a window. The defaults now come first, your entries follow, and duplicates collapse. To drop a default, ask for its negation — --nosocket=wayland, --unshare=ipc, --nodevice=dri — which is what flatpak build-finish takes. cleanup merges the same way but has no negation for a pattern; an empty array turns cleanup off entirely.

A project that set finishArgs to a list narrower than the defaults now builds a flatpak with wider permissions than before, and one that set cleanup now has /include, /share/pkgconfig, *.la and *.a stripped as well. Compare the finish-args and cleanup blocks of the generated manifest against the previous release's, and negate or empty what you meant to exclude.

emit only accepts a signal the class declares

v1.2.0 narrowed connect, on, off and friends to the class's own signal map but left every generated class a second emit(sigName: string, ...args: unknown[]): unknown overload, which any string satisfied: button.emit("clickd") type-checked and threw Unknown signal 'clickd' at runtime, and a correctly spelled name resolving through it lost its argument types and returned unknown. That overload now exists only on GObject.Object. Correct the name, or go through the root with (button as GObject.Object).emit(name).

Bugfixes

  • Fixed record fields with no generated accessor being declared in the .d.ts anyway, so GLib.Queue.head, GLib.List.next, Pango.LayoutLine.runs and GObject.SignalQuery.paramTypes typed as their GIR type and every one read undefined at runtime. Fields backed by a GList, GSList, GHashTable or a sibling-length array are gone from the class and the declarations alike, and the one shape that can be read — a null-terminated pointer array — gained a real accessor, so Gio.DBusNodeInfo.interfaces, Gio.DBusInterfaceInfo.methods, GdkPixbuf.PixbufFormat.mimeTypes and Gtk.RecentData.groups decode instead of answering undefined.
  • Fixed array-typed record fields being offered as writable when writing one always threw, since a field descriptor is transfer-none and the native writer refuses a transfer-none container outright. Gdk.TimeCoord.axes and Gsk.RoundedRect.corner had setters and appeared in ConstructorProps, so new Gdk.TimeCoord({ axes }) raised on every call. Array, list and hash-table fields are read-only now and out of ConstructorProps; inline struct arrays keep their element-wise setters.
  • Fixed a signal GIR marks detailed="1" accepting no ::detail suffix in its generated map, although the runtime has always stripped the suffix before looking the signal up. Only notify was special-cased, so settings.on("changed::theme", handler) and bar.on("offset-changed::low", handler) stopped type-checking once v1.2.0 removed the plain-string overloads. Each detailed signal now contributes a [detail: `changed::${string}`] index signature to both its maps, covering Gio.Settings, Gio.DBusProxy, Gio.ActionGroup, Gtk.LevelBar, Gtk.AppChooserButton, Adw.MessageDialog and WebKit.
  • Fixed gtkx codegen not counting @gtkx/runtime among the packages that must be able to import the generated @gtkx/gi, so a layout that installs it above the node_modules the store is written into produced a store the runtime could never resolve. Codegen now stops with the same error it gives for the other @gtkx packages.
  • Fixed a ColumnView over items with children drawing a Gtk.TreeExpander in every cell of a row instead of one per row, so a three-column tree gave each row three toggles, stamped expanderDescriptions on all three, and indented every column by the row's depth, leaving child cells further right than their parent's instead of lining up. Exactly one column carries the expander now — the first whose visible is not false — and the rest render renderCell directly and stay aligned at every depth. expandedIds and onExpandedChange are unaffected; a tree that wants the toggle elsewhere lists that column first.
  • Fixed useSignal running the handler captured on the first render for every emission inside a component wrapped in memo or forwardRef, so a memoized row that re-rendered with a new closure kept reading stale props and state for the life of the component. The hook built on React 19.2's useEffectEvent, which does not pick up the updated function through those wrappers; the handler is now held in a ref written from useInsertionEffect, so every emission runs the latest committed render's handler whether the component is wrapped or not, and a changing handler still never reconnects the signal.
  • Fixed deploy.extraFiles installing every file 644, or 755 when the destination name ended in .node or .so, so a helper script listed there arrived unrunnable however it had been staged. An entry without an explicit mode now takes 755 when the source file has any execute bit and 644 when it has none, and the extension decides nothing.
  • Fixed deploy.flatpak.mode: "source" emitting a manifest that installs no MIME package, no license file and none of the deploy.extraFiles entries, all three of which the prebuilt flatpak installs, so a Flathub submission registered none of the app's fileAssociations and shipped without its license. All three install now, and because a source build has only the git chec...

Read more

v1.2.0

This release adds a future block to gtkx.config.ts, a way to take one of the next major version's behaviors at a time rather than all of them at an upgrade, and its first flag, v2ByteArrays, binds every GIR byte sequence as a Uint8Array instead of a number[], decoded as a single copy in the addon rather than one JavaScript number per byte. One half of that work lands whether or not a project opts in: a byte-sequence parameter takes Uint8Array | number[] everywhere, and a GByteArray argument accepts a typed array for the first time.

The rest of 1.2 is a round of corrections in the generated bindings and in codegen itself. Pointer-typed values are bigints, which is also what lets a GList or GPtrArray of raw pointers marshal at all; connect, on and off accept only a signal the class declares, which is what was letting useToast's handlers run with no toast; property accessors read and write through GObject carrying the property's own declared type; and the reference counting and free methods GIR declares are no longer bound, since the native layer owns those references. Codegen emits the generated stores without building a TypeScript program over them, which makes a store ECMAScript modules whatever the project's package.json declares and makes the cost of writing one track the number of modules emitted. Namespaces that could not be bound at all now bind: one whose static function narrows an inherited one, which is every gdbus-codegen proxy, and one carrying a type whose name starts with a digit. And deploy.flatpak.mode: "source" renders a manifest a pnpm project can actually build.

Changelog

New Features

Future flags, starting with v2ByteArrays

gtkx.config.ts takes a future block, which opts a project into behavior that becomes the default in the next major version, so an upgrade can be taken one change at a time instead of all at once. Every flag is off by default, codegen never warns about one that is not set, and a value that is not a boolean fails config validation.

export default defineConfig({
    applicationId: "com.example.Tasks",
    future: { v2ByteArrays: true },
});

The first flag is v2ByteArrays, which represents GIR byte sequences as Uint8Array rather than number[]. It covers guint8 C arrays and GByteArray wherever they are read: return values, out parameters, record fields and properties. GLib.fileGetContents becomes (filename: string) => [boolean, Uint8Array], GLib.base64Decode returns a Uint8Array and so does the remainder GLib.utf8Validate reports, Gio.File.loadContents returns [boolean, Uint8Array, string | null], GdkPixbuf.Pixbuf.getPixels returns a Uint8Array, and Gio.TlsCertificate's certificate and privateKey read Uint8Array | null, in the @gtkx/gi classes, the @gtkx/jsx props and the matching onNotify handlers alike. The descriptor emitted behind such a value carries isBytes: true, or t.byteArray for a GByteArray, so the bytes cross the boundary as one copy instead of being unpacked element by element. The handwritten Cairo overrides and the OpenGL bindings are untouched, since they already use typed arrays.

Method parameters are the same either way, Uint8Array | number[], so the flag never breaks an imperative call that passes bytes in. What changes is what comes back, plus the construct-time inputs that carry a byte sequence: the @gtkx/jsx props, the @gtkx/gi constructor props, record initializers and writable record field setters all narrow from number[] to Uint8Array. Code that calls .push, .concat, Array.isArray or JSON.stringify on a byte-sequence result has to be updated as well. Flip the flag and run tsc: every site that needs attention is a type error.

The setting is hashed into the generated store's fingerprint, so changing it makes the next gtkx dev, gtkx build or gtkx codegen regenerate @gtkx/gi and @gtkx/jsx on its own, and codegen reports the enabled flags on its own line as codegen: future=v2ByteArrays. gtkx docs and the @gtkx/mcp API reference read the same setting, so the reference pages and gtkx_list_api, gtkx_search_api and gtkx_get_api_docs describe byte sequences exactly the way the installed store carries them.

Flathub source builds for pnpm projects

deploy.flatpak.mode: "source" now renders a manifest a pnpm project can build. The Node SDK extension ships no pnpm and the Flathub sandbox has no network to fetch one, so the module vendors pnpm itself: an archive source pinned to the pnpm tarball's sha512 and unpacked into flatpak-pnpm, a script source whose dest-filename is pnpm and which execs node /run/build/<binaryName>/flatpak-pnpm/bin/pnpm.cjs, and that directory prepended to build-options.append-path ahead of the Node extension's bin. The install command is pnpm install --offline --frozen-lockfile, with --trust-lockfile appended on pnpm 11, whose supply-chain check otherwise reaches the registry. flatpak-node-generator is invoked with --pnpm-store-version v10 or v11 matching the pinned major, so the vendored store layout matches the pnpm that reads it, and the npm-only npm_config_cache and npm_config_offline variables are no longer set on a pnpm build, leaving npm_config_nodedir alone. Before, the same manifest ran a pnpm command that nothing in the sandbox provided, and generated the offline sources without a store version.

The pnpm version comes from packageManager in package.json. Write it with corepack use pnpm@<version>, which records the +sha512. digest every Flathub source has to carry; a project with no packageManager field gets pnpm 11.21.0. gtkx deploy refuses to render when the field names a manager other than pnpm while the build installs with pnpm (a pnpm-lock.yaml in the project root, or deploy.flatpak.packageManager: "pnpm"), when it carries no sha512 digest, and when it pins a pnpm outside 10.x and 11.3.0 or newer, since --trust-lockfile does not exist before 11.3.0. Vendoring also needs a flatpak-node-generator that supports --pnpm-store-version, an option newer than the generator's last tagged release: preflight runs flatpak-node-generator --help, counts a copy without that option as missing, and the tool's install hint gains the pipx install --force command that replaces a copy too old to vendor pnpm. npm and yarn manifests keep the same sources, append-path and install commands as before.

Breaking Changes

These are corrections to the generated bindings and to @gtkx/runtime's descriptor surface. Most of them are type-level, and tsc names every site that has to change once the store is regenerated, which happens on the first gtkx dev, gtkx build or gtkx codegen after the upgrade.

connect, on and off only accept a signal the class declares

Each generated class declared its signal methods twice: once keyed on its own signal map and once over a plain string. The second overload accepted anything, so a misspelled signal name, a signal belonging to a different class, and a handler declaring parameters the signal never supplies all type-checked and then misbehaved at runtime. @gtkx/components hit exactly that: toast.on("button-clicked", onButtonClicked) resolved through the string overload, and the callback ran with undefined where the toast was meant to be.

Every generated class now declares connect, on, once, off, addEventListener and removeEventListener only in the K extends keyof <Class>Signals form; the plain-string overloads are gone. emit(sigName: string, ...args: unknown[]): unknown is unchanged. Code that connected through a widget typed as an ancestor has to narrow it first, so a Gtk.Widget handed back by a query has to be resolved as a Gtk.Entry before connect("delete-text", ...) compiles, and a handler that declares more parameters than the signal passes has to be wrapped.

Detailed notify names are covered by an index signature on GObject.ObjectSignals and GObject.ObjectSignalEmit, so on(`notify::${string}`) type-checks for any property, including one installed through registerClass({ properties }), where 1.1 listed only the properties GIR declares. addEventListener and removeEventListener are also marked @deprecated in favor of on and off, and are to be removed in v2.

gpointer values are bigints

Every GIR type that resolves to a raw pointer, gpointer and gconstpointer, was declared number and marshalled through t.uint64, the unsigned 64-bit descriptor that decodes to a JavaScript number, which cannot represent an address above 2^53 exactly. Both are bound as t.biguint64 and typed bigint now, everywhere they appear: method parameters and returns, record fields, properties, element props, constants, and the userData argument of every generated vfunc signature. GObject.Value.getPointer() answers 0n rather than 0 and setPointer takes bigint | null, GObject.Object.getData, setData and stealData take and return bigint | null, Gtk.TreeIter.userData, userData2 and userData3 read and write bigint, GdkPixbuf.Pixbuf's pixels prop is bigint | null, Gtk.BuildableParseContext.pop() returns bigint | null, GLib.Sequence.append takes bigint | null, and Gio.TlsClientConnection.getAcceptedCas() returns bigint[]. It is also what lets a GList or GPtrArray of pointers marshal at all.

An integer Number inside the 2^53 safe range is still accepted where a pointer is passed in, so the work is on the way out: code that stores a returned pointer in a number, compares it against a number literal or does arithmetic on it has to move to bigint, so value.getPointer() === 0 becomes value.getPointer() === 0n. The API reference pages render the new type as well, since they share the same primitive table.

Reference counting and free methods are no longer bound

Every generated cla...

Read more

v1.1.0

Read the announcement: GTKX 1.1: Introducing gtkx deploy.

This release adds gtkx deploy, which turns a project into the packages a Linux user actually installs: a Flatpak bundle, a .deb, an .rpm and an .AppImage, with the desktop entry, the AppStream metainfo, the Flatpak manifest and the deb and rpm control metadata generated from a deploy block in gtkx.config.ts rather than written by hand. The rest of 1.1 is a large round of defect fixes across every layer. The FFI runtime stops leaking subclasses that override a teardown slot, stops freeing the memory an async call lends, and reads the out parameters and struct fields it used to refuse or misread. The generated bindings declare abstract types abstract, type property accessors per direction and drop the return values GIR marks as skipped. gtkx dev survives a throwing component and stops reporting refreshes it did not apply, gtkx build refuses a bundle that would resolve anything at runtime, and @gtkx/testing follows GTK4's own key propagation, selection handling and window state instead of approximating them.

Changelog

New Features

gtkx deploy builds the packages a Linux user installs

gtkx deploy turns a project into installable packages. One command builds a .flatpak bundle, a .deb, an .rpm and an .AppImage, and the desktop entry, the AppStream metainfo, the Flatpak manifest and the deb and rpm control metadata are generated rather than written by hand. A 1.0 project that carried bundling scripts, a sea-config.json, a desktop entry, a metainfo file and a Flatpak manifest of its own can delete all of them.

deploy.targets picks the default set and --target deb,rpm overrides it for a single run. With neither, the command builds a Flatpak. --out changes the output directory, which defaults to build, and --skip-build packages what is already in dist/ instead of rebuilding.

Everything derivable is derived from what the project already declares

A small application configures three or four keys. summary, categories and targets are usually the whole block, because name, version, license, developer, homepage and description fall back to package.json, the icons come from the data/icons/ tree gtkx build already reads, and the metadata license, copyright and a first release entry are filled in. The deb Section and the rpm Group come from categories, the deb Depends and rpm Requires come from the libraries already declared, so ["Gtk-4.0", "Adw-1"] becomes libgtk-4-1, libadwaita-1-0 on Debian and gtk4, libadwaita on Fedora, and the glibc floor is read out of the built binaries rather than guessed.

Run the command with no deploy block and it prints a starter block with every derivable value already filled in, ready to paste into gtkx.config.ts. The application icon is the one file that has to exist and has to be named after the application ID, since the desktop entry names the ID as its icon, and the command says so when it is missing.

Metadata is validated before the application is built

The desktop entry and the metainfo depend only on configuration, so they are rendered and checked with desktop-file-validate and appstreamcli before the app is bundled. A category typo or a summary ending in a period fails in seconds, with the validator's own message, rather than after a full Flatpak build.

An AppStream error always stops the deploy. A warning stops it too when the run is a Flathub source submission, meaning deploy.flatpak.mode: "source" with the flatpak target selected, since that metainfo goes to a software center; for the other formats it is reported and the build continues. Either way, a rule that maps to configuration is printed with the key that fixes it, so url-homepage-missing says to set deploy.homepage or homepage in package.json. gtkx deploy --print-manifests renders and validates the metadata and every target's manifest, then stops without packaging, and needs none of the packaging tools.

One staged tree, and every target installs it unchanged

Every format installs the same payload: a launcher script at bin/<binaryName>, the bundled Node.js, the app bundle, gtkx.node and the compiled settings schemas under lib/<binaryName>/, and the desktop entry, metainfo, icons and schemas under share/. It goes under /usr for deb, rpm and AppImage and under /app for Flatpak, and nothing is rewritten between them, because the launcher resolves everything relative to its own location and the bundle resolves the addon and the compiled schemas from beside itself.

Node.js is bundled because GTKX needs Node.js 24 and the distributions ship older. gtkx deploy downloads the official nodejs.org build matching the Node.js running the deploy, verifies it against the published SHA-256 and caches the archive under ~/.cache/gtkx/node/, so only the first deploy needs the network. deploy.node.source: "host" copies the Node.js running the build instead, and is refused with an explanation when that binary links against something the target machine will not have, which is the case for the Node.js packages Fedora and Debian ship. deploy.node.path points at a binary of your own.

Flathub submissions build from source

gtkx deploy --target flatpak builds from the staged tree, offline and in seconds. Flathub builds every submission from source instead, so deploy.flatpak.mode: "source" emits a manifest that does the same: a git source pinned to the release, taken from the origin remote when deploy.flatpak.source.url names none, dependencies vendored for the network-isolated sandbox with flatpak-node-generator, and the generated metadata carried as inline sources, so nothing generated has to be committed to the repository. It wants a lockfile, a package-lock.json, a pnpm-lock.yaml or a yarn.lock, because the sandbox installs offline and has no network to fetch anything else; deploy.flatpak.packageManager picks which one when the project carries several.

Sandbox permissions are the one thing still written by hand, through deploy.flatpak.finishArgs. They are a security decision rather than something to infer from configuration, and the default asks for a window and hardware rendering and nothing else. deploy.flatpak.modules and deploy.flatpak.buildCommands add modules and build steps on top.

New projects are ready to deploy

create-gtkx writes a deploy block into gtkx.config.ts with the display name, summary, description and categories already filled in, an application icon at data/icons/hicolor/scalable/apps/<applicationId>.svg, and a deploy script, so gtkx deploy builds a package out of a fresh project without any further configuration. package.json gains a description, an MIT license and an author taken from git config user.name and user.email, all of which the deploy metadata falls back to.

Two smaller template changes come with it. start runs dist/bundle.mjs, matching the extension the build now emits, and tsconfig.json maps @gtkx/gi/* and @gtkx/jsx/* onto node_modules/.gtkx directly, so type-checking survives a package manager that prunes the store's links. A project scaffolded at 1.0 wants the same paths entry.

Smaller additions

  • nfpm and appimagetool are downloaded, checksum-verified against pinned digests and cached under ~/.cache/gtkx/, so a Fedora machine builds .deb packages and a Debian machine builds .rpm packages. desktop-file-validate, appstreamcli and, for Flatpak, flatpak-builder are yours to install, and a run missing any of them lists all of them at once with the install command for the detected distribution.
  • deploy.isDbusActivatable writes share/dbus-1/services/<id>.service next to the desktop entry for deb, rpm and Flatpak, pointing at the installed launcher with --gapplication-service, and drops DBusActivatable from the AppImage copy, where no service file can be installed.
  • A deploy.screenshots entry can name a file in the repository instead of an absolute url, and the base URL is derived from the origin remote for GitHub and GitLab, including the project's path inside the repository. Set deploy.screenshotBaseUrl when the remote is somewhere else.
  • deploy.releases, deploy.contentRating, deploy.branding and deploy.urls fill the AppStream sections a software center reads, and deploy.fileAssociations, deploy.protocols and deploy.desktopActions fill the desktop entry, its MIME package and its action groups.
  • deploy.desktopEntry adds or overrides desktop entry keys, deploy.extraFiles maps prefix-relative destinations to files in the project, deploy.depends and deploy.relations add package relationships per format, deploy.scripts supplies maintainer scripts, and deploy.signing signs the .deb, the .rpm, the Flatpak repository or the AppImage.
  • The whole deploy block is validated with the rest of gtkx.config.ts, so a misspelled key or a badly typed value is reported as gtkx.config.ts: `deploy.<key>` ... before anything is built.
  • Deploying documents every field, and the tutorial's packaging and Flathub appendices are rewritten around the command instead of the eleven hand-written packaging files they used to walk through.
  • A method whose caller-allocated out parameter is a record with no public constructor used to be dropped from the bindings entirely. Codegen allocates the struct from the layout it already knows, so those methods are emitted and callable.
  • t.cursorArray joins the runtime's descriptor surface, for a pointer that reports a position inside a buffer the call was already given.
  • configure({ windowActivationTimeout }) sets how long render waits for the window it shows the tree in to become active, defaulting to five seconds. actionabilityTimeout keeps its 500 ms and now covers user even...

Read more

v1.0.0

Read the announcement: GTKX 1.0: The React framework for Linux.

This release makes GObject subclassing a first-class part of the API and rebuilds @gtkx/testing on GTK's own accessibility tree and input handling. Virtual functions are reachable by name and chainable through super, a class can adopt interfaces its parent never implemented, and property writes are checked against the ParamSpec. An application parses its command line the way a C application does, and the widgets GTK4 forbids from being parented portal themselves. Every accessible read a test makes goes through gtk_test_accessible_check_* instead of the props the React layer recorded, and userEvent.click reproduces GTK's own targeting rather than forcing widget state. The generated bindings carry full documentation from the GIR data, and api.json declares which entrypoints are public.

Breaking changes

The generated store has to be rebuilt

gtkx dev, gtkx build and gtkx codegen regenerate node_modules/.gtkx on the first run after upgrading. An rc.4 store will not run against 1.0: the generated modules import runtime symbols that do not exist at rc.4, and the vtable and callback descriptors changed. A project with codegen: false needs a store built by 1.0.

Virtual method overrides are named with a vfunc prefix

A subclass no longer fills a C vtable slot by overriding the plain method name. Every slot is keyed vfunc plus the PascalCased vtable field, and the generated wrapper classes declare those members, so super.vfuncMeasure(...) chains up. Code keeping the old spelling silently stops overriding anything.

// Before
class ReturningModel extends Gtk.StringList {
    override getNItems(): number {
        return 1;
    }
}
// After
class ReturningModel extends Gtk.StringList {
    override vfuncGetNItems(): number {
        return 1;
    }
}

Since the override no longer shadows the generated binding, model.getItem(0) reaches it through the vtable and the rc.4 prototype.getItem.call(...) workaround is gone. A class field holding a function never reaches the vtable, because it is assigned per instance after registration. vfuncDispose, vfuncFinalize, vfuncGetProperty and vfuncSetProperty get no typed member, but declaring them still works.

runApplication takes an argv, and applications come from createApplication

runApplication(application, argv) returns { isPrimary, exitStatus }. Rather than registering and activating itself, it hands the argv to GLib's local command line handling, so the application's own options are parsed, --help prints, and a second instance forwards its command line to the process owning the application ID. It throws when handed an application GTKX did not derive, because GLib parses a given application's command line at most once; createApplication(base, props) builds one.

// Before
const application = new Gtk.Application({ applicationId: "org.example.App" });
runApplication(application);
// After
const application = createApplication(Gtk.Application, { applicationId: "org.example.App" });
const { exitStatus } = runApplication(application, ["App", ...process.argv.slice(2)]);
process.exitCode = exitStatus;

Applications rendered as <GtkApplication> or <AdwApplication> are carried over. gtkx dev forwards everything after -- to the application. An unrecognized option is now rejected by GApplication instead of ignored, and post-activate props such as menubar apply only when activation happens.

Windows, applications, dialogs and size groups portal themselves

GtkWindow and its subclasses, GtkApplication, GtkSizeGroup and the Adwaita dialogs mount at the top level wherever they sit in JSX, so the rc.4 pattern of writing createPortal(..., rootElement) by hand is gone. Two behavior changes come with it: transientFor defaults to the nearest enclosing window when the prop is undefined (pass null to keep a window free-standing), and GtkApplicationWindow portals into the application useApplication returns, throwing when there is no <GtkApplication> ancestor.

A child no behavior claims throws instead of being dropped

The reconciler used to ignore a child left in the default slot that no attach behavior claimed. It now throws, naming the child type, the parent type and the three remedies: pass it to the prop that takes it, portal it to rootElement, or register an attach behavior with defineElements.

// Before: the label was silently dropped
<GtkPaned>
    <GtkLabel>Start</GtkLabel>
</GtkPaned>
// After
<GtkPaned startChild={<GtkLabel>Start</GtkLabel>} />

This is a runtime error, not a compile error: every widget's generated props declare children, so the old tree still type-checks.

Deprecated containers lost their child behaviors

AdwLeaflet, AdwSqueezer, AdwPreferencesWindow and GtkComboBox no longer take children, and AdwFlap no longer takes content, so children written under any of them hit the unclaimed-child error. Migrate to AdwNavigationSplitView or AdwNavigationView, AdwBreakpointBin or AdwMultiLayoutView, AdwPreferencesDialog, and AdwOverlaySplitView. AdwFlapProps.content and GtkComboBoxProps.child are settable instead. AdwClampScrollable's child must implement Gtk.Scrollable, and AdwExpanderRow dropped its deprecated actions slot in favor of suffix.

AdwAlertDialog children fill the extra child

Children now go through adw_alert_dialog_set_extra_child and land below the heading and above the response buttons, where they used to replace the dialog's whole content. The extraChild prop is gone.

// Before
<AdwAlertDialog responses={responses} extraChild={<InteractiveFields />} />
// After
<AdwAlertDialog responses={responses}>
    <InteractiveFields />
</AdwAlertDialog>

ElementBehavior loses mount and unmount, and gains constructOnly

The two node-level hooks are gone with no direct replacement; the nearest substitute is flush(object, context), which runs after every commit touching the node. In their place, constructOnly?: string[] names props a behavior can apply only while the element is being built. The list helper sets it for any list prop declared with neither a remove nor a clear hook, which covers GtkAboutDialog.creditSections and GtkApplication.mainOptions: changing either after a non-empty value has been applied now throws.

@gtkx/testing reads accessibility from GTK

Every accessible read goes through gtk_test_accessible_check_state, check_property and check_relation instead of the props the React layer recorded. A widget GTK publishes no attribute for makes the matcher throw rather than falling back to a getter; a widget GTK does publish one for answers even when nothing declared it in JSX. What this changes in existing suites:

  • Accessible names follow WAI-ARIA naming, so a name-prohibited role computes no name and a GtkFrame's label no longer names it for getByRole. Use getByLabelText.
  • Mnemonic markers are stripped from any widget whose getUseUnderline() is true, so { name: "_OK" } becomes { name: "OK" }.
  • Numeric values are compared within 0.001, and GTK reports the reachable maximum, so a scrollbar's max is its upper bound less one page.
  • A MIXED pressed tristate matches neither pressed: true nor pressed: false. Assert it with toBePartiallyPressed.
  • An AdwSwitchRow publishes SWITCH on the row as well as its inner Gtk.Switch, so a role query needs { as: Gtk.Switch } to disambiguate.

Three matchers were removed and one renamed:

// Before
expect(expander).toBeExpanded();
expect(entry).toHavePlaceholderText("Search");
expect(box).toBeEmpty();
// After
expect(expander).toHaveAccessibleState(Gtk.AccessibleState.EXPANDED, true);
expect(entry).toHaveAccessibleProperty(Gtk.AccessibleProperty.PLACEHOLDER, "Search");
expect(box).toBeEmptyWidget();

toBeSelected moved to toHaveAccessibleState the same way. Matchers are registered by name, so a call to a removed one fails at assertion time in a loosely typed suite. On the reader side, getWidgetNodeText is now getWidgetText, and getWidgetTextContent, getWidgetInvalidState and getWidgetErrorMessage left the entrypoint in favor of toHaveTextContent, toBeInvalid and toHaveAccessibleErrorMessage.

Queries only see mapped widgets

The tree walk backing every query, getRoles, logRoles and the tree dump now filters on widget.getMapped(). A widget rendered with visible={false} is not findable and { hidden: true } does not bring it back, a widget on a non-visible Gtk.Stack page is invisible until that page shows, and a closed popover's contents (including a Gtk.DropDown's popup list) are not findable until it pops up. Capture such widgets through a ref, or make them visible first.

userEvent.click follows GTK's own targeting

click used to special-case Gtk.Button, force setActive on a Gtk.Switch, then fall back to a synthesized gesture at the nearest clickable ancestor's center. It now walks outwards from the clicked widget, keeps every widget carrying a primary-button click gesture with a tracked listener, and stops at the first widget that claims the press. Presses are delivered at coordinates derived from the clicked widget's bounds, so a container's gesture reads the child's position.

So clicking a label inside a row targets the row, onPressed on ancestor boxes fires for clicks on descendants, and no gesture is ever synthesized on a widget that has none. Where GTK4 implements a click in C on its own gesture, the outcome is applied through the public action GTK's handler invokes:

  • A Gtk.ListView, Gtk.GridView or Gtk.ColumnView row grabs focus, r...

Read more

v1.0.0-rc.4

Pre-release

This release settles what GTKX's public API is. Every package declares its exported surface as an explicit list, so the types that were only ever implementation details are off the entrypoints they leaked from, and what remains is documented. Every boolean GTKX names itself now reads as a predicate, which renames options and config keys across the packages. @gtkx/components loses SizeGroup and gives its collection types List and ColumnView names.

Breaking changes

The generated store has to be rebuilt

gtkx dev, gtkx build and gtkx codegen regenerate node_modules/.gtkx on the first run after upgrading. An rc.3 store exports the old metadata names and passes descriptor options the runtime no longer reads, so an app built against one will not start. A project with codegen: false needs a store built by rc.4.

Booleans read as predicates

Every boolean GTKX names itself is prefixed with is, are, has, can or should. Options that mirror Testing Library, jest-dom and GTK keep their upstream names.

In @gtkx/react, useSignal takes isAfter and isImmediate, and an <AdwAlertDialog> response carries isEnabled. In @gtkx/components, a column is made sortable with isSortable. In @gtkx/testing, render takes isReactStrictMode and areAnimationsEnabled, prettyWidget takes shouldHighlight, and userEvent.tab takes isShiftHeld; userEvent.type's skipClick becomes shouldFocus, so write shouldFocus: false where you wrote skipClick: true. gtkx-mcp reports isSensitive and isVisible.

// Before
useSignal(scale, "value-changed", onChanged, { after: true, immediate: true });
// After
useSignal(scale, "value-changed", onChanged, { isAfter: true, isImmediate: true });

An elements.config entry takes isLazy and omittedProps, in gtkx.config.ts and on the ElementConfig a behaviors module exports. TypeScript rejects the old spellings, but the config schema strips keys it does not know, so an unmigrated gtkx.config.ts validates and loses them silently.

The flags on the descriptors a binding builds are renamed too: canThrow, isCallerAllocated, isConsumed, isInline, isSigned, and isAfter on connectSignal. Codegen rewrites the generated bindings, so only a hand-written descriptor needs the edit.

SizeGroup was removed from @gtkx/components

The component, its Child and the SizeGroupProps, SizeGroupChildProps and ChildProps types are gone. <GtkSizeGroup> from @gtkx/jsx/gtk takes the widgets through its widgets prop, so capture each one in state and pass the array. A useRef will not drive it.

// Before
import { SizeGroup } from "@gtkx/components";
<SizeGroup mode={Gtk.SizeGroupMode.HORIZONTAL}>
    <SizeGroup.Child component={DropDown} items={options} />
</SizeGroup>
// After
import { GtkSizeGroup } from "@gtkx/jsx/gtk";
const [dropdown, setDropdown] = useState<Gtk.DropDown | null>(null);
<GtkSizeGroup mode={Gtk.SizeGroupMode.HORIZONTAL} widgets={dropdown ? [dropdown] : []} />
<DropDown ref={setDropdown} items={options} />

The collection types were renamed

In @gtkx/components, Item is now ListItem, Section is ListSection, RenderItemArgs is ListItemRenderArgs, RenderHeaderArgs is ListSectionRenderArgs, ItemRenderer is ListItemRenderer, HeaderRenderer is ListSectionRenderer, and Column is ColumnViewColumn. Their members are unchanged, so the edit is to the names alone. The view prop types and everything in @gtkx/components/adw keep theirs.

Types that were never public left their entrypoints

@gtkx/react no longer exports MenuItem, VflConstraints, SettingsSchema, SettingsSchemaKeys or SettingValue; @gtkx/react/config no longer exports ModuleExport, forTypes or internal, and gained Props; @gtkx/runtime no longer exports ApplicationLike or FinishResult; @gtkx/config no longer exports ResolvedReactCompilerOptions; and @gtkx/vitest's GtkxPluginOptions is PluginOptions. Everything that took or produced them is unchanged, and the shapes are structural, so pass the value and drop the annotation. Where a name is still wanted, MenuItem and VflConstraints come off the generated GMenuProps["items"] and GtkConstraintLayoutProps["vfl"], and ResolvedReactCompilerOptions off NonNullable<ResolvedConfig["reactCompiler"]>. Write forTypes as an object literal and internal(name) as the { module, export } pair it built.

A hand-written GSettings schema now needs as const, or every key kind widens to string and useSetting hands back unknown. A schema generated from a .gschema.xml already types them as literals.

ElementBehavior.createContext is now initialize

The signature and everything downstream of it are unchanged, so the value still reaches update, flush, mount and unmount as context and attach, reorder and detach as info.context.

virtual:gtkx-config metadata is camelCased

SIGNALS, CONSTRUCT_ONLY_PROPS, CONSTRUCT_PROPS and DEFAULT_PROPS are signals, constructOnlyProps, constructProps and defaultProps, in the virtual module and in the @gtkx/jsx/metadata it re-exports.

A failed query throws ElementError

@gtkx/testing's default query error, and the name that appears with it in test output, was GtkxElementError. Its message is unchanged.

@gtkx/codegen is a smaller API

The ./gi and ./jsx entrypoints are gone, replaced by an ./internal that is not meant to be consumed, so generation runs through runCodegen or the gtkx CLI. runCodegen requires libraries, girPath and gi rather than accepting them as optional, takes isForced and userOmittedProps, reports isRegenerated, and no longer generates OpenGL. mergeOmitProps is mergeOmittedProps, readBuiltinElements reports omittedProps, and writeDocs and the Docs* types left the root entrypoint. gtkx docs resolves the stores through @gtkx/runtime now, so it fails in a project that does not have it installed, where it used to render the reference from GIR alone.

New features

@gtkx/codegen resolves a project's stores and inventories its elements

resolveStore(projectRoot) works out where a project's gi and jsx stores belong and stamps each with the installed @gtkx/runtime and @gtkx/react versions. The jsx store now ships an elements.json listing every element it emitted, read with readGeneratedElements. ApiReference gained symbols(query), which lists what the reference holds filtered by namespace and kind, and loadApiReference takes the project's props and omittedProps, which make its element pages show inherited props and leave out the ones the project removed.

Bug fixes

  • Fixed every generated store containing a symlink at node_modules/@gtkx/<name> pointing back at the store's own root, which made anything walking the tree recurse without bound and broke archiving, copying and packaging a project that had run codegen.
  • Fixed the generated stores being created readable only by the user who ran codegen, so a store built in one container layer or CI cache could not be read from another uid. They are created 0755.

v1.0.0-rc.3

Pre-release

This release finishes replacing codegen's name-based heuristics with explicit lists and real field analysis: a widget's single child is expressed one way only, and a record is constructible when its bytes can actually be copied rather than when its name looks right. @gtkx/testing gains a typed as option, jest-dom's matcher vocabulary and public event-controller helpers, so a test asserts on widgets instead of casting them and calling getters. @gtkx/config loses its implicit-default mode: a project with no gtkx.config.ts is an error rather than a silent no-op.

Breaking changes

Widgets that hold one child no longer take it as a prop

content is gone from AdwApplicationWindow, AdwWindow, AdwToolbarView, AdwNavigationSplitView, AdwOverlaySplitView, AdwBottomSheet and AdwFlap. child, along with its onNotifyChild, is gone from every widget whose child a behavior adopts: GtkButton, GtkFrame, GtkScrolledWindow, GtkPopover, GtkWindow, GtkOverlay, AdwBin, AdwStatusPage, AdwDialog and the rest. Pass the widget as a child.

// Before
<AdwNavigationSplitView
    sidebar={<AdwNavigationPage title="Lists"><Sidebar /></AdwNavigationPage>}
    content={<AdwNavigationPage title="Items"><ContentPane /></AdwNavigationPage>}
/>
// After
<AdwNavigationSplitView sidebar={<AdwNavigationPage title="Lists"><Sidebar /></AdwNavigationPage>}>
    <AdwNavigationPage title="Items">
        <ContentPane />
    </AdwNavigationPage>
</AdwNavigationSplitView>

Which properties an element omits is an explicit per-type list now, not something codegen infers from a set_child method, so rc.2's half-measure of rejecting an element while still accepting a widget instance is gone. Sibling props such as sidebar are unchanged, and with the heuristic dropped every remaining writable object property accepts a ReactElement as well as an instance: GtkColumnViewCell.child, which no behavior claims, went from Gtk.Widget | null to Gtk.Widget | ReactElement | null.

A record is constructible only when its bytes can be copied

A record with no copy or free function of its own is duplicated by copying its bytes, which aliases anything those bytes point at, so it is now constructible only when its fields are transitively scalar. Otherwise its constructor throws Cannot construct <Name>: opaque boxed type with no known layout and its …ConstructorProps is empty. Gtk.RecentData, GLib.OptionEntry, Gio.ActionEntry, GObject.EnumValue, GObject.Parameter, Pango.Analysis and GdkPixbuf.PixbufModule are among them: take an instance from the API that produces it, whose fields stay readable and writable. A record carrying its own copy and free pair keeps its property constructor.

The same analysis gates field accessors. A field embedding another record by value is exposed only when that record is itself copyable, so Pango.Item.analysis, the attr of every Pango.Attr*, GObject.Parameter.value, GObject.CClosure.closure and HarfBuzz.segment_properties_t.language are gone from the accessors and from the constructor props. A field holding a pointer to a record is unaffected.

A missing gtkx.config.ts is now an error

loadConfig validates before it looks for a file, so it rejects with a gtkx.config.ts: error naming the missing applicationId instead of handing back an empty config. gtkx codegen, the preflight gtkx dev and gtkx build run, and @gtkx/mcp all fail there now, where codegen --force used to emit default Gtk-4.0 bindings and the rest used to do nothing silently. LoadedConfig.configFile is a string to match.

DropDown no longer takes a component prop

DropDown renders a Gtk.DropDown and nothing else. Use ComboRow from @gtkx/components/adw to present the same choice as a preferences row.

// Before
import { DropDown } from "@gtkx/components";
import { AdwComboRow } from "@gtkx/jsx/adw";
<DropDown component={AdwComboRow} title="Theme" items={themes} selectedId={theme} onSelectionChanged={setTheme} />
// After
import { ComboRow } from "@gtkx/components/adw";
<ComboRow title="Theme" items={themes} selectedId={theme} onSelectionChanged={setTheme} />

ComboRow<T, S> takes the same collection props and adds the row's own, so title, subtitle, useSubtitle and enableSearch work on it. DropDownProps loses its third type parameter, and WidgetProps, which described the swap, is gone from @gtkx/components. ChildProps is unchanged.

@gtkx/testing reads widget state more strictly

toHaveTextContent no longer falls back to the accessible name, matching jest-dom, which reads textContent and nothing else. It reads the widget's own label, text or title, and otherwise its descendants' text joined by a space, so a widget named only by accessibleLabel or a tooltip has no text content at all: assert toHaveAccessibleName instead. The text is trimmed and its whitespace collapsed before comparing, which { normalizeWhitespace: false } reduces to turning non-breaking spaces into regular ones.

toHaveDisplayValue throws widget does not expose a display value rather than comparing against null, so a negated assertion on an unrelated widget no longer passes for the wrong reason.

The checked state is tri-state. A Gtk.CheckButton marked inconsistent reads as mixed, so toBeChecked() fails for it, the new toBePartiallyChecked() passes, and it answers a checked filter for neither true nor false.

New features

defineBehavior types an element behavior against the class it applies to

A behavior written as a bare object literal gets never for every hook's object parameter, so the first member access reports Property 'x' does not exist on type 'never', naming a type you never wrote. defineBehavior from @gtkx/react/config takes the GObject class as a type argument and infers the rest, and reports a wrong type argument where it is written rather than at the first member access. Bare object literals keep working.

import { defineBehavior, defineElements } from "@gtkx/react/config";
export default defineElements({
    GtkWidget: {
        behaviors: [
            defineBehavior<Gtk.Widget>({
                update: (widget, prev, next) => {
                    if (typeof next.cursorName === "string") widget.setCursorFromName(next.cursorName);
                    return ["cursorName"];
                },
            }),
        ],
    },
});

omitProps in gtkx.config.ts

elements.config[type].omitProps lists properties to leave out of that element's generated props, for a property a behavior already writes from children. It is the same list the built-in Adwaita and GTK entries now use.

Class and interface structs are bound, and functions that mention one are no longer dropped

A vtable such as GtkButtonClass or GtkBuildableIface exists as a type now, which is enough for the functions taking one to be bound. Constructing one throws unless its fields are transitively scalar, as they are for GObject.TypeClass and Gtk.OrientableIface, and a field gets an accessor wherever it can be marshalled, so Gtk.BuildableIface exposes gIface and GObject.EnumClass its values while a function-pointer field gets none.

The recovered surface includes Soup's websocket handshake functions, whose supportedExtensions is a GObject.TypeClass[], along with GObject.enumGetValue* and flagsGetValue*. Those last are emitted but not callable yet: nothing in the bound API produces the EnumClass or FlagsClass they take.

Typed queries and jest-dom's matchers in @gtkx/testing

Every query takes as, naming a widget class. It filters candidates by instanceof and narrows the return type, so the cast that used to follow a query disappears, on screen, on a render result and through within.

// Before
const scale = (await screen.findByRole(Gtk.AccessibleRole.SLIDER)) as Gtk.Scale;
// After
const scale = await screen.findByRole(Gtk.AccessibleRole.SLIDER, { as: Gtk.Scale });

The matcher set closes most of the gap with jest-dom, adding toBeDisabled, toBeEnabled, toBeVisible, toBeRooted, toBeEmpty, toBeInvalid, toBeValid, toBeRequired, toBePartiallyChecked, toHaveFocus, toHaveRole, toHaveSelection, toHaveAccessibleDescription, toHaveAccessibleErrorMessage, toContainElement, toHaveClass and toHaveObjectProperty. toHaveObjectProperty works on any GObject.Object and replaces asserting on a getter's return value, and toHaveValue now also takes a string, delegating to the display-value matcher.

// Before
expect(scale.getAdjustment().getUpper()).toBe(100);
expect(button.getSensitive()).toBe(true);
// After
expect(scale.getAdjustment()).toHaveObjectProperty("upper", 100);
expect(button).toBeEnabled();

queryController, queryAllControllers, getAllControllers and the new getController are exported too, replacing the hand-written loop a test needed to reach a Gtk.EventController.

const drag = getController(handle, Gtk.GestureDrag);

Bug fixes

  • Fixed a transfer-none field write releasing the value it displaced. Writing a boxed or fundamental field gives GTKX no claim on the pointer already in the slot, but the write freed or unreffed it anyway, and the copy it stored was never handed back to be owned. Reachable from Pango.GlyphItem.item, GdkPixbuf.PixbufModule.info and, worst, GObject.ValueArray.values, where an array of GValue was freed as though it were one.
  • Fixed writing an inline fundamental field. The codec has no size for one, so it stored a pointer into the embedded struct and destroyed the bytes it displaced; it reports an error instead. The Pango.Attr*.attr fields that reached it are no longer generated at all.
  • Fixed an inline GValue field being copied byte for byte, which aliased the source's contents without taking a reference. It i...

Read more

v1.0.0-rc.2

Pre-release

This release moves declarative placement out of @gtkx/components and onto the generated JSX elements themselves, so menus, grid and fixed placement, overlays, size groups and constraint layouts are expressed with the real GTK types. Element customization changes shape too: the declarative elementProps grammar in gtkx.config.ts is replaced by typed behavior hooks written in TypeScript. GSettings values are now typed and marshalled from each key's GVariant type string, and signal delivery during a React commit no longer swallows handlers it should never have swallowed. The collection views were rebuilt around one subscription per bound cell and a lazily built model, so scrolling re-renders only the cells whose own item changed and a collection of millions of rows no longer materializes an object per row.

Breaking changes

Menu, Grid, Fixed, Overlay and ConstraintLayout were removed from @gtkx/components

All five components, their sub-components and their prop types are gone. Placement is now expressed by wrapping each child in the generated GtkGridLayoutChild, GtkFixedLayoutChild or GtkOverlayLayoutChild element. Because those set properties on the real Gtk.LayoutChild, changing a cell repositions the widget in place instead of removing and re-attaching it.

// Before
import { Grid } from "@gtkx/components";
<Grid>
    <Grid.Child component={GtkLabel} column={0} row={0} />
    <Grid.Child component={GtkEntry} column={1} row={0} columnSpan={2} />
</Grid>
// After
import { GtkGrid, GtkGridLayoutChild } from "@gtkx/jsx/gtk";
<GtkGrid>
    <GtkGridLayoutChild column={0} row={0}>
        <GtkLabel />
    </GtkGridLayoutChild>
    <GtkGridLayoutChild column={1} row={0} columnSpan={2}>
        <GtkEntry />
    </GtkGridLayoutChild>
</GtkGrid>

Menu becomes <GMenu items={items}> from @gtkx/jsx/gio, taking the same array, with the entry type now exported from @gtkx/react as MenuItem. Overlay.Child becomes a GtkOverlayLayoutChild passed in GtkOverlay's new overlays prop. Fixed's x/y shorthand has no equivalent: GtkFixedLayoutChild exposes only the Gsk.Transform. An unwrapped GtkGrid or GtkFixed child is attached with default placement.

ConstraintLayout, its Constraint, Guide and Vfl sub-components and the ConstraintLayoutProps, ConstraintProps, ConstraintGuideProps and ConstraintVflProps types are gone as well. Constraints are now GtkConstraint and GtkConstraintGuide elements passed in GtkConstraintLayout's constraints, guides and vfl props. The participants are Gtk.ConstraintTarget objects rather than names, so capture each widget in state and render the constraints once it resolves; an omitted source still means the widget that owns the layout, and omitting both source and sourceAttribute still makes the relation a constant.

// Before
import { ConstraintLayout } from "@gtkx/components";
<GtkBox layoutManager={(
    <ConstraintLayout>
        <ConstraintLayout.Constraint
            target="button"
            targetAttribute={Gtk.ConstraintAttribute.START}
            sourceAttribute={Gtk.ConstraintAttribute.START}
            constant={8}
        />
    </ConstraintLayout>
)}>
    <GtkButton name="button" label="Constrained" />
</GtkBox>
// After
import { GtkConstraint, GtkConstraintLayout } from "@gtkx/jsx/gtk";
const [button, setButton] = useState<Gtk.Button | null>(null);
<GtkBox layoutManager={(
    <GtkConstraintLayout constraints={button && (
        <GtkConstraint
            target={button}
            targetAttribute={Gtk.ConstraintAttribute.START}
            sourceAttribute={Gtk.ConstraintAttribute.START}
            constant={8}
        />
    )}
    />
)}>
    <GtkButton ref={setButton} label="Constrained" />
</GtkBox>

A vfl block keeps its lines, hspacing and vspacing fields and gains views, the map from the names used in the description to targets. Blocks are compared field by field and views by identity, so memoize that map or every render tears the parsed constraints down and rebuilds them. GtkConstraintGuide's properties are ordinary, so it updates in place; every GtkConstraint property is construct-only, so a constraint that has to change needs a key that changes with it.

Element customization moved from elementProps to elements and @gtkx/react/config

The elementProps option and the Arg/Call/ArgRef prop-mapping grammar are removed. Custom elements are now authored as behavior objects with defineElements from the new @gtkx/react/config entrypoint, pointed at by elements.behaviors in gtkx.config.ts. Behaviors carry no type information, so a prop a behavior introduces has to be declared by hand.

Besides update, a behavior can hook attach, detach, reorder and resolve for children, createContext for private per-element state, mount, unmount and flush for commit-time work, and create for a type whose constructor does more than set properties (create is used for its own type only, never inherited by subtypes).

// gtkx.config.ts
export default defineConfig({
    applicationId: "com.example.app",
    elements: { behaviors: "./src/elements.ts" },
});
// src/elements.ts
import { defineElements } from "@gtkx/react/config";
export default defineElements({
    GtkWidget: {
        behaviors: [
            {
                update: (widget: Gtk.Widget, prev, next) => {
                    if (typeof next.cursorName === "string") widget.setCursorFromName(next.cursorName);
                    return ["cursorName"];
                },
            },
        ],
    },
});
declare module "@gtkx/jsx/gtk" {
    interface GtkWidgetProps {
        cursorName?: string | null | undefined;
    }
}

Signals are no longer blocked during a commit unless you declare them

Previously every handler was suppressed while React applied a commit, apart from a fixed allowlist of eleven lifecycle signals. That is now inverted: only signals classified as user-event signals for the emitting type are suppressed, resolved through the type's ancestry from a built-in table covering the state-echo signals (GObject::notify, GtkEditable::changed, GtkToggleButton::toggled, GtkRange::value-changed, and more). Handlers that used to be dropped silently, including onClicked, onActivate, onRowActivated and every app-defined custom signal, now fire. To suppress one, add it under its emitting type in userEventSignals in gtkx.config.ts; entries there are merged into the built-in table rather than replacing it.

GSettings value types come from the schema's type strings, and useBindSetting takes an options object

SchemaRef<K> is replaced by SettingsSchema<K>, whose generic parameter is the record of GVariant type strings rather than a hand-written map of value types, so every key's value type is derived instead of declared. That is what lets tuples, dictionaries, maybe types and nested arrays unpack into plain JavaScript values instead of a raw GLib.Variant. Two kinds change at runtime: enum keys are now a number read through getEnum rather than a nick string, and flags keys a bitfield number rather than string[].

// Before
const SCHEMA: SchemaRef<{ enabled: boolean; "wrap-mode": "none" | "word"; "window-size": GLib.Variant }> = {
    id: "com.example.app",
    path: null,
    keys: { enabled: "b", "wrap-mode": "enum", "window-size": "(ii)" },
};
useBindSetting(SCHEMA, "enabled", switchRef, "active");
// After
const SCHEMA: SettingsSchema<{ enabled: "b"; "wrap-mode": "enum"; "window-size": "(ii)" }> = {
    id: "com.example.app",
    path: null,
    keys: { enabled: "b", "wrap-mode": "enum", "window-size": "(ii)" },
};
useBindSetting({ schema: SCHEMA, key: "enabled", object: switchRef, property: "active" });

useSetting(schema, key) keeps its positional signature. SettingsSchemaKeys and SettingValue are exported alongside SettingsSchema from @gtkx/react.

Text buffer content uses GtkTextChildAnchor for both widgets and paintables

The synthetic <GtkTextAnchor> and <GtkTextPaintable> elements are gone, and so is the TextPaintableProps type that @gtkx/react exported for the latter. Both kinds of embedded object are now the real GtkTextChildAnchor element from @gtkx/jsx/gtk: give it a child widget, or give it a paintable prop. Passing both throws, as mixing a text prop with content children on <GtkTextBuffer> does.

// Before
<GtkTextBuffer>
    {"The buffer can have images in it: "}
    <GtkTextPaintable paintable={texture} />
</GtkTextBuffer>
// After
<GtkTextBuffer>
    {"The buffer can have images in it: "}
    <GtkTextChildAnchor paintable={texture} />
</GtkTextBuffer>

Because the paintable is inserted while the buffer is built, an enclosing <GtkTextTag> covers it and its character counts towards the offsets of the text after it. An anchor with a custom replacement character still has to be built with Gtk.TextChildAnchor.newWithReplacement and inserted through buffer.insertChildAnchor.

Changing a construct-only prop throws instead of being ignored

GTK accepts a construct-only property only while the object is being built, so a later change never reached the widget. That was silent before; it now throws, naming the prop and the element and telling you to give the element a key that changes with the prop so React builds a new one. GtkConstraint is the type this matters most for, since every one of its properties is construct-only.

Renamed types

In @gtkx/components: ItemNode is now Item, SectionNode is Section, RenderItemProps is RenderItemArgs, DropDownItemRenderer is ItemRenderer, and ColumnDef is Column. Prop names and runtime shapes are unchanged, so these are mechanical, but one behavior change rides along: GridView is pinned to a flat collection, so nested Item.children no lon...

Read more

v1.0.0-rc.1

Pre-release

GTKX 1.0.0-rc.1 is a ground-up rework of the framework since v0.21.0 (265 commits). What was a React reconciler over a curated set of GTK4 widgets is now a general platform for driving GObject libraries from TypeScript.

Read the announcement: https://gtkx.dev/blog/gtkx-1-0-rc-1

This is a release candidate. The API it ships is what 1.0 will ship.

What changed in 1.0

  • Bindings are generated on your machine. Earlier versions shipped pregenerated bindings baked into @gtkx/ffi. Now the CLI runs codegen against the GObject-Introspection data installed with your development libraries. gtkx.config.ts declares which libraries to bind, and codegen emits @gtkx/gi/<namespace> (typed classes, enums, signal and property maps) and @gtkx/jsx/<namespace> (one typed React component per widget) into node_modules.
  • Every element is a GObject, driven by one generic reconciler. The hand-written per-widget node classes were replaced by a single reconciler that instantiates any GObject class by type and attaches children from generated metadata. Elements pass as prop values (buffer={<GtkTextBuffer/>}), and controllers, layout managers, actions, and menu models are declarative props. Apps boot with createRoot().render(tree) and a generated <GtkApplication>.
  • The native core was rewritten from a two-thread Neon addon into a single-threaded napi-rs crate that drives the GLib main context on the Node thread, so GTK4 and JavaScript share one thread with no cross-thread marshaling. Libraries load on demand, which is what makes GTKX a general GObject bridge. GLib criticals and Rust panics now surface as Node fatal exceptions.
  • Typed signals now include notify::<property> detail suffixes, with a useSignal hook and a useSetting hook typed against imported GSettings schemas.
  • New build tooling: a GResource asset pipeline, so import icon from "#data/icon.png" bundles the file into the app's resources, and React Compiler enabled by default.
  • Configuration moved from a package.json field to gtkx.config.ts, and the minimum Node.js is now 24.

Packages

  • New: create-gtkx (the npm create gtkx scaffolder), @gtkx/components (collection views, a Menu builder, layout helpers), and @gtkx/gl (OpenGL 4.6 core bindings).
  • Renamed: @gtkx/ffi became @gtkx/runtime, the FFI runtime the generated bindings call into; it now also lets you subclass any GObject with registerClass and bind native functions with the typed t descriptor DSL.

Requirements

  • Node.js 24 or later.
  • Linux with the GTK4 and GLib (2.68 or later) development libraries. Adwaita is needed only once you add Adw-1 to your libraries.
  • @gtkx/native ships prebuilt binaries for x64 and arm64 glibc Linux; other targets build it from the GTKX repository with a Rust toolchain.

Install

npm create gtkx@rc

This RC is published under the rc npm dist-tag, so @latest still resolves to the current stable release.

Links

Read the original on github.com ↗