<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: fromthearchitect</title>
    <description>The latest articles on Forem by fromthearchitect (@fromthearchitect).</description>
    <link>https://forem.com/fromthearchitect</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3846970%2F105073c7-9d87-4514-b33f-d466199f9dd2.jpeg</url>
      <title>Forem: fromthearchitect</title>
      <link>https://forem.com/fromthearchitect</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/fromthearchitect"/>
    <language>en</language>
    <item>
      <title>Building GNOME Apps with Rust, Part 3: Your First App"</title>
      <dc:creator>fromthearchitect</dc:creator>
      <pubDate>Mon, 04 May 2026 13:43:25 +0000</pubDate>
      <link>https://forem.com/fromthearchitect/building-gnome-apps-with-rust-part-3-your-first-app-4hnd</link>
      <guid>https://forem.com/fromthearchitect/building-gnome-apps-with-rust-part-3-your-first-app-4hnd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 3 of a series that takes a GNOME application from an empty directory to acceptance into GNOME Circle. &lt;a href="https://dev.to/posts/gnome-rust-part-2-gobject/"&gt;Part 2&lt;/a&gt; covered GObject's type system — properties, signals, and the inner/outer type pattern. Now we'll use everything we learned to build a real application.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  From theory to a running window
&lt;/h2&gt;

&lt;p&gt;In Part 2 we built a GObject subclass by hand — a &lt;code&gt;Feed&lt;/code&gt; model with properties and signals, no GTK in sight. That was deliberate. Understanding GObject's inner/outer type split, the &lt;code&gt;ObjectSubclass&lt;/code&gt; trait, and the &lt;code&gt;mod imp&lt;/code&gt; pattern is the foundation that everything else rests on.&lt;/p&gt;

&lt;p&gt;Now we're going to see those same patterns in context. By the end of this post, you'll have a running GTK4 + libadwaita application with a header bar, a menu, keyboard shortcuts, and a properly structured project that's ready for Flatpak packaging. And you won't have written most of it by hand — GNOME Builder will generate it for you. Our job is to understand what it generated and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating the project in GNOME Builder
&lt;/h2&gt;

&lt;p&gt;Open GNOME Builder and click &lt;strong&gt;Create New Project&lt;/strong&gt;. Fill in the dialog like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3b2ccw5gvp86ahuh5p5n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3b2ccw5gvp86ahuh5p5n.png" alt="GNOME Builder's Create New Project dialog, configured for Gazette" width="800" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project Name&lt;/strong&gt;: &lt;code&gt;gazette&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application ID&lt;/strong&gt;: &lt;code&gt;io.github.fromthearchitect.gazette&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location&lt;/strong&gt;: wherever you keep your projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: Rust&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: GPL-3.0-or-later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Control&lt;/strong&gt;: enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template&lt;/strong&gt;: GNOME Application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Create Project&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on application IDs
&lt;/h3&gt;

&lt;p&gt;The application ID is a reverse domain name that uniquely identifies your app across the entire Linux desktop. It's used for the D-Bus service name, the GSettings schema path, the desktop file name, the icon name, and the Flatpak bundle ID. Everything keys off this single string.&lt;/p&gt;

&lt;p&gt;The convention is to use a domain you control. If you're publishing to a GitHub organisation, &lt;code&gt;io.github.yourorg.yourapp&lt;/code&gt; is the standard pattern. If you own a domain, use it: &lt;code&gt;dev.fromthearchitect.gazette&lt;/code&gt; would also work. The important thing is that this ID is globally unique and stable — changing it later means renaming dozens of files and updating every reference.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hit Run
&lt;/h2&gt;

&lt;p&gt;Before we look at any code, hit the &lt;strong&gt;Run&lt;/strong&gt; button (the play icon in the header bar) or press &lt;strong&gt;Ctrl+F5&lt;/strong&gt;. Builder will configure the Flatpak environment, download the SDK if needed, compile the Rust code, bundle the resources, and launch the application.&lt;/p&gt;

&lt;p&gt;The first build takes a while — Cargo is downloading and compiling every dependency inside the Flatpak sandbox. Subsequent builds are much faster thanks to caching.&lt;/p&gt;

&lt;p&gt;When it finishes, you'll see this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fkj0mksiaivvlx2fmj7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5fkj0mksiaivvlx2fmj7.png" alt="The Gazette application running — an empty window with a header bar showing " width="800" height="628"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A window. A header bar with a title. A hamburger menu with Preferences, Keyboard Shortcuts, and About entries. A "Hello, World!" label in the centre. That's your application.&lt;/p&gt;

&lt;p&gt;It doesn't look like much yet, but there's a surprising amount happening behind the scenes. Let's look at what Builder actually created.&lt;/p&gt;




&lt;h2&gt;
  
  
  The project structure
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjde9ishi4jszifeayrcx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjde9ishi4jszifeayrcx.png" alt="GNOME Builder showing the Gazette project file tree" width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what Builder generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gazette/
├── Cargo.toml
├── COPYING
├── meson.build
├── io.github.fromthearchitect.gazette.json
├── data/
│   ├── meson.build
│   ├── icons/
│   │   ├── meson.build
│   │   ├── hicolor/scalable/apps/
│   │   │   └── io.github.fromthearchitect.gazette.svg
│   │   └── hicolor/symbolic/apps/
│   │       └── io.github.fromthearchitect.gazette-symbolic.svg
│   ├── io.github.fromthearchitect.gazette.desktop.in
│   ├── io.github.fromthearchitect.gazette.gschema.xml
│   ├── io.github.fromthearchitect.gazette.metainfo.xml.in
│   └── io.github.fromthearchitect.gazette.service.in
├── po/
│   ├── LINGUAS
│   ├── POTFILES.in
│   └── meson.build
└── src/
    ├── meson.build
    ├── main.rs
    ├── application.rs
    ├── config.rs.in
    ├── window.rs
    ├── window.ui
    ├── shortcuts-dialog.ui
    └── gazette.gresource.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a lot of files for "Hello, World!" — and every single one of them is there for a reason. Here's what's in each group.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rust source code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  main.rs — the entry point
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;application&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GazetteApplication&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;window&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GazetteWindow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;config&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;GETTEXT_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LOCALEDIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PKGDATADIR&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gettextrs&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;bind_textdomain_codeset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bindtextdomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;textdomain&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ExitCode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Set up gettext translations&lt;/span&gt;
    &lt;span class="nf"&gt;bindtextdomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETTEXT_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LOCALEDIR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to bind the text domain"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;bind_textdomain_codeset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETTEXT_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to set the text domain encoding"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;textdomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GETTEXT_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unable to switch to the text domain"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Load resources&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;PKGDATADIR&lt;/span&gt;&lt;span class="nf"&gt;.to_owned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/gazette.gresource"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Could not load resources"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;resources_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;GazetteApplication&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"io.github.fromthearchitect.gazette"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ApplicationFlags&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things happen here before the application starts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gettext initialisation.&lt;/strong&gt; This wires up the translation system so that strings marked as translatable in the UI files and source code can be looked up in the user's language. Even if you never translate your app, the plumbing needs to be in place.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Resource loading.&lt;/strong&gt; The compiled GResource bundle (containing the UI templates, icons, and other assets) is loaded from disk and registered globally. After this call, any code can access bundled files by their resource path — for example, &lt;code&gt;/io/github/fromthearchitect/gazette/window.ui&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application creation and run.&lt;/strong&gt; &lt;code&gt;GazetteApplication::new()&lt;/code&gt; creates our application object, and &lt;code&gt;app.run()&lt;/code&gt; hands control to the GLib main loop. The main loop processes events — user input, window management, D-Bus messages — until the application quits.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;config&lt;/code&gt; module isn't a regular Rust file — it's generated at build time by Meson from a template. More on that shortly.&lt;/p&gt;

&lt;h3&gt;
  
  
  application.rs — the GtkApplication subclass
&lt;/h3&gt;

&lt;p&gt;This is where you'll recognise the GObject patterns from Part 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gettextrs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;gettext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;adw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;adw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;subclass&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;config&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GazetteWindow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;imp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Default)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;GazetteApplication&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="nd"&gt;#[glib::object_subclass]&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ObjectSubclass&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteApplication&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GazetteApplication"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GazetteApplication&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ParentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;adw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ObjectImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteApplication&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;constructed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.parent_constructed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.obj&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="nf"&gt;.setup_gactions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="nf"&gt;.set_accels_for_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.quit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;control&amp;gt;q"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ApplicationImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteApplication&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.obj&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="nf"&gt;.active_window&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or_else&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;GazetteWindow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;*&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.upcast&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="nf"&gt;.present&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;GtkApplicationImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteApplication&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;AdwApplicationImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteApplication&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;wrapper!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;GazetteApplication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ObjectSubclass&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GazetteApplication&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;adw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;implements&lt;/span&gt; &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ActionGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ActionMap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern as Part 2's &lt;code&gt;Feed&lt;/code&gt;: an inner type in &lt;code&gt;mod imp&lt;/code&gt; that holds the state, &lt;code&gt;ObjectSubclass&lt;/code&gt; to register it with GObject, and a &lt;code&gt;glib::wrapper!&lt;/code&gt; macro to create the outer type. The difference is what we're subclassing — &lt;code&gt;adw::Application&lt;/code&gt; instead of &lt;code&gt;glib::Object&lt;/code&gt; — and the trait implementations that come with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ObjectImpl::constructed&lt;/code&gt;&lt;/strong&gt; is called once when the application is first created. It's the place to set up actions and keyboard shortcuts. This is analogous to a constructor, but in GObject-land, construction happens in phases — &lt;code&gt;constructed&lt;/code&gt; runs after all properties have been set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ApplicationImpl::activate&lt;/code&gt;&lt;/strong&gt; is the most important callback. It's called when the application is launched (or when the user tries to open a second instance). The pattern here is standard: check if there's already a window, create one if there isn't, and present it. This is how GNOME apps implement single-instance behaviour — the platform enforces one running instance per application ID, and subsequent launches just activate the existing one.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;impl GtkApplicationImpl&lt;/code&gt; and &lt;code&gt;impl AdwApplicationImpl&lt;/code&gt; lines are empty but required. They tell the GObject type system that we're implementing the full trait chain from &lt;code&gt;gio::Application&lt;/code&gt; through &lt;code&gt;gtk::Application&lt;/code&gt; to &lt;code&gt;adw::Application&lt;/code&gt;. If you leave one out, you'll get a compile error.&lt;/p&gt;

&lt;p&gt;Further down in the file, there's the actions setup and the About dialog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;GazetteApplication&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ApplicationFlags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;application_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"flags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"resource-base-path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="s"&gt;"/io/github/fromthearchitect/gazette"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;setup_gactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;quit_action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ActionEntry&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"quit"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.quit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;about_action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ActionEntry&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"about"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.show_about&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.add_action_entries&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;quit_action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;about_action&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;show_about&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.active_window&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;about&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;adw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;AboutDialog&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.application_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Gazette"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.application_icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.fromthearchitect.gazette"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.developer_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unknown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.developers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Unknown"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="nf"&gt;.translator_credits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nf"&gt;gettext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"translator-credits"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;.copyright&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"© 2026 Unknown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;about&lt;/span&gt;&lt;span class="nf"&gt;.present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the placeholder values — &lt;code&gt;developer_name("Unknown")&lt;/code&gt;, &lt;code&gt;developers(vec!["Unknown"])&lt;/code&gt;, &lt;code&gt;copyright("© 2026 Unknown")&lt;/code&gt;. Builder generates these as placeholders. Replace them with your actual name before publishing your app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GActions&lt;/strong&gt; are GNOME's action system. Instead of connecting button clicks directly to functions, you define named actions (&lt;code&gt;app.quit&lt;/code&gt;, &lt;code&gt;app.about&lt;/code&gt;) and connect UI elements to those names. The menu items in the UI template reference &lt;code&gt;app.about&lt;/code&gt; — the action system routes that to &lt;code&gt;show_about()&lt;/code&gt;. This decoupling means you can trigger the same action from a menu, a keyboard shortcut, a command-line argument, or a D-Bus message.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;resource-base-path&lt;/code&gt; property tells the application where to look for automatically loaded resources. When we set it to &lt;code&gt;/io/github/fromthearchitect/gazette&lt;/code&gt;, the application will automatically load the shortcuts dialog from that path without us doing anything extra.&lt;/p&gt;

&lt;h3&gt;
  
  
  window.rs — the application window
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;adw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;subclass&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;imp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Default,&lt;/span&gt; &lt;span class="nd"&gt;gtk::CompositeTemplate)]&lt;/span&gt;
    &lt;span class="nd"&gt;#[template(resource&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/io/github/fromthearchitect/gazette/window.ui"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;GazetteWindow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;#[template_child]&lt;/span&gt;
        &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TemplateChild&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Label&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[glib::object_subclass]&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ObjectSubclass&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteWindow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GazetteWindow"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GazetteWindow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ParentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;adw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ApplicationWindow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;class_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="nf"&gt;.bind_template&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;instance_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;subclass&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InitializingObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="nf"&gt;.init_template&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ObjectImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteWindow&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;WidgetImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteWindow&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;WindowImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteWindow&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ApplicationWindowImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteWindow&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;AdwApplicationWindowImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;GazetteWindow&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;wrapper!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;GazetteWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ObjectSubclass&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GazetteWindow&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;extends&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ApplicationWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="nn"&gt;adw&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ApplicationWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;implements&lt;/span&gt; &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ActionGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;gio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ActionMap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;GazetteWindow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IsA&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The &lt;code&gt;@implements&lt;/code&gt; list above is truncated for readability — the actual generated code also includes &lt;code&gt;gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager&lt;/code&gt;. See the source on the &lt;a href="https://github.com/fromthearchitect/gnome-rust-gazette/tree/part-3" rel="noopener noreferrer"&gt;&lt;code&gt;part-3&lt;/code&gt; branch&lt;/a&gt; for the full version.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This introduces two new concepts: &lt;strong&gt;composite templates&lt;/strong&gt; and the &lt;strong&gt;widget trait chain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;#[derive(gtk::CompositeTemplate)]&lt;/code&gt;&lt;/strong&gt; connects this Rust struct to a UI definition file. The &lt;code&gt;#[template(resource = "...")]&lt;/code&gt; attribute tells GTK where to find the template, and &lt;code&gt;#[template_child]&lt;/code&gt; fields create typed references to named widgets in that template. Here, &lt;code&gt;self.label&lt;/code&gt; gives us direct access to the &lt;code&gt;GtkLabel&lt;/code&gt; widget with &lt;code&gt;id="label"&lt;/code&gt; defined in the UI file.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;class_init&lt;/code&gt; and &lt;code&gt;instance_init&lt;/code&gt; functions bind the template at the class level (once) and initialise it for each instance. This is boilerplate that every composite template widget needs.&lt;/p&gt;

&lt;p&gt;The trait chain — &lt;code&gt;WidgetImpl&lt;/code&gt;, &lt;code&gt;WindowImpl&lt;/code&gt;, &lt;code&gt;ApplicationWindowImpl&lt;/code&gt;, &lt;code&gt;AdwApplicationWindowImpl&lt;/code&gt; — reflects the GTK class hierarchy. &lt;code&gt;AdwApplicationWindow&lt;/code&gt; extends &lt;code&gt;gtk::ApplicationWindow&lt;/code&gt;, which extends &lt;code&gt;gtk::Window&lt;/code&gt;, which extends &lt;code&gt;gtk::Widget&lt;/code&gt;. Each level of the hierarchy can override behaviour. For now, all these implementations are empty, but they must be present.&lt;/p&gt;




&lt;h2&gt;
  
  
  The UI template
&lt;/h2&gt;

&lt;h3&gt;
  
  
  window.ui
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;interface&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;requires&lt;/span&gt; &lt;span class="na"&gt;lib=&lt;/span&gt;&lt;span class="s"&gt;"gtk"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"4.0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;requires&lt;/span&gt; &lt;span class="na"&gt;lib=&lt;/span&gt;&lt;span class="s"&gt;"Adw"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"GazetteWindow"&lt;/span&gt; &lt;span class="na"&gt;parent=&lt;/span&gt;&lt;span class="s"&gt;"AdwApplicationWindow"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt; &lt;span class="na"&gt;translatable=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Gazette&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"default-width"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;800&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"default-height"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;600&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;object&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"AdwToolbarView"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;child&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"top"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;object&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"AdwHeaderBar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;child&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;object&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"GtkMenuButton"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;True&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"icon-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;open-menu-symbolic&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"tooltip-text"&lt;/span&gt;
                          &lt;span class="na"&gt;translatable=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Main Menu&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"menu-model"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;primary_menu&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/object&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/child&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/object&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/child&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;object&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"GtkLabel"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;
                      &lt;span class="na"&gt;translatable=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Hello, World!&lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;class&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"title-1"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/object&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/object&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/property&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;menu&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"primary_menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;section&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;attribute&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;
                   &lt;span class="na"&gt;translatable=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;_Preferences&lt;span class="nt"&gt;&amp;lt;/attribute&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;attribute&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;app.preferences&lt;span class="nt"&gt;&amp;lt;/attribute&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;attribute&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;
                   &lt;span class="na"&gt;translatable=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;_Keyboard Shortcuts&lt;span class="nt"&gt;&amp;lt;/attribute&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;attribute&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;app.shortcuts&lt;span class="nt"&gt;&amp;lt;/attribute&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;item&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;attribute&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;
                   &lt;span class="na"&gt;translatable=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;_About Gazette&lt;span class="nt"&gt;&amp;lt;/attribute&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;attribute&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;app.about&lt;span class="nt"&gt;&amp;lt;/attribute&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/menu&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/interface&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is GTK's XML-based UI definition format.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;&lt;/strong&gt; element defines a composite template — it's bound to the &lt;code&gt;GazetteWindow&lt;/code&gt; class we defined in Rust. The &lt;code&gt;parent&lt;/code&gt; attribute must match the &lt;code&gt;ParentType&lt;/code&gt; in our &lt;code&gt;ObjectSubclass&lt;/code&gt; implementation.&lt;/p&gt;

&lt;p&gt;The widget hierarchy is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AdwToolbarView&lt;/code&gt;&lt;/strong&gt; — a libadwaita container that manages header bars and content areas

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AdwHeaderBar&lt;/code&gt;&lt;/strong&gt; (top) — the standard GNOME header bar with the window title and controls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GtkMenuButton&lt;/code&gt;&lt;/strong&gt; — the hamburger menu button in the top-right corner&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GtkLabel&lt;/code&gt;&lt;/strong&gt; (content) — our "Hello, World!" text&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;&amp;lt;menu&amp;gt;&lt;/code&gt;&lt;/strong&gt; element defines the hamburger menu model. Each item has a label (the &lt;code&gt;_&lt;/code&gt; prefix marks the keyboard accelerator character) and an action name. The &lt;code&gt;app.about&lt;/code&gt; entry maps back to the action we defined in &lt;code&gt;application.rs&lt;/code&gt;. The &lt;code&gt;app.preferences&lt;/code&gt; action isn't connected yet — clicking it does nothing. The &lt;code&gt;app.shortcuts&lt;/code&gt; action, however, already works: GTK automatically loads &lt;code&gt;shortcuts-dialog.ui&lt;/code&gt; from the resource base path we set in &lt;code&gt;application.rs&lt;/code&gt;. We'll wire up preferences in a later post.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;translatable="yes"&lt;/code&gt; attribute on strings marks them for extraction by gettext. Even if you don't plan to translate your app initially, marking strings as translatable from the start saves you from a painful retrofit later.&lt;/p&gt;

&lt;p&gt;In Part 4, we'll convert this XML template to Blueprint, which is dramatically more readable. But it's worth understanding the XML format first, since Blueprint compiles to it — and when things go wrong, the error messages reference XML structures.&lt;/p&gt;




&lt;h2&gt;
  
  
  The build system
&lt;/h2&gt;

&lt;p&gt;GNOME apps don't use Cargo as the top-level build system. They use &lt;strong&gt;Meson&lt;/strong&gt;. Cargo still compiles the Rust code, but Meson orchestrates everything else — and there's a lot of "everything else" in a GNOME app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root meson.build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight meson"&gt;&lt;code&gt;&lt;span class="nb"&gt;project&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'gazette'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'rust'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'0.1.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;meson_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'&amp;gt;= 1.0.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;default_options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s"&gt;'warning_level=2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'werror=false'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'i18n'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gnome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'gnome'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;subdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'data'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;subdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'src'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;subdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'po'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;gnome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="n"&gt;glib_compile_schemas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gtk_update_icon_cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;update_desktop_database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root build file declares the project, imports the &lt;code&gt;i18n&lt;/code&gt; and &lt;code&gt;gnome&lt;/code&gt; Meson modules, and includes three subdirectories. The &lt;code&gt;post_install&lt;/code&gt; block runs after installation to compile GSettings schemas, update the icon cache, and refresh the desktop file database. These are standard GNOME post-install steps — without them, your app's icon won't appear in the application launcher and your settings won't be accessible.&lt;/p&gt;

&lt;h3&gt;
  
  
  src/meson.build — where Rust meets Meson
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight meson"&gt;&lt;code&gt;&lt;span class="n"&gt;pkgdatadir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'prefix'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'datadir'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;meson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;gnome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'gnome'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;gnome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile_resources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'gazette'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'gazette.gresource.xml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;gresource_bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;install_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pkgdatadir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;configuration_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_quoted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'VERSION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;meson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_version&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_quoted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'GETTEXT_PACKAGE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'gazette'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_quoted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'LOCALEDIR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'prefix'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;get_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'localedir'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_quoted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'PKGDATADIR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkgdatadir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;configure_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'config.rs.in'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'config.rs'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what it handles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compiles GResources.&lt;/strong&gt; The &lt;code&gt;gazette.gresource.xml&lt;/code&gt; manifest lists the UI files and other assets that should be bundled. Meson compiles them into a single binary blob (&lt;code&gt;gazette.gresource&lt;/code&gt;) and installs it alongside the binary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generates &lt;code&gt;config.rs&lt;/code&gt;.&lt;/strong&gt; The &lt;code&gt;config.rs.in&lt;/code&gt; template contains placeholders like &lt;code&gt;@VERSION@&lt;/code&gt; and &lt;code&gt;@PKGDATADIR@&lt;/code&gt;. Meson replaces them with real values at build time. This is how the Rust code knows where to find its installed resources — the paths are baked in at compile time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Invokes Cargo.&lt;/strong&gt; Further down in the file, Meson runs &lt;code&gt;cargo build&lt;/code&gt; with the right flags and copies the resulting binary to the install location.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  gazette.gresource.xml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;gresources&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;gresource&lt;/span&gt; &lt;span class="na"&gt;prefix=&lt;/span&gt;&lt;span class="s"&gt;"/io/github/fromthearchitect/gazette"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;file&lt;/span&gt; &lt;span class="na"&gt;preprocess=&lt;/span&gt;&lt;span class="s"&gt;"xml-stripblanks"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;window.ui&lt;span class="nt"&gt;&amp;lt;/file&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;file&lt;/span&gt; &lt;span class="na"&gt;preprocess=&lt;/span&gt;&lt;span class="s"&gt;"xml-stripblanks"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;shortcuts-dialog.ui&lt;span class="nt"&gt;&amp;lt;/file&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/gresource&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/gresources&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This manifest lists every file that should be compiled into the GResource bundle. The &lt;code&gt;prefix&lt;/code&gt; attribute defines the virtual path — when our window template asks for &lt;code&gt;/io/github/fromthearchitect/gazette/window.ui&lt;/code&gt;, it's loading from this bundle, not from the filesystem. The &lt;code&gt;xml-stripblanks&lt;/code&gt; flag tells Meson to strip whitespace from the XML before bundling. It makes no real difference for an app this size, but it's standard practice and every GNOME app does it.&lt;/p&gt;

&lt;p&gt;Whenever you add a new UI file, icon, or CSS stylesheet, it needs to be listed here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Desktop integration files
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;data/&lt;/code&gt; directory contains files that integrate your application with the Linux desktop. None of these are specific to GNOME or GTK — they're freedesktop.org standards used across all Linux desktop environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Desktop file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Desktop Entry]&lt;/span&gt;
&lt;span class="py"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Gazette&lt;/span&gt;
&lt;span class="py"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;gazette&lt;/span&gt;
&lt;span class="py"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;io.github.fromthearchitect.gazette&lt;/span&gt;
&lt;span class="py"&gt;Terminal&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="py"&gt;Categories&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Utility;&lt;/span&gt;
&lt;span class="py"&gt;Keywords&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;GTK;&lt;/span&gt;
&lt;span class="py"&gt;StartupNotify&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;DBusActivatable&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the desktop environment how to launch your app, what icon to display, and what category to put it in. The &lt;code&gt;.in&lt;/code&gt; suffix on the source file means Meson processes it through gettext to produce translated versions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DBusActivatable=true&lt;/code&gt; means the app can be launched via D-Bus, which enables the single-instance behaviour we saw in &lt;code&gt;application.rs&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GSettings schema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;schemalist&lt;/span&gt; &lt;span class="na"&gt;gettext-domain=&lt;/span&gt;&lt;span class="s"&gt;"gazette"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;schema&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"io.github.fromthearchitect.gazette"&lt;/span&gt;
          &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"/io/github/fromthearchitect/gazette/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/schema&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/schemalist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GSettings is GNOME's configuration system — think of it as a typed key-value store for application preferences. The schema is empty now, but when we add user preferences (feed refresh interval, dark mode preference, etc.), this is where we'll define them.&lt;/p&gt;

&lt;h3&gt;
  
  
  AppStream metadata
&lt;/h3&gt;

&lt;p&gt;The metainfo file is what Flathub and GNOME Software actually read when they display your app. Get it wrong and your app looks abandoned regardless of how polished the code is. Builder generated a placeholder — we'll fill it in properly when we prepare for Flathub submission in Part 9.&lt;/p&gt;

&lt;h3&gt;
  
  
  D-Bus service file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[D-BUS Service]&lt;/span&gt;
&lt;span class="py"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;io.github.fromthearchitect.gazette&lt;/span&gt;
&lt;span class="py"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;@bindir@/gazette --gapplication-service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells D-Bus how to activate your application on demand. The &lt;code&gt;@bindir@&lt;/code&gt; placeholder is replaced by Meson at build time with the actual installation path.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Flatpak manifest
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"io.github.fromthearchitect.gazette"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"runtime"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org.gnome.Platform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"runtime-version"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"48"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sdk"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org.gnome.Sdk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sdk-extensions"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"org.freedesktop.Sdk.Extension.rust-stable"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gazette"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"finish-args"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--share=network"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--share=ipc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--socket=fallback-x11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--device=dri"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--socket=wayland"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines how to build and sandbox the application as a Flatpak. The key fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;runtime&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;runtime-version&lt;/code&gt;&lt;/strong&gt;: The GNOME platform libraries your app links against at runtime. Version 48 corresponds to GNOME 48.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sdk&lt;/code&gt;&lt;/strong&gt;: The matching SDK used at build time, which includes header files and development tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sdk-extensions&lt;/code&gt;&lt;/strong&gt;: Additional SDK components — here, the Rust toolchain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;finish-args&lt;/code&gt;&lt;/strong&gt;: The sandbox permissions your app needs. Network access, Wayland display, GPU acceleration. Every permission you add here needs to be justified when you submit to Flathub. You might wonder why an RSS reader needs &lt;code&gt;--device=dri&lt;/code&gt; — GTK4 uses GPU-accelerated rendering by default, so it's a legitimate permission for virtually any GTK4 app.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Upgrading to the GNOME 50 platform
&lt;/h2&gt;

&lt;p&gt;Builder generated this project against &lt;strong&gt;GNOME 48&lt;/strong&gt; — that's what the current template targets. But in &lt;a href="https://dev.to/posts/gnome-rust-part-1-getting-started/"&gt;Part 1&lt;/a&gt;, we installed the &lt;strong&gt;GNOME 50 SDK&lt;/strong&gt;. Let's upgrade the Rust crates and Flatpak runtime to match.&lt;/p&gt;

&lt;p&gt;Three files need changes:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cargo.toml — Rust crate versions
&lt;/h3&gt;

&lt;p&gt;The generated &lt;code&gt;Cargo.toml&lt;/code&gt; uses &lt;code&gt;gtk4&lt;/code&gt; 0.9 and &lt;code&gt;libadwaita&lt;/code&gt; 0.7:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;gettext-rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"gettext-system"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;gtk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gtk4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"gnome_47"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;[dependencies.adw]&lt;/span&gt;
&lt;span class="py"&gt;package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"libadwaita"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.7"&lt;/span&gt;
&lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"v1_6"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update it to the latest versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;gettext-rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"gettext-system"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;gtk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gtk4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"gnome_50"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;adw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"libadwaita"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"v1_9"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The version numbers here are the Rust crate versions, not the GTK version. &lt;code&gt;gtk4&lt;/code&gt; crate 0.11 wraps GTK 4.18, and &lt;code&gt;libadwaita&lt;/code&gt; crate 0.9 wraps libadwaita 1.9. The &lt;code&gt;gnome_50&lt;/code&gt; feature flag is a convenience flag that enables all the version-specific API features matching the GNOME 50 SDK — it saves you from having to figure out which individual &lt;code&gt;v4_xx&lt;/code&gt; flags correspond to which GNOME release.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Flatpak manifest — runtime version
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;io.github.fromthearchitect.gazette.json&lt;/code&gt;, update the runtime version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"runtime-version"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Flatpak to use the GNOME 50 runtime and SDK when building and running your app.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rebuild
&lt;/h3&gt;

&lt;p&gt;Hit &lt;strong&gt;Run&lt;/strong&gt; in Builder again. Builder will download the GNOME 50 SDK if it's not already installed, rebuild the project against the new dependencies, and launch the app. If everything compiles and runs — and it should, since we haven't used any version-specific APIs yet — you're on the latest platform.&lt;/p&gt;

&lt;p&gt;This kind of version bump is something you'll do roughly every six months as GNOME releases new platform versions. Getting comfortable with it now means it's routine rather than stressful later.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we have so far
&lt;/h2&gt;

&lt;p&gt;The thing that takes longest to internalise when you start GNOME development is how much of this is not GTK. The desktop file, the D-Bus service, the AppStream metadata, GSettings — these are platform concerns, not application concerns. Builder stitches them all together, but you need to know what they are and why they exist, or you'll never debug a broken Flatpak submission.&lt;/p&gt;

&lt;p&gt;The application ID is the load-bearing string across all of it. It ties the desktop file to the D-Bus service to the GSettings schema to the Flatpak bundle. Getting this scaffolding right by hand would take hours of cross-referencing documentation. Builder gives it to you in thirty seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;We've got a running app, but the UI is still "Hello, World!". Before we start building the actual RSS reader interface, we need a better way to describe user interfaces than raw XML. &lt;/p&gt;

&lt;p&gt;In Part 4, we'll introduce Blueprint — a markup language that compiles to GTK's UI XML but is dramatically more readable. We'll convert the generated window template to Blueprint, build the initial Gazette layout with a sidebar and content pane, and start making it look like a real application.&lt;/p&gt;




&lt;p&gt;The source code at the end of this post lives on the &lt;code&gt;part-3&lt;/code&gt; branch of &lt;a href="https://github.com/fromthearchitect/gnome-rust-gazette/tree/part-3" rel="noopener noreferrer"&gt;fromthearchitect/gnome-rust-gazette&lt;/a&gt;. Subsequent posts in this series build on the same repo, with a branch per part.&lt;/p&gt;

</description>
      <category>gnome</category>
      <category>rust</category>
      <category>opensource</category>
      <category>gtk</category>
    </item>
    <item>
      <title>Making a Dumb Fridge Smart</title>
      <dc:creator>fromthearchitect</dc:creator>
      <pubDate>Sun, 03 May 2026 11:20:00 +0000</pubDate>
      <link>https://forem.com/fromthearchitect/making-a-dumb-fridge-smart-3ah6</link>
      <guid>https://forem.com/fromthearchitect/making-a-dumb-fridge-smart-3ah6</guid>
      <description>&lt;p&gt;There's about $400 of meat, milk, and miscellaneous condiments in my kitchen fridge at any given time. It runs 24/7, makes a quiet humming noise, and gives no indication when something's wrong until you open the door three days later and recoil. The freezer compartment is worse: a slow failure can defrost everything before you notice the puddle.&lt;/p&gt;

&lt;p&gt;I already had a TP-Link P110 smart plug on the fridge — originally for energy monitoring, because I'm on a spot-priced electricity tariff and I like knowing what each appliance costs me. But the same wattage stream that tells you "the fridge used 1.4 kWh today" tells you almost everything you need to know about whether the fridge is &lt;em&gt;healthy&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal
&lt;/h2&gt;

&lt;p&gt;The P110 reports &lt;code&gt;sensor.fridge_current_consumption&lt;/code&gt; in watts, updating every ~5 seconds (the current tplink integration default). A typical fridge has two power states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standby&lt;/strong&gt; — 1 to 3 W. Everything dark, compressor off, just the controller and a sensor or two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compressor running&lt;/strong&gt; — 50 to 200 W, depending on age and ambient temperature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you imagine that as a graph, you get a square wave: long flats near zero, periodic blocks at ~120 W, repeating every 30–60 minutes. Almost everything interesting about the fridge — door open, seal failing, mechanical struggle, total failure — shows up as a deformation of that square wave.&lt;/p&gt;

&lt;p&gt;The first thing to build is a binary sensor that tracks compressor on/off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: detecting the compressor cycle (with hysteresis)
&lt;/h2&gt;

&lt;p&gt;The naive version is "compressor is on if watts &amp;gt; 30." This breaks immediately, because the wattage can hover within a few watts of any threshold you pick, flapping the sensor on and off ten times a second. You need &lt;em&gt;hysteresis&lt;/em&gt;: a high threshold to turn on, a lower threshold to turn off.&lt;/p&gt;

&lt;p&gt;Home Assistant ships a &lt;code&gt;binary_sensor&lt;/code&gt; platform called &lt;code&gt;threshold&lt;/code&gt; that does upper/lower bounds with hysteresis in three lines of YAML. It's the obvious tool for this job, and for a long time it was what I would have reached for. I don't anymore, for three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can't have a &lt;code&gt;unique_id&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;platform: threshold&lt;/code&gt; predates the entity registry and was never updated to support it. That means the entity it produces lives outside the registry — you can't rename it from the UI, can't apply labels, can't hide it from a dashboard cleanly, and tools like &lt;a href="https://spook.boo/" rel="noopener noreferrer"&gt;Spook&lt;/a&gt; that operate on registry entities can't see it. In a config where every other YAML-defined entity is registry-managed, having one or two threshold sensors floating outside that system is a constant small irritation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can't propagate &lt;code&gt;unavailable&lt;/code&gt;.&lt;/strong&gt; When the source sensor goes unavailable, &lt;code&gt;platform: threshold&lt;/code&gt; evaluates against a missing value and the result is unhelpful — usually it sticks at its last state. A template sensor with an explicit &lt;code&gt;availability:&lt;/code&gt; block bubbles the unknown state through, which is what every downstream alert and history-stats sensor actually wants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hysteresis you can express is limited.&lt;/strong&gt; &lt;code&gt;platform: threshold&lt;/code&gt; gives you one upper bound and one lower bound. The self-referential template pattern lets the thresholds be &lt;em&gt;anything you can compute&lt;/em&gt; — different in summer vs. winter, different by time of day, different depending on whether another appliance is also drawing power. You almost never need that flexibility, but on the day you do, you don't have to rewrite the sensor.&lt;/p&gt;

&lt;p&gt;So instead I use a &lt;code&gt;template:&lt;/code&gt; binary_sensor that references its own state. It's six more lines than the threshold version. For a sensor I'm going to depend on for years, that's a trade I'll happily make every time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;binary_sensor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fridge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Compressor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Running"&lt;/span&gt;
        &lt;span class="na"&gt;unique_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2236e0fe-63a0-4d11-b6a0-c0efd94d5f69&lt;/span&gt;
        &lt;span class="na"&gt;device_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;running&lt;/span&gt;
        &lt;span class="na"&gt;availability&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;{{ states('sensor.fridge_current_consumption')&lt;/span&gt;
             &lt;span class="s"&gt;not in ['unknown', 'unavailable', 'none'] }}&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;{% set w = states('sensor.fridge_current_consumption') | float(0) %}&lt;/span&gt;
          &lt;span class="s"&gt;{% if is_state('binary_sensor.fridge_compressor_running', 'on') %}&lt;/span&gt;
            &lt;span class="s"&gt;{{ w &amp;gt; 10 }}&lt;/span&gt;
          &lt;span class="s"&gt;{% else %}&lt;/span&gt;
            &lt;span class="s"&gt;{{ w &amp;gt; 20 }}&lt;/span&gt;
          &lt;span class="s"&gt;{% endif %}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;availability&lt;/code&gt; block matters: if the smart plug loses Wi-Fi or the integration glitches, &lt;code&gt;states(...)&lt;/code&gt; returns &lt;code&gt;'unavailable'&lt;/code&gt;, and &lt;code&gt;| float(0)&lt;/code&gt; would silently turn that into "compressor is off" — exactly the wrong story to tell. Marking the sensor itself unavailable when its source is unavailable propagates the unknown state through everything downstream instead of papering over it with zeros.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;state&lt;/code&gt; block self-references: &lt;code&gt;binary_sensor.fridge_compressor_running&lt;/code&gt; reads its &lt;em&gt;own&lt;/em&gt; current state to decide which threshold to apply. The HA template engine is fine with this — it just uses the previous state from before this evaluation. On startup, with no prior state to read, the template falls through to the "currently off" branch and uses the higher 20 W threshold — the safe direction to fail.&lt;/p&gt;

&lt;p&gt;This binary sensor is the foundation. Everything that follows is built on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step two: rolling-window aggregations
&lt;/h2&gt;

&lt;p&gt;Two &lt;code&gt;history_stats&lt;/code&gt; sensors derive the shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;sensor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;history_stats&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fridge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Compressor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Duty&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6h"&lt;/span&gt;
    &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;binary_sensor.fridge_compressor_running&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on"&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ratio&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;6&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;now()&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;history_stats&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fridge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Compressor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Cycle&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6h"&lt;/span&gt;
    &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;binary_sensor.fridge_compressor_running&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on"&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;6&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;now()&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;type: ratio&lt;/code&gt; gives me a percentage: "the compressor was on 32 % of the last six hours." &lt;code&gt;type: count&lt;/code&gt; gives me a cycle count: "the compressor turned on 8 times in the last six hours." Together, those two numbers describe the &lt;em&gt;shape&lt;/em&gt; of the duty cycle — and the shape is what the alerts key off.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;history_stats&lt;/code&gt; and &lt;code&gt;statistics&lt;/code&gt; sensors reload via &lt;code&gt;homeassistant.reload_all&lt;/code&gt; (or their own &lt;code&gt;history_stats.reload&lt;/code&gt; / &lt;code&gt;statistics.reload&lt;/code&gt; services) most of the time. I've occasionally hit cases where a brand-new sensor wouldn't appear in the entity registry until after a full HA restart — if &lt;code&gt;reload_all&lt;/code&gt; returns success but the entity never shows up, that's the next thing to try.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step three: the three alerts
&lt;/h2&gt;

&lt;p&gt;All three are &lt;code&gt;binary_sensor&lt;/code&gt; entities with &lt;code&gt;device_class: problem&lt;/code&gt;, which makes them render as "OK / Problem" in the UI. The convention I use across the house is: &lt;strong&gt;the binary sensor is the &lt;em&gt;signal&lt;/em&gt;, a separate automation is the &lt;em&gt;action&lt;/em&gt;.&lt;/strong&gt; Notification logic, cooldowns, channel routing — all of that lives in automations subscribed to these entities, and the alert sensors themselves stay declarative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 1: Fridge offline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alert:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fridge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;offline"&lt;/span&gt;
  &lt;span class="na"&gt;device_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;problem&lt;/span&gt;
  &lt;span class="na"&gt;delay_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;00:02:00"&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;{{ states('switch.fridge') in ['off', 'unavailable', 'unknown', 'none'] }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either the smart plug lost comms, or the fridge lost mains power. The two-minute &lt;code&gt;delay_on&lt;/code&gt; filters out the brief blips that happen when the broker reconnects or the plug reboots itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 2: Fridge stuck on
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alert:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fridge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stuck&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on"&lt;/span&gt;
  &lt;span class="na"&gt;device_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;problem&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;{% set duty = states('sensor.fridge_compressor_duty_6h') | float(0) %}&lt;/span&gt;
    &lt;span class="s"&gt;{% set cyc  = states('sensor.fridge_compressor_cycle_6h') | int(0) %}&lt;/span&gt;
    &lt;span class="s"&gt;{{ duty &amp;gt; 85 and cyc &amp;lt; 3 }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the interesting one. A healthy fridge cycles: maybe 8–15 times in 6 hours, with the compressor on perhaps 25–40 % of the time. &lt;strong&gt;High duty plus low cycle count&lt;/strong&gt; is a specific failure shape — the compressor is running almost continuously without ever satisfying the thermostat. That's either a thermostat fault, a refrigerant problem, or a seal so bad the compressor can't keep up. Distinguishing it from "it's just a hot day" is exactly what the cycle-count condition does: a hot-day fridge still cycles, just more often.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 3: Fridge door open
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;state&lt;/span&gt;
        &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;binary_sensor.fridge_compressor_running&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on"&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;00:45:00"&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alert_on&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;state&lt;/span&gt;
        &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;binary_sensor.fridge_compressor_running&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off"&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alert_off&lt;/span&gt;
    &lt;span class="na"&gt;binary_sensor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alert:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fridge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;door&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;open"&lt;/span&gt;
        &lt;span class="na"&gt;device_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;problem&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trigger.id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'alert_on'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single compressor run longer than 45 minutes almost always means the door is ajar — the compressor can't pull the temperature down because warm air keeps coming in.&lt;/p&gt;

&lt;p&gt;This one is structured as a &lt;strong&gt;trigger-based template binary sensor&lt;/strong&gt;, not a regular state-based one with &lt;code&gt;delay_on&lt;/code&gt;. Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;delay_on&lt;/code&gt; leaves the sensor in &lt;code&gt;unknown&lt;/code&gt; if its template evaluates &lt;code&gt;true&lt;/code&gt; at HA startup, until the next state change. A trigger-based sensor has a clean state machine.&lt;/li&gt;
&lt;li&gt;Templates that look at &lt;code&gt;last_changed&lt;/code&gt; and &lt;code&gt;now()&lt;/code&gt; don't re-evaluate on a clock — they only re-evaluate when one of the entities they reference changes state. A trigger-based template wakes up on a &lt;em&gt;time-bounded state change&lt;/em&gt; (&lt;code&gt;to: "on" for: "00:45:00"&lt;/code&gt;), which is exactly the right shape for "this condition has been true continuously for X."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern shows up everywhere once you start looking for it: laundry left running, garage door open, lights on with no motion. Memorise it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: the alerts gave me cost tracking for free
&lt;/h2&gt;

&lt;p&gt;Once you have a wattage stream and a binary "is it running" sensor, multiplying watts by the live spot price gives you instantaneous dollars per hour. (My &lt;code&gt;$/kWh&lt;/code&gt; sensor comes from &lt;a href="https://github.com/cabberley/amber2mqtt" rel="noopener noreferrer"&gt;cabberley/amber2mqtt&lt;/a&gt;, a community bridge that polls Amber Electric's API and republishes 5-minute spot prices to my Mosquitto broker. The official Amber HA integration would do the same job, with minor entity-name changes.) Feed &lt;em&gt;that&lt;/em&gt; into a Riemann-sum integration sensor and you have cumulative dollars. Wrap it in a &lt;code&gt;utility_meter&lt;/code&gt; and you get daily and monthly totals that reset on schedule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integration&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fridge&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Cost&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Total"&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sensor.fridge_cost_rate&lt;/span&gt;
  &lt;span class="na"&gt;unit_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;h&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;left&lt;/span&gt;   &lt;span class="c1"&gt;# left-Riemann: correct for step-function spot prices&lt;/span&gt;
  &lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;

&lt;span class="na"&gt;utility_meter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fridge_cost_today&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sensor.fridge_cost_total&lt;/span&gt;
    &lt;span class="na"&gt;cycle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily&lt;/span&gt;
  &lt;span class="na"&gt;fridge_cost_month&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sensor.fridge_cost_total&lt;/span&gt;
    &lt;span class="na"&gt;cycle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monthly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;method: left&lt;/code&gt; matters: spot prices are step functions (they hold a value for 5 minutes, then jump), and left-Riemann sums them correctly. Trapezoidal would smear the jumps and give you slightly wrong numbers.&lt;/p&gt;

&lt;p&gt;The fridge runs me about $3.20 a month at current prices.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it has actually caught
&lt;/h2&gt;

&lt;p&gt;In the months since deploying this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The "fridge offline" alert fires occasionally when the IoT VLAN has a hiccup. Two-minute &lt;code&gt;delay_on&lt;/code&gt; filters most of it; what remains is genuinely useful.&lt;/li&gt;
&lt;li&gt;The "door open" alert has fired three times. All three were genuinely ajar doors.&lt;/li&gt;
&lt;li&gt;"Stuck on" has not fired. My fridge is fine and the alert is correctly silent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point of monitoring isn't to generate notifications — it's to give you confidence that the absence of a notification means something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general pattern
&lt;/h2&gt;

&lt;p&gt;This whole thing — power signal → derived running state → rolling windows → behaviour-shaped alerts — generalises. I have the same skeleton on the dishwasher, washing machine, and dryer, each with appliance-specific thresholds. Anything that draws meaningful power and has a "is it doing its job" question worth answering is a candidate.&lt;/p&gt;

&lt;p&gt;The cost is one smart plug and a few hundred lines of YAML. The benefit is silent confidence that, when something does fail, you'll know within minutes — not when the smell hits you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Setup notes: Home Assistant 2026.x, TP-Link P110 via the Kasa integration, MQTT broker for Amber Electric pricing data. Full YAML available on request.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homeassistant</category>
      <category>monitoring</category>
      <category>smarthome</category>
      <category>iot</category>
    </item>
    <item>
      <title>Building GNOME Apps with Rust, Part 2: GObject in Rust — The Type System Explained</title>
      <dc:creator>fromthearchitect</dc:creator>
      <pubDate>Sun, 12 Apr 2026 22:27:00 +0000</pubDate>
      <link>https://forem.com/fromthearchitect/building-gnome-apps-with-rust-part-2-gobject-in-rust-the-type-system-explained-310n</link>
      <guid>https://forem.com/fromthearchitect/building-gnome-apps-with-rust-part-2-gobject-in-rust-the-type-system-explained-310n</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 2 of a series taking a GNOME app from an empty directory to GNOME Circle. &lt;a href="https://dev.to/posts/gnome-rust-part-1-getting-started/"&gt;Part 1&lt;/a&gt; covered the why and the dev environment.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The app we're building
&lt;/h2&gt;

&lt;p&gt;Throughout this series, we'll be building &lt;strong&gt;Gazette&lt;/strong&gt; — an RSS reader. I picked it because it naturally exercises every pattern a non-trivial GNOME app needs: networking, data persistence, list/detail UI, adaptive layouts, settings, and state management. You don't need to build an RSS reader to follow along — the patterns are universal.&lt;/p&gt;

&lt;p&gt;But before we touch GTK, before we create a window or a header bar, we need to understand the type system that everything in GNOME is built on — GObject. Every hour you spend on it before touching widgets will save you two hours of confusion when your property binding silently doesn't work or your signal callback never fires.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is GObject and why should you care
&lt;/h2&gt;

&lt;p&gt;Every widget in GTK is a GObject. That sounds like a detail you can safely skip. It isn't — it explains why property bindings work the way they do, why signals don't need explicit subscriber lists, and why half the confusion I see in gtk-rs questions comes from not knowing this layer exists.&lt;/p&gt;

&lt;p&gt;GObject gives C what it was never designed to have: objects. You get inheritance, properties that can be observed and bound, a signal system for decoupled events, and reference counting. All achieved through C macros and conventions, which is exactly as readable as it sounds.&lt;/p&gt;

&lt;p&gt;The challenge for Rust developers is that these two type systems have fundamentally different ideas about ownership. Rust doesn't have inheritance. GObject is built on it. Rust tracks lifetimes at compile time. GObject uses runtime reference counting. The gtk-rs bindings bridge this gap with a specific pattern — and once you've seen it once, you'll recognise it everywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  A simple GObject: the Feed model
&lt;/h2&gt;

&lt;p&gt;We're going to create a &lt;code&gt;Feed&lt;/code&gt; type — an RSS feed with a title, a URL, and an unread count. No widgets, no UI. Just a plain GObject that holds data, exposes properties, and emits a signal. This is the simplest way to see every piece of the pattern without distractions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo init gobject-example
&lt;span class="nb"&gt;cd &lt;/span&gt;gobject-example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[package]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gobject-example"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;edition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2021"&lt;/span&gt;

&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;gtk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gtk4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.11"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We only need gtk4 — it exposes &lt;code&gt;glib&lt;/code&gt; and &lt;code&gt;gio&lt;/code&gt; as &lt;code&gt;gtk::glib&lt;/code&gt; and &lt;code&gt;gtk::gio&lt;/code&gt;. No libadwaita for this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The inner/outer type split
&lt;/h2&gt;

&lt;p&gt;Every custom GObject in Rust is two types: an inner type that lives in &lt;code&gt;mod imp&lt;/code&gt; and holds the actual state, and a lightweight outer type that's what your code actually touches. GObject owns the inner; the outer is just a reference-counted handle to it.&lt;/p&gt;

&lt;p&gt;This split exists because both GObject and Rust want to own the data. GObject manages lifecycles through reference counting. Rust enforces ownership at compile time. The inner/outer pattern resolves the tension — GObject gets ownership of the inner type, and you get a cheaply-cloneable handle to pass around. If you've used &lt;code&gt;Rc&amp;lt;RefCell&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;, the mental model is similar — though GObject's ref count is atomic, so &lt;code&gt;Arc&amp;lt;RefCell&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; is closer to the truth. The atomicity is about safe counting, not thread-safe access; GTK objects still need to stay on the main thread.&lt;/p&gt;




&lt;h2&gt;
  
  
  The inner type
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/feed.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;imp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;cell&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Cell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RefCell&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;OnceLock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;subclass&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;subclass&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;#[derive(Properties,&lt;/span&gt; &lt;span class="nd"&gt;Default)]&lt;/span&gt;
    &lt;span class="nd"&gt;#[properties(wrapper_type&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;super::Feed)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Feed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;#[property(get,&lt;/span&gt; &lt;span class="nd"&gt;set)]&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RefCell&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="nd"&gt;#[property(get,&lt;/span&gt; &lt;span class="nd"&gt;set)]&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RefCell&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="nd"&gt;#[property(get,&lt;/span&gt; &lt;span class="nd"&gt;set)]&lt;/span&gt;
        &lt;span class="n"&gt;unread_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Cell&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[glib::object_subclass]&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ObjectSubclass&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Feed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GazetteFeed"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;#[glib::derived_properties]&lt;/span&gt;
    &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ObjectImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Feed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;SIGNALS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OnceLock&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;OnceLock&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;SIGNALS&lt;/span&gt;&lt;span class="nf"&gt;.get_or_init&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"items-updated"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nf"&gt;.param_types&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nn"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;static_type&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
                    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's unpack the important parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Properties and interior mutability.&lt;/strong&gt; Each field marked &lt;code&gt;#[property(get, set)]&lt;/code&gt; becomes a GObject property — accessible by string name, bindable, observable. The &lt;code&gt;RefCell&amp;lt;String&amp;gt;&lt;/code&gt; and &lt;code&gt;Cell&amp;lt;u32&amp;gt;&lt;/code&gt; types are required because GObject's API is &lt;code&gt;&amp;amp;self&lt;/code&gt; everywhere, never &lt;code&gt;&amp;amp;mut self&lt;/code&gt;. Use &lt;code&gt;Cell&amp;lt;T&amp;gt;&lt;/code&gt; for &lt;code&gt;Copy&lt;/code&gt; types, &lt;code&gt;RefCell&amp;lt;T&amp;gt;&lt;/code&gt; for everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ObjectSubclass.&lt;/strong&gt; This registers the type with GObject. &lt;code&gt;NAME&lt;/code&gt; must be globally unique — duplicates cause a runtime panic. We don't set &lt;code&gt;ParentType&lt;/code&gt; because the default is &lt;code&gt;glib::Object&lt;/code&gt;. When we get to widgets, we'll set it explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ObjectImpl and signals.&lt;/strong&gt; The &lt;code&gt;#[glib::derived_properties]&lt;/code&gt; attribute wires up the getters and setters. The &lt;code&gt;signals&lt;/code&gt; method defines a custom &lt;code&gt;items-updated&lt;/code&gt; signal that carries a &lt;code&gt;u32&lt;/code&gt;. Signals are GObject's event system — listeners connect without the emitter knowing who's listening.&lt;/p&gt;




&lt;h2&gt;
  
  
  The outer type
&lt;/h2&gt;

&lt;p&gt;Still in &lt;code&gt;src/feed.rs&lt;/code&gt;, below &lt;code&gt;mod imp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;wrapper!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ObjectSubclass&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Feed&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Feed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;mark_items_updated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.set_unread_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.emit_by_name&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"items-updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;new_count&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;glib::wrapper!&lt;/code&gt; macro generates a thin, reference-counted pointer to the inner type. Clone it freely — it just bumps the ref count.&lt;/p&gt;

&lt;p&gt;GObjects are built through the builder pattern, setting properties by name. &lt;code&gt;mark_items_updated&lt;/code&gt; deliberately sets the property &lt;em&gt;and&lt;/em&gt; emits the signal as a single operation — in a real app, you don't want callers updating the count without notifying listeners.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;src/main.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;gtk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GNOME Planet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://planet.gnome.org/atom.xml"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Read properties&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Feed: {} ({})"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.title&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.url&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unread: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.unread_count&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// Connect to the signal&lt;/span&gt;
    &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.connect_closure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"items-updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;closure_local!&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Feed updated — {} new items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Connect to property change notifications&lt;/span&gt;
    &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.connect_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unread-count"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unread count changed to {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.unread_count&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Update the feed&lt;/span&gt;
    &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.mark_items_updated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Property binding — one Feed mirroring another's unread count&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mirror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Mirror"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://example.com/feed.xml"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.bind_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unread-count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mirror&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unread-count"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.sync_create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Mirror unread: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mirror&lt;/span&gt;&lt;span class="nf"&gt;.unread_count&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 5&lt;/span&gt;

    &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="nf"&gt;.mark_items_updated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Mirror unread: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mirror&lt;/span&gt;&lt;span class="nf"&gt;.unread_count&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// 12&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feed: GNOME Planet (https://planet.gnome.org/atom.xml)
Unread: 0
Unread count changed to 5
Feed updated — 5 new items
Mirror unread: 5
Unread count changed to 12
Feed updated — 12 new items
Mirror unread: 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;gtk::init()&lt;/code&gt; call — we're only using GLib objects here, no widgets, no event loop. Synchronous signal emission works fine without one; once you're dispatching across async boundaries, you'll need a running main loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Property binding is the one worth sitting with
&lt;/h3&gt;

&lt;p&gt;Properties, signals, notifications — you wire those up manually. Binding is different. It's declarative: state a relationship once and the type system enforces it. When we build Gazette's UI, this is how we'll connect models to widgets — a property changes on a model and the bound widget updates automatically, no event handler code. The first time that happens, you stop thinking in callbacks. That's the shift.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern, summarised
&lt;/h2&gt;

&lt;p&gt;Every custom GObject in Rust follows this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;my_type&lt;/span&gt;&lt;span class="py"&gt;.rs&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="k"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;imp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;MyType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;           &lt;span class="c1"&gt;// State (with Cell/RefCell)&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ObjectSubclass&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;MyType&lt;/span&gt;  &lt;span class="c1"&gt;// Registration (NAME, Type, ParentType)&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;ObjectImpl&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;MyType&lt;/span&gt;      &lt;span class="c1"&gt;// Lifecycle (properties, signals)&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;WidgetImpl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;           &lt;span class="c1"&gt;// Parent traits (only for widgets)&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nn"&gt;glib&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;wrapper!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;             &lt;span class="c1"&gt;// Outer type declaration&lt;/span&gt;
&lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;MyType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                &lt;span class="c1"&gt;// Public API&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it's a lot of ceremony for a type that holds three fields. That's the price of entry — GObject was designed for C, and bridging two type systems isn't free. But the payoff is that every GObject you write after this one is the same shape. You stop thinking about the structure and start thinking about what the type actually does. The boilerplate becomes invisible by the third or fourth time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Forgetting interior mutability&lt;/strong&gt; — &lt;code&gt;title: String&lt;/code&gt; instead of &lt;code&gt;title: RefCell&amp;lt;String&amp;gt;&lt;/code&gt; will fail at compile time. GObject gives you &lt;code&gt;&amp;amp;self&lt;/code&gt;, never &lt;code&gt;&amp;amp;mut self&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicate &lt;code&gt;NAME&lt;/code&gt; strings&lt;/strong&gt; — runtime panic, not a compile error. Namespace them: &lt;code&gt;GazetteFeed&lt;/code&gt;, not &lt;code&gt;Feed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Property name casing&lt;/strong&gt; — GObject properties use kebab-case (&lt;code&gt;unread-count&lt;/code&gt;), Rust methods use snake_case (&lt;code&gt;unread_count()&lt;/code&gt;). When referencing by string — &lt;code&gt;connect_notify&lt;/code&gt;, &lt;code&gt;bind_property&lt;/code&gt;, &lt;code&gt;Object::builder()&lt;/code&gt; — use kebab-case. Get it wrong and it silently fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;#[glib::derived_properties]&lt;/code&gt;&lt;/strong&gt; — without it, the &lt;code&gt;Properties&lt;/code&gt; derive generates Rust methods but the GObject type system doesn't know about them. Bindings and notifications won't work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;Now that we understand GObject's type system — properties, signals, the inner/outer split — we're ready to apply it. In Part 3, we'll build Gazette's application skeleton: an &lt;code&gt;adw::Application&lt;/code&gt;, an &lt;code&gt;adw::ApplicationWindow&lt;/code&gt;, a header bar, and a Blueprint template. The GObject pattern is exactly the same; the trait chain is just longer.&lt;/p&gt;




&lt;p&gt;The source code for this post is available on &lt;a href="https://github.com/fromthearchitect/gnome-rust-part-2-gobject" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>gnome</category>
      <category>rust</category>
      <category>gtk</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building GNOME Apps with Rust, Part 1: Getting Started</title>
      <dc:creator>fromthearchitect</dc:creator>
      <pubDate>Mon, 06 Apr 2026 22:11:23 +0000</pubDate>
      <link>https://forem.com/fromthearchitect/building-gnome-apps-with-rust-part-1-getting-started-25ol</link>
      <guid>https://forem.com/fromthearchitect/building-gnome-apps-with-rust-part-1-getting-started-25ol</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 1 of a series that takes a GNOME application from an empty directory to acceptance into GNOME Circle. Each post is self-contained, but the series follows a single arc — and a real app — through every stage of the journey.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why GNOME
&lt;/h2&gt;

&lt;p&gt;If you're building a desktop Linux application in 2026, you've got choices. KDE Plasma has Kirigami. Elementary has Granite. You can reach for Electron, Tauri, or a dozen other cross-platform toolkits and call it a day.&lt;/p&gt;

&lt;p&gt;I chose GNOME because of what it feels like to use. The GNOME desktop provides a clean, distraction-free approach to computing. Its consistency rivals early macOS — there's a feeling that everything is in its place. When I started building Moments, my photo management app, that consistency carried over into the development experience in a way I didn't expect. The &lt;a href="https://developer.gnome.org/hig/" rel="noopener noreferrer"&gt;GNOME Human Interface Guidelines&lt;/a&gt; aren't suggestions — they're a design language. Follow them and your app inherits a coherent visual identity, consistent interaction patterns, and accessibility support you didn't have to design yourself. Your header bar looks right. Your adaptive layout works the way users expect. Your keyboard navigation is correct.&lt;/p&gt;

&lt;p&gt;That matters more than it sounds like it should. Users develop muscle memory around their environment. An app that respects that muscle memory earns trust immediately. An app that doesn't — even if it's technically superior — creates friction. GNOME gives you a head start on eliminating that friction, but only if you actually use the toolkit the way it's meant to be used.&lt;/p&gt;

&lt;p&gt;The ecosystem is also unusually healthy for an open source desktop project. &lt;a href="https://flathub.org/" rel="noopener noreferrer"&gt;Flathub&lt;/a&gt; provides distribution. &lt;a href="https://circle.gnome.org/" rel="noopener noreferrer"&gt;GNOME Circle&lt;/a&gt; provides recognition, infrastructure, and community. The tooling — Builder, Workbench, Cambalache — is actively maintained and improving. And the community, while not enormous, is engaged and welcoming in a way that larger ecosystems sometimes aren't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Rust
&lt;/h2&gt;

&lt;p&gt;GNOME has traditionally been a C ecosystem. GTK is written in C. GLib is written in C. The GObject type system — which underpins everything — is C's answer to object-oriented programming, complete with manual reference counting and a macro-heavy convention system that works remarkably well once you've internalised it.&lt;/p&gt;

&lt;p&gt;You don't need to write C to build GNOME apps. The GObject Introspection system generates bindings for dozens of languages, and three in particular have strong GNOME stories: Python with PyGObject, Vala (a language designed specifically for GObject development), and Rust with gtk-rs.&lt;/p&gt;

&lt;p&gt;I'm writing this series in Rust, and I think it's the right default choice for new GNOME apps in 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ecosystem has moved.&lt;/strong&gt; Look at recent GNOME Circle apps — Fractal, Switcheroo, Hieroglyphic — the Rust cohort is growing fast. When you hit a problem building a Rust/GTK app, there's now a meaningful body of real-world code to reference. That wasn't true even two years ago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gtk-rs bindings are mature.&lt;/strong&gt; The gtk4-rs and libadwaita-rs crates provide safe, idiomatic Rust wrappers around the full GTK4 and libadwaita API surface. They handle reference counting, type casting, and signal connections in a way that feels natural in Rust. These aren't thin C bindings with unsafe blocks everywhere — they're a genuine Rust API for building GTK applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust's strengths align with desktop app needs.&lt;/strong&gt; Memory safety without garbage collection. Fearless concurrency. A type system that catches entire categories of bugs at compile time. These aren't just theoretical benefits — they show up in practice when you're managing complex UI state, handling async operations, and trying to ship software that doesn't crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tooling works.&lt;/strong&gt; rust-analyzer provides excellent IDE support. Cargo handles dependency management. The Flatpak SDK includes a full Rust toolchain. You can get from &lt;code&gt;cargo init&lt;/code&gt; to a running Flatpak application without fighting your tools.&lt;/p&gt;

&lt;p&gt;There are tradeoffs. Rust has a steeper learning curve than Python, and the GObject subclassing pattern in Rust adds a layer of complexity that doesn't exist in Python or Vala. Compile times are slower. Iteration speed — especially building inside Flatpak — is meaningfully worse than Python's edit-and-run loop. These are real costs, and I won't pretend otherwise. But the learning curve is a curve, not a wall, and this series will walk through every part of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting up the development environment
&lt;/h2&gt;

&lt;p&gt;Before you write a single line of Rust or GTK code, you need three things: GNOME Builder, the Flatpak SDK, and a working understanding of why we're using Flatpak from day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Flatpak from the start
&lt;/h3&gt;

&lt;p&gt;Most development guides have you install GTK libraries on your host system, write code against them, and worry about packaging later. This works — until it doesn't. The failure mode is specific and painful: you develop against GTK 4.18 on your host, everything works, you go to package the app for Flathub six months later, and discover that the GNOME SDK version you need to target ships GTK 4.16, and three APIs you depend on don't exist.&lt;/p&gt;

&lt;p&gt;Building inside Flatpak from the beginning eliminates this entire class of problem. Your development environment matches your deployment environment. The libraries you link against are the libraries your users will have. The dependencies are explicit and reproducible. When you submit to Flathub, there are no surprises.&lt;/p&gt;

&lt;p&gt;It's slightly more friction up front. It's dramatically less friction in total.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Flatpak and Flathub
&lt;/h3&gt;

&lt;p&gt;If you're running Fedora, Flatpak is already configured. For other distributions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install Flatpak (if not present)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;flatpak    &lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; flatpak      &lt;span class="c"&gt;# Arch&lt;/span&gt;

&lt;span class="c"&gt;# Add the Flathub remote&lt;/span&gt;
flatpak remote-add &lt;span class="nt"&gt;--if-not-exists&lt;/span&gt; flathub https://flathub.org/repo/flathub.flatpakrepo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install the GNOME SDK
&lt;/h3&gt;

&lt;p&gt;The GNOME SDK provides the full development environment — GTK4, libadwaita, GLib, and the Rust toolchain — inside a Flatpak runtime. You need both the runtime and the SDK, plus the Rust extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the GNOME 50 SDK and runtime&lt;/span&gt;
flatpak &lt;span class="nb"&gt;install &lt;/span&gt;flathub org.gnome.Sdk//50 org.gnome.Platform//50

&lt;span class="c"&gt;# Install the Rust extension for the SDK&lt;/span&gt;
flatpak &lt;span class="nb"&gt;install &lt;/span&gt;flathub org.freedesktop.Sdk.Extension.rust-stable//25.08
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A note on version numbers: the GNOME SDK version (50, at time of writing) corresponds to the GNOME release. The Rust extension version (25.08) corresponds to the freedesktop SDK version that the GNOME SDK is built on. These version numbers will change — check the &lt;a href="https://docs.flathub.org/docs/for-app-authors/requirements" rel="noopener noreferrer"&gt;Flathub runtime page&lt;/a&gt; for current versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install GNOME Builder
&lt;/h3&gt;

&lt;p&gt;GNOME Builder is the IDE purpose-built for GNOME development. It understands Meson, Flatpak manifests, and the GNOME SDK. It can build your application inside the Flatpak sandbox, run it, and provide code intelligence — all without you manually configuring build environments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flatpak &lt;span class="nb"&gt;install &lt;/span&gt;flathub org.gnome.Builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Builder isn't the only option. You can use VS Code, Neovim, or any editor you prefer — I'll cover that setup in a later post on developer experience. But Builder eliminates the most configuration friction for getting started, and it's what I'd recommend for your first project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the toolchain
&lt;/h3&gt;

&lt;p&gt;Open a terminal inside the Flatpak SDK environment to confirm everything is in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enter the SDK environment&lt;/span&gt;
flatpak run &lt;span class="nt"&gt;--command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bash org.gnome.Sdk//50

&lt;span class="c"&gt;# Inside the SDK shell:&lt;/span&gt;
rustc &lt;span class="nt"&gt;--version&lt;/span&gt;
cargo &lt;span class="nt"&gt;--version&lt;/span&gt;
pkg-config &lt;span class="nt"&gt;--libs&lt;/span&gt; gtk4
pkg-config &lt;span class="nt"&gt;--libs&lt;/span&gt; libadwaita-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a recent stable Rust version and successful pkg-config output for both GTK4 and libadwaita. If any of these fail, the SDK or Rust extension didn't install correctly — reinstall them before continuing.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on host development
&lt;/h3&gt;

&lt;p&gt;You can also develop on your host system by installing the GTK4 and libadwaita development packages directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Fedora&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;gtk4-devel libadwaita-devel

&lt;span class="c"&gt;# Arch&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; gtk4 libadwaita

&lt;span class="c"&gt;# Ubuntu/Debian (may lag behind on versions)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libgtk-4-dev libadwaita-1-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you faster compile times and a tighter edit-compile-run loop, which matters during active development. Many Rust/GTK developers work this way day-to-day, using host-installed libraries for rapid iteration and Flatpak builds for testing and release.&lt;/p&gt;

&lt;p&gt;The risk is version drift between your host libraries and the Flatpak SDK. If you go this route, check which GTK4 and libadwaita versions your host provides and compare them to the GNOME SDK you're targeting. As long as your host version is equal to or newer than the SDK version, you're fine. If it's older, you'll hit missing APIs.&lt;/p&gt;

&lt;p&gt;My recommendation: start with Flatpak-only builds until you've got a working app, then add host builds as an optimisation once you understand the version boundaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tools you will be using
&lt;/h2&gt;

&lt;p&gt;Before we start writing code in Part 2, here's a brief orientation to the tools that'll appear throughout this series. You don't need deep knowledge of any of them yet — just awareness that they exist and what role they play.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://mesonbuild.com/" rel="noopener noreferrer"&gt;Meson&lt;/a&gt;&lt;/strong&gt; — The build system. GNOME apps use Meson, not Cargo, as the top-level build system. Cargo still builds your Rust code, but Meson orchestrates everything else: compiling GResources, installing desktop files, generating application metadata, and invoking Cargo as part of the build. Part 3 covers Meson in detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://gnome.pages.gitlab.gnome.org/blueprint-compiler/" rel="noopener noreferrer"&gt;Blueprint&lt;/a&gt;&lt;/strong&gt; — A markup language for defining GTK user interfaces. Blueprint compiles to the XML that GTK's GtkBuilder expects, but it's dramatically more readable. You can also build your entire UI in Rust code without Blueprint — we'll cover both approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.gtk.org/gio/struct.Resource.html" rel="noopener noreferrer"&gt;GResource&lt;/a&gt;&lt;/strong&gt; — GNOME's system for bundling assets (UI templates, icons, CSS) into your application binary. Instead of loading files from disk at runtime, GResources get compiled into the binary and accessed by path. It's how GNOME apps ship self-contained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.freedesktop.org/software/appstream/docs/" rel="noopener noreferrer"&gt;Appstream&lt;/a&gt;&lt;/strong&gt; — The metadata standard that app stores (including Flathub) use to display your app's name, description, screenshots, and release notes. You'll need to get this right for Flathub acceptance — Part 9 covers it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apps.gnome.org/Workbench/" rel="noopener noreferrer"&gt;Workbench&lt;/a&gt;&lt;/strong&gt; — A playground app for experimenting with GTK widgets, CSS, and Blueprint templates. If you want to test a UI idea without rebuilding your whole app, Workbench is where you do it. Grab it from Flathub.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;Part 2 opens with introducing the app we'll build throughout this series. It won't be a toy counter or a TODO list — it'll be something with enough complexity to hit real architectural decisions, real packaging challenges, and real Flathub review feedback.&lt;/p&gt;

&lt;p&gt;Every post will be self-contained enough that you can follow along building your own app. The problems we'll solve — state management, async operations, data persistence, adaptive layouts, packaging — are universal to any non-trivial GTK application. We're documenting the actual experience of taking a GNOME app from zero to Circle, including the parts that aren't in any documentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes next
&lt;/h2&gt;

&lt;p&gt;Before we build a window, we need to understand the type system underneath it. Every widget, every signal, every property binding in GTK is built on GObject — and the way GObject maps to Rust is the single biggest conceptual hurdle for developers coming from other Rust backgrounds.&lt;/p&gt;

&lt;p&gt;In Part 2, we'll build a simple data model from scratch and use it to understand GObject's inner/outer type pattern, properties, signals, and property bindings. No GTK widgets yet — just the foundation that makes everything else click once we start building the actual app in Part 3.&lt;/p&gt;

</description>
      <category>gnome</category>
      <category>rust</category>
      <category>gtk</category>
      <category>linux</category>
    </item>
    <item>
      <title>The AI Pair Programmer: Why the Human Loop Is About Partnership, Not Review</title>
      <dc:creator>fromthearchitect</dc:creator>
      <pubDate>Sat, 28 Mar 2026 04:01:49 +0000</pubDate>
      <link>https://forem.com/fromthearchitect/the-ai-pair-programmer-why-the-human-loop-is-about-partnership-not-review-2pa6</link>
      <guid>https://forem.com/fromthearchitect/the-ai-pair-programmer-why-the-human-loop-is-about-partnership-not-review-2pa6</guid>
      <description>&lt;h2&gt;
  
  
  The current conversation is missing something
&lt;/h2&gt;

&lt;p&gt;If you spend any time in developer circles right now, the conversation about AI coding tools tends to collapse into one of two camps. The first camp believes we are months away from AI replacing software developers entirely — that the role of the human is already vestigial, a temporary inconvenience on the road to full automation. The second camp pushes back hard, arguing that AI is little more than a sophisticated autocomplete — useful for boilerplate, dangerous if trusted, and nowhere near capable of producing anything a senior engineer couldn't do faster with a clear head and a good keyboard shortcut.&lt;/p&gt;

&lt;p&gt;Both positions are wrong. Not subtly wrong — fundamentally wrong. And the gap between them is where something genuinely interesting is happening.&lt;/p&gt;

&lt;p&gt;The developers who are getting the most out of AI coding tools are not the ones treating the AI as an oracle to be prompted, nor the ones dismissing it as a toy. They are the ones who have stumbled onto — or deliberately adopted — a different mental model entirely. One that has a name, a history, and a body of practice behind it. One that the software industry actually worked out decades ago, in a different context, for different reasons.&lt;/p&gt;

&lt;p&gt;It's called pair programming. And it turns out it maps onto working with an LLM almost perfectly.&lt;/p&gt;




&lt;h2&gt;
  
  
  A brief primer on XP pair programming
&lt;/h2&gt;

&lt;p&gt;Extreme Programming — XP — emerged in the late 1990s as a reaction to the bloated, process-heavy software development methodologies of the time. It was opinionated, practical, and in many ways ahead of its time. Among its practices, pair programming stood out as the one that generated the most scepticism from people who had never tried it, and the most loyalty from people who had.&lt;/p&gt;

&lt;p&gt;The premise is simple: two developers, one keyboard, one screen. But the premise is also misleading, because it implies the value is in the typing — that you are getting two people's worth of keystrokes for the price of one. That is not what pair programming is about. In fact, that framing completely misses the point.&lt;/p&gt;

&lt;p&gt;XP described two roles in a pairing session. The driver holds the keyboard and writes the code. The navigator holds the broader picture — watching for mistakes, thinking about what comes next, asking the question the driver is too focused to ask. Neither role is superior. Neither is passive. The navigator is not watching. The navigator is thinking, out loud, in dialogue with the driver. The value is in that continuous conversation — the shared context that builds between two people working through a problem together.&lt;/p&gt;

&lt;p&gt;What pair programming actually produces is a tighter feedback loop than solo development. Mistakes get caught earlier. Dead ends get identified faster. The solution that emerges has been stress-tested in real time by two different minds approaching the problem from slightly different angles. The code is better not because two people wrote it, but because two people thought about it simultaneously.&lt;/p&gt;

&lt;p&gt;This is why studies on pair programming consistently show that while it does take more developer hours per feature, it produces significantly fewer defects and requires less rework. The investment pays for itself. The conversation is the work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI as pair programmer
&lt;/h2&gt;

&lt;p&gt;Here is where the analogy earns its keep.&lt;/p&gt;

&lt;p&gt;When most developers describe their workflow with an AI coding tool, they describe something that sounds like issuing instructions. They have a task. They describe it to the AI. The AI produces output. They evaluate the output, accept it or reject it, and move on. The human is a reviewer. The AI is a very fast junior developer who never gets tired and never takes offence.&lt;/p&gt;

&lt;p&gt;That model works. It produces results. But it is leaving an enormous amount of value on the table.&lt;/p&gt;

&lt;p&gt;The developers getting disproportionate results from AI tools are doing something subtly but importantly different. They are not issuing instructions — they are having a conversation. They bring the problem, not the solution. They share context before asking for code. They push back when something feels wrong, even if they cannot immediately articulate why. They ask the AI to explain its reasoning, then challenge that reasoning. They treat the session as a dialogue, not a transaction.&lt;/p&gt;

&lt;p&gt;In XP terms, they are navigating. The AI is driving.&lt;/p&gt;

&lt;p&gt;This is not a metaphor. The navigator role in pair programming is precisely about holding the bigger picture while the driver handles execution. It means knowing where you are trying to go, recognising when the current path is taking you somewhere else, and having the conversation that corrects course before you have written a thousand lines in the wrong direction. That is exactly what a skilled human brings to a session with an AI coding tool.&lt;/p&gt;

&lt;p&gt;The AI, for its part, brings things that complement the navigator role almost perfectly. Breadth of knowledge across languages, frameworks, and patterns that no single human could match. Speed of execution that removes the friction between idea and implementation. Tireless willingness to explore alternatives, rewrite sections, and try a different approach without frustration or ego. And crucially — no stake in being right. An AI does not defend its previous output. Ask it to reconsider and it will.&lt;/p&gt;

&lt;p&gt;What neither party brings alone is sufficient. The AI without a thoughtful navigator produces technically correct output that solves the wrong problem, or solves the right problem in a way that does not fit the broader system, or makes decisions that are locally sensible and globally incoherent. The human without the AI's execution speed and breadth spends too much time in the details, loses the thread of the bigger picture, and runs out of energy before the interesting problems get solved.&lt;/p&gt;

&lt;p&gt;Together, the feedback loop tightens in exactly the way XP pair programming described. The conversation is still the work. It has just moved to a different medium.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the human loop actually looks like
&lt;/h2&gt;

&lt;p&gt;It is easy to describe the partnership in the abstract. It is more useful to describe what it actually looks like in practice — the specific moments where the human contribution is irreplaceable, and where the temptation to disengage is strongest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining the problem before touching the keyboard
&lt;/h3&gt;

&lt;p&gt;The single highest-leverage thing a human brings to an AI pairing session is a clear, well-considered problem definition. Not a feature request. Not a task. A genuine understanding of what you are trying to achieve and why, what constraints matter, and what success looks like.&lt;/p&gt;

&lt;p&gt;This sounds obvious. It is surprisingly rare. The temptation with fast AI tools is to start immediately — to get something on screen quickly and iterate from there. Sometimes that works. More often, the cost of an underspecified problem shows up three hours later when you have a working implementation of the wrong thing.&lt;/p&gt;

&lt;p&gt;The navigator's first job is to think before the driver starts moving. Spend time on the problem. Write it down. Share it with the AI not as a prompt but as a briefing. The quality of everything that follows is shaped by the quality of this moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Knowing when to push back
&lt;/h3&gt;

&lt;p&gt;AI coding tools are confident. They produce output that looks authoritative, is well-structured, and compiles. This is both their greatest strength and their most significant risk. Technically correct output that solves the wrong problem is harder to catch than broken code, because nothing obviously fails.&lt;/p&gt;

&lt;p&gt;The human navigator's most important skill is the ability to look at plausible output and ask whether it is actually right — not syntactically, but conceptually. Does this approach fit the broader architecture? Does this abstraction hold up under the cases we haven't discussed yet? Is this solving the problem we defined, or a simpler adjacent problem that is easier to solve?&lt;/p&gt;

&lt;p&gt;Pushing back does not require being certain the AI is wrong. It requires being willing to have the conversation. Ask it to explain its reasoning. Ask whether there is an alternative approach. Ask what the tradeoffs are. The AI will engage with these questions genuinely, and more often than not the dialogue surfaces something important that the initial output missed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bringing taste the AI doesn't have
&lt;/h3&gt;

&lt;p&gt;There are decisions in software development that are not technical. They are aesthetic, strategic, or deeply contextual — shaped by knowing your users, your constraints, your history, and your values. These decisions do not have correct answers that can be derived from training data.&lt;/p&gt;

&lt;p&gt;What belongs in version one and what gets deferred? Which abstraction is clean enough to be worth the indirection? Does this interaction pattern feel right for the people who will use it? Is this the kind of code a contributor joining the project in six months will be able to understand?&lt;/p&gt;

&lt;p&gt;These are navigator questions. The AI can inform them, offer perspectives, and flag tradeoffs — but it cannot answer them. The human is not in the loop for these decisions because the process requires it. The human is in the loop because the human is the only one who actually knows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recognising when the output is wrong
&lt;/h3&gt;

&lt;p&gt;This is the hardest skill to describe and the most important to develop. It is the ability to read AI-generated output and feel that something is off — before you can articulate why. Before the tests fail. Before the architecture review. Before the bug report.&lt;/p&gt;

&lt;p&gt;It is, in essence, experience. The same pattern recognition that a senior engineer develops over years of reading code, debugging systems, and watching abstractions fail in production. AI tools do not compress this. They make it more valuable, because the volume of output has increased while the need for judgment has not decreased.&lt;/p&gt;

&lt;p&gt;The human who can generate a working implementation in an afternoon and also recognise which parts of it will hurt them in three months is in a fundamentally different position than the one who can only do the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steering, not accepting
&lt;/h3&gt;

&lt;p&gt;Perhaps the simplest way to describe the human loop is this: your job is not to evaluate what the AI gives you. Your job is to steer toward what you actually need.&lt;/p&gt;

&lt;p&gt;Evaluation is passive. Steering is active. It means coming to the session with a direction in mind, holding that direction as the work progresses, and continuously asking whether the current path is still heading the right way. It means being willing to say "this is good, but it's not quite right" and continuing the conversation until it is. It means treating the first output as the beginning of a dialogue, not the end of one.&lt;/p&gt;

&lt;p&gt;The developers who get the most out of AI tools are not the ones who are best at prompting. They are the ones who are best at knowing what they want — and staying in the conversation until they get it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this changes for independent open source development
&lt;/h2&gt;

&lt;p&gt;Open source software has always had a talent distribution problem. The ideas are abundant. The developers who want to build are abundant. The time to build is not.&lt;/p&gt;

&lt;p&gt;A motivated independent developer working evenings and weekends on a project they care about has historically been constrained not by ambition or skill, but by hours. A genuinely useful, well-architected application — something with multiple backends, proper data persistence, a thoughtful UI, comprehensive documentation, Flatpak packaging, CI/CD, and the hundred other things that separate a hobby project from something people can actually rely on — has traditionally taken years of sustained effort from a solo developer. Many projects never get there. They stall at the interesting-but-incomplete stage, maintained inconsistently, never quite reaching the quality bar that would attract users or contributors.&lt;/p&gt;

&lt;p&gt;That constraint is loosening.&lt;/p&gt;

&lt;p&gt;The AI pair programming model compresses the distance between idea and implementation in a way that changes the economics of independent open source development fundamentally. Not because the AI does the work — but because the conversation-driven development loop eliminates the specific kinds of friction that cause projects to stall. The boilerplate that nobody wants to write. The documentation that always gets deferred. The architectural decision that requires holding too many things in your head simultaneously. The test scaffolding that feels important but not urgent. These are exactly the tasks where AI assistance is most effective and most reliable.&lt;/p&gt;

&lt;p&gt;What remains — and what the human navigator must still bring — is everything that cannot be generated. The decision about which problem is worth solving. The product sense that shapes a feature into something users will actually understand. The judgment that says this abstraction is clean and that one will hurt you in six months. The taste that knows what polished looks like, because you have used enough polished software to have internalised the standard.&lt;/p&gt;

&lt;p&gt;This has an important implication. As the execution barrier drops, the bottleneck shifts. The scarce resource in open source software development is no longer time — it is judgment. Developers who bring genuine domain knowledge, strong product instincts, and the ability to recognise quality will produce disproportionate results. Developers who treat AI tools as a shortcut around thinking will produce more output, faster, with the same fundamental limitations they had before.&lt;/p&gt;

&lt;p&gt;The other shift is in portfolio. The "one developer, one project" pattern that has characterised most independent open source work is giving way to something different. A developer with strong judgment and a productive AI partnership can now maintain multiple substantial projects simultaneously — not by spreading themselves thin, but by changing the nature of the work. The parts of software maintenance that consumed the most time — implementing well-understood features, writing documentation, managing boilerplate, scaffolding tests — are no longer the limiting factor. What remains is the interesting work. The work that required a human anyway.&lt;/p&gt;

&lt;p&gt;For ecosystems like GNOME, where the quality bar for inclusion is genuinely high and the community of active developers is relatively small, this could be transformative. The gap between "interesting idea" and "production quality app" has historically been where most projects died. That gap is narrowing. The question is whether the developers entering the ecosystem with AI-assisted workflows bring the judgment to match the pace — and whether the ecosystem's review and mentorship structures can scale to meet the increased volume of serious submissions that will follow.&lt;/p&gt;

&lt;p&gt;The opportunity is real. So is the responsibility that comes with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The risks worth naming honestly
&lt;/h2&gt;

&lt;p&gt;Any argument for a new way of working that does not acknowledge its failure modes is not a balanced argument — it is advocacy. The AI pair programming model is genuinely powerful. It is also genuinely risky, in specific ways that are worth naming clearly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The flood of mediocre output
&lt;/h3&gt;

&lt;p&gt;The same forces that allow a thoughtful developer to ship a production-quality application in days also allow a less thoughtful developer to ship something that looks like a production-quality application in days. The difference is not always visible on the surface. The code compiles. The UI renders. The README is comprehensive. The architecture document exists.&lt;/p&gt;

&lt;p&gt;What may be missing is the judgment that shaped the decisions underneath. An abstraction that seemed clean to the AI but will not survive contact with real usage. A data model that works for the happy path and fails at the edges. A feature set that was easy to generate but does not reflect what users actually need.&lt;/p&gt;

&lt;p&gt;Open source ecosystems that rely on community review as their quality filter — Flathub, GNOME Circle, and others — will face increased volume as the execution barrier drops. The risk is not that reviewers will be fooled by AI-generated mediocrity. Experienced reviewers are good at finding the problems underneath a polished surface. The risk is that the volume of submissions outpaces the community's capacity to review them thoughtfully, and that the filter becomes less effective simply because it is overwhelmed.&lt;/p&gt;

&lt;p&gt;This is not a reason to avoid AI-assisted development. It is a reason for the ecosystem to think ahead about how its quality gates scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding what you have built
&lt;/h3&gt;

&lt;p&gt;There is a specific failure mode in AI-assisted development that has no real equivalent in traditional solo development. It is possible to arrive at a working implementation without fully understanding it. The code is correct. The tests pass. The feature works. But the developer who accepted the output without interrogating it cannot explain why certain decisions were made, cannot predict how the system will behave under conditions that were not discussed, and cannot confidently modify it when requirements change.&lt;/p&gt;

&lt;p&gt;This is not the AI's failure. It is the navigator's failure — a failure to stay in the conversation long enough to genuinely understand what was built and why. The fix is not to distrust AI output. It is to hold yourself to the same standard of understanding you would apply to code you wrote yourself. If you cannot explain a decision, ask until you can. If an abstraction feels opaque, explore it. The AI will not tire of the conversation. Use that.&lt;/p&gt;

&lt;h3&gt;
  
  
  The expertise illusion
&lt;/h3&gt;

&lt;p&gt;AI tools are fluent. They produce confident, well-structured output across an enormous range of domains. This fluency can create the impression of expertise where expertise does not exist — in the AI's output and, more dangerously, in the developer's self-assessment.&lt;/p&gt;

&lt;p&gt;A developer who has shipped several AI-assisted projects may have genuine expertise in the problems those projects solved — or they may have accumulated a portfolio of working code without accumulating the underlying understanding that expertise actually represents. The distinction matters when things go wrong. When a system behaves unexpectedly in production. When a security issue emerges in a dependency. When the architecture needs to change in a fundamental way. These are the moments that separate the developer who understands their system from the one who generated it.&lt;/p&gt;

&lt;p&gt;The partnership model described in this post is specifically designed to develop genuine understanding alongside working software. The navigator who asks why, pushes back on decisions, and steers the conversation toward clarity is building expertise as they build the system. The developer who accepts output uncritically is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  The temptation to move on
&lt;/h3&gt;

&lt;p&gt;Fast tools create an appetite for speed. When you can scaffold a feature in an hour that would have taken a day, the temptation is to do ten features instead of one — to keep moving, keep building, keep generating. This is a real risk.&lt;/p&gt;

&lt;p&gt;The parts of software development that AI does not accelerate — thinking carefully about the problem, sitting with an architectural decision before committing to it, getting feedback from real users before adding the next feature — are the parts that tend to get skipped when the rest of the loop feels fast. The navigator's discipline is not just about what to build. It is about when to stop building and think.&lt;/p&gt;

&lt;p&gt;Pace is a tool. Used well, it lets you reach a quality threshold faster than was previously possible. Used poorly, it lets you reach the wrong destination faster than was previously possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  The invitation
&lt;/h2&gt;

&lt;p&gt;There is a version of working with AI coding tools that is transactional. You have a task. You describe it. You evaluate the output. You move on. It works, up to a point. It will continue to work, up to a point.&lt;/p&gt;

&lt;p&gt;There is another version that is something closer to a genuine intellectual partnership. You bring the problem, the context, the taste, and the judgment. The AI brings the breadth, the speed, and the tireless willingness to explore. Together you have a conversation — the kind of conversation that pair programming has always held up as the ideal — and the work that emerges from that conversation is better than either party could produce alone.&lt;/p&gt;

&lt;p&gt;The shift between these two versions is not about tools. It is not about prompting techniques or context window sizes or which model you are using. It is about how you show up to the session. Whether you come with a direction or just a task. Whether you interrogate the output or accept it. Whether you are willing to stay in the conversation until you genuinely understand what has been built and why.&lt;/p&gt;

&lt;p&gt;The XP community figured out decades ago that the most productive unit in software development is not the individual developer working alone — it is two people thinking together. That insight did not age. It just found a new form.&lt;/p&gt;

&lt;p&gt;Stop prompting. Start partnering. The results will surprise you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>agile</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
