v1.3.0
Read the announcement: GTKX 1.3: Introducing @gtkx/animated.
New Features
@gtkx/animatedbrings React Spring to GTK —useSpring,useSprings,useTrail,useTransition,useChainanduseSpringValue, theSpring,TrailandTransitioncomponents, and theanimated(Component)wrapper with itsanimated.GtkLabelshorthand 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 theaccessible*props, falls back to a React render per frame. Frames come from the GTK frame clock, anduseReducedMotionreads GTK'sgtk-enable-animationsandgtk-interface-reduced-motionsettings.- A
styleprop on every widget — Every element that renders aGtk.Widgettakes astyleobject of CSS declarations, the way React DOM spells them. GTK4 has no inline styles, so the object compiles to a single rule in aGtk.CssProviderthat belongs to that widget alone, registered one step aboveSTYLE_PROVIDER_PRIORITY_APPLICATIONsostyleoutranks anything a class incssClassessets. 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 toundefinedornullremoves 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 getspxwhere 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, sostyle={{ display: "flex" }}fails to compile instead of becoming a runtime warning — layout stays in the widget's own props.StyleandStylePropertiesare new public types. - Springs drive
style—@gtkx/animatedwritesstyleper frame the way it writes a GObject property, through the widget'srefand without re-rendering the component, which is what makes a color, aborder-radiusor abox-shadowanimatable 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 tostyleas it is, the way React Spring is written for the DOM. Onlystyleis 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,PatternandFontFaceare abstract, and instances arrive as the most specific class the package models, sosurface instanceof ImageSurfaceandctx.getSource() instanceof LinearPatternnarrow, while kinds with no dedicated subclass — a solid or surface pattern, a group target — come back as the base class. registerClassdeclares signals — Asignalsoption creates GObject signals on the new type, each naming itsparamTypesand optionalreturnTypeas numeric GTypes or as classes carrying one, itsflags, and anaccumulatoroffirst-winsortrue-handled.connect,emit,on,once,offanduseSignaltake the declared names, withprogress_changedandprogress-changedreaching the same signal, andemitconverts each argument into aGValueof the declared type.SignalSpecandSignalGTypeare new public types.GValueparameters take a plain JavaScript value — Every parameter the callee only reads is typedGObject.Value | JsValueand accepts the JavaScript value itself, with the GType inferred: string togchararray, boolean togboolean, a whole in-range number togint, any other number togdouble,biginttogint64orguint64, an array of strings toGStrv, a wrapper to the GType it carries, andnullto a NULLgpointer. Arguments of an emitted signal infer the same way, and passing aGObject.Valuebuilt by hand still works wherever inference would guess something else. A signal handler still receives a realGObject.Value.- GType parameters take a class — Every parameter that takes a GType accepts the class registered under it alongside the numeric
bigint, soGio.ListStore.new(Gtk.Label)andGObject.typeName(Gtk.Label)work, as do signal arguments declared as GTypes. A class that never went throughregisterClassis 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 types —
abstract: trueregisters the type withG_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. classInitandcssNameonregisterClass—registerClasstakes aclassInithook and acssName, and every generated*ClassGTypeStruct wrapper is now paired with its class.GObject.ObjectClass.peekandGtk.WidgetClass.peekhand back any type's class struct outside aclassInithook, soGObject.ObjectClass.peek(Gtk.Label).findProperty("label")works, backed by the newpeekTypeClassandregisterClassStructruntime exports.- Property overrides with
paramSpecOverride—paramSpecOverride(name, source)redeclares a property a parent class or an implemented interface already carries, the wayg_param_spec_overridedoes in C, giving the subclass its own storage andnotifyemission 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.newParamSpecOverrideis the matching@gtkx/runtimeexport. ParamSpecintrospection — EveryGObject.ParamSpeccarries readonlyname,nick,blurb,flags,valueTypeandownerTypegetters, whether it comes from aparamSpec*constructor, anotifyhandler, orfindPropertyandlistProperties.flagsis theParamFlagsbitfield the spec was created with,valueTypethe GType of the values the property holds, andownerTypethe GType the spec is installed on,TYPE_INVALIDuntil it is installed on one.getParamSpecFlags,getParamSpecValueTypeandgetParamSpecOwnerTypeback them from@gtkx/runtime.fromVarianttakes a variant alone and unpacks recursively —fromVariantno longer requires a type string: it accepts aGLib.Varianton its own, and an options object withrecursive: trueunwraps every nested variant into the value it holds, all the way down.toVariantacceptsUint8Arrayornumber[]for a byte array either way.FromVariantOptions,RecursiveFromVariantOptions,RecursiveVariantValue,VariantInputandByteArrayare new exported types.- Bare
@gtkx/jsximports — The generated JSX store gains an index module, soimport { 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,v2FinishResultsandv2InoutReturnsfuture flags — Thefutureconfig block takes three new flags, all off by default and unconditional in 2.0.v2ValueReturnsmakes the bindings whose return or caller-allocated out parameter is aGValue—Gtk.DropTarget.getValue,Gtk.ConstantExpression.getValue,Gdk.Clipboard.readValueAsync,Gtk.Builder.valueFromStringType,Gtk.TreeModel.getValueand a handful more — hand back what the value holds, typedunknown.v2FinishResultsdrops the always-true leading success boolean from promisified async methods, soGio.File.loadContentsAsyncresolves to[number[], string | null], or[Uint8Array, string | null]withv2ByteArraysalso enabled, and a call left with a single out parameter, such asreplaceContentsAsync, resolves to that value directly.v2InoutReturnsstops 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, soGsk.Path.getNext(point)becomesbooleanrather than[boolean, PathPoint]andPango.Matrix.transformRectangle(rect)becomesvoid; primitive inout parameters, which cannot be mutated in place, stay in the result either way. Flip them and runtsc: every site that needs attention is a type error.- New
@gtkx/runtimeexports — TheGValuemarshalling setfromValue,toValueHandle,tryToValueHandle,ValueMarshalErrorandJsValueis public, alongsidecoerceObjectProperty,registerWrapperClassResolverwith itsWrapperClassResolvertype,matchRegexandmatchAllRegex, which keep a regex subject's bytes alive alongside itsMatchInfo, andtrimFinish.
Breaking Changes
on<SignalName>methods become signal default handlers —registerClassinstalls 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: anonShow,onDestroy,onMaporonNotifymethod on a subclass now runs on every emission of that inherited signal. Audit every registered class foron-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/cairois deprecated —@gtkx/gi/cairostill resolves for all of 1.x and re-exports@gtkx/cairo, but a project that does not declare@gtkx/cairogets the copy the cod...
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.
registerInterfacetags the interface's prototype through the same path a class goes through, soGtk.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.tsanyway, soGLib.Queue.head,GLib.List.next,Pango.LayoutLine.runsandGObject.SignalQuery.paramTypestyped as their GIR type and every one readundefinedat runtime. Fields backed by aGList,GSList,GHashTableor a sibling-length array are gone from the class and the declarations alike, and the one shape that can be read — anull-terminated pointer array — gained a real accessor, soGio.DBusNodeInfo.interfaces,Gio.DBusInterfaceInfo.methods,GdkPixbuf.PixbufFormat.mimeTypesandGtk.RecentData.groupsdecode instead of answeringundefined. - 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.axesandGsk.RoundedRect.cornerhad setters and appeared inConstructorProps, sonew Gdk.TimeCoord({ axes })raised on every call. Array, list and hash-table fields are read-only now and out ofConstructorProps; inline struct arrays keep their element-wise setters. - Fixed a signal GIR marks
detailed="1"accepting no::detailsuffix in its generated map, although the runtime has always stripped the suffix before looking the signal up. Onlynotifywas special-cased, sosettings.on("changed::theme", handler)andbar.on("offset-changed::low", handler)stopped type-checking once v1.2.0 removed the plain-stringoverloads. Each detailed signal now contributes a[detail: `changed::${string}`]index signature to both its maps, coveringGio.Settings,Gio.DBusProxy,Gio.ActionGroup,Gtk.LevelBar,Gtk.AppChooserButton,Adw.MessageDialogand WebKit. - Fixed
gtkx codegennot counting@gtkx/runtimeamong the packages that must be able to import the generated@gtkx/gi, so a layout that installs it above thenode_modulesthe store is written into produced a store the runtime could never resolve. Codegen now stops with the same error it gives for the other@gtkxpackages. - Fixed a
ColumnViewover items withchildrendrawing aGtk.TreeExpanderin every cell of a row instead of one per row, so a three-column tree gave each row three toggles, stampedexpanderDescriptionson 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 whosevisibleis notfalse— and the rest renderrenderCelldirectly and stay aligned at every depth.expandedIdsandonExpandedChangeare unaffected; a tree that wants the toggle elsewhere lists that column first. - Fixed
useSignalrunning the handler captured on the first render for every emission inside a component wrapped inmemoorforwardRef, 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'suseEffectEvent, which does not pick up the updated function through those wrappers; the handler is now held in a ref written fromuseInsertionEffect, 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.extraFilesinstalling every file 644, or 755 when the destination name ended in.nodeor.so, so a helper script listed there arrived unrunnable however it had been staged. An entry without an explicitmodenow 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 thedeploy.extraFilesentries, all three of which the prebuilt flatpak installs, so a Flathub submission registered none of the app'sfileAssociationsand shipped without its license. All three install now, and because a source build has only the git chec...
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...
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
nfpmandappimagetoolare downloaded, checksum-verified against pinned digests and cached under~/.cache/gtkx/, so a Fedora machine builds.debpackages and a Debian machine builds.rpmpackages.desktop-file-validate,appstreamcliand, for Flatpak,flatpak-builderare 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.isDbusActivatablewritesshare/dbus-1/services/<id>.servicenext to the desktop entry for deb, rpm and Flatpak, pointing at the installed launcher with--gapplication-service, and dropsDBusActivatablefrom the AppImage copy, where no service file can be installed.- A
deploy.screenshotsentry can name afilein the repository instead of an absoluteurl, and the base URL is derived from theoriginremote for GitHub and GitLab, including the project's path inside the repository. Setdeploy.screenshotBaseUrlwhen the remote is somewhere else. deploy.releases,deploy.contentRating,deploy.brandinganddeploy.urlsfill the AppStream sections a software center reads, anddeploy.fileAssociations,deploy.protocolsanddeploy.desktopActionsfill the desktop entry, its MIME package and its action groups.deploy.desktopEntryadds or overrides desktop entry keys,deploy.extraFilesmaps prefix-relative destinations to files in the project,deploy.dependsanddeploy.relationsadd package relationships per format,deploy.scriptssupplies maintainer scripts, anddeploy.signingsigns the.deb, the.rpm, the Flatpak repository or the AppImage.- The whole
deployblock is validated with the rest ofgtkx.config.ts, so a misspelled key or a badly typed value is reported asgtkx.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.cursorArrayjoins the runtime's descriptor surface, for a pointer that reports a position inside a buffer the call was already given.configure({ windowActivationTimeout })sets how longrenderwaits for the window it shows the tree in to become active, defaulting to five seconds.actionabilityTimeoutkeeps its 500 ms and now covers user even...
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 forgetByRole. UsegetByLabelText. - 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
maxis its upper bound less one page. - A
MIXEDpressed tristate matches neitherpressed: truenorpressed: false. Assert it withtoBePartiallyPressed. - An
AdwSwitchRowpublishesSWITCHon the row as well as its innerGtk.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.GridVieworGtk.ColumnViewrow grabs focus, r...
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.infoand, worst,GObject.ValueArray.values, where an array ofGValuewas 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*.attrfields that reached it are no longer generated at all. - Fixed an inline
GValuefield being copied byte for byte, which aliased the source's contents without taking a reference. It i...
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...
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.tsdeclares 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) intonode_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 withcreateRoot().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 auseSignalhook and auseSettinghook 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.jsonfield togtkx.config.ts, and the minimum Node.js is now 24.
Packages
- New:
create-gtkx(thenpm create gtkxscaffolder),@gtkx/components(collection views, aMenubuilder, layout helpers), and@gtkx/gl(OpenGL 4.6 core bindings). - Renamed:
@gtkx/ffibecame@gtkx/runtime, the FFI runtime the generated bindings call into; it now also lets you subclass any GObject withregisterClassand bind native functions with the typedtdescriptor 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-1to yourlibraries. @gtkx/nativeships 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
- Announcement: https://gtkx.dev/blog/gtkx-1-0-rc-1
- Guide: https://gtkx.dev/guide/why-gtkx
- Tutorial: https://gtkx.dev/tutorial/
- Discussions: https://github.com/gtkx-org/gtkx/discussions
- Issues: https://github.com/gtkx-org/gtkx/issues