GTKX

repository·main·Indexed 43 days ago

https://github.com/gtkx-org/gtkx

A React framework for Linux that enables developers to build native GNOME applications using TypeScript and standard web tooling. It provides a declarative layer over Adwaita and GTK4, utilizing a custom React reconciler to render real native widgets directly via a Rust core and Node.js runtime.

Tokens
199.7K
Snippets
524
Records
680
Agent score
55%

What's inside GTKX

  1. Understand the GTKX architecture and core features

    main

    GTKX is a React-based framework for building Linux applications using the GNOME stack (GTK4 and libadwaita). It provides a declarative layer over the imperative GObject/GTK system.

    Key features include:

    • React Reconciler: Exposes the GNOME object graph as JSX elements.
    • Development Tooling: A CLI for scaffolding and builds, and a dev server with Fast Refresh for live UI patching.
    • Styling & Animation: CSS-in-JS styling and React Spring animations.
    • Navigation: Built-in navigators for stacks, tabs, drawers, and split views.
    • UI Components: High-level list/grid components and mount-based dialogs.
    • Testing: A Testing Library-style API for querying and driving widgets.
    • AI Integration: A Model Context Protocol (MCP) server to expose live apps to AI agents.
  2. Manage Search State in the UI Store

    main

    When implementing search, your UI store should track both the visibility of the search bar and the current query string. This allows you to reset the search state globally when navigating between different views.

    Required state properties:

    • searchMode: A boolean indicating if the search bar is currently revealed.
    • searchQuery: The current string being searched.

    Recommended actions:

    • setSearchMode(boolean): Updates visibility.
    • setSearchQuery(string): Updates the query.
    • resetSearch(): Clears both searchMode and searchQuery simultaneously. This should be called when a user selects a new view from a sidebar to prevent showing an empty list due to a stale search.
  3. Understand CRITICALs during binding calls

    main

    When a native function rejects an invalid argument using g_return_if_fail, it logs a CRITICAL and returns a sentinel value.

    If this happens while a JavaScript-initiated GTKX binding call is on the stack, the addon traps the critical and throws an ordinary JavaScript Error instead of returning the sentinel. This error is not a GLib.Error and does not have a domain or code. You can catch this using a standard try/catch block around the call or by handling the rejection of a promisified method.

  4. Manage application assets and GSettings

    main

    GTKX supports importing assets and GSettings schemas using the #data/* prefix.

    • Assets: Files imported via #data/* are automatically compiled with glib-compile-resources and registered as resource:// URIs. Changing an asset re-registers the bundle without requiring an app restart.
    • GSettings: Schemas can be imported, and their keys are available as typed values via the useSetting hook from @gtkx/react.
    import { useSetting } from "@gtkx/react";
    import schema from "#data/com.gtkx.tutorial.gschema.xml";
    
    export const useSortOrder = () => {
        const [value, setValue] = useSetting(schema, "sort-order");
        // ...
    };
  5. Understand how Zustand persist handles hydration and writes

    main

    Automatic Writes

    persist automatically subscribes to the store and writes to the storage backend on every successful set operation. There is no need to manually call a save function or implement a window close handler to flush data.

    Hydration

    Because the storage backend is synchronous, persist reads the file from disk while the store is being created, before React performs its first render. This means the first frame rendered by the UI will already reflect the persisted state from the disk, preventing 'flicker' between empty and loaded states.

  6. Import assets and resources in GTKX 2.0

    main

    GTKX 2.0 uses explicit query specifiers in module imports to define how an asset should be handled by the build system. The global #data/ map has been removed in favor of relative imports with specifiers:

    • ?icon: For icon assets.
    • ?resource: For general resources.
    • ?url: For URL-based assets.
    • Settings schemas: Use query-free relative imports.

    These imports are resolved via standard module resolution.

    import icon from "../data/tasks.svg?icon";
    import logo from "../data/logo.png?resource";
    import schema from "../data/com.example.Tasks.gschema.xml";
  7. How @gtkx/forms works with Adwaita rows

    main

    The @gtkx/forms library bridges native Adwaita widgets with React Hook Form.

    • State Ownership: The form (via React Hook Form) owns the values, while the rows (EntryRow, SwitchRow, etc.) behave like native widgets.
    • Context: Use FormProvider to make the form state available to all descendant form rows. This allows rows to use a name prop to link to a field without needing an explicit control prop.
    • Typing: Passing a generic type to rows (e.g., EntryRow<TaskFields>) ensures that the name prop is validated against your data schema at compile time.
    import { EntryRow, FormProvider, useForm } from "@gtkx/forms";
    
    // ... inside component
    const form = useForm<TaskFields>({ defaultValues: { ... } });
    
    return (
      <FormProvider {...form}>
        <EntryRow<TaskFields> name="title" title="Title" />
      </FormProvider>
    );
  8. Understand the application resource base

    main

    GTKX automatically derives a resource base path from your applicationId. It prefixes the ID with / and replaces dots with slashes. For example, com.example.Tasks becomes /com/example/Tasks.

    This path is accessible via the resourceBasePath export from the virtual:gtkx-config module. It is used by default for ?resource paths, ?icon assets, and the resourceBasePath prop in AdwApplication and GtkApplication elements.

    Note: If you override an element's applicationId prop, the resource base does not automatically move. You must also pass a new resourceBasePath if you intend to use a different resource tree.

  9. Implement Zustand slices for state management

    main

    Instead of a single monolithic store, split your state into slices. A slice is a function that returns a portion of the state and its associated actions.

    When creating a slice, use the StateCreator type with the following signature: StateCreator<Store, Mutators, [], SliceType>

    Key components of this signature:

    • Store: The type of the entire combined store. This allows a slice to access state from other slices via set((state) => ...).
    • Mutators: The middleware applied to the store (e.g., [["zustand/persist", unknown]]). This ensures set has the correct types for persisted stores.
    • []: An empty tuple representing middleware applied specifically to this slice (slices themselves should not apply middleware; middleware is applied once to the combined store).
    • SliceType: The specific shape of the state and actions this slice contributes.
    import type { StateCreator } from "zustand";
    import type { Store, Mutators } from "./index.js";
    import type { TasksSlice } from "./tasks.js";
    
    export const createTasksSlice: StateCreator<Store, Mutators, [], TasksSlice> = (set) => ({
        // ... slice implementation
    });
  10. Organize state by persistence and lifecycle

    main

    To maintain a clean architecture, categorize new state based on its purpose and lifecycle:

    1. Persisted Data: Information the user typed that must survive a restart (e.g., tasks, lists). This goes into a persisted Zustand slice.
    2. Navigation/Location State: Where the user is currently located in the app (e.g., the selected list). This should be stored in the route's params managed by a navigator, not the global store.
    3. Transient UI State: Interface behaviors that don't represent a 'place' (e.g., whether a layout is collapsed, which dialog is open, search queries). This goes into a UI slice that is explicitly excluded from persistence via partialize.
    4. User Preferences: Intentional settings (e.g., theme choices). These should be stored in GSettings.
  11. Handle GTKX 2.0 behavior changes

    main

    GTKX 2.0 introduces several breaking changes in data types and object behaviors:

    • GIR byte sequences: Now use Uint8Array. Avoid Array.isArray or JSON serialization if you rely on specific array mutation behaviors.
    • GObject.Value: Returned values are now unwrapped and typed as unknown. You must narrow the type at the call site.
    • Promisified finish methods: Omit the leading success boolean. For example, loadContentsAsync now resolves to [Uint8Array, string | null].
    • Inout records and boxed values: These now mutate the object passed in directly instead of returning a new object.
    • Assets: Use relative imports with query parameters: ?resource, ?icon, ?font, or ?url. Settings schemas use query-free relative imports.
    • Class Registration: Generated classes register with their own definitions. If GObject.typeFromName must find a class, ensure you explicitly import it.
  12. Implement adaptive Master/Detail with createSplitViewNavigator

    main

    The createSplitViewNavigator is designed for GNOME-style master/detail layouts using AdwNavigationSplitView.

    • Structure: The first screen in your route tree is treated as the sidebar (master). All subsequent screens belong to the content stack (detail).
    • Navigation: Navigating to a route in the content stack pushes it. If the route is already present below the current detail page, it returns to it.
    • Adaptive Behavior: Use AdwBreakpoint to drive the collapsed prop.
      • Wide windows: Sidebar and content occupy separate panes.
      • Narrow windows: The view folds into a single navigation view. The 'Back' button automatically transitions from the first content page back to the sidebar.