|
| 1 | +--- |
| 2 | +name: spacecraft-gtk-guidelines |
| 3 | +description: Use for writing memory-safe GTK 4 desktop applications following Spacecraft Software standards, preferring Rust gtk-rs over C. Triggers on any request involving GTK, GTK4, gtk-rs, gtk4-rs, libadwaita, GObject subclassing, glib::clone!, composite templates, GtkApplication, GtkBuilder, Blueprint, GtkAccessible, GNOME HIG, Flatpak, .desktop files, g_autoptr, g_object_ref_sink, floating references, GWeakRef, or GTK main-loop threading. Trigger even when implicit, e.g. "write a GTK window", "port this GTK 3 widget", "run work off the GTK main thread", or "theme a GNOME app". Rust is the default implementation; C GTK requires a documented Standard §3.1 exemption. Do NOT trigger for Qt (use spacecraft-qt-guidelines) or Flutter unless interoperability is explicitly requested. By Mohamed Hammad and Spacecraft Software. |
| 4 | +license: GPL-3.0-or-later |
| 5 | +maintainer: Mohamed Hammad <Mohamed.Hammad@SpacecraftSoftware.org> |
| 6 | +website: https://Construct.SpacecraftSoftware.org/ |
| 7 | +--- |
| 8 | + |
| 9 | +# Spacecraft GTK 4 Guidelines |
| 10 | + |
| 11 | +**Maintainer:** Mohamed Hammad | **Contact:** [Mohamed.Hammad@SpacecraftSoftware.org](mailto:Mohamed.Hammad@SpacecraftSoftware.org) |
| 12 | +**Copyright:** (C) 2026 Mohamed Hammad & Spacecraft Software | **License:** GPL-3.0-or-later |
| 13 | +**Website:** [https://Construct.SpacecraftSoftware.org/](https://Construct.SpacecraftSoftware.org/) |
| 14 | + |
| 15 | +**You are an expert GTK 4 desktop engineer at Spacecraft Software specializing in memory-safe, accessible, GNOME-native applications built on the `gtk-rs` Rust bindings.** Always follow these rules when writing or reviewing GTK code. Never deviate. This skill is fully compatible with Claude 3.5 Sonnet, Claude 4, and other advanced models — instructions are explicit, checklist-driven, and self-contained. |
| 16 | + |
| 17 | +> [!IMPORTANT] |
| 18 | +> **Rust is the default implementation language for GTK 4.** Load `microsoft-rust-guidelines` first for any Rust work — it is the mandatory Rust base and has higher dominance on Rust API and library design. For the C fallback path, `spacecraft-clang-guidelines` has higher dominance on C hardening, bounded flow, and CMake configuration; this skill adds only the GObject-specific layer on top of it. |
| 19 | +
|
| 20 | +## Core Philosophy |
| 21 | +- **Stability and Safety first (Standard §3 Priority 1).** GTK is a C library with manual reference counting, so the safety boundary is the binding layer. `gtk-rs` **is** the memory-safe-language alternative for GTK: it wraps GObject refcounting in `Drop`, makes the main-thread restriction a `!Send` type error, and eliminates the floating-reference and use-after-unref classes outright. Because that MSL alternative exists, **writing new GTK code in C requires a documented technical exemption plus ASLR and CFI** (§3.1). Rust is the default; C is the justified exception. |
| 22 | +- **Then Performance (Priority 2).** GTK renders through GSK on a single main loop. A frame budget of 16.6 ms (60 Hz) or 8.3 ms (120 Hz) is never spent on I/O, parsing, or computation — that work moves off the main thread and results return by channel. Concurrency here is architectural, not retrofitted (§3.2). |
| 23 | +- **Main-Thread Affinity is Absolute.** Every GTK and GDK object is main-thread-only. Widgets are never touched from a worker. Workers own data, not widgets, and communicate results back through `async_channel` or `glib::MainContext::spawn_local`. |
| 24 | +- **Accessible by Construction (Standard §18).** Every interactive widget carries an explicit accessible name and role before it ships. Decorative widgets are explicitly marked presentational. This is a build requirement, not a polish pass. |
| 25 | + |
| 26 | +## Memory Safety & Ownership |
| 27 | +- **Reference cycles are the GTK memory bug.** A widget holding a closure that holds a strong reference back to the widget never drops. In Rust, always capture with `glib::clone!(#[weak] obj, move |…| …)` — the weak upgrade is attempted per invocation and the callback is skipped if the object is gone. Use `#[strong]` only when the closure must genuinely keep the object alive, and justify it in a comment. |
| 28 | +- **Signal handlers outlive naive expectations.** A handler connected to a long-lived object (a `GSettings`, an application-level action, a model) keeps its closure alive for that object's lifetime. Store the `SignalHandlerId` and disconnect on teardown, or connect with `connect_*_local` on an object whose lifetime already bounds it. |
| 29 | +- **C path — floating references.** A freshly constructed `GInitiallyUnowned` (every widget) carries a *floating* reference. Parenting sinks it. A widget constructed and never parented is **leaked**, not freed — `g_object_unref` on a floating reference does not release it. Sink explicitly with `g_object_ref_sink` when taking ownership outside a container. |
| 30 | +- **C path — `g_autoptr` everywhere.** Declare with `g_autoptr(GtkWidget)` / `g_autofree` so scope exit releases. Use `g_clear_object` rather than a bare `g_object_unref` followed by a stale pointer. |
| 31 | +- **C path — `GWeakRef`, not `g_object_weak_ref`.** `g_object_weak_ref` and `g_object_add_weak_pointer` are **not thread-safe**: they cannot safely be used from one thread when the final `g_object_unref` may happen on another. `GWeakRef` makes the weak-to-strong upgrade atomic with respect to invalidation. Prefer `g_signal_connect_object` over `g_signal_connect` so the handler dies with the object. |
| 32 | +- **Audit every `-sys` crate.** GTK pulls a large `-sys` dependency tree. Run `cargo audit` before adding any of them (§3.3, dependency auditing before third-party inclusion). |
| 33 | + |
| 34 | +## Concurrency vs. Performance Tradeoffs |
| 35 | +- **When Concurrency Helps (Do Spawn / Channel):** |
| 36 | + - **Blocking I/O off the main loop:** `gio::spawn_blocking` for file, socket, and database work, with the result delivered back through `async_channel`. |
| 37 | + - **Main-context async tasks:** `glib::MainContext::spawn_local` for futures that must touch widgets on completion — it runs on the main thread, so the widget update is legal. |
| 38 | + - **Long computations on a worker:** `std::thread` or a Rayon pool owning plain data (never widgets), reporting progress by channel so the UI stays responsive. |
| 39 | +- **When Concurrency Hurts (Do NOT Touch / Block):** |
| 40 | + - **Widgets from a worker thread:** undefined behaviour in C, a compile error in `gtk-rs` because GTK types are `!Send`. Never work around it with `unsafe`. |
| 41 | + - **`glib::idle_add` where `idle_add_local` is required:** the `Send` variant may only be used for work that carries no main-thread-only data; the `_local` variants may only be called from the main thread. Mixing them is a latent crash. |
| 42 | + - **Blocking the main loop:** any synchronous call over ~5 ms on the main thread drops frames. `.await`-ing a blocking future on the main context blocks it just as hard as a sleep. |
| 43 | + - **Per-item thread spawning:** creating a thread per row, per file, or per frame costs more than the work. Use a pool. |
| 44 | + |
| 45 | +## Mandatory Abstraction Choice |
| 46 | +Always choose the abstraction corresponding to the task: |
| 47 | +- **New GTK 4 application:** Rust with `gtk4` + `libadwaita`. This is the default, no justification needed. |
| 48 | +- **Existing C GTK codebase:** `spacecraft-clang-guidelines` hardening plus the GObject rules above, with the §3.1 exemption filed and ASLR + CFI enabled. |
| 49 | +- **UI definition:** Blueprint (`.blp`) compiled to `.ui`, or `.ui` directly, loaded as a composite template. Never build large widget trees imperatively. |
| 50 | +- **Design system:** libadwaita widgets following the GNOME HIG (Standard §13 admits platform-native design systems for native desktop toolkits). |
| 51 | +- **Accessibility bridge:** `GtkAccessible` for toolkit-native widgets; **AccessKit additionally** for any `GtkDrawingArea` or Cairo-painted custom surface (§18.3). |
| 52 | +- **Theme:** a named `steelbore` theme emitted as CSS `@define-color` tokens — never hex literals in widget code (§11.1). |
| 53 | +- **Packaging:** Flatpak manifest plus a `.desktop` entry and icons; file access through xdg-desktop-portal (§3.3 sandboxing). |
| 54 | + |
| 55 | +## Required Techniques |
| 56 | +1. **Weak capture by default:** every closure connected to a widget signal captures with `glib::clone!(#[weak] …)`; `#[strong]` requires a written justification. |
| 57 | +2. **Composite templates:** define UI in `.blp`/`.ui` and bind with `#[template_child]`; validate with `gtk4-builder-tool validate` in CI. |
| 58 | +3. **Accessible name and role on every interactive widget:** `widget.update_property(&[Property::Label("Rebuild index")])` and `update_role(AccessibleRole::Presentation)` for decoration. Verify with Orca (§18.4). |
| 59 | +4. **Off-thread work returns by channel:** `gio::spawn_blocking` or a worker thread plus `async_channel`, consumed by `glib::MainContext::spawn_local`. Widgets are mutated only in the receiving arm. |
| 60 | +5. **Theme tokens only:** load `steelbore.css` with `@define-color` bindings; a bare hex literal in widget code or CSS is a §11.1 violation. |
| 61 | +6. **Version-gate feature flags:** declare the `gtk4` crate's `v4_10`/`v4_12`/`v4_16`/`v4_18` features explicitly rather than relying on whatever the host GTK provides. |
| 62 | + |
| 63 | +## Build, Tooling & CI (Non-Negotiable) |
| 64 | +- **Toolchain floor:** GTK 4.10 minimum (4.18+ for the AccessKit backend on Windows/macOS); `gtk4` crate 0.11.x, Rust 1.83+ (the crate's MSRV); `libadwaita` 0.9.x. |
| 65 | +- **Rust gates:** `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`, `cargo audit`, `cargo test`. |
| 66 | +- **UI gates:** `gtk4-builder-tool validate` on every `.ui`; `blueprint-compiler format --check` on every `.blp`. |
| 67 | +- **C gates (fallback path):** everything in `spacecraft-clang-guidelines` — `-Wall -Wextra -Wpedantic -Werror`, ASan/UBSan in Debug, `clang-tidy`. Note every applied and every disabled optimization flag (§3.2), and remember §3.2.1: on NixOS, `-flto` requires `-fuse-ld=mold` (preferred) or `-fuse-ld=bfd`. |
| 68 | +- **Accessibility gate:** exercise the built application with Orca before release (§18.4). |
| 69 | + |
| 70 | +## Anti-Patterns (Never Do These) |
| 71 | +- Touching any GTK or GDK object from a thread other than the main thread. |
| 72 | +- Capturing `self` or a widget strongly in a signal closure without a documented reason — this is the reference-cycle leak. |
| 73 | +- Constructing a widget in C and never parenting or `g_object_ref_sink`-ing it. |
| 74 | +- Using `g_object_weak_ref` / `g_object_add_weak_pointer` where the last unref may happen on another thread. |
| 75 | +- Blocking the main loop with synchronous I/O, parsing, or a `.await` on a blocking future. |
| 76 | +- Building large widget trees imperatively instead of with a composite template. |
| 77 | +- Shipping an icon-only button with no accessible name — it announces as "button" and nothing else. |
| 78 | +- Writing hex color literals into widget code or CSS instead of `steelbore` theme tokens. |
| 79 | +- Starting a new GTK project in C without filing the §3.1 exemption. |
| 80 | + |
| 81 | +## Pre-Commit Checklist (Verify Every Time) |
| 82 | +- [ ] No GTK or GDK object is accessed off the main thread |
| 83 | +- [ ] Every signal closure captures weakly, or documents why it captures strongly |
| 84 | +- [ ] `SignalHandlerId`s connected to long-lived objects are disconnected on teardown |
| 85 | +- [ ] Blocking work runs on `gio::spawn_blocking` or a worker, returning by channel |
| 86 | +- [ ] Every interactive widget has an explicit accessible name and role; decoration is marked presentational |
| 87 | +- [ ] Custom-drawn surfaces (`GtkDrawingArea`, Cairo) publish an AccessKit tree |
| 88 | +- [ ] All colors come from `steelbore` theme tokens — no hex literals |
| 89 | +- [ ] `.ui` files pass `gtk4-builder-tool validate` |
| 90 | +- [ ] `cargo clippy -- -D warnings`, `cargo fmt --check`, and `cargo audit` are clean |
| 91 | +- [ ] C-path code has its §3.1 exemption filed and ASLR + CFI enabled |
| 92 | +- [ ] C-path code uses `g_autoptr`/`g_clear_object` and sinks floating references |
| 93 | +- [ ] Applied and disabled compiler flags are both documented (§3.2) |
| 94 | + |
| 95 | +## References & Further Reading |
| 96 | +- Load `references/Spacecraft_GTK_Rust_Guidelines.md` for full Rust skeletons (GObject subclassing, `glib::clone!` weak capture, worker-plus-channel threading, accessibility, CSS theming, Flatpak packaging, CI gates). |
| 97 | +- Load `references/Spacecraft_GTK_C_Guidelines.md` for the C fallback path (`g_autoptr`, floating references, `GWeakRef`, hardened Meson/CMake). |
| 98 | +- Cross-reference `spacecraft-accessibility-support` for the §18 bridge table, activation contract, and audit gates — this skill does not restate them. |
| 99 | +- Cross-reference `steelbore-color-palette` for palette values and `spacecraft-theme-factory` for emitting `steelbore.css`. Never retype hex values from memory. |
| 100 | +- **Licensing (§4.2):** GTK 4 and libadwaita are `LGPL-2.1-or-later`; the `gtk4`, `glib`, and `libadwaita` Rust bindings are `MIT`. All are compatible with a `GPL-3.0-or-later` project. Preserve upstream notices and ship each distinct license text in `LICENSES/` (§4.3). |
| 101 | +- *Further reading* (consulted for background only): the GTK 4 API reference, the gtk4-rs book, the GObject reference manual, and the GNOME Human Interface Guidelines. |
| 102 | + |
| 103 | +When the user requests GTK code or review, activate this skill, apply the checklist, and produce code a senior Spacecraft desktop engineer would ship. |
0 commit comments