Skip to content

Configuration and Codegen

Codegen is driven from gtkx.config.ts, which declares which libraries to generate bindings for, and your application ID.

The config file

defineConfig from @gtkx/config types your config for editor completion and validates it when the CLI loads the file:

ts
import { defineConfig } from "@gtkx/config";

export default defineConfig({
    libraries: ["Gtk-4.0", "Adw-1"],
    applicationId: "com.gtkx.tutorial",
});

mergeConfig(base, override) layers a project config over a shared base. A $development or $production block layers over the top level, per mode.

Every option

applicationId is the only required key; the rest have defaults.

  • applicationId: the GApplication identifier the app registers under, in reverse-DNS form (com.example.Tasks).
  • libraries: the GIR libraries to bind, as Name-Version. Gtk-4.0 is the default, and joins any list that does not already name a Gtk version; the bare string "*" (never an array entry) binds everything on the GIR path.
  • girPath: directories searched for .gir files ahead of the standard locations.
  • reactCompiler: the React Compiler, on by default. false disables it; an object forwards compilationMode and panicThreshold.
  • codegen: false: skips generation, so the project imports whatever binding store is already installed.
  • userEventSignals: signals, keyed by GLib type name, that GTKX suppresses while writing to a widget itself. Entries merge into the defaults.
  • elements: the element customizations: behaviors is the module default-exporting your defineElements map, config sets per-type codegen output (component, props, omittedProps, isLazy).

What codegen emits

Codegen writes packages into node_modules/.gtkx and links them into node_modules/@gtkx, so imports resolve without either appearing in your package.json:

  • @gtkx/gi is the introspected API, one subpath per namespace (@gtkx/gi/gtk, @gtkx/gi/adw): the classes, enums, and functions you call imperatively, for refs and values such as Gtk.Orientation.VERTICAL.
  • @gtkx/jsx is the React layer, likewise per namespace (@gtkx/jsx/gtk, @gtkx/jsx/adw): a PascalCase component per widget (GtkButton, AdwHeaderBar), a Props interface for each, and a React.JSX.IntrinsicElements augmentation.

A few bindings take a NUL-terminated C string that GIR describes as a byte array (GLib.Variant.newBytestring(string: number[])), so the value silently stops at the first zero byte. Binary payloads go through GLib.Bytes and GLib.Variant.newFromBytes.

The JSX prop model

Every GIR class descending from GObject becomes an intrinsic element whose props follow five rules:

  • Properties become camelCase props. Writable, construct, and construct-only properties become optional props: show-title-buttons is showTitleButtons.
  • Almost every property gets a notify handler. onNotifyX receives (value, self), read-only properties included, so you can observe what GTK4 changes on its own. The element-accepting object properties below are the exception: they carry their value as a child element instead.
  • Object-typed props also accept elements. A writable, non-construct-only property typed as a GObject class takes a ReactElement as well as an instance, and the reconciler manages the child.
  • Signals become on handlers. clicked is onClicked, row-activated is onRowActivated, and the handler receives the signal's parameters followed by self.
  • ref yields the @gtkx/gi instance. Every element accepts ref?: Ref<Self | null> (Gtk.Button, Adw.ToastOverlay), the escape hatch to the imperative API.
tsx
import type * as Gtk from "@gtkx/gi/gtk";
import { GtkButton } from "@gtkx/jsx/gtk";
import { useRef } from "react";

const SaveButton = () => {
    const buttonRef = useRef<Gtk.Button | null>(null);

    return <GtkButton label="Save" onClicked={(self) => self.setSensitive(false)} ref={buttonRef} />;
};

Generating element reference docs

gtkx docs writes one markdown page per JSX element, by default into docs/reference:

bash
gtkx docs

Each page carries the widget's documentation, hierarchy, slot rules, props, signal handlers, and ref methods with their signatures. gtkx docs --help covers the output directory and link root.

Advanced: Customizing elements

A GtkScale's marks have no property behind them, only addMark and clearMarks, and adding a child is insertChildAfter on a GtkBox but addTopBar on an AdwToolbarView. Element behaviors cover what property setting cannot: lifecycle hooks bound to a GLib type, which the reconciler calls as elements of that type are created, populated, updated, and removed. Every hook is listed in the ElementBehavior reference: attach, reorder, and detach place, move, and remove a child in a slot, resolve returns the object the container made for that child, and flush runs once the surrounding commit has placed every child. update runs on each commit with the previous and next props, and the prop names it returns are the ones GTKX will not also set as plain properties.

setCursorFromName is another method with no property behind it. Default-export a map keyed by GLib type name, wrapping each behavior in defineBehavior with the class it applies to:

ts
// src/elements.ts
import type * as Gtk from "@gtkx/gi/gtk";
import { defineBehavior, defineElements } from "@gtkx/react/config";

export default defineElements({
    GtkWidget: {
        behaviors: [
            defineBehavior<Gtk.Widget>({
                update: (widget, prev, next) => {
                    if (!Object.is(prev.cursorName, next.cursorName) && typeof next.cursorName === "string") {
                        widget.setCursorFromName(next.cursorName);
                    }
                    return ["cursorName"];
                },
            }),
        ],
    },
});

Pass the class as the type argument so the hooks are typed. Point elements.behaviors at the module, then declare the prop on the generated interface:

ts
// gtkx.config.ts
import { defineConfig } from "@gtkx/config";

export default defineConfig({
    libraries: ["Gtk-4.0", "Adw-1"],
    applicationId: "com.gtkx.tutorial",
    elements: { behaviors: "./src/elements.ts" },
});
ts
// src/augmentations.d.ts
import "@gtkx/jsx/gtk";

declare module "@gtkx/jsx/gtk" {
    interface GtkWidgetProps {
        cursorName?: string | null | undefined;
    }
}

The leading import is what makes this an augmentation. Without a top-level import or export, declare module becomes an ambient module declaration that shadows the generated one, and @gtkx/jsx/gtk stops exporting elements.

A behavior on a type covers every element descending from it, and your behaviors run before the built-in ones, so they override existing prop and slot handling. isLazy: true in the same map marks a type whose GObject its parent container creates.

Next

With the codegen pipeline in hand, continue to Async Operations.

Released under the MPL-2.0 License.