# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build

GPaste uses Meson + Ninja:

```sh
meson setup build
ninja -C build
```

The build directory is `build`, not `builddir`.

Build options (`meson setup build -Doption=value`, or `meson configure build`).
The `feature` ones default to `auto`: enabled when the dependency is found,
skipped otherwise, so a build silently loses a feature if its dependency is
missing — pass `enabled` to make that fatal.

| Option | Type | Default | Description |
|---|---|---|---|
| `encryption` | feature | auto | libsodium-based history encryption (`G_PASTE_ENABLE_ENCRYPTION`) |
| `sqlite` | feature | auto | SQLite storage backends, needs SQLite ≥ 3.35 (`G_PASTE_ENABLE_SQLITE`) |
| `libsecret` | feature | auto | passphrase in the keyring, only with `encryption` (`G_PASTE_ENABLE_LIBSECRET`) |
| `pwquality` | feature | auto | passphrase strength rating, only with `encryption` (`G_PASTE_ENABLE_PWQUALITY`) |
| `gnome-shell` | bool | true | GNOME Shell extension + mutter clipboard backend (`G_PASTE_ENABLE_GNOME_SHELL`) |
| `introspection` | bool | true | Generate GIR data |
| `vapi` | bool | true | Generate Vala bindings (errors out without `introspection`) |
| `systemd` | bool | true | systemd user unit |
| `bash-completion`, `zsh-completion`, `fish-completion` | bool | true | Shell completions |

Four string options — `dbus-services-dir`, `dbus-interfaces-dir`,
`control-center-keybindings-dir` and `systemd-user-unit-dir` — override install
paths that are otherwise queried from `dbus-1` (the first two),
`gnome-keybindings` and `systemd` via pkg-config. Setting them is how a packager
avoids pulling those in as build dependencies; they are still required at
runtime.

For a lighter build that skips the GNOME Shell extension, GIR introspection data
and Vala bindings (the daemon, UI and preferences apps are always built):

```sh
meson setup build -Dgnome-shell=false -Dintrospection=false -Dvapi=false
```

Run the tests:

```sh
ninja -C build test          # or: meson test -C build
```

Tests live under `tests/`. `tests/history/` unit-tests the `GPasteHistory` model
(add/dedup/size-enforcement/remove/select) against an in-memory `GSettings`
(`GSETTINGS_BACKEND=memory` + the schema compiled into the build tree) and a
throwaway `XDG_DATA_HOME`, so they need no display server or dconf. The `eslint`
test lints the GNOME Shell extension JS, and `completions` checks the shell
completions and the man page against `gpaste-client`'s own list of verbs. None
of the three needs a session bus.

## Code style

- C standard: GNU17
- Formatting: ClangFormat (see `.clang-format`). Key rules: Allman braces, 4-space indent, no column limit, space before parens, no tabs.
- clang-format is not yet enforced; do not run it automatically.
- **Braces**: Remove braces from `if`/`else if`/`else` branches whose body is a single statement on a single line. Keep braces when the body has multiple statements OR spans multiple lines (e.g. a nested if-else chain). Multi-statement macros that need to appear as a single statement must use the `do { ... } while (0)` idiom — `SWITCH_STATE` in `gpaste-file-backend.c` does this and can safely appear without surrounding braces.
- **Alignment**: a parameter list that wraps has every continuation line starting at the column of the opening parenthesis, and the parameter names line up in one column, `PointerAlignment: Right` putting the stars against the name. That column is the largest `type` + `*`s + 1 over the parameters, taken per parameter — `GError **error` needs `6 + 2 + 1`, `GPasteClient *self` needs `12 + 1 + 1`, so the wider of the two sets it. A run of struct members aligns the same way, blank lines and comments included in the run (the `parent_instance` line stands on its own). A header may share one column across a whole block of declarations, so match the block rather than tightening one signature out of it.
- A rename that shortens or lengthens a type silently leaves these behind, so **re-align the whole signature or run, not just the line you touched** — and re-read the comments you touched against the rule below. Both are part of finishing an edit, not a separate pass.
- **Comments say what the code does**, not what it once did nor why it was changed: no "used to", no "no longer", no "instead of the old", no commit hashes. The domain has a past of its own — an old file format, a stale passphrase, an item the history no longer holds — and that is fair game; the code's does not. Rationale is welcome, and most of this tree's comments are rationale, but it has to be a rationale for what is there now.
- **A comment earns its lines.** Rationale, an invariant, an ownership rule, a trap the next reader would fall into: those pay. Restating the call below it, or the ownership its `g_autoptr` already declares, does not. Say the same thing once — a second site that needs it points at the first ("As in `on_unlock_reply()`: …") rather than repeating it, so the two cannot drift apart.
- **Re-read every comment a change adds before its commit lands.** `git diff --cached -U0 | grep -E '^\+\s*(/\*|\*|//)'` lists them; each has to pass the two rules above on its own. Writing them is when the change is freshest in mind, which is exactly when narrating it reads as natural prose — "rather than the old", "which is what retires", "the branch X left behind", "what just changed". Only a re-read from cold catches those, so it is a step of its own, before the commit rather than a follow-up to it. Amend when one slips through.

### GObject conventions

- **No `const` on instance pointers.** Methods take `GPasteFoo *self`, never `const GPasteFoo *`, the same way GLib and GTK do — a getter is free to populate a cache, take a lock or notify. The `G_PASTE_CONST_*` shims that used to launder a `gconstpointer` through the type macros are gone; use the stock `G_PASTE_IS_FOO`, `G_PASTE_FOO`, `G_PASTE_FOO_GET_CLASS` and `g_paste_foo_get_instance_private`. `const` still belongs on plain structs and enums (`GPasteDaemonMethods`, `GPasteClipboardContent`, `GPasteStorage`, the `*Private` and `*Class` structs) where it is ordinary C const-correctness. A `GCompareFunc` or similar that hands you a `gconstpointer` casts it at the call site, as GLib's own comparison functions do.
- **State is a property, an occurrence is a signal.** Something the object *is* (`GPasteScreensaverClient:active`, `GPasteUiHistory:selection-mode`) is a property with `notify::`; something that *happens* (`name-lost`, `reexecute-self`, `keybinding-activated`, `GPasteHistory::update`) stays a signal. Install properties with the `g_object_class_install_properties` array form and `G_PARAM_EXPLICIT_NOTIFY`, so the setter decides whether anything actually moved. Do not add a property that merely restates a signal already carrying the same value in-band.
- **Never emit under `G_PASTE_LOCK_HISTORY`.** Handlers call back into `GPasteHistory`, and the lock is not recursive — this is why the `selected` emission is deferred until the lock is released. The same applies to `g_object_notify*`.

### D-Bus

Every interface GPaste speaks is described by an XML file that `gdbus-codegen` turns into both halves of the wire. No method hand-marshals a `GVariant` any more. One place still does: `g_paste_client_g_signal()` unpacks the incoming signals by hand, because `GPasteClient` implements the interface rather than deriving from the generated proxy (below) and so never gets the proxy's own demarshalling.

- **The XML is the source of truth.** `data/dbus/org.gnome.GPaste3.xml` is the daemon's own interface; it is installed to `$datadir/dbus-1/interfaces` (path from `dbus-1.pc`'s `interfaces_dir`, overridable with `-Ddbus-interfaces-dir`) so anything can read the contract without building GPaste. `src/libgpaste/gpaste-daemon/org.gnome.Shell.SearchProvider2.xml` is gnome-shell's interface, kept only to feed codegen and deliberately **not** installed — gnome-shell ships the authoritative copy.
- **The generated code is internal.** It is produced by `gnome.gdbus_codegen()` from `src/libgpaste/gpaste-3/meson.build` and `src/libgpaste/gpaste-daemon/meson.build`, which exist only so the output lands in those subdirectories and the `<gpaste-3/…>` include style keeps working. It is never installed and never introspected: `GPasteClient` is the whole public story and is hand-written. Confirm with `grep -c Daemon3 build/src/libgpaste/GPaste-3.gir` — it must stay `0`.
- **`org.gnome.GPaste3` is generated into libgpaste**, the lowest library, because a GType may only be registered once per process and gnome-shell loads libgpaste and libgpaste-daemon together. libgpaste-daemon uses the skeleton half, so the generated declarations carry `G_PASTE_VISIBLE` via `--symbol-decorator`; without it, hidden visibility would leave them unlinkable.
- **A `declare_dependency()` passes the generated *header*, never the whole codegen output.** `gnome.gdbus_codegen()` returns `[source, header]`, and `sources:` on a dependency is inherited by every target that uses it — handing over the list would compile `gpaste-daemon3.c` into each of them, which is precisely the one-registration-per-process rule above being broken, once per dependent. Only the library named in `link_with:` builds the source. Same for `gpaste-shell-search-provider2.c` in `gpaste_daemon_headers_dep`. Check with `nm -D --defined-only build/src/libgpaste/libgpaste-gtk4.so.* | grep -c g_paste_daemon3_` — it must stay `0`, and `149` for `libgpaste-3.so.0.0.0` alone (name the file, not the glob: `.so.*` matches the symlink too and doubles the count).
- **Naming.** The `org.gtk.GDBus.C.Name` annotation maps the interface to `PasteDaemon3`, so the generated symbols are `g_paste_daemon3_*` and never collide with the `g_paste_daemon_*` of the `GPasteDaemon` object that implements it. The `3` is the interface version, bumped for the 51.0 break; the bus name and object path are not versioned and did not move.
- **`GPasteClient` implements `GPasteDaemon3` rather than deriving from the generated proxy.** Deriving would drag the generated class struct into the installed `gpaste-client.h`; implementing the interface is what makes `g_paste_daemon3_call_*()` usable while the public header, the GIR and the VAPI stay exactly as they were. It therefore overrides the interface's `Active`, `History` and `Version` properties (`g_paste_daemon3_override_properties`) and keeps its own `g_signal`/`g_properties_changed` handlers, which turn the wire signals into GPaste's richer ones.
- **An item travels as one struct, `(ssub)`**: uuid, value, kind, favourite. The kind rides along because every client drawing a row wanted it and had to ask separately — `GetElementKind` is gone. `GetItem`, `GetItemAtIndex`, `GetItems`, `GetHistory`, `GetFavourites` and `Search` all answer it, `Search` included so a search view needs no follow-up call. **`GetFavourites` is a listing of its own** rather than a flag on `GetHistory`: the pinned items are typically a handful out of a history that is not, and the flag lives daemon-side, so a favourites view that pulled the whole history across the bus to keep three rows would be paying for the filter twice. A client wanting the pinned *matches of a search* still sifts those itself, the matches being the narrow set already. The type is spelled once in C, as `G_PASTE_ITEM_VARIANT_STRING` in `gpaste-3/gpaste-client-item.h`, next to the `GPasteClientItem` it builds; the daemon's builder and the client's parser both go through it. `gpaste-3/gpaste-client-history.h` is the same arrangement one size down, for the `(st)` a history travels as and the `GPasteClientHistory` `ListHistories` hands back. **One value per item**, the one a user is shown: the `GetRaw*` twins that answered `g_paste_item_get_value()` instead are gone, since for plain text — nearly all of a history — the two strings are equal, and neither was ever a password's real value. That is what leaves `g_paste_daemon_methods_item_variant()` with nothing to choose between; `g_paste_item_get_value()` itself stays, for the in-daemon callers (the image loader, `GetImage`, `Merge`, the growing-line test, the search regex, both storage backends) that never went near the wire.
- **The current history is what every item call acts on.** Reading items (`GetHistory`, `GetItem`, `GetItemAtIndex`, `GetItems`, `GetFavourites`, `GetImage`, `GetUris`, `GetPasswordTimeout`, `Search`, `GetHistorySize`) and changing them (`Add*`, `Merge`, `Replace`, `MakePassword`, `Select`, `SetFavourite`, `DeleteItem`, `DeletePassword`) never name a history; the calls that do — `ListHistories`, `BackupHistory`, `EmptyHistory`, `DeleteHistory`, `SwitchHistory` — are the ones treating histories as objects rather than as a place to put items. Inspecting another history means switching to it. `GetHistorySize` used to be the one exception, and paid for it: sizing a history that is not the current one meant building a `GPasteHistory` and loading it *synchronously*, and `GPasteUiPanel` did that once per row. **`ListHistories` answers `a(st)` instead** — name and size together, in the store's own order — so a sidebar of them costs one call. The size comes from `g_paste_storage_backend_count_history()`, which every backend overrides so that no listing has to materialize a history: `GPasteSqliteBackend` answers a `COUNT (*)` over the very predicate `read_history_file` selects on — cap, favourites and, when images are turned off, the image rows a read would drop — on a short-lived read-only connection of its own, since counting needs no content and so no key, and borrowing the cached one would evict the current history's connection and its derived key once per history listed. `GPasteFileBackend` has no cheaper question to ask than parsing, so it walks the document through the same parser, version gate and cap as a read, in a counting mode that builds no item and touches no image side file. The default — read and count — is what a backend with neither gets. Only a read creates the placeholder file for a history that is not there; counting must not bring one into existence.
- **Every mutation answers what it did.** The `Add*` calls, `Merge`, `Replace` and `MakePassword` all return the uuid of the item that landed. Policy can refuse an add outright — `min-text-item-size`, `max-text-item-size`, `trim-items`, an item over `max-memory-usage` — and that is **`.Error.Rejected`** (`G_PASTE_ERROR_REJECTED`), which says the request was well formed and nothing went wrong: the daemon simply kept nothing. Without it a caller could not tell an add that worked from one that quietly did not, short of racing the asynchronous `Update`. Content the current selection already says is *not* a refusal: the history keeps what it has and the add answers the uuid it already holds it under, so the same value added twice in a row succeeds twice. `Replace` and `MakePassword` matter most: both mint a *new* uuid and report `target == ALL`, so before this the new item's uuid existed nowhere a caller could reach. **`MakePassword` is the exception to its own rule**, being the one method that takes an item of the kind it produces: handed a password rather than a text item it updates that one in place -- its name and its `timeout`, never its value -- so it answers the uuid it was given and reports `ITEM`, since nothing was replaced and a view listing the item can repaint one row. The name is part of what the item weighs, so that update moves `GPasteHistory`'s own memory total with it — the bookkeeping `_g_paste_history_replace()` does around the object it swaps in, for an item that stays where it is; left out, the difference is subtracted a second time when the item eventually goes, and that total is unsigned. A total that moved is a cap that may have given way, so the rename re-runs `private_check_memory_usage()` itself — only that one, the item count being unchanged — and, exactly as in `set_favourite()`, persists a full rewrite when evictions rode along, since the renamed item may be among them. Left to whatever is added next, the eviction would be blamed on that instead. That is also what retired `RenamePassword`, which did the strictly smaller half of it and had already drifted where it mattered: it checked no collision at all, so it could put two passwords under one name, after which `GetPassword` answered whichever it reached first and `DeletePassword` destroyed an arbitrary one. Renaming now needs the password's uuid, which a listing carries. The collision check exempts the item itself, and exempts `G_PASTE_PASSWORD_ITEM_NO_NAME` (`"******"`) outright: that is what a password with no name of its own reads as rather than a name anybody picked, so it is the one name a history may hold several entries under — the make-password keyboard shortcut, which has no name to hand over, is what makes them. That shortcut acts on a **text item alone**: a password is one already, and a shortcut carries neither a name nor a timeout, so there is nothing about one it could change. **The `timeout` both it and `AddPassword` take is in seconds**, `0` for none: it is how long the password may stay on the clipboard once it is the active item, after which the daemon takes it off every selection still carrying it (`GPasteClipboardsManager`, below). The wire type is `u` and the item holds a `guint`, but the range is `G_PASTE_PASSWORD_TIMEOUT_MAX` — 65535, the `password-timeout` key's own `<range>`, which the preferences row and the Make/Edit Password composer both offer and `g_paste_password_item_set_timeout()` caps everything down to, both storage backends reading one back included. Past it a countdown outlives every session it could run in, which reads as expiring and never does. `gpaste-client --timeout` **refuses** a value above it rather than let the item cap it: a number silently becoming a smaller one is a password expiring at a time nobody asked for. **`MakePassword` also takes `G_PASTE_PASSWORD_TIMEOUT_UNCHANGED`** (`G_MAXUINT`, above the range so it collides with no real timeout), which writes no timeout at all: a password keeps the one it carries — and the countdown running for it — while a text item becoming one takes the `password-timeout` default, `g_paste_history_make_password()` resolving the sentinel in the one place. That is what a caller changing only the *name* sends, and it is why the sentinel exists at all: `0` is a timeout of its own, so a client with no timeout to write had to read the current one back with `GetPasswordTimeout` and echo it, and any that forgot — `gpaste-client make-password` did — cancelled a running countdown by sending the setting's default over it. `NULL` and the sentinel are the same story for the two writable fields, rather than one having it and the other not. **Updating a password publishes nothing**: what a password is put on a selection as is `g_paste_item_get_real_value()`, which neither branch of `MakePassword` touches, and the item at the front is not necessarily what a selection holds — selecting it would be writing a password's cleartext over whatever the user has copied since. The new timeout still has to reach the running countdowns, and that is `g_paste_clipboards_manager_rearm_password()`, which the D-Bus method and the make-password shortcut both call: it re-arms each selection that currently carries the item, and leaves the countdown of one that has moved on alone — that selection is watching an exposure of its own. A selection whose *duration* did not actually move keeps the countdown it is running: `arm_password()` compares the item **and** the seconds it was armed for, so a rename is no reason to hand the password more time on the clipboard. A `NULL` `@name` leaves a password's own name alone rather than writing the placeholder over it — only a text item becoming a password takes `G_PASTE_PASSWORD_ITEM_NO_NAME` from one. **`GetPasswordTimeout` reads it back**, and is a method rather than a sixth field on `(ssub)` for the reason `GetImage` and `GetUris` are ones: nothing drawing a row wants it, only the client putting up a dialog to change it, so as a field it would be a number every item of every kind carried for the password's sake.
- **`Upload` waits for the upload.** It answers the url, and an upload that failed fails the call — not a reply the moment `wgetpaste` has been spawned, with the failure going to a `g_warning` and the url into the history behind the caller's back. Adding it to the history is the *keyboard shortcut's* behaviour, which has nobody to answer to, and lives in `on_keybinding_upload_done()`; the method adds nothing. An empty `uuid` means the current item, since that is what the shortcut uploads and a D-Bus string is never absent. Because the reply waits on a network round trip, it is the one call `GPasteClient` gives a timeout of its own (`G_PASTE_CLIENT_TIMEOUT_UPLOAD`) instead of the `GDBusProxy` default of 25 seconds — otherwise a large paste or a slow service fails on the caller's side while succeeding on the daemon's.
- **`ShowAbout` is not the daemon's business.** `ShowHistory` is a signal, because only the extension can act on it; an about dialog is the UI app's own `about` action, which any client reaches with `g_paste_util_activate_ui ("about", NULL)`. The daemon's method did no more than make that same call, so `gpaste-client about` makes it itself and the method is gone — which is also what lets `gpaste-client about` work with no daemon running: the window registers `about` and `preferences` (`add_daemonless_window_actions()`) whichever way the connection concluded, and holds neither back when it failed. The extension offers no About at all: a panel menu is not where an application's about dialog belongs.
- **Errors mean the same thing everywhere.** `NOT_FOUND` for any uuid or name that matches nothing, `INVALID_INDEX` only for `GetItemAtIndex` (its sole user), `INVALID_ARGUMENT` for an argument missing, empty, or not something the method can act on — a history name that is a path is the second kind, refused by `g_paste_util_history_name_is_valid()` because the name goes straight into the file the history is stored in and into the images directory it owns, `ALREADY_EXISTS` for a name already taken — `MakePassword`'s, and `BackupHistory`'s, which refuses rather than overwrite an existing history, since a backup silently destroying an older one of the same name is a loss noticed far too late. `DeletePassword` reports `NOT_FOUND` rather than succeeding at nothing, and `EmptyHistory` can fail at all. `MakePassword`'s `ALREADY_EXISTS` is about *another* password: a password keeping the name it already carries has collided with nothing, which is the ordinary case now that changing only a timeout goes through the same method.
- **`Update` names the item it is about.** `Update(u action, u target, s uuid, t index)`: a position means nothing to a view listing something other than positions, which both a search and the favourites filter are, so the uuid is what lets them apply a change instead of re-running their query. The favourites view is the one that cannot take that shortcut: whether a row still belongs in a list of pinned items is exactly what a pin/un-pin changes, and the update names the item rather than what became of its flag, so it re-queries (`GetFavourites`, which is cheap for it). `target == ALL` carries neither. A replace that mints a *new* uuid (editing a text item, turning one into a password, a growing line standing where the shorter one it grew from stood) reports `ALL` rather than `ITEM`, because one uuid cannot say both which item went and which arrived.
- **The current history is a property, not a signal.** `History` replaces `GetHistoryName()` and the `SwitchHistory` *signal* (the method stays), so `GPasteClient` reads it off the proxy's cache with no round trip and clients follow `notify::history`. What a switch cannot express is a history appearing *beside* the current one, which is what a backup does — hence **`HistoriesChanged`**, a signal of its own. `BackupHistory` used to emit `SwitchHistory` three times running to force GUIs to re-list; that hack is what the new signal replaces. **It fires for every change to the set**, not just a backup: a `DeleteHistory`, an `EmptyHistory` naming a history there was none of, and a `SwitchHistory` that had to create its target. `HistoryDeleted`/`HistoryEmptied` still carry the name beside it, so a sidebar can update one row instead of re-listing; a client that only lists needs `HistoriesChanged` alone.
- **`ReportExtensionState`, not `SetExtensionState`.** The extension reports a fact about itself; whether that should stop the tracking is the daemon's `track-extension-state` policy, and a client does not second-guess it.
- **The interface's signals carry `Raw*` C names.** That is not decoration: without it the generated `update`, `history-deleted`, … would collide with the identically named signals `GPasteClient` declares itself. The daemon emits them with `g_paste_daemon3_emit_raw_*()`.
- **No signal shares a name with a method.** `HistoryEmptied` and `HistoryDeleted` are named for what happened, not for the `EmptyHistory`/`DeleteHistory` that did it. D-Bus allows the collision, but most language bindings map an interface's methods and signals onto members of one class, where the second of a pair has nowhere to go.
- **The numbers on the wire are documented in the XML itself.** `(ssub)`'s kind and `Update`'s action/target travel as bare `GPasteItemKind`/`GPasteUpdateAction`/`GPasteUpdateTarget` values, which is nothing a client outside this tree can read off the signature. The mappings are carried in annotations — `org.gnome.GPaste3.ItemKind` on the interface, `org.gnome.GPaste3.UpdateAction`/`.UpdateTarget` on `Update`'s two arguments — rather than only in the XML comments, because an annotation comes back from `Introspect()` over the bus where a comment does not. **Under a name of GPaste's own, never `org.gtk.GDBus.DocString`**: gdbus-codegen treats everything in its own namespace as an instruction to itself and emits none of it into the `GDBusInterfaceInfo` the skeleton answers `Introspect()` from, which is the one place these have to reach. Check with `grep -c org.gnome.GPaste3.ItemKind build/src/libgpaste/gpaste-3/gpaste-daemon3.c`.
- **`Merge` has to be daemon-side.** What it joins is each item's *real* value, and the only string an item travels with here is the one a user is shown, so a client holding a listing cannot do it itself. That is the reason the method exists at all now that the `GetRaw*` twins are gone.
- **`ay` needs `org.gtk.GDBus.C.ForceGVariant`.** The default mapping for a byte array is a C bytestring, which stops at the first NUL — that would truncate every PNG `GetImage` returns. The annotation is load-bearing; do not drop it.
- **Handler signatures are not checked by the compiler.** `g_signal_connect_swapped (skeleton, "handle-x", G_CALLBACK (…), self)` erases the prototype, so a handler whose arguments do not match the generated `handle_x` vfunc is undefined behaviour at runtime, not a build error. When adding or changing a method, compare the handler against `struct _GPasteDaemon3Iface` in the generated header by hand. The `G_PASTE_DAEMON_HANDLER*` macros in `gpaste-daemon.c` are what keep that to four places instead of thirty-one.
- **The three flavours of a client method are generated, and so are the handlers.** A method reaches the daemon as `g_paste_client_x_sync()`, `g_paste_client_x()` and `g_paste_client_x_finish()`, and comes back through one handler; both ends are written from macros — `G_PASTE_CLIENT_METHOD*` in `gpaste-client.c`, `G_PASTE_DAEMON_HANDLER*` in `gpaste-daemon.c` — with the signature and the forwarded arguments passed as parenthesized lists and `ARGLIST` supplying the comma that vanishes for a method with no arguments of its own. **The gtk-doc block goes above the invocation**: g-ir-scanner matches a block to a symbol by the name on its first line, not by what follows it, which is the same trick `gpaste-settings.c` uses for its accessors. Add a method by adding an invocation; only reach for a hand-written body when there is genuinely one (a relative path to resolve, two out parameters, a precondition of its own).

### Documenting errors

GIR cannot express *which* domain a function throws: the format only has a boolean `throws="1"` per callable, plus `glib:error-domain` on the enumeration (which `GPasteError` carries). A `@error:` parameter line does not survive into the GIR at all — introspection drops the throws parameter — so anything a binding consumer needs to know has to be in the function's *description*, not on its `@error:` line.

- The `@error:` line is always exactly `@error: return location for a #GError, or %NULL`. It carries no annotations: `(nullable)` on a throws parameter is discarded along with the parameter.
- State the domain in the description of any function that throws, naming the concrete domains (`%G_PASTE_ERROR`, `%G_DBUS_ERROR`, `%G_IO_ERROR`, …). Where a whole class shares one error story, the class doc carries it instead of repeating it on every method — `GPasteClient` does this for its D-Bus methods, and only its constructors, which never reach the daemon, restate theirs.
- Say so explicitly when a failure is *not* an error: `g_paste_prompt_passphrase_finish()` and `g_paste_prompt_migration_finish()` report a dismissal through the return value with `@error` left unset.
- The name in a doc block's first line must match the symbol exactly. A typo silently detaches the whole block — the function then reaches the GIR with no documentation at all, with nothing to warn you.

### JavaScript (GNOME Shell extension)

The `src/gnome-shell/` extension follows upstream GNOME Shell's JS conventions, enforced by the **same tooling, layout, and configuration** as upstream:

- The npm project lives in `tools/` (`tools/package.json`, `tools/package-lock.json`, `tools/eslint.config.js`), mirroring gnome-shell. The repo-root `eslint.config.js` is a **symlink** to `tools/eslint.config.js`.
- **ESLint** with [`eslint-config-gnome`](https://gitlab.gnome.org/World/javascript/eslint-config-gnome) (`recommended` + `jsdoc` configs) and the [`ci-run-eslint`](https://gitlab.gnome.org/World/javascript/ci-run-eslint) runner, both pinned to the same commits upstream uses. The config mirrors upstream's custom rule overrides (`camelcase`, `consistent-return`, `eqeqeq: smart`, `key-spacing`, `prefer-arrow-callback`, `prefer-const`, jsdoc tweaks). Shell-extension globals (`global`, `_`, `C_`, `N_`, `ngettext`) are declared for `src/gnome-shell/**`.
- Style basics live in `src/gnome-shell/.editorconfig` (LF, UTF-8, trim trailing whitespace, 4-space indent for `*.js`).
- Run it with `tools/run-eslint.sh` — exactly the upstream wrapper. It `npm clean-install`s into `tools/` on first run, symlinks `node_modules` into the repo root for import resolution, then lints `src/gnome-shell`. Pass `--fix` to auto-fix formatting.
- The same script is the single entry point everywhere: the meson `eslint` test (`meson test -C build eslint`, skipped when `npm` is absent) and the GitHub Actions workflow (`.github/workflows/eslint.yml`, runs on pushes/PRs touching the JS or tooling) both invoke it. Upstream runs lint from GitLab CI; GPaste runs it from GitHub Actions, but the toolchain, config, layout, and `run-eslint.sh` are otherwise identical.
- This tooling applies **only** to the JavaScript code; it does not affect the C/meson sources.

Code conventions (also following upstream):
- **Don't version-pin core `gi://` imports** — write `gi://GObject`, `gi://GLib`, `gi://Gio`, `gi://Pango`, `gi://Clutter`, `gi://St`. Only pin typelibs that genuinely ship multiple versions: `gi://GPaste?version=3`, `gi://GPasteDaemon?version=1`, `gi://GPasteGtk?version=4`.
- **Manage signal lifecycles with `connectObject`/`disconnectObject`** (owner = `this`) for connections to long-lived non-actor GObjects (settings, the `GPaste.Client`, the daemon runner's bus and daemon), rather than tracking handler ids and disconnecting them by hand. They auto-disconnect when the owner actor is destroyed; an owner that is *not* an actor — `GPasteDaemonRunner`, a plain class — gets no such teardown, so it must still call `disconnectObject(this)` itself (`daemon.js` does, from `_teardownBus()` and `_releaseDaemon()`).
- **Use a standard `constructor()` (calling `super(...)`) in `GObject.registerClass` classes**, not `_init()`/`super._init()`. GJS bridges to the `_init()`-based shell/St/Clutter base classes transparently (positional args like `super(0.0, 'GPaste')` and property dicts like `super({...})` both work).

Async / `Gio._promisify` conventions (these bit us — keep them):
- **`Gio._promisify` works on instance methods (`SomeClass.prototype`) but NOT on anything static** — neither a constructor (`SomeClass.new`) nor a namespace-level function (`SomeNamespace.foo_async`) — **inside gnome-shell.** The wrapper assignment silently fails to stick on the class object (it works in standalone `gjs`, so it is easy to miss), and the *raw* introspected `new` then throws *"At least 1 argument required, but only 0 passed"* when awaited with no args. Even where the assignment does stick, the wrapper's body reads `this[originalFuncName]`, which is a `TypeError` when the wrapper is called unbound — which is what calling it off a plain value in a strict-mode ES module does. Promise-wrap the raw `_async` + `_finish` pair by hand instead — see `_connect` in `indicator.js` and `callStorageConcern` in `daemon.js`. `dependencies.js` promisifies only the instance methods.
- **Don't pass a trailing `null` to a promisified async method that has no `cancellable`.** GPaste.Client's async methods are `(…args, callback, user_data)` with no cancellable; a trailing `null` lands in the callback slot and defeats `Gio._promisify` (the call hangs or returns `undefined`). Call them with their real args only: `await client.get_history_size(name)`, not `(name, null)`.
- **`GLib.idle_add_once` / `GLib.timeout_add_once` take `(priority, func)`**, not `(func)`. Passing only the callback makes GJS treat the callback as the priority and dispatch with no function → *"callback is not a function"*. Always pass an explicit priority (e.g. `GLib.PRIORITY_DEFAULT_IDLE`) and keep the `GLib.Source.set_name_by_id(...)` wrapper.

## Memory management

Always use GLib automatic memory management. Apply to every C file touched, not only the file under edit.

| Macro | Use when |
|---|---|
| `g_autofree` | Plain heap allocation: `g_strdup`, `g_malloc`, `g_new` for non-GObject structs |
| `g_autoptr(Type)` | GObject-derived or boxed types with a registered cleanup (`GError`, `GFile`, `GString`, `GMenu`, `GSimpleAction`, `GSimpleActionGroup`, `GSList`, `GList`, …) |
| `g_auto(Type)` | Stack-allocated types with a cleanup function: `GStrv`, `GVariantBuilder`, … |
| `g_autolist(Type)` | `GList *` of **owned** GObject elements — cleanup calls `g_list_free_full(list, g_object_unref)` |
| `g_autoslist(Type)` | `GSList *` of **owned** GObject elements — cleanup calls `g_slist_free_full(list, g_object_unref)` |
| `g_autoptr(GList)` | `GList *` that does **not** own its elements — cleanup calls `g_list_free` only |
| `g_autoptr(GSList)` | `GSList *` that does **not** own its elements — cleanup calls `g_slist_free` only |

Key rules:
- Replace `g_free(old); ptr = g_strdup(new)` with `g_set_str(&ptr, new)` — but **only when `new` is borrowed**. `g_set_str` does an internal `g_strdup`, so if `new` is a freshly-allocated string you already own (e.g. from `g_strdup_printf`, `g_strconcat`, `g_settings_get_string`), that dup is wasted.
- When the new value is already owned, use `g_set_str_take(&ptr, new)` (GLib 2.90), which frees the old value and consumes the new one — never `g_free(ptr); ptr = new`, which silently leaks if the assignment is ever reached without the free. Same split for string arrays: `g_set_strv` when borrowed, `g_set_strv_take` when owned.
- A setter whose callers always pass a freshly-built string should take ownership of it (`gchar *` arg, annotated `(transfer full)`) and store it with `g_set_str_take` rather than dup it — this is how `g_paste_item_set_display_string` and `g_paste_clipboard_content_set_text_take` work, and callers hand off with `g_steal_pointer(&local)`. Where both shapes have real callers, provide the copying setter and a `_take` variant beside it.
- Use `g_steal_pointer(&ptr)` when transferring ownership out of an auto-managed variable — including when passing a `g_autofree`/`g_autoptr` local into a container that takes ownership (e.g. `g_ptr_array_add (arr, g_steal_pointer (&local))`).
- In `dispose`: use `g_clear_object`, `g_clear_pointer`, `g_clear_list`, `g_clear_slist` — they null the pointer, making double-dispose safe. Release all reference-counted objects here (GObject, GBytes, GPtrArray, GHashTable, …), not in `finalize`.
- In `finalize`: use `g_free` for plain heap allocations (`gchar *`, plain structs). Do **not** call `g_object_unref` or other ref-counting unrefs here — those belong in `dispose`.
- When a type needs to release GObject refs but currently only has `finalize`, add a `dispose` function and register it with `object_class->dispose`.
- A value owned for the length of one loop iteration is declared **inside** the loop, where its cleanup macro frees it at the end of that iteration. Declared around the loop and freed by hand at the bottom of the body (or with `g_clear_pointer`, which only nulls what the next iteration overwrites anyway), the cleanup it declared never fires and the free is back to being one an early `break` can forget.
- A plain allocation belongs in `dispose` too, when a disposed object has to stop *answering* for it: `GPasteClipboardMetaSource` drops its mimetype list there — nothing in that list needs an unref, but it is what the tables released beside it serve reads from, and a disposed source still naming its formats would be offering reads it can only refuse. The bullets above say where each kind of thing goes by default, not where it must go regardless; say which, and why, in a comment.
- The mirror of that: an owned list belongs in `finalize` when something can hold a ref across `dispose` and still read through it. `GPasteClipboardsManager` frees its `_Clipboard` records there because a clipboard update in flight refs the manager precisely so the record its reply reads is still standing.
- A plain struct with a free function of its own gets `G_DEFINE_AUTOPTR_CLEANUP_FUNC (Type, type_free)` beside that function, which is what makes `g_autoptr(Type)` and `g_autoslist(Type)` work for something that is not a GObject (`DeferredAction` in `gpaste-ui-window.c`). A list of such structs freed by hand is the same omission as a list of GObjects freed by hand.
- Do **not** use `g_autolist`/`g_autoslist` on lists that do not own their elements. `gdk_file_list_get_files` is **`(transfer container)`** — it returns a fresh `g_slist_copy()` whose `GFile`s stay owned by the `GdkFileList`, so its result must be freed with `g_autoptr(GSList)` (never leaked, never `g_autoslist`'d). `g_hash_table_get_values` is the same shape — use `g_autoptr(GList)`.
- Do **not** use auto-cleanup on a variable whose ownership is intentionally transferred — use `g_steal_pointer` to make the handoff explicit.
- Do **not** use `if (ptr) g_slist_free_full (g_steal_pointer (&ptr), fn)` in dispose — use `g_clear_slist (&ptr, fn)` instead (handles NULL safely).
- When storing a `g_settings_get_string` value (owned, `(transfer full)`) into a `gchar *` field, free the old value and take ownership directly: `g_free(field); field = g_settings_get_string(...)`. Do **not** assign without freeing first (leak on re-assignment), and do **not** route it through `g_set_str` (that would re-`g_strdup` an already-owned string — a wasted allocation).
- For a stored `GSource`/timeout/idle/signal handler id, prefer `g_clear_handle_id (&id, remove_func)` over the hand-rolled `if (id) { remove_func (id); id = 0; }` — it is NULL-safe and nulls the field in one call (e.g. `g_clear_handle_id (&priv->retry_source, g_source_remove)`). Use this in new code and when touching such cleanup.
- Build NULL-terminated string vectors with `GStrvBuilder` (`g_strv_builder_add` for borrowed strings, `g_strv_builder_take` for owned ones, `g_strv_builder_end` to get the `GStrv`) rather than hand-managing a `GPtrArray` + manual `NULL` terminator + `(GStrv) ->pdata` cast. It also avoids the deep `g_strdupv` copy that returning a `GPtrArray`'s contents requires.
- Prefer `g_signal_connect_object (source, sig, cb, gobject, 0)` over plain `g_signal_connect` when the handler's data is a **GObject** shorter-lived than the signal source and you would otherwise not disconnect — it auto-disconnects when that object is finalized. Do **not** convert sites that pass non-GObject data (a `priv`/struct pointer), pass `NULL` data, or that deliberately disconnect early in `dispose` for ordering reasons (where `g_signal_connect_object` would only disconnect at finalize and regress that ordering).

## Single sources of truth

Four lists in this tree used to exist in several copies and drift apart. Each is
now written down once; add to the list, not to its consumers.

| List | Lives in | Drives |
|---|---|---|
| The keyboard shortcuts | `G_PASTE_FOR_EACH_KEYBINDING` in `gpaste-3/gpaste-keybindings.h` | the shortcuts dialog, the preferences' shortcuts page, `data/control-center/42-gpaste.xml` (generated at build time by `tools/gen-keybindings-xml.py`), and the daemon's own bindings. Each carries two names: the sentence the shortcut UIs show and the few words the desktop portal lists it under |
| The command line's verbs | `commands[]` in `src/client/gpaste-client.c` | dispatch, and `show_help()`, printed straight from it |
| A `GPasteItem`'s kind | `GPasteItemKind` in `gpaste-3/gpaste-item-enums.h` | the item classes and both storage backends, through `g_paste_item_kind_to_string()` / `_from_string()`. The wire carries the enum *value*, not its nick, so the nicks are the on-disk format's business alone |
| The prompts' wording | `GPastePromptText` in `gpaste-daemon/gpaste-prompt.h` | both passphrase/migration dialogs -- the Adwaita one out of process and the St one in the shell |
| The D-Bus methods | `data/dbus/org.gnome.GPaste3.xml` | both halves of the wire, via gdbus-codegen |

**The window's own shortcuts are not in that table.** `G_PASTE_FOR_EACH_KEYBINDING` is the single source for the *global* keybindings because three consumers have to agree on them. The graphical tool's own — search, new item, paste-by-index, preferences, close — are configurable by nobody, work only while the window has the focus, and have no business in the desktop portal's list, so they live in a small table in `src/ui/gpaste-ui-shortcuts-window.c`, beside the window that answers them. Do not merge the two.

**The prompts are built twice, but say one thing.** St and Adwaita share no
widget vocabulary, so `src/gnome-shell/prompt.js` and `src/daemon/gpaste-prompt-adw.c`
each build their own dialog -- but every *decision* (`can_import`,
`backend_changes`, `passphrase_is_complete`, `remember_state`,
`passphrase_strength`) and now every *word* (`g_paste_prompt_text()`,
`g_paste_prompt_storage_label()`) comes from the C side. Add a string there, not
in a dialog.

**A history is identified by its name.** Every `GPasteStorageBackendClass` vfunc
takes one; a backend derives its own path with
`g_paste_storage_backend_get_history_file_path()` rather than being handed one,
so nothing has to reverse the mapping to get the name back.

The shell completions and the man page are the exception: each verb completes
its arguments differently and the man page says more than a table could carry,
so they stay hand-written and `tests/completions` checks them against
`commands[]` instead — every verb and alias has to be offered by all three
shells and every canonical verb documented in the man page.

An enumeration exposed to a binding needs a GType: `GPasteItemKind`,
`GPasteStorage` and `GPasteUpdateAction` each register one, which is what lets a
setting like `storage-backend` be a `g_param_spec_enum` rather than a widened
integer.

## Maintenance rules

- A public library function is exported by marking its definition `G_PASTE_VISIBLE` (the libraries build with `gnu_symbol_visibility: 'hidden'`, so anything unmarked stays internal). There are no `.sym` version scripts to maintain; the `G_PASTE_*_TYPE` macros already mark their generated `get_type`. Conditionally-compiled (feature-gated) symbols just need the marker — they export only in configurations where they are compiled.
- **`G_PASTE_VISIBLE` belongs to the libraries only.** `src/ui/`, `src/client/`, `src/daemon/` and `src/preferences/` build executables, which export nothing; their headers are reached through their own directory and carry no umbrella guard either.
- When updating source files in this repository, keep `AGENTS.md` up to date to reflect any new patterns, rules, or architectural decisions introduced.

## Architecture

GPaste is a GNOME clipboard manager split across several binaries and a shared library.

### `src/libgpaste/` — shared libraries

The core libraries used by all other components. Two sub-modules:

- `gpaste-3/` → **libgpaste** — daemon-agnostic types: `GPasteClient` (D-Bus client), `GPasteClientItem` (the lightweight item representation transferred over D-Bus), `GPasteSettings` (GSettings wrapper), enums, utilities.
- `gpaste-gtk4/` → **libgpaste-gtk4** — GTK4 + Adwaita helpers (the preferences widgets).

**All three libraries split their headers into an installed half and an internal one**, and the internal half is what a new header joins unless something outside the library names it. libgpaste-gtk4 installs exactly two types — the preferences dialog and the preferences widget `prefs.js` embeds — while the groups, the shortcut row and the four pages it is built from stay in; its GIR is generated from `libgpaste_gtk4_public_sources` alone, so an internal symbol cannot leak into it. The four pages are plain functions returning an `AdwPreferencesPage`, not types: they carry no state, and both callers build them from one list (`g_paste_gtk_preferences_pages_new()`) so they cannot drift apart.

**The core library's directory carries `apiversion`, so it is renamed on every major bump** (`gpaste-2/` → `gpaste-3/` for this release), and its includes are spelled `<gpaste-3/gpaste-macros.h>`. That is not decoration: the installed headers live at `$prefix/include/gpaste/gpaste-3/` with `-I …/include/gpaste`, and the *same* include text has to resolve both in-tree and against the install prefix, since these are the same files. In-tree it resolves through `include_directories('.')` = `src/libgpaste`. Keeping the two spellings identical is what makes a broken installed header a build failure here rather than a downstream bug report. `gpaste-gtk4/` and `gpaste-daemon/` are named for their library rather than for a version, so they never move.

The installed layout is one shared directory, with each library owning a subdirectory of it:

```
include/gpaste/{gpaste.h, gpaste-gtk4.h, gpaste-daemon.h}   <- the three umbrellas
include/gpaste/{gpaste-3/, gpaste-gtk4/, gpaste-daemon/}    <- per-library headers
```

All three `.pc` files therefore declare `Cflags: -I${includedir}/gpaste` (meson `subdirs: 'gpaste'`) — that line is what puts the include path on a consumer's command line, so it is the one to check when the layout changes. `libgpaste` is versioned all the way through (`libgpaste-3.so`, `gpaste-3.pc`, `GPaste-3` typelib, `gpaste-3.vapi`, all derived from `apiversion`); `libgpaste-gtk4`'s `4` is GTK's version and `GPasteDaemon-1`'s is its own, so neither tracks the GPaste major.

**Each library ships a `.pc`**: `gpaste-3`, `gpaste-gtk4` and `gpaste-daemon`. What goes in `requires:` is the set a *consumer* must also satisfy — i.e. what the **installed** headers name, not what the library links. `gpaste-daemon` therefore requires only `gpaste-3`: no public daemon header names a GTK, GDK or GCR type, so those stay in the `Requires.private:` meson derives from the library's own `dependencies:`, alongside the optional ones (libsodium, sqlite3, libsecret-1, pwquality, libmutter). Use `requires:`, not `libraries:` — the latter means "put this in `Libs:`".

**Observing a setting: use `notify::<key>`.** Every setting is a GObject property named exactly like its GSettings key, so `notify::track-changes` *is* the change notification — there is no separate `changed` signal, and `g_object_bind_property()` works directly. `GPasteSettings` emits exactly one signal of its own, **`rebind::<key>`**, and it is not a duplicate: a rebind is an action to take (re-register a keybinding), not a value that changed, and only the keybinding settings carry it. Prefer the detailed form; `notify` undetailed fires for all twenty-nine keys. Note the callback takes a `GParamSpec *`, not the key string.

A third library, **libgpaste-daemon**, lives under `src/libgpaste/gpaste-daemon/` and holds the rich clipboard item hierarchy along with the rest of the daemon objects, plus the utility helpers nothing outside it calls: `gpaste-daemon-util.h` has the history-path helpers and the file backend's XML escaping, which is why `gpaste-3/gpaste-util.h` no longer declares them (nor includes `gpaste-client.h`). Each library exports the symbols marked `G_PASTE_VISIBLE` (built with hidden default visibility), and GIR + Vala bindings are generated from it.

**Favourites are pinned against both caps.** `g_paste_item_set_favourite()` marks an item and `g_paste_history_set_favourite()` is what a caller uses, because everything that follows from the flag is the history's business. `private_check_size()` walks the tail dropping only non-favourites, and `elect_new_biggest()` never elects one, so the memory cap's loop stops on its own once nothing is electable — a history with nothing else in it therefore grows past `max-history-size` rather than let one go. Both skip index 0 as well: that is the item just added or selected, and evicting it would mean a clipboard that keeps nothing at all once the rest is pinned. Un-pinning is not symmetric with pinning: pinning shrinks the electable set, so only a full re-election can name the runner-up (and only when the item was the elected one), while un-pinning grows it by one and needs a single comparison — then both caps re-run, and may well evict the very item that was just let go. When they do, the change is persisted as a full rewrite: a replace carries no snapshot for an incremental backend to reconcile against, unlike an add, which is the one operation that expects to displace anything. **The pin belongs to the entry, not to the object holding its contents.** Whenever one item takes another's place — `_g_paste_history_replace()` behind `Replace`/`MakePassword`, and the growing-line and dedup branches of `_g_paste_history_add()` — the flag is carried over: editing a pinned item, retyping it, or turning it into a password is not the user letting go of it, and silently un-pinning there would make what they asked to keep evictable again by nothing more than using it.

**The history model is a `GPtrArray` plus a uuid `GHashTable`**, newest first. The array owns a ref per item; the index borrows both halves from it (the key is the item's own uuid string, the value the item the array owns), so the two are always updated together — `g_paste_history_private_remove()` drops the index entry *before* stealing the array's ref, since the key belongs to the item. That is only sound because a uuid never changes once its item is in the history: `g_paste_item_set_uuid()` is called by the storage backends alone, while loading. Positions are *not* indexed — adding prepends, which shifts every position, so a uuid→position map would have to be rewritten on every add; `g_paste_history_private_get_indexed_by_uuid()` scans for it instead, comparing pointers. **`GList` remains the storage layer's interchange type**: `read_history_file`/`write_history_file`, the saver's snapshots and the migration code all still take one, because persisting is a one-shot bulk operation where a list costs nothing. `g_paste_history_snapshot()` and `g_paste_history_private_set_from_list()` are the two conversion points. Note the three ways items leave the history and how they differ: `private_remove(…, TRUE)` disposes of the item *and its backing file*, `private_clear()` swaps a history out with a plain unref (files kept, since the items are only being put away), and `dispose` destroys the containers themselves.

**libgpaste-daemon has a public/internal header split.** `meson.build` keeps two lists, and the distinction is real rather than cosmetic:

- `gpaste_daemon_public_headers` — installed, introspected, and included by the `gpaste-daemon.h` umbrella. Seven of them exist because the gnome-shell extension drives them (`GPasteBus`, `GPasteDaemon`, `GPasteSearchProvider`, `GPastePrompt` — which `prompt.js` *implements* — `GPastePassphrase`, `GPasteStorageMigration`, and `GPasteStorageBackend` for its static passphrase helpers). The other eight are there only because those headers pull them in: `gpaste-daemon.h` declares `g_paste_daemon_new()`, hence the clipboard provider, hence the history, hence the item hierarchy they hand around.
- `gpaste_daemon_internal_headers` — compiled into the library but **never installed and never introspected**. In-tree consumers (`src/daemon/`, `src/ui/`, `tests/`) reach them through `gpaste_daemon_headers_dep`'s `include_directories`, never the install prefix. The optional features (libsodium, SQLite, libsecret, mutter) append to the union lists, so their headers are internal by construction and the GIR does not change shape with the feature set.

**A new header goes in the internal list unless the extension needs it.** Two rules follow from the split: the umbrella must only include installed headers (an installed umbrella pulling an uninstalled header is a broken install), and adding a header to the public list means committing to it in the GIR.

**GObject type macros** — use these in `.c` files:

| Macro | Use when |
|---|---|
| `G_PASTE_DEFINE_TYPE` | Concrete type — this is the normal case |
| `G_PASTE_DEFINE_TYPE_WITH_INTERFACE` | Concrete type implementing an interface |
| `G_PASTE_DEFINE_TYPE_WITH_PRIVATE` | **Derivable** type with a `Private` struct |
| `G_PASTE_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE` | Abstract base class with private data |

**A final type puts its fields straight in `struct _GPasteX`, with no `Private` struct.** `G_DECLARE_FINAL_TYPE` already keeps that struct in the `.c` file, where nothing outside can reach it, so a private struct adds only an offset lookup per access and a `priv = …` line in every function. Access the fields as `self->field`. Private structs are for the **derivable and abstract** types (`GPasteItem`, `GPasteStorageBackend`, `GPasteBusObject`, …), whose instance struct *is* public and so cannot carry them — those keep `G_PASTE_DEFINE_TYPE_WITH_PRIVATE` and the `_g_paste_x_get_instance_private()` const shim it emits.

### `src/daemon/` — `gpaste-daemon` + **libgpaste-daemon**

The background service. Owns the clipboard history and exposes it over D-Bus (`org.gnome.GPaste`). Almost all of its objects live in the installed, introspectable **libgpaste-daemon** library (sources under `src/libgpaste/gpaste-daemon/`, umbrella header `src/libgpaste/gpaste-daemon.h`, `GPasteDaemon-1` GIR/typelib); `src/daemon/gpaste-daemon.c` is the thin executable entry point that links it. What stays in `src/daemon/` is what only that executable uses: the **GDK clipboard backend** (`gpaste-clipboard-gdk.{c,h}` and its `gpaste-text-content-provider.{c,h}`), which is why `gtk4-x11` is a dependency of the executable and not of the library. They are compiled straight into the binary, include each other with quoted local includes, and carry no `G_PASTE_VISIBLE` — nothing exports them. Its types keep the `GPaste`/`g_paste_` prefix, so the GIR passes an explicit `identifier_prefix: 'GPaste'` / `symbol_prefix: 'g_paste'` to place them in the `GPasteDaemon` namespace. Handles:

- Clipboard watching (primary + clipboard selections), through the backend-agnostic `GPasteClipboardProvider` interface
- The rich clipboard item type hierarchy: the abstract `GPasteItem` base and the five kinds deriving from it directly — `GPasteTextItem`, `GPastePasswordItem`, `GPasteColorItem`, `GPasteImageItem`, `GPasteUrisItem` — plus the `GPasteSpecialAtom`/`GPasteBinaryData` helpers (the UI/client instead use libgpaste's lightweight `GPasteClientItem`). **The hierarchy is flat on purpose.** Uris and password items used to derive from `GPasteTextItem` while inheriting nothing from it, which is what forced `g_paste_item_equals()` to dispatch in both directions and made `G_PASTE_IS_TEXT_ITEM()` true for things that were not text. The base's `equals` compares kind and value, which is symmetric, so one dispatch does; only the image (checksum) and the password (never equal) override it.
- Keybinding registration through the XDG GlobalShortcuts portal (`GPasteGlobalShortcutClient`, used directly by the keybinder); if the portal is unavailable, keyboard shortcuts are simply disabled
- History persistence to disk: `GPasteHistory` owns the in-memory model and dedup/size policy, but delegates the asynchronous I/O to `GPasteHistorySaver`. Each mutation records an operation (`g_paste_history_saver_record` with a `GPasteHistorySaveOp` — add/remove/replace/clear/full — plus the item/uuid involved and, when the backend will consume it, a snapshot of the items: incremental backends only get one on *add*, where it is reconciled against; the other operations pass NULL). An incremental backend (see below) applies the queued operations in order; a non-incremental one rewrites the whole snapshot, so the saver collapses the queue down to the latest snapshot (the previous coalescing behaviour). Loads carry a `save_after` flag so a snapshot-rewriting backend persists its read-time normalization after a history switch; the saver drops it for incremental backends (they normalize on open): a load reads at most `max-history-size` rows, and the next add reconciles the store down to the loaded snapshot, so rows beyond the cap survive only until then. The saver never touches the live list, so it shares no locking with the model; it hands load results back through a callback. The model itself is guarded by a **non-recursive** `GMutex` taken through two macros: `G_PASTE_DO_LOCK_HISTORY` just locks for the rest of the scope, while `G_PASTE_LOCK_HISTORY` — what public entry points use — additionally declares a `g_auto` scope guard *before* the locker, so its cleanup runs after the unlock. That guard is what emits the `SELECTED` signal: `g_paste_history_selected()` only records the item in `priv->pending_selection`, because emitting inline would drive the clipboards manager (hence GTK's clipboard and the X11 selection machinery) with the lock held, queueing every other caller — all the D-Bus methods included — behind it, and a handler calling back into us (`g_paste_history_remove()` when an item turns out to be invalid) would deadlock outright. The signal is still emitted before the caller regains control, so ordering towards clients is unchanged. Any new code path emitting a signal that can re-enter the history must go through the same mechanism.

**Bus-name ownership & handover.** Both daemons — the standalone `gpaste-daemon` and the one hosted in the gnome-shell extension (`daemon.js`) — own the single well-known name `org.gnome.GPaste`, and a clean handover between them is done through D-Bus name replacement rather than any stop/proxy protocol. `GPasteBus` always owns the name with `G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT`; `g_paste_bus_own_name_full (self, replace)` — the only entry point, there is no plain `own_name()` — adds `G_BUS_NAME_OWNER_FLAGS_REPLACE` when `replace` is set. The extension **always** replaces (hosting the daemon in-shell is the future-proof path since GTK will drop X11); the standalone daemon replaces **only** when started with `--replace` (a bare argv flag stripped before GApplication sees it) — never from D-Bus activation or the systemd unit, so those automated paths can't ping-pong with the extension. Both daemons are **own-first**: `main()` (and `daemon.js`) create the bus and own the name *before* building the daemon, then do the storage work and build the `GPasteDaemon` from the `name-acquired` signal. Owning first fails fast (`name-lost` with `was_owned == FALSE`) when another daemon already holds the name and refused replacement, without popping a migration/passphrase dialog first. On a takeover the evicted daemon gets `name-lost` with `was_owned == TRUE`: it flushes the history, releases the storage lock and exits `0` (so a `Type=dbus` systemd unit doesn't land in `failed`). `was_owned` distinguishes a real takeover from a startup failure only. Failing to *export* an object is neither, so it gets its own **`export-failed`** signal (the bus warns about the cause itself; the listener decides what to do) rather than riding on `name-lost`: it is detected *after* the name was acquired (both daemons own the name first and add their objects from `name-acquired`), and reporting it as a lost name would have a listener blame a conflict with another daemon that never happened. Both daemons treat it as fatal — a name we own but cannot serve is worse than not owning it, since it also stops D-Bus from activating a daemon that would work: the standalone one flushes, unlocks and exits `EXIT_FAILURE`, and the in-shell runner stands down (releasing the name) exactly as it does on a takeover. The in-shell runner tells a deliberate takeover from the session bus connection simply dropping with `g_paste_bus_is_connected()`: with a live connection it stands down for good, otherwise it tears the bus down and re-owns the name on a fresh connection a second later (`g_bus_own_name()` does not survive its connection closing; a retry that still cannot acquire lands in `name-lost` with `was_owned == FALSE` and gives up, so a flapping bus can't spin). **`g_paste_bus_unown_name()` also unexports every object it registered** (`g_paste_bus_object_unregister_on_connection()`): a `GDBusInterfaceSkeleton` stays exported, and its path stays taken, until it is explicitly unexported — on gnome-shell's *shared, process-lifetime* session-bus connection that would make the next `enable()` fail to export and stand the runner down for good. The objects stay in the bus' list, so re-owning the name re-exports them. The GApplication singleton (`org.gnome.GPaste.Daemon`, distinct from the owned bus name) still dedups two standalone daemons, so `--replace` only ever targets the in-shell one. The activation/desktop/systemd files are unchanged and remain the fallback for non-GNOME or GNOME-on-X11 sessions without the extension.

**History flush & the storage lock.** Handover is race-free because history *loads* are gated on a process-wide advisory lock (`g_paste_storage_backend_lock`/`_unlock`, a `flock` on `<history-dir>/lock`) acquired inside the saver's load worker thread before the first read and held for the process lifetime; the successor blocks on it until the previous owner releases it, so it never loads a stale history mid-flush (the kernel drops the lock if the holder crashes). That wait deliberately happens *outside* the global mutex guarding the lock fd — it can last as long as the previous daemon takes to finish writing, and `_unlock()` runs on the **main** thread of every exit path, so holding the mutex across it would block (in the gnome-shell host, freeze) the compositor for that whole time; `_unlock()` instead raises a flag the waiter polls, so a teardown while still waiting gives up rather than installing a lock nobody wants. Every exit/handover path first calls `g_paste_daemon_flush` → `g_paste_history_flush`, which sets a `stopped` flag (no further changes are recorded) and `g_paste_history_saver_drain`s the saver: it waits for the in-flight worker write via a `GMutex`/`GCond` handshake (it can't spin the main loop, which is where the write-done callback runs) then applies any queued writes synchronously. So after `flush()` returns the on-disk history is complete *without* pumping the main loop, and the lock is released only afterwards. `drain()` deliberately leaves `write_in_progress` set: the drained task's completion callback is still queued on the main context and will clear it (and pick up anything recorded meanwhile), whereas clearing it in `drain()` would let a `record()` made after a *resumed* handover start a second worker while that callback starts a third — two threads rewriting the same history at once. **`DeleteHistory` is ordered against all of this**, since a delete that loses its race silently resurrects everything the user asked to destroy: `g_paste_history_delete` refuses outright while `stopped` (the store belongs to the successor daemon, or to a migration rewriting it — the D-Bus method turns that `G_IO_ERROR_BUSY` into the reply and skips the deleted/switch signals), and for the *current* history it first `g_paste_history_saver_abandon_load`s (a load in flight is reading the very file being unlinked; its result would be installed right after, and the next clipboard change would write the whole pre-delete history back), then empties, then `drain`s before unlinking — the emptying records a write for this very history, which both backends would otherwise apply *after* the file is gone, re-creating it on open. The standalone daemon flushes+unlocks on `SIGTERM`/`SIGINT` and on a takeover (`name-lost` with `was_owned`). **Re-exec** is a special exit that must not lose the daemon: `reexec()` flushes (so the successor loads the final state) but does **not** quit or explicitly unlock, then `execl`s — first the installed `PKGLIBEXECDIR/gpaste-daemon` (correct after an upgrade: same path, new inode), then `/proc/self/exe` as a fallback (so a daemon run uninstalled from the build tree still re-execs itself). `execl` only returns on failure, and quitting *before* it (the historical bug) meant a missing/failed target left **no daemon at all**; instead, on a successful exec the process is replaced (its `O_CLOEXEC` storage-lock fd releasing automatically), and if every exec fails the daemon logs a warning and `g_paste_daemon_resume`s (clearing the `stopped` flag `flush()` set) so it keeps running rather than exiting.

**Prompts.** Everything the storage layer needs to ask the user goes through `GPastePrompt` (`gpaste-prompt.{c,h}`), a GObject *interface* in the same spirit as `GPasteClipboardProvider`: **no toolkit type appears in any signature**, so the migration/unlock/re-key logic lives in one place while its UI does not. Two concerns — `passphrase` (one entry, or two plus a strength meter when `confirm`) and `migration` (pick a backend, import, cleanup) — plus `report`, which is not one: `g_paste_prompt_report (title, message)` takes no request and has no `_finish`, because it asks nothing. It tells the user that something they set in motion did not happen (a passphrase that did not change, old data that was not deleted) where a log line would leave them believing it did; its **default implementation is that log line**, so a backend that skips it loses the dialog, not the report. Callers get the usual `_async`/`_finish` pair, but the **vfuncs take a `GPastePromptRequest` rather than a `GAsyncReadyCallback`**: GJS refuses to implement a vfunc that accepts a callback (*"VFunc … accepts another callback as a parameter. This is not supported"*), and being implementable from GJS is the entire point — the gnome-shell-hosted daemon can run neither `gtk_init` nor `adw_init`. A backend shows its UI, keeps the request, and answers it with `reply_passphrase()`/`reply_migration()`/`dismiss()` whenever the user gets round to it; the first answer wins and later ones are no-ops, so a dialog that both replies and then reports its own destruction is harmless. **Every way of losing the window must answer**, since the whole daemon startup is waiting on that answer — and it must answer from the window's **`destroy`** (dispose-time), never from a `g_object_set_data_full()` destroy notify alone: qdata is cleared in `finalize`, so anything still holding a reference (an animation, an a11y peer, a queued frame callback) would defer the answer for as long as it holds it. Both backends hook `destroy`; the Adwaita one keeps the destroy notify only to free the dialog struct, once the widgets carrying it are gone. The **default vfuncs dismiss**, which is the safe answer everywhere: nothing is migrated, and no passphrase is set, so an encrypted history stays locked rather than being loaded empty and overwritten. Anything both backends would otherwise duplicate is exported next to the interface — including `g_paste_prompt_passphrase_is_complete()` (what counts as an answer) and `G_PASTE_PROMPT_STRENGTH_MAX` (the scale a meter is built against), so neither prompt can come to disagree with the other about them — `g_paste_prompt_list_storage_backends()` (the offered flavors, keeping the feature `#ifdef` ladder in one place) with `g_paste_prompt_storage_label()`, the live sensitivity rules `g_paste_prompt_can_import()` / `g_paste_prompt_backend_changes()`, and `g_paste_prompt_passphrase_strength()` (libpwquality, which is not introspectable, so this is the only way a JS prompt can show the same rating). `passphrase_finish()` hands back a **`GPastePassphrase`** (`gpaste-passphrase.{c,h}`), an opaque boxed type holding the cleartext in gcr secure memory (non-pageable, wiped on free) — never a `gchar *`: the whole point of the boxed type is that it carries its own free function, so `g_autoptr()` enforces `g_paste_passphrase_free()` at compile time and introspected callers cannot `g_free()` gcr memory. Build one with `g_paste_passphrase_new()` (%NULL and the empty string both give %NULL — an empty passphrase is no passphrase), read it with `g_paste_passphrase_peek()`. The libadwaita backend is `GPastePromptAdw` (`src/daemon/gpaste-prompt-adw.{c,h}`) — executable-only, like the GDK clipboard, since only the standalone daemon builds one. It is also why **libgpaste-daemon links neither libadwaita nor libgpaste-gtk4** (nor carries `Adw-1` or `GPasteGtk-4` in its GIR): the storage dialogs were the last thing in it that built one, and the single function it still wanted from libgpaste-gtk4 — an image checksum, pure GDK — moved to `g_paste_image_item_compute_checksum()`. `gtk4` itself stays, for `GdkTexture`, `GdkRGBA` and `gtk_accelerator_parse`; the widget stack does not, which is what matters for a library gnome-shell hosts and can initialise neither of.

**Storage backends & migration.** *Where* the history is persisted is abstracted behind `GPasteStorageBackend` (`gpaste-storage-backend.{c,h}`), a derivable GObject with `read_history_file`/`write_history_file`/`get_kind` plus optional `delete_history`/`list_histories` vfuncs, and optional incremental `add_item`/`remove_item`/`replace_item`/`clear_history` vfuncs (a backend that implements **all** of them reports `g_paste_storage_backend_is_incremental () == TRUE`; a partial implementation counts as non-incremental and the public wrappers fall back to a full `write_history`, which is what the file backend relies on). `read_history_file` returns a `gboolean`: **%FALSE means the history is on disk but could not be read back** (failed decryption, parse or I/O error), as opposed to a genuinely empty or absent one — an empty file, which is the placeholder the file backend creates for a never-written history, is `TRUE`. Every caller must honour it, because an unreadable history that looks empty is one save away from being destroyed: `import_histories` aborts the migration, and `GPasteHistory` sets its `unreadable` flag so nothing is ever written back over the intact data. That flag is deliberately distinct from the `stopped` one `flush()` sets (below) and tracks *the history currently loaded*, not the process: starting a load clears it and the load's own result sets it, so an unreadable history stops persistence for itself alone and never silently disables it for whichever history is switched to next — and deleting it clears the flag too, since the file it was protecting is the one being unlinked (leaving it set would shut persistence down for the rest of the process). A history whose XML carries an **unknown `<history version=>`** counts as unreadable rather than empty: its items are all skipped (we know neither where their value lives nor how to decode it), so reporting it readable would have the daemon persist that empty model over a file written by a newer GPaste on the very next clipboard change. A favourite is written as a `favourite="true"` attribute, and only when set, so an ordinary history's file is unchanged by its existence. A password's `timeout` rides beside its `name` on the same terms — written only when non-zero, so every password an older GPaste wrote reads back with no timeout, which is what it had. The format version deliberately **stays `2.0`** for it: bumping it would make an older GPaste treat the whole history as unreadable, where leaving it merely has that older GPaste warn about an attribute it does not know and drop the flag. **`2.0` is the only format read.** `1.0` — which held an item's value as text directly inside `<item>` rather than in a `<value>` child — was dropped, and takes exactly that path: an ancient history is refused and left byte-for-byte intact rather than migrated in place, so it can still be converted by loading it once with GPaste 2. It gets its own diagnostic saying so, since "unknown version" would be misleading for a format we ourselves wrote. `list_histories` has a shared default in the base class (enumerate the history dir for files ending in this backend's `get_extension`), so a subclass only overrides it when its listing is not one-file-per-history; **Both capped reads count only the items the cap can evict**: the file backend's parser tallies non-favourites against `max-history-size` and always admits a favourite, and the SQLite read applies its `LIMIT` to a `favourite = 0` subquery. A favourite that has sunk past the cap would otherwise be dropped on load and destroyed by the next save — the one thing pinning exists to prevent. That read therefore hands back more than `max-history-size` once the favourites are counted, which is how the caps themselves count, so **`g_paste_history_on_loaded()` runs both caps on what it just read** rather than leaving the difference to the next clipboard change: the two would otherwise disagree about what the history holds, and the rows over the cap would show until that change came and then vanish all at once. A trim there frees the backing files of what it drops, so it forces the save the load may not have asked for — and is skipped outright for a history that is `unreadable` or `stopped`, neither of which can be saved, where it would be deleting the images a store still points at (a history switch can land inside that flushed window). A truncated enumeration returns `NULL` + `GError` rather than a partial list, since a migration would otherwise import — and its cleanup delete — only the histories it happened to reach. **Every directory listing in this library goes through `g_paste_util_list_directory()`** (`gpaste-daemon-util.{c,h}`), which is where that rule and its companion ("a directory that is not there lists nothing, and is not a failure") are decided once rather than at each of the four places that enumerate one — as is the third rule they all depend on: an entry the enumerator did not answer with the requested attribute fails the listing rather than being added as a `NULL` that every reader takes for its end. It is asked for `G_FILE_ATTRIBUTE_STANDARD_NAME` everywhere, the display name being a UTF-8 *rendering* of those bytes: a history listed under a name that names no file would have `read`/`delete`/`rekey` all act on nothing and report that they acted. `g_paste_storage_backend_new (GPasteStorage kind, settings)` is the factory; `GPasteStorage` enumerates `NOOP` (`GPasteNoopBackend`, "no storage" — reads empty, drops writes, lists nothing, so the history lives only in memory for the session), `FILE` (the XML on-disk backend, `GPasteFileBackend`), `ENCRYPTED_FILE` (the same `GPasteFileBackend` created via `g_paste_file_backend_new_encrypted (settings, passphrase)` — an encrypted ".xmls" history; only offered when the encryption feature is built), `SQLITE` (`GPasteSqliteBackend`, below; only offered when the `sqlite` feature is built) and `ENCRYPTED_SQLITE` (the same `GPasteSqliteBackend` created via `g_paste_sqlite_backend_new_encrypted (settings, passphrase)`; only offered when both features are built). The *kind→flavor* logic is centralized: `g_paste_storage_is_encrypted (kind)` classifies the two encrypted kinds independently of the built features (a build unable to construct one degrades it to `NOOP`, never plaintext), and `g_paste_storage_passphrase_can_decrypt (kind, settings, passphrase)` verifies one, so the factory, keyring fallback, unlock prompts and migration dialog all work per-flavor. It scans **every** history and refuses if any data-holding one fails to open, rather than accepting on the first that does: histories can be keyed differently (a re-key that fails partway leaves exactly that, and `rekey_histories()` warns about it by name), and accepting then would load the others empty and let their next save destroy the real content. The per-history verdict is the `history_refutes_passphrase` vfunc -- only encrypted data the backend cannot open refutes a passphrase; a corrupt or foreign file says nothing either way, since telling the user their correct passphrase is wrong is worse than refusing the read. `GPasteHistory` picks its backend from the `storage-backend` GSettings key (a gschema `<enum>` of string nicks, read via `g_paste_settings_get_storage_backend`). The encrypted backend needs a passphrase, which the factory takes from a process-wide gcr-secure global (`g_paste_storage_backend_set_passphrase`/`get_passphrase`) set once at startup, so no constructor signature has to thread the secret; with no passphrase set it degrades to `NOOP` rather than writing plaintext. **A backend answers which flavour it is, and everything else follows.** `get_kind` is the only vfunc about its identity: `g_paste_storage_backend_get_extension()`, `..._is_encrypted()` and the rule that only an encrypted flavour keeps password items all derive from it in the base class, so a backend never spells out its own extension. The kinds themselves live in one `storage_flavours[]` table in `gpaste-storage-backend.c`, indexed by `GPasteStorage`, carrying the on-disk extension (`xml`/`xmls`/`db`/`dbs`, and the never-matching `noop`) and which class implements it; the shared `list_histories`, the migration's `detect_current_backend()` and the images cleanup's "is this name still stored by some flavour" check all read through it, iterating up to `G_PASTE_N_STORAGE` rather than carrying their own list. To add a backend: add an enum value + nick, a row in that table, and a `GPasteStorageBackend` subclass; implement the incremental vfuncs to persist single changes (the saver already feeds them the per-operation add/remove/replace/clear info), or just `write_history_file` to keep rewriting whole snapshots. **The SQLite backend** (`gpaste-sqlite-backend.{c,h}`, gated by the `sqlite` meson feature / `G_PASTE_ENABLE_SQLITE`) is the incremental reference: one database per history (`<name>.db`), an `items` table ordered by a monotonic `rank` (highest = front, so adds/selects never renumber; compacted on open in the astronomically unlikely case it nears overflow) plus a `special_values` child table (raw BLOBs, `ON DELETE CASCADE`), schema versioned through `PRAGMA user_version` (`migrate_schema` upgrades an older database stepwise on open; a database from a newer GPaste is refused and every operation no-ops rather than corrupting it). **Version 2 added the `favourite` column and version 3 the password `timeout`**, which is the mechanism's real use: one `case` per version, each running a single `ALTER TABLE … DEFAULT 0` and falling through to the next, so every item an older GPaste stored comes back unpinned and every password with no timeout — which is what they were. Both new columns stay plaintext in the encrypted flavour, like `rank` and `date`: a pin and a duration are metadata, not content. `add_item` is an upsert-and-reconcile, not a bare INSERT: a `SAVE_ADD` also covers selects (move to front) and dedup re-adds, and size/memory evictions ride along without their own remove op, so after the upsert any stored uuid missing from the snapshot is deleted. `write_history_file` (still reached by the migration import and `g_paste_history_save`) rewrites everything inside one transaction, so a crash rolls back instead of losing data. Password items are skipped exactly like the plain XML backend. **Images are stored as blobs** (the `items.image` column, encrypted like any content column in the encrypted flavor — the one place the SQLite flavors beat `.xmls`, whose PNGs sit in plaintext next to it): on write the blob comes from the item's PNG bytes, falling back to reading the file a path-loaded item was read from when it carries none (e.g. a plain file → sqlite import); on read a stored blob builds the item via `g_paste_image_item_new_from_bytes` (no file, and no cache path on the item — nothing on disk backs a row). `GPasteImageItem` carries its encoded PNG across IDLE for this (`get_png_bytes`; capture encodes once with `gdk_texture_save_to_png_bytes` and **writes nothing to disk** — persisting the image data is entirely the storage backend's job, so the NOOP backend genuinely stores nothing; the texture is rebuilt from the bytes on activation, and the bytes count towards the item's size for the memory cap). **An image item's value is its checksum**, not a path: what says *which* image it is, the same string whichever backend the bytes came back from (which is also what lets the migration verifier compare items by real value alone). *Where* the bytes were materialized is a separate, **nullable** `get_cache_path()` — set only by the backend that read the item off a file (`new_from_file`, `new_from_bytes_at_path`), `NULL` for a fresh capture and for anything the SQLite backend hands back, so everything reaching for a file (activation, `GetImage`, the blob fallback above) has one thing to check and does nothing when there is nothing to check. The `images/<history_name>/<checksum>.png` layout is the **file backend's own** (`g_paste_file_backend_image_path`, `_images_dir`, `_encrypted_path`, `_delete_image`) — per history so the same image copied in several histories gets one file each and evicting it from one never breaks the others — and it is *derived* at write time from the checksum and the history being written, never remembered by the item. Each backend materializes what it needs on write: SQLite the blob, the plain *file* backend the referenced `.png` (from item bytes, falling back to reading an existing file for path-loaded items), and the **encrypted file backend an encrypted `.pngs` side file** through the same stream converter as the history — its XML parser reads the `.pngs` back into item bytes, falling back to the plaintext path for not-yet-migrated entries, so no pixel reaches the disk in clear on any encrypted flavor. History eviction asks the storage backend to drop what it materialized outside its own store (`drop_item_data`, an optional vfunc): the file flavours sweep both the `.png` and `.pngs` siblings — the canonical one plus the item's own cache path when it sits elsewhere, absent ones being fine — and SQLite implements nothing, a blob going with its row. the base `delete_history` wrapper removes the history's whole `images/<name>/` directory — but only once **no flavor** still stores that name on disk, since a migration's cleanup deletes the source while the destination (same name) still references, or just materialized, images in that directory. That sweep **reports what it could not remove** through `delete_history`'s `GError` (never over the store's own failure, which is the one that says why): on the plain flavours those files are the clipboard's screenshots in cleartext, so "the history is gone" must not be answered while they are still sitting there. It sweeps everything it can regardless, and only the first failure is kept — and `import_histories` verifies by uuid *and content* (kind and real value, an image's being its checksum), not size — a bytes-carrying item legitimately weighs more than its path-based twin. The XML keeps naming those files: an image's item text is the path this backend materializes it in, its checksum riding along as an attribute, so a history file still points at the files beside it and a load hands the path back as the item's cache path. When the file backend writes items under a name that is *not* the one they were read from (`BackupHistory` writes the source's items under the backup's name), that derivation alone puts the references and the materialized copies under the target history without touching the shared items — backups own their images, and legacy shared-directory entries migrate to their history's own directory on their next save the same way. The connection is cached per path and a mutex serializes the vfuncs (a background load can overlap an in-flight write). **The encrypted flavor** (`g_paste_sqlite_backend_new_encrypted`, ".dbs" extension, needs the `encryption` feature too) encrypts the content columns — `items.value`, `items.name`, `special_values.data` — with libsodium's `crypto_secretbox`, each stored blob being nonce ‖ ciphertext; uuid/kind/rank/date/checksum stay plaintext so ordering, the LIMIT read and uuid reconciliation are untouched (an accepted metadata leak, traded for incremental updates; SQLCipher was rejected as an ABI-incompatible sqlite3 fork). The key is derived per database through the shared `g_paste_crypto_derive_key()` (Argon2id, MODERATE — the same entry point the stream converter uses, and the reason the bound below cannot be forgotten); the salt, Argon2 parameters and a key-check secretbox live in a `meta` table, **and both Argon2 parameters are read back from that table, so they are only as trustworthy as the file**: `g_paste_crypto_derive_key()` refuses anything past `SENSITIVE`, because an absurd opslimit would otherwise make the derivation run effectively forever -- a denial of service just from opening a history. A wrong passphrase is detected on open and refused like a newer schema (never given the chance to overwrite real data) — except an *empty* history, which is silently re-keyed for the new passphrase since there is nothing to lose (the shared verification applies the same rule, quietly). The passphrase is a `GPastePassphrase` and the derived key gcr secure memory, both on the instance; unlike the plain flavor it persists password items (real value + name). **Migration** (`gpaste-storage-migration.{c,h}`) is two concerns the standalone daemon runs back-to-back from its `name-acquired` handler (in `gpaste-daemon.c`, after registering the `AdwApplication` and owning the bus name, before the `GPasteDaemon` is built): the *migration* dialog (`g_paste_storage_migration_async()`, shown when the stored `storage-backend-revision` differs from `G_PASTE_STORAGE_BACKEND_REVISION` — `g_paste_storage_migration_needed()`) and the encrypted-history *decryption* (`g_paste_storage_decryption_async()`, below). All three concerns (those two plus the *re-key*) are ordinary `GAsyncResult` operations — `_async()` starts it, `_finish()` reads the outcome — chained migration → decryption → build the daemon; the library never spins a `GMainLoop` of its own and never calls `adw_init()`. They take **no `GCancellable`**: a concern is a dialog the user is looking at, and neither `GPastePrompt` nor the portal underneath it can be told to take it back down, so one could only ever be ignored. `_finish()` returns `%FALSE` when the prompt implementation itself failed — and, for the *re-key* alone, when the histories on disk could not be re-encrypted — the one outcome the user has to be told about (a passphrase change that did not happen must never be reported as one that did), so the re-key **says so itself** through `g_paste_prompt_report()` rather than leaving it to a caller that can only log: both of them do only that, and the `ChangePassphrase` D-Bus call was answered before any of this ran. Which of the two re-key failures it was decides the wording: a run whose rollback also failed left the histories split across both passphrases, and the reassuring "everything still opens with the previous one" is exactly what is untrue there. The migration's cleanup does the same when the old data it was told to delete is still there — it cannot be offered again (the dialog only offers a cleanup for a migration that changes backend, and the backend is now the chosen one), so the revision is bumped and the user is told instead. That outcome is also what the keyring decision reads: leaving encryption behind only drops the remembered passphrase once the encrypted data it opens is **actually gone**, not once its deletion was asked for; every ordinary outcome, dismissal included, is `%TRUE`, because what the user chose has already been applied to the settings and to the process-wide passphrase by the time the callback runs. A prompt that **fails ends its concern** rather than being asked again: a dismissal is a question the user answered, an implementation error is not one they can — re-asking would loop forever against an implementation that fails reliably, and would let a failure recorded on one round be reported over a later round that succeeded. Each executable registers an `AdwApplication` — registering it is what initialises libadwaita — then kicks off the dialog right after (before `run`) and lets its own `g_application_run()` loop drive it; the shared `G_PASTE_GTK_INIT_APPLICATION` macro passes no command line to `g_application_run()` on the remote-instance path, so GApplication does not mistake our flags and subcommands for files to open (none of our executables takes a file argument; the storage helper's subcommand is what used to hit this, before it was retired). The migration prompt (run *before* the `GPasteDaemon` is built so the chosen backend is in effect) lets the user pick a backend, optionally import the current history into it and optionally delete the old on-disk data afterwards (with a warning when cleanup is ticked without import). It is answered once per invocation, so backing out of a passphrase prompt re-asks it rather than leaving a dialog up — the state gathered so far is kept, so the second time round only asks what is still missing. A prompt's answer is **not trusted**: `import`/`cleanup` are clamped against `g_paste_prompt_can_import()`/`g_paste_prompt_backend_changes()` the moment the reply lands (in `on_migration_reply`, *before* `continue_apply()` reads them), not later on when they are acted on — a backend that answers "import and delete" for a migration going nowhere would otherwise make the user unlock the encrypted history for work that is then clamped away, and greying the toggles out is our own backends' courtesy, not a guarantee. The *current* backend is detected from the files on disk, not from the (possibly stale or unset) setting — `detect_current_backend()` walks a single table of the storing flavours in precedence order (`.xmls` encrypted, `.dbs` encrypted sqlite, `.db` sqlite, `.xml` plain), looking for the active history's file first, then falling back to whichever flavour has the most files (that same order breaks ties, so encrypted flavours win), else `none`; a listing it could not read answers *nothing at all* rather than `none`, leaving the stored setting alone, since "nothing is stored" and "we could not look" produce the same zero count and it is that answer which gets written down — and the detected value is both written to `storage-backend` up front (so dismissing the dialog keeps the right backend, and an encrypted history is never silently downgraded to `none`) and used as the import source / cleanup target, so migrating *any* direction works (e.g. encrypted → file). Import is only offered when the current backend stores data and differs from a storing target; importing from (or deleting) an encrypted current first tries the keyring (`g_paste_storage_keyring_apply_verified`, flavour-aware and shared with the startup unlock and the backend factory) and only prompts for its existing passphrase when none is remembered or the remembered one is stale (verified, with re-prompt), just as switching *to* encrypted prompts for a new one. Confirming writes `storage-backend` + bumps the revision; dismissing (which the prompt reports as its answer) leaves the revision so the dialog returns next start (forcing a deliberate choice — the privacy default of #357), keeping the detected current backend for the session. Between the two sits **"Keep as is"** (`G_PASTE_PROMPT_TEXT_MIGRATION_KEEP`, in both prompt backends, where a cancel button would be): an ordinary `reply_migration()` carrying the *current* backend and neither toggle — never what the combo happens to be showing, so it undoes the fiddling too — which settles the question through the confirm path, revision bump and all. The deliberate choice #357 asks for is a choice to leave things alone as much as a choice to move them, and it was already expressible (pick the current backend, both switches off, Apply); what it was not is findable, which is why the description names the button and says that closing the window is the other thing. **Neither the button nor a preselection is offered when the current backend is not one this build can construct** (an `.xmls` or a `.db` opened without encryption or sqlite): keeping a backend that is not on the list is not an answer `reply_migration()` accepts — it validates `chosen` against the offered set and dismisses anything else — so the button would silently become the dismissal it exists to avoid, and preselecting the first row instead would have the dialog claim a backend the user never chose. Nothing is selected until they say so, and Apply stays insensitive (in the shell backend, `_onApply()` also returns early: Enter reaches the default action through a key binding rather than through the greyed-out button). The same dialog is reached **on demand** without racing the running daemon: the preferences button and `gpaste-client migrate` reset `storage-backend-revision` (opening the same `migration_needed` gate) and call the existing `Reexecute` D-Bus method, rather than spawning the helper behind the daemon's back. That whole sequence is `g_paste_util_trigger_storage_migration()`: it opens the gate (reset + `g_paste_settings_sync()`, so the daemon sees the write whichever path lands) and then runs `g_paste_util_reexecute_daemon()` — the *same* re-exec a plain `gpaste-client daemon-reexec` runs, so the two verbs can never drift. A daemon honouring the re-exec tears its D-Bus connection down before replying, so a missing reply (`G_DBUS_ERROR_NO_REPLY`, matched with its domain — a bare code comparison would also match `G_IO_ERROR_NOT_DIRECTORY`) is the expected success and leaves the `GError` unset; anything else propagates, and `gpaste-client` (only there — it is the one caller that can find the pid file) falls back to `SIGUSR1`-ing the pid the daemon wrote, for a daemon too old for the method. The standalone daemon re-execs and re-runs exactly this startup migration; the gnome-shell-hosted daemon, which cannot re-exec, instead handles `reexecute-self` by flushing, running the `migrate`/`decrypt` concerns and reloading its backend in place (`g_paste_daemon_reload_storage` → `g_paste_history_reload_backend`, guarded by the same `migration_needed` check so a plain `daemon-reexec` stays a no-op there). Both gates and the reload read the keys back with **`g_paste_settings_reload()`** first: `storage-backend-revision` and `storage-backend` are written by *another* process (`gpaste-client`, opening the gate before it asks for the re-exec) and there is no ordering guarantee that dconf's change notification reaches the shell before the `Reexecute` call does, so trusting the cached copy would silently skip the migration or resurrect the flavour just left behind. `reload_backend` also **detaches** the saver it replaces (`g_paste_history_saver_detach`): an in-flight load keeps the old saver alive through its own task reference, and its pre-migration result would otherwise be installed over the freshly loaded one. The reload itself is asynchronous like every other load — reading (and, encrypted, deriving the Argon2id key) inline would freeze the whole compositor. A cancelled on-demand migration leaves the revision reset, so the dialog returns next start — the same deliberate-choice behaviour as an upgrade. When an **encrypted flavour** is chosen, applying first raises the passphrase prompt with `confirm=TRUE` (two fields, a GNOME-style strength meter — `g_paste_prompt_passphrase_strength()` scores it with libpwquality and the Adwaita backend shows a colour-graded `GtkLevelBar` plus libpwquality's hint, gated on the optional `pwquality` feature — and a data-loss warning) and sets the global passphrase before importing/storing; an already-configured encrypted history is unlocked (the *decryption* concern, `g_paste_storage_decryption_async()`) with the same prompt (`confirm=FALSE`) through the shared `unlock_prompt` helper — keyring first, then prompt — which the migrate-from-encrypted import path reuses too. The entered passphrase is *verified* before use (`g_paste_storage_passphrase_can_decrypt`, which asks each flavour per history — the file one decrypts the `.xmls`, the sqlite one opens the key-check secretbox in the `.dbs`): a wrong one re-prompts with an error (the `error_message` argument) instead of being accepted — accepting it would load an empty history and the next save would overwrite, and destroy, the real still-encrypted data. The prompt lives in the daemon, not the UI. When the `libsecret` feature is built (`G_PASTE_ENABLE_LIBSECRET`, which also requires encryption), the prompt grows a "Remember this passphrase" toggle that stores it in the keyring (`gpaste-storage-keyring.{c,h}`, a single attribute-less `SecretSchema`); the decryption concern tries the keyring first (`g_paste_storage_decryption_needed()` → `g_paste_storage_keyring_apply_verified()`) and only prompts if nothing was remembered or the remembered passphrase no longer decrypts the history (a stale entry is discarded, not used). The same verification guards the in-process keyring fallback in `g_paste_storage_backend_new` (the gnome-shell daemon path, which never runs the prompt). gnome-shell runs the very same `_async` functions **in process**, passing its own `GPastePrompt` (`src/gnome-shell/prompt.js`) instead of the libadwaita one; `daemon.js` promise-wraps the three `_async`/`_finish` pairs by hand in `callStorageConcern()`, and `_runStorageConcern()` logs a rejection and carries on to the next concern. **`Gio._promisify` cannot be used on them**: they are static functions on the GI *namespace* object rather than methods on a prototype (see the JS conventions above), so the wrapper would either fail to stick or, called off a plain value in an ES module, throw a `TypeError` out of its own first line — either way every storage concern would silently do nothing, the migration dialog and the unlock prompt would never appear, and the daemon would load an unreadable history. There is no helper process and no passphrase crossing one: whatever a prompt settles on is set process-wide by the storage code itself, in the shell's own process, before `new_meta` loads the history. Only **one settle may be in flight**: a prompt can sit there for minutes, and a bus reconnect during that window runs `_onNameAcquired()` again, so `_settleStorage()` is a thin gate that hands a later caller the promise of the running settle (`_doSettleStorage`) instead of raising a second set of dialogs over the very store the first is rewriting. The standalone daemon guards the same window with a `settling` flag on its `DaemonContext`: its `ctx->daemon` check cannot, since the daemon is only built once the settle is over — precisely the minutes a dialog can be up for. Every other concern gets its own turn on `_serializeStorage()`'s chain instead — and **flushes from inside that turn**, never before queueing: `flush()` stops the history recording and makes every `Delete` over the bus fail with `G_IO_ERROR_BUSY`, so flushing up front would leave it that way for as long as an unrelated concern ahead of it keeps a dialog up, and a flush that ran before a settle the caller merely *joined* would be a flush that settle had already read past. That is why `_migrateInPlace()` runs `_doSettleStorage()` in its own `_serializeStorage()` turn rather than through `_settleStorage()`.

**History encryption** (`gpaste-secret-stream-converter.{c,h}`, optional — built only when the `encryption` meson feature finds libsodium, which also defines `G_PASTE_ENABLE_ENCRYPTION`): a bidirectional `GConverter` (the same final type does both directions via a `GPasteSecretStreamDirection` ctor argument) built on libsodium's `crypto_secretstream` (XChaCha20-Poly1305). The symmetric key is derived from a user passphrase with `crypto_pwhash` (Argon2id, MODERATE limits); the random salt and the Argon2 parameters are written into the stream header (`MAGIC · salt · opslimit · memlimit · secretstream-header`) so decryption reproduces the key. Plaintext is split into ≤4 KiB chunks, each emitted as a `u32` length prefix + ciphertext, the last carrying the secretstream FINAL tag (so truncation is caught). Both directions buffer internally (a `GByteArray` for pending input and one for produced output) to cope with the arbitrary buffer splits `GConverter::convert` hands them; "no progress" maps to `G_IO_ERROR_NO_SPACE`/`PARTIAL_INPUT` like GLib's own converters. The passphrase is a `GPastePassphrase` and the derived key **gcr secure memory** (`gcr_secure_memory_*`, non-swappable, wiped on free); the passphrase is retained for the converter's lifetime because the key is salt-dependent and `reset()` must re-derive it for a fresh stream. `GPasteFileBackend` uses it for the `ENCRYPTED_FILE` backend: it wraps its read/write file streams with the converter, uses the ".xmls" extension, and (unlike the plain backend) persists password entries — writing their real value (`g_paste_item_get_real_value`) plus the `name` attribute, since the file is unreadable without the passphrase.

**`GPasteGlobalShortcutClient`** wraps the XDG GlobalShortcuts portal (`org.freedesktop.portal.Desktop`) as a `GDBusProxy` subclass. It is the daemon's only keybinding mechanism: `g_paste_global_shortcut_client_grab_all(accels[])` / `_ungrab_all()` register/release shortcuts and the `keybinding-activated(const gchar *id)` signal fires when one is pressed. The portal session is created lazily on the first `grab_all` call. The `GPasteKeybindingAccelerator` struct `{ const gchar *id; const gchar *accelerator; const gchar *description; }` is the transfer type between keybinder and client; arrays are null-terminated by `.id = NULL`. Internally it stores registered shortcuts as a `GPtrArray` of private `_Shortcut` structs and handles the portal's Request/Response async pattern transparently.

**A password does not sit on the clipboard forever.** The countdown belongs to the *selection*, not to the manager: it lives on `GPasteClipboardsManager`'s per-provider `_Clipboard` record, one deadline each. A password on the primary and a password on the clipboard are two separate exposures, and none of the routes that put an item on a selection reaches every one of them — `select()` gives up on the first provider to refuse, `ensure_not_empty()` is one provider acting alone — so a single shared countdown could be cancelled for a selection its password had never left, which is that cleartext sitting there for the rest of the session with nothing to take it off. Per selection there is nothing to guard against: a route arms and retires the countdown of the selection it just wrote, and has nothing to say about any other's. Arming is `g_paste_clipboards_manager_arm_password()`, which every route that *publishes an item* ends in: `g_paste_clipboards_manager_select()` for the history's `selected` signal and the add path alike, the manager's own `ensure_not_empty()` wrapper for a provider re-owning a selection it found empty, and `rearm_password()` for a timeout written under a countdown already running. The two writes that skip it are the ones taking a password back *off*: `deselect_password()` putting the replacement or the empty string there, and `notify_finish()` copying one selection's text onto the other. Neither can be publishing a password — the first picks the first item that is not one, the second carries the text a provider just read — so neither has a countdown to arm. That last one is why `g_paste_clipboard_provider_ensure_not_empty()` hands back what it put there: it selects the history's head without going through `select()`, so a password bootstrapped onto a fresh daemon's empty selection would otherwise carry no countdown at all. `select()` arms **inside** its loop, as each provider accepts, which is where the item reaching a selection is known: a provider refusing it never took it and needs no countdown, and a refusal does not leave what the providers before it accepted sitting on their selections with nothing to take it off. **What the countdown is armed for is the item, held by reference, and what makes a firing act is a selection still carrying it** — `g_paste_clipboards_manager_selection_holds()`, comparing `g_paste_item_get_real_value()` against what the provider reports it holds. The history's head is not that question: an add can change the head without touching a selection (a tracked copy inserts at index 0 and emits no `selected`), and a selection can change without touching the head (tracking off, or a copy the size caps refused), so a head-as-proxy test both leaves passwords published forever and clears selections it never put one on. The reference is what keeps the item readable after the history has dropped it, and the value is what a provider can be asked about at all. Selecting the same password again does not restart its countdown: a selection re-taken every few seconds would otherwise push the exposure back for as long as that kept happening. The record therefore keeps the *duration* it armed for beside the item, since that is the one thing about an already-armed item `MakePassword` can change under it: same item and same seconds is the exposure already running out, a different number is a countdown to start over. **Expiring acts per selection.** When every one of them still carries the password, the history's own selection is what changes — `g_paste_history_select()` on the first non-password item, which moves it to the front, out of the active slot where `ensure_not_empty()` would put the password straight back on the next selection to fall empty — and that drives every provider through the ordinary path. When some have moved on, only those still carrying it are written, since what replaced it elsewhere is the user's; and when none do, the firing is stale (the item deleted, the history switched) and does nothing, which is what spares every one of those a cancellation of its own. **An emptied history is not a stale firing**: an `Empty` removes every item without emitting `selected`, so the password is still on the selection with nothing having replaced it, and with no item left to put there the empty string goes on through `select_text()` rather than the content being dropped — a provider reports emptiness by the *kind* it holds, so `""` still counts as something and `ensure_not_empty()` leaves it alone, where a genuinely empty selection would have it re-select the history's head, the very password just taken off. **An update in flight owns what its reply reads.** A provider refs itself for the whole read (`gpaste-clipboard-gdk.c`, `gpaste-clipboard-meta.c`) because the read spans main-loop iterations; the manager's side of it holds a `_Clipboard` record the manager owns, so `bootstrap_ready`/`update_ready` are handed a struct carrying a **ref on the manager** rather than the bare record, the records are freed in `finalize` rather than `dispose`, and both callbacks bail (releasing the item they were handed) when `history` is already `NULL` — a `g_object_run_dispose()` during a pending read is dispose running while such a ref is out. **`g_paste_clipboards_manager_expire_password()` runs it early**, from `dispose` and from `g_paste_daemon_reexecute()`: a daemon standing down leaves the selection behind it, and `store()` hands it to the clipboard manager to survive the exec while the successor arms nothing for an item it merely loaded — either way a password left there would outlive its timeout by the whole session.

**Clipboard backends.** `GPasteClipboardProvider` (`gpaste-clipboard-provider.{c,h}`) is a GObject *interface* that abstracts a single selection (clipboard or primary): it caches the current content, exposes `update`/`select_text`/`select_item`/`sync_text`/`ensure_not_empty`/`store`/`get_text`/`get_image_checksum`/`is_clipboard` as vfuncs, and declares the `changed` signal (emitted by implementations via `g_paste_clipboard_provider_emit_changed`). Crucially, none of its method signatures expose toolkit types, so the `GPasteClipboardsManager` drives every backend through this one contract. Both concrete backends hold their current selection in a shared `GPasteClipboardContent` (`gpaste-clipboard-content.{c,h}`): a content-kind enum plus a tagged union (text/image-checksum string, file list, or RGBA) with shared `clear`/`get_text`/`get_image_checksum` helpers, so only the field matching the kind is ever live. The same module owns the cross-backend policy helpers — `g_paste_clipboard_content_classify_text` (the trim/min-max/dedup decision both backends apply to incoming text) and `g_paste_clipboard_file_list_equal` (file-list dedup). The nine interface thunks each backend needs are generated by the `G_PASTE_CLIPBOARD_PROVIDER_DEFINE_VFUNCS (lc, UC)` macro; `ensure_not_empty` is a concrete method on the interface itself (driven by a per-backend `is_empty` vfunc), not reimplemented in each backend; it answers the item it put on the selection, or `NULL` when it left the selection as it found it, so a caller can act on a selection it did not perform itself. The daemon stays backend-agnostic: `g_paste_daemon_new (GPasteSettings *settings, GPasteClipboardProvider *clipboard, GPasteClipboardProvider *primary)` feeds the two providers the caller built to the manager, so the choice of backend lives entirely in the caller. A single `GPasteSettings` is threaded all the way through — the caller creates it once and the daemon (history, clipboards manager) and both providers share that instance rather than each making their own. The library ships one convenience wrapper, `g_paste_daemon_new_meta (settings, selection)` (mutter backend, used by the in-shell `daemon.js`, built only with `gnome-shell`); the standalone daemon builds its two GDK providers in `on_storage_ready` and calls `g_paste_daemon_new` itself, since that backend is not part of the library. `GPasteClipboardContent`'s helpers are marked `G_PASTE_VISIBLE` for exactly that reason — a provider living outside libgpaste-daemon still needs them. `new_meta`'s `selection` is the `MetaSelection`, typed `GObject *` so the introspected header needs no libmutter; GJS passes a `Meta.Selection` straight in. `GPasteClipboardGdk` (`src/daemon/gpaste-clipboard-gdk.{c,h}`) is the GDK implementation (the former `GPasteClipboard`), built on `GdkClipboard`; it requires the X11/XWayland backend because only X11 delivers *global* selection-ownership notifications. Construct it with `g_paste_clipboard_gdk_new_clipboard()` / `_new_primary()` (both returning a `GPasteClipboardProvider *`). A colour item is published as both `application/x-color` and a `text/plain` fallback (its `gdk_rgba_to_string()` form) so it can also be pasted into plain-text targets; the meta backend does the same. Text is *never* published through `gdk_content_provider_new_typed (G_TYPE_STRING, …)`: GTK serializes such a value through a `GConverterOutputStream` nobody owns, so the stream chain is finalized from the very main loop dispatch completing its write, and GIO's dispose then closes it synchronously — on X11 that lands in `gdk_x11_selection_output_stream_flush()` waiting on a `GCond` only this main loop could signal, deadlocking the daemon (most visibly on larger items, which go through INCR). `GPasteTextContentProvider` (`src/daemon/gpaste-text-content-provider.{c,h}`) replaces it everywhere text is offered (`select_text`, `select_item`, and the colour item's textual half). It keeps `G_TYPE_STRING` in its formats so our own selections still answer `gdk_content_formats_contain_gtype()` and local readers — one clipboard syncing into the other — get the value straight from `get_value()` with no serialize/deserialize round trip; it just never lets GTK serialize it. It also advertises the mime types itself (`gdk_clipboard_write_async()` only calls into a provider for the mime types the provider claims, and serializes the value for all the others), so it claims exactly what the `G_TYPE_STRING` serializers would have provided — `text/plain`, `text/plain;charset=utf-8` and, on a non-utf-8 locale, `text/plain;charset=<locale>` — and encodes all of it into the stream GDK owns and closes asynchronously. `text/plain` gets the utf-8 bytes, where GTK would have encoded it as ASCII with `?` replacements.

`GPasteClipboardMeta` (`gpaste-clipboard-meta.{c,h}`) is the experimental mutter implementation, built on `MetaSelection`. Unlike the GDK backend it is not a display client: it talks to mutter's server-side selection tracker, so it sees *every* selection ownership change globally (no keyboard-focus gating) — but that object is only reachable from inside the gnome-shell process, so the constructors (`g_paste_clipboard_meta_new_clipboard()` / `_new_primary()`) take the `MetaSelection` handed in by the shell glue (`global.display.get_selection()`). Reads go through `meta_selection_transfer_async()` into an in-memory stream; writes publish a source we keep a ref to so the resulting `owner-changed` is recognised as ours and skipped. Since mutter's `MetaSelectionSourceMemory` only ever advertises a single mimetype, writes use a file-local `GPasteClipboardMetaSource` (a `MetaSelectionSource` subclass holding an ordered mimetype list, a mimetype→`GBytes` map for byte payloads, and an optional typed `GValue` for GDK-serialised payloads). Plain text is stored as bytes under both `text/plain;charset=utf-8` and `text/plain`, and a text/uris item's special values (rich-text/HTML/XML, `x-special/gnome-copied-files`) are appended via `g_paste_special_atom_get()`, matching the `GdkContentProvider` union the GDK backend builds. Everything else is bridged through GDK rather than hardcoded mimetypes/byte formats: images (`GDK_TYPE_TEXTURE`), colors (`GDK_TYPE_RGBA`) and file lists (`GDK_TYPE_FILE_LIST`) are held as a `GValue`, the source advertises every format `gdk_content_formats_union_serialize_mime_types(type)` lists, and `read_async` lazily serialises the value (via `gdk_content_serialize_async`, closing the memory stream before `steal_as_bytes`) only for the format actually requested. Everything that source holds goes in **`dispose`** — the two tables, the `GValue`, and the mimetype list along with them — so `read_async` opens by refusing with `G_IO_ERROR_CLOSED` when they are gone: mutter can still be holding a source that has been disposed, and one that has must answer that it has nothing rather than look in tables that are no longer there. On read, `g_paste_clipboard_meta_pick_mime()` matches the offered mimetypes against the cached `gdk_content_formats_union_deserialize_mime_types(type)` set (preferring the canonical `image/png` / `text/uri-list`) and one `update_on_value`/`on_value_deserialized` pair decodes through `gdk_content_deserialize_async(.., type)`. So format coverage tracks GDK's installed loaders automatically, colors and uris are byte-identical to the GDK backend (fixing an `application/x-color` endianness mismatch the old hand-rolled ICCCM codec had, and gaining the `application/vnd.portal.filetransfer`/`files` mimetypes sandboxed apps need for file paste), and `images-support` gates capture in `update()` exactly as in the GDK backend. Only the text path keeps literal mimetypes (its GDK set is already exactly those two, and it needs the backend's own trim/length/dedup logic); `image/png`/`text/uri-list` survive solely as read-preference hints. It is gated on the `gnome-shell` build option and pulls `libmutter-51`; when that option is on it is compiled into `libgpaste-daemon` (which then links libmutter). The in-shell glue is `src/gnome-shell/daemon.js` (`GPasteDaemonRunner`): it runs the daemon inside gnome-shell — `g_paste_daemon_new_meta (settings, global.display.get_selection ())`, plus a `GPasteBus`/`GPasteSearchProvider` — with none of the standalone daemon's GApplication, signal handling or re-exec (its lifetime is the extension's; it owns the name first and always with replacement, builds the daemon from `name-acquired`, and exposes a `ready` promise the indicator awaits — see the ownership/handover notes above). gnome-shell cannot reload an extension's code from disk on wayland, so a plain `reexecute-self` is still a no-op; the exception is an **on-demand storage migration** (flagged by the client resetting `storage-backend-revision`), which is handled in place — deferred to an idle so the `Reexecute` dispatch unwinds first, it flushes, runs the `migrate`/`decrypt` concerns against its own prompt and calls `reload_storage` on the live daemon rather than recreating it (the daemon's self-referential D-Bus registration on gnome-shell's persistent connection makes a clean rebuild impractical). On a takeover (`name-lost` with `was_owned`, e.g. a manual `gpaste-daemon --replace`) it flushes, releases the lock and stands down for good until the next enable. The storage-migration dialog and encrypted-history unlock run in process, against `GPasteShellPrompt` (see Migration and Prompts). Note the `StorageBackend` static functions are introspected as `GPasteDaemon.StorageBackend.lock/unlock/set_passphrase` (class statics), not namespace-level functions. The same `MetaSelection` is used for both the clipboard and the primary provider: there is one per display and the two constructors select the `MetaSelectionType` (`CLIPBOARD` vs `PRIMARY`) internally.

To define a GObject that implements an interface, use `G_PASTE_DEFINE_TYPE_WITH_INTERFACE (TypeName, type_name, ParentType, IFACE_TYPE, iface_init)` (in `gpaste-macros.h`), as both clipboard implementations do.

The D-Bus method surface lives in `gpaste-daemon-methods.{c,h}`: free functions that take a small `GPasteDaemonMethods` context `{ skeleton, history, settings, clipboards_manager }` rather than the daemon's instance-private struct. **Errors are plain `GError`s in the `G_PASTE_ERROR` domain** (`gpaste-3/gpaste-error.{c,h}`, in *libgpaste* rather than the daemon library): a handler bails out with `G_PASTE_DBUS_ASSERT (cond, code, msg)` and the dispatcher has a single out-param to check. The domain is registered with `g_dbus_error_register_error_domain()` (from `g_paste_error_quark()`, which `g_paste_client_class_init()` calls so a client has it registered before any reply can carry one), which is what makes the mapping work both ways — the daemon's code picks the error name on the wire (`org.gnome.GPaste.Error.NotFound` and friends), and a client gets a `GError` back in the same domain instead of an opaque `G_DBUS_ERROR_REMOTE_EXCEPTION` string, so it can switch on the code rather than match on the message. That is also why the domain lives in libgpaste: both sides link it, so neither can drift. Adding a code means adding it to the enum *and* to the `GDBusErrorEntry` table beside it (a `G_STATIC_ASSERT` catches forgetting the second half); the names in that table are API. Image items are handed to clients as PNG bytes (`GetImage(uuid) → ay`) rather than through their file path, so where and how the daemon stores images stays its own business (which is what lets the SQLite backend keep them as database blobs); the toolkit-free `GPasteClient` exposes it as `GBytes` (`g_paste_client_get_image{,_sync,_finish}`) and libgpaste-gtk4's `g_paste_gtk_util_get_image_finish` is the GTK-side conversion to a ready-to-display `GdkTexture` (used by gpaste-ui for its thumbnails). **A colour needs no method of its own**, because the value on the wire *is* the colour: the daemon no longer decorates it (below), so a client drawing a swatch parses what it already has. **`GetUris(uuid) → as` stays** where that leaves nothing to ask for, because what it answers is an *array* and an item's value is one string: shipping the uris joined and having every client split them back apart is the parsing this whole arrangement exists to avoid. It answers uris rather than paths because any scheme reaches a uris item (what makes one is GDK deserialising the offer into `GdkFileList`, and `text/uri-list` carries `smb://` and `trash://` just as happily as `file://`), and `g_file_get_path()` has no answer for those. `GPasteUrisItem` reads them off its own `GdkFileList` (`g_paste_uris_item_get_uris()`) rather than splitting its value back apart, and the reply is a plain `GStrv`: `GPasteClient` lives in the toolkit-free libgpaste-3, so a `GdkFileList` would belong in libgpaste-gtk4 beside `g_paste_gtk_util_get_image_finish()`, the day a front end wants one. A password item still has no such method, deliberately: what it holds is the password. **The decoration itself is the client's**, which is what leaves only those two: the daemon sends an item's own content — a colour's colour, a files item's uris newline-joined, an image's dimensions and date, a password's *name* (never the password, which `get_value()` masks as `"******"`) — and each front end renders it, translating only the bare words `Files`, `Color`, `Image` and `Password` in its own catalogue. A files item is where rendering is more than a prefix: its uris are split, a local one is shown as its percent-decoded path with `$HOME` written `~` (a whole leading component only, so `/home/joe` never shortens `/home/joey`), anything else is left as the uri it is, and the lot is joined by spaces. That ran in the daemon once and got it wrong — a blind substitution over the uri spelled a home file `file://~/a`. The decoration itself is written once, in `g_paste_util_display_string (value, kind)` (`gpaste-3/gpaste-util.c`): `g_paste_client_item_get_display_string()` is that call cached per item, the daemon's search provider describes its results with it (the shell's interface takes finished strings, so those are the daemon's locale — nothing else can be done there), and the gnome-shell extension calls it straight through introspection (`GPaste.util_display_string()`), which is what keeps the extension from carrying a JavaScript copy of the same rules. It translates through the `GPaste` domain, which is also the extension's own (`metadata.json`), so the words come out in the locale of whichever process draws the row. `gpaste-client` deliberately decorates nothing: it is the scripting surface. The brackets are punctuation rather than language, so they are spelled in code — and the image is the one whose payload goes *inside* them, after a comma, because it reads as a description of the image rather than a name in front of one. The daemon still formats an image's dimensions and date, so those come out in *its* locale rather than the client's; structuring them would mean a field on the wire, and the divergence needs a daemon and a client running under different locales to show at all. `gpaste-daemon.c` keeps the object lifecycle, the signal emitters, the controller actions that need the GObject (`upload`, `reexecute`, `show_history`), and a dispatcher that builds the context and forwards. Everything but the `gpaste-daemon.c` entry point is compiled into the installed `libgpaste-daemon` shared library (exposed to in-tree consumers as `gpaste_daemon_internal_dep`), which both the `gpaste-daemon` executable and the test suite link.

### `src/client/` — `gpaste-client`

CLI tool for scripting and shell integration. Talks to the daemon via `GpasteClient`. Entry point for subcommands like `ui` (launches the GTK UI), `daemon-reexec` and `migrate` (resets the backend revision and re-execs the daemon to run an on-demand storage migration).

### `src/ui/` — `gpaste-ui` (GTK4 + libadwaita)

The main graphical history browser. Launched via `gpaste-client ui`. Contains widgets for the history list, item actions, search, and settings panel. Uses libadwaita widgets extensively:

- `AdwApplicationWindow` (via `G_PASTE_GTK_INIT_APPLICATION` which uses `AdwApplication`)
- `AdwHeaderBar` with `AdwWindowTitle` (current history name shown as subtitle), one per `AdwNavigationPage`
- `AdwShortcutsDialog` / `AdwShortcutsSection` / `AdwShortcutsItem` (keyboard shortcut help)
- `AdwToastOverlay` (wraps main content; every failed call reports through it)
- `AdwBanner` (shown on daemon connection failure)
- `AdwNavigationSplitView` (responsive split: history selector panel + history list), collapsed by an `AdwBreakpoint` below 600px
- `AdwStatusPage` (shown when history is empty or search has no results)
- `AdwEntryRow` (panel's "Switch or Create" entry, in a one-row `boxed-list` `GtkListBox` in the sidebar's `suffix` slot)
- `AdwSidebar` / `AdwSidebarSection` / `AdwSidebarItem` (history selector list in the panel sidebar)
- `AdwAboutDialog` (about dialog)

**Subclassing notes:** GTK4 made many widget classes final (`G_DECLARE_FINAL_TYPE`), preventing subclassing. Libadwaita re-enables subclassing for its own derivable types. Within the ui, internal widgets subclass derivable types: `GtkBox` (for `GPasteUiPanel`, `GPasteUiHistory`, `GPasteUiItem`), `AdwApplicationWindow` (for `GPasteUiWindow`), `AdwSidebarItem` (for `GPasteUiPanelHistory` — a GObject, not a widget), etc. `GtkStack` is final in GTK4 and cannot be subclassed — use `GtkBox` with manual visibility toggling instead. **`AdwHeaderBar` is final too**, which is why the window's header bar is a plain one carrying its buttons as qdata rather than a `GPasteUiHeader` type: with no type of its own, `ADW_IS_HEADER_BAR()` cannot tell it from anyone else's, so its accessors check that the qdata is there instead of assuming it.

**The header bar and the window's actions.** Everything the header carries drives a `win.` action installed on `GPasteUiWindow` (a `GActionMap`), so a button, a menu item and an accelerator can all ask for the same thing and the window owns the state. The bar holds what acts on the history — New Item at the start, Search, Pinned and Select at the end — and one `GtkMenuButton` (`open-menu-symbolic`) for what acts on the application: Track Clipboard Changes (a stateful boolean, so it renders as a check), Restart Daemon, Preferences, Keyboard Shortcuts, About GPaste. **The tracking check follows the daemon**: activating it *asks* (`SetActive`, with a confirmation on the way down) and sets nothing; the client's `tracking` signal is what moves it, so it can never sit somewhere the daemon is not. It used to be a `GtkSwitch` whose confirmation hung off a capture-phase `GtkGestureClick` — which meant the keyboard bypassed the confirmation *and* the D-Bus call entirely. Icon-only buttons set both a tooltip and `GTK_ACCESSIBLE_PROPERTY_LABEL`: GTK feeds a tooltip in as the accessible *description*, and an icon gives the button no name.

**Adaptive layout** (`GPasteUiWindow`): the window is an `AdwToastOverlay` over a `GtkBox` of an `AdwBanner` and an `AdwNavigationSplitView`, and nothing else — there is no window-level `AdwToolbarView`, because every bar belongs to one page or the other. Each `AdwNavigationPage` wraps its content in an `AdwToolbarView` with its own `AdwHeaderBar` (the sidebar's is bare, so it shows its page title; the content's is `g_paste_ui_header_new ()`, with the search bar as a second top bar and the merge `GtkActionBar` at the bottom). That is what gives a collapsed window a back button. An `AdwBreakpoint` added with `adw_application_window_add_breakpoint()` collapses the split view below 600px, and `on_history_changed` shows the content page when it is collapsed, so picking a history navigates to it. The minimum size is 360x294, the narrowest the HIG asks a window to survive.

**Reporting failures.** Client calls the user set off are finished, not discarded: a failed one says so in a toast (`g_paste_gtk_util_toast`, which takes any widget and finds the toast overlay from it) as well as in a `g_warning`. Two callbacks cover every call, because a `GPasteClient` `_finish ()` comes in two shapes — one answering nothing and one answering the uuid it created (which nothing in the UI wants; the item arrives as an `update`). Pair `g_paste_ui_report_void_cb` with what `g_paste_ui_report_void (origin, finish, message)` returns, or the `_string` pair; `origin` is any widget in the window, and the message is a translated literal kept by pointer. An `AdwSidebarItem` is a `GObject` with no widget to find a window from, so `g_paste_ui_panel_history_activate()` is handed one.

**Dialogs** (`gpaste-gtk4/gpaste-gtk-util.c`) are shared with the daemon's prompts. `g_paste_gtk_util_confirm_dialog()` takes a **heading** (the question — never the application's name and version, which asks nothing), a body (what confirming does), the confirm label and an `AdwResponseAppearance`: destructive is for what is actually lost, so restarting the daemon and pausing the clipboard are not. `g_paste_gtk_util_form_dialog()` is the shell every composer puts up — an `AdwDialog` with an `AdwHeaderBar` offering to cancel or to confirm, not an alert with a form in its extra child — built around whatever content widget the caller hands it and answering the confirm button, which is the caller's to follow: insensitive while the form is incomplete, and insensitive again once pressed, since `adw_dialog_close()` is asynchronous and the button stays live until it happens. `g_paste_gtk_util_text_dialog()` is that shell around a text view, the composer both New Item and Edit put up, and it hands the caller what was written (`NULL` when cancelled) rather than a buffer to read; `gpaste-ui-make-password.c` is the same shell around a two-row `AdwPreferencesPage`.

**Image previews** in `GPasteUiItem` rows are a small inline `GtkPicture` thumbnail (sized by `images-preview-size`, toggled by `images-preview`). Hovering it shows a larger detail preview via a custom `query-tooltip` (`gtk_tooltip_set_custom`), capped and aspect-preserved. The inline thumbnail is kept deliberately (glanceable, keyboard/touch-friendly); the tooltip only enlarges it on demand.

**Colour swatches** give colour items the same glanceable preview, in the same slot at the end of the row (an item is either an image or a colour, never both). The colour costs no call of its own, unlike an image's bytes: a colour item's value *is* its colour, the `[Color]` in front of it on the row being the client's own decoration (see the D-Bus section), so the swatch parses what the row already holds. A value `gdk_rgba_parse()` cannot read back costs the swatch and nothing else. The swatch itself (`gpaste-ui-color-swatch.{c,h}`) draws nothing and styles nothing: it is a `GtkPicture` showing a **one-pixel `GdkMemoryTexture`** of the colour, stretched over the widget with `GTK_CONTENT_FIT_FILL`, wearing Adwaita's **`.frame`** style class — *"1px solid currentColor at the border opacity"*, which is exactly what a hand-drawn outline has to work out for itself, except that the theme keeps it right when the theme changes — and `GTK_OVERFLOW_HIDDEN`, so the colour stays inside that frame whatever radius a theme gives it. No graphene, no GSK, no cairo, and no snapshot to rebuild per binding: setting a colour builds four bytes. The two alternatives both cost more than they give. **CSS** cannot do it at all: GTK4 has no supported way to give *one* widget its own colour — providers are per-display, and the per-widget pair (`gtk_widget_get_style_context()` + `gtk_style_context_add_provider()`) has been deprecated since 4.10, which the project's `GDK_VERSION_MAX_ALLOWED` gating would flag. **Drawing it** (a `GtkSnapshot` with a rounded clip and a border) works, but then the outline colour is ours to keep in step with the theme by hand. Its size is a constant, deliberately not `images-preview-size`: turning image previews off must not take the colours with it.

**Row actions** (`GPasteUiItem`): every per-item action is a `GSimpleAction` in a `GSimpleActionGroup` inserted on the row under the `item.` prefix — `edit`, `make-password`, `upload`, `pin` (stateful boolean, so the menu shows a check) and `delete`. **What a row can do follows its kind**, which it stores as the `GPasteItemKind` the item arrived with rather than as a boolean per action derived from it: one fact, so no two copies can drift. It is reset to `INVALID` on every (re)binding, in `_g_paste_ui_item_set_index()` — the search-row path writes the new uuid before the item's reply lands, so a kind left standing would steer the actions at the row's *previous* item while the uuid already names the new one, and `edit`, which opens one composer or the other, would hand a text item to the password composer. `upload` and `make-password` want a text item, `edit` a text item *or* a password — on a password `edit` opens the make-password composer (`gpaste-ui-make-password.{c,h}`) rather than the text editor, since its name and its timeout are the only things about it that can be written, and its value is not something any client can read back. That is also why `make-password` is offered on a text row alone: on a password row it would open the very dialog `edit` already does. Both go through `MakePassword`, which takes a password as readily as a text item, so there is one call and one dialog for the two. The inline buttons and the context menu are both named after them, so there is one implementation of each and one place that says whether the row can perform it: `g_paste_ui_item_update_actions()` disables the lot when the row has no uuid, which is what a row waiting for its content — or one the list view has just recycled — has. That replaced handing every button its own copy of the uuid and disarming them one at a time. Only **pin and delete** are inline, flat, and revealed on hover (a `GtkEventControllerMotion` on the row); a *pinned* row shows its star always, because there it is a badge rather than a button. Everything is in a `GtkPopoverMenu` parented to the row with `gtk_widget_set_parent()` — safe inside a `GtkBox`, since `GtkPopover` is a `GtkNative` and layout managers skip those — opened by a secondary-button `GtkGestureClick`, a touch `GtkGestureLongPress`, or `Menu`/`Shift+F10`. Those two keys are a `GtkShortcutController` on the **list view**, not on the row: what takes the focus is the `GtkListItem` above the row, so a controller on the row would never see them.

**Label widgets in list rows** use `GtkInscription` (not `GtkLabel`) for the main text display. `GtkInscription` is optimised for list-item cells and avoids overhead from markup/accessibility features not needed there. Set overflow explicitly with `gtk_inscription_set_text_overflow(GTK_INSCRIPTION_OVERFLOW_ELLIPSIZE_END)` — the default is `CLIP`. To display bold text, use `pango_parse_markup` to convert a markup string into `PangoAttrList` and pass it to `gtk_inscription_set_attributes`; call `gtk_inscription_set_attributes(NULL)` before `set_text` when switching back to plain text.

**Property-driven CSS classes** use `adw_bind_property_to_css_class()` rather than paired `gtk_widget_add_css_class`/`remove_css_class` calls: promote the state to a GObject property and bind the class to it (`GPasteGtkShortcutRow:capturing` → `accent`). Only worth it for a class that actually toggles — classes set once at construction stay plain `gtk_widget_add_css_class` calls, and a state that is not a property on some object (or has no object at all) is not a candidate.

**AdwSidebar** is used in `GPasteUiPanel` to list available histories. Its `suffix` slot holds the "Switch or Create" `AdwEntryRow` — inside a one-row `boxed-list` `GtkListBox`, because an `AdwEntryRow` warns when it is put anywhere else and the slot itself is an `AdwBin`. `GPasteUiPanelHistory` subclasses `AdwSidebarItem` (a `GObject`, not a widget) to represent each history entry. The right-click context menu (backup/empty/delete) is driven by a `GMenuModel` set on the sidebar, with `GSimpleAction`s installed on the panel widget under the `panel.` prefix.

**The history list** (`GPasteUiHistory`) is a `GtkListView` over `GPasteUiHistoryModel`, a `GListModel` of `GPasteUiHistoryItem` rows, driven by a `GtkSignalListItemFactory`. A row carries no history index of its own: in the positional view its index *is* its position, read from `gtk_list_item_get_position()` at bind time, so there is no second copy to go stale; in a uuid-driven view (a search, and later the favourites filter) it carries the uuid it displays instead — which is the model's `by_uuid` mode, entered through `g_paste_ui_history_model_set_uuids()`. `setup` builds one empty `GPasteUiItem`, `bind` points it at its model item (kicking off the async fetch of its content), `unbind` clears it. Because the view recycles a small pool of row widgets, `GPasteUiItem` carries a `generation` counter bumped on every (re)binding and on `refresh`, and every step of its fill chain drops a reply whose generation is no longer current — otherwise a D-Bus result for a previous binding would overwrite the row it has since been rebound to. That check (`g_paste_ui_item_still_wants()`) is made **before** the matching `_finish()` and also covers a cleared `client`: the callback data's ref keeps a disposed row allocated, not alive, and `dispose` drops the very client the call was issued on. `unbind` also drops the row's uuid, so a recycled row can be neither activated nor picked as the item it used to display. Model updates map to `g_list_model_items_changed()`: the tail on a resize (`set_size`), one row on an in-place replace (`item_replaced`), and the rows that kept their positions but not their contents on a refresh (`invalidate` — history entries shift as one is added or removed). `invalidate` **replaces the `GPasteUiHistoryItem`s** in the range as well as emitting the signal: `GtkListView` pairs each released row widget with the item pointer it was bound to, so handing the same pointers back makes it reuse the widget as-is and run neither `unbind` nor `bind` — the rows would go on showing what they showed. Since only realised rows are bound, an activation may find no widget to ask: a search row's uuid is in the model, and a positional row is at the index it sits at, one `GetItemAtIndex` away.

**Lazy history loading** (`GPasteUiHistory`): the display count is computed at runtime to fill the window — there is no `max-displayed-history-size` setting. `self->limit` is the count of items currently allowed on screen; `self->size` (= number of rows in the model) is `MIN (available, limit)`. The batch size (`g_paste_ui_history_batch()`) is one viewport's worth of items — `page_size / row-height + 1` — falling back to `G_PASTE_UI_HISTORY_DEFAULT_BATCH` until the viewport is allocated. The list view owns the rows, so the row height is not measured but derived from the laid-out content (`upper / size`, as the shell extension does) rather than from a widget we do not hold. The view grows `limit` by one batch on demand, driven by the scrolled window: the vertical adjustment's `changed` signal keeps loading batches while the content does not yet overflow the viewport (so the window always fills), and `GtkScrolledWindow::edge-reached` (`GTK_POS_BOTTOM`) loads the next batch each time the user scrolls to the bottom — lazily pulling in the whole history. A `self->loading` flag guards against re-entrant growth while a refresh is in flight. When the history shrinks, `limit` is clamped back down (`MIN (limit, MAX (batch, available))`) so lazy growth restarts from a single batch rather than eagerly reloading the old depth. There is no libadwaita lazy-scroll helper; `edge-reached` is the idiomatic GTK primitive for the bottom trigger (preferred over hand-computing the adjustment's value/page/upper). Growth applies to the positional view only: a search lists every match, since the view realises just the rows on screen and each fetches its own content on demand.

**Window state** (`GPasteUiWindow`) is left to GTK: there is no geometry GSettings key and none is needed. `GtkApplication` saves and restores it on its own — from its shutdown unless the app was forgotten, and on a timer while the app is focused. GPaste used to prompt a save right after a resize settled, through `gtk_application_save()`, but GTK added that API (22e45968e2) and then hid it again (eabaa008d5) before 4.24, so it is private and the call, along with the timeout that coalesced the per-frame resize notifications, is gone. `gtk_window_set_default_size (800, 600)` (plus a 400x300 minimum) is what `init` sets, as the fallback for a session with nothing saved. Should the API come back, the thing to restore is the coalescing: geometry notifications arrive on every frame of a drag-resize, and the default size must be applied *before* any `notify::default-width`/`-height`/`maximized`/`fullscreened` handler is connected, or every launch would save our own hardcoded default over the geometry the session was about to restore — the window is only presented from the async client callback.

**Window shortcuts** (`GPasteUiWindow`) go through a bubble-phase `GtkShortcutController`, not a hand-written `GtkEventControllerKey` handler: Ctrl+0-9 activates the item at that index, Escape leaves selection mode or closes. Build the digit triggers with `gtk_shortcut_trigger_create_with_aliases()` — GTK knows `KP_0`-`KP_9` are aliases of `0`-`9`, so one trigger covers the keypad twin and there is no second range to spell out. The index is passed as the shortcut's `GVariant` arguments rather than recovered from the keyval. A `gtk_callback_action_new()` callback returning `FALSE` propagates, which is how Escape defers to the search bar when it is open. Note `question` has *no* aliases, so `win.show-help-overlay` stays on `gtk_application_set_accels_for_action()` — an alias trigger would buy it nothing. The rest of the window's accelerators are registered the same way in `add_window_actions()`: `Ctrl+F` search, `Ctrl+N` new item, `Ctrl+,` preferences, `Ctrl+W` on GTK's own `window.close`.

**The initial focus** (`GPasteUiHistory`) is taken by the history list, from the list view's own `map`. Left to GTK it goes to the first focusable widget in the window, which is the sidebar's "Switch to history" `AdwEntryRow` — an editable, and the search bar captures keys in the *bubble* phase (`gtk_search_bar_set_key_capture_widget()` is only named "capture"), so whatever the user typed went into that entry instead of starting a search. `map` rather than the moment the list is revealed because a widget can only take the focus once it is mapped, and the list is hidden until there is something to list; the handler disconnects itself, so the grab happens once and no later refresh can pull the focus out of what the user is doing. A history that is empty on open therefore still leaves the focus in the entry — there is no list to map, and nothing to search either.

**Merge selection mode** (`GPasteUiHistory` + `GPasteUiHeader` + `GPasteUiWindow`): the header's "merge" button puts the list into a multi-selection mode to merge several entries into one. Selectability belongs to the selection model rather than to the rows, so `g_paste_ui_history_set_selection_mode()` swaps the one installed on the `GtkListView`: a `GtkMultiSelection` to pick with, a `GtkNoSelection` the rest of the time (and `single-click-activate` off while picking, so clicks select instead of pasting). Nothing has to be applied per row, which is what makes rows loaded lazily — or recycled — need no treatment of their own. A capture-phase `GtkGestureClick` added to each row in the factory's `setup` implements click-to-toggle (plain multi-select would need Ctrl/Shift), claiming the press and calling `select_item`/`unselect_item` at the row's current position, which it reads from the borrowed `GtkListItem`. The native row highlight is the only selection cue. Selection *order* is tracked in a `GArray` of **positions**, mirrored from the selection model's `selection-changed` (a `GtkBitset` cannot express order), so the merge keeps the order the user picked them, not the list order. Positions rather than uuids because an unrealised row has no widget to read a uuid off, and select-all over a lazily loaded history picks plenty of those — skipping them would silently merge only the on-screen subset. `g_paste_ui_history_get_selected_uuids()` resolves them at merge time: a search row carries its uuid in the model, and positional ones are looked up in the history (one `GetHistory`, on the first row that needs it, since a position *is* a history index). The list emits `selection-changed(count)`; the window updates the header count (`g_paste_ui_header_set_selection_count`) and the "Merge" button's sensitivity (needs ≥2). The header swaps to a selection look by *emptying itself*: everything that does not belong to picking is hidden, Cancel replaces the merge button and the title shows "N selected". There is no style class for it — libadwaita defines `.selection-mode` for `checkbutton` alone, so the one the header used to add styled nothing. The rows' own actions are hidden with them, through `GPasteUiItem:selection-mode` bound to `GPasteUiHistory:selection-mode` at factory `setup`. The merge controls live in a bottom `GtkActionBar` (`adw_toolbar_view_add_bottom_bar`) revealed with the mode; "Merge" is a `GtkMenuButton` whose popover offers separator presets plus a custom-separator entry — choosing one collects the ordered uuids (`g_paste_ui_history_get_selected_uuids`) and calls `g_paste_client_merge()` (empty decoration), then leaves the mode. Escape and the cancel button also leave it.

### `src/preferences/` — GTK4 + Adwaita preferences application

Standalone preferences window. Uses GTK4 and libadwaita for a modern look.

The four pages come from `g_paste_gtk_preferences_pages_new()`, which two things consume: `GPasteGtkPreferencesWidget`, the embeddable view stack with a switcher and a breakpoint of its own, and `g_paste_gtk_preferences_pages_list()`, an introspectable `GPtrArray` of the same pages for a caller that has a preferences window already — which is what the GNOME Shell extension has. The pages bind to a `GPasteSettings` without holding a reference, so whoever hands them out keeps one alive: the widget in its private struct, the list on each page's qdata.

### `src/gnome-shell/` — GNOME Shell extension (JavaScript)

Native shell integration. Provides the panel indicator and quick-access popover, and — only when the opt-in `experimental-meta-daemon` setting is set (read once in `enable()` via `new GPaste.Settings()`) — hosts the in-process daemon (`GPasteDaemonRunner`, see the meta clipboard backend); by default it hosts no daemon and is a pure client of the standalone one. Communicates with the daemon via D-Bus. Lives under the standard GNOME Shell extension layout with `extension.js` as entry point. **The UUID, `GPaste@gnome-shell-extensions.gnome.org`, claims the domain of the official gnome-shell-extensions bundle, which GPaste is not part of** — against extensions.gnome.org's own rules. Renaming it orphans every user's `enabled-extensions` entry, so it stays until there is a migration to ship with it. `metadata.json` carries the integer `version` that site expects, with the string in `version-name`; meson encodes it as `major * 100 + minor`, since e.g.o refuses a re-upload of a number it already has and the major alone would leave every point release in a series unpublishable. A pre-release names its minor instead of numbering it (`51.beta`), so there is nothing to add: meson maps the name to one of the ten numbers just below `major * 100` from a table (`alpha`/`beta`/`rc`, each with an optional `1` or `2`), which keeps every pre-release under each point release of the series it leads to and keeps them in order among themselves. A name not in that table is a configure-time error rather than a shared number: two pre-releases encoding the same integer means e.g.o refusing the second, so the shell keeps the code of the first. `prefs.js` implements `fillPreferencesWindow()` and adds the pages from `g_paste_gtk_preferences_pages_list()`: the default `getPreferencesWidget()` path wraps whatever it is given in an `AdwPreferencesGroup` inside an `AdwPreferencesPage`, which nested GPaste's own switcher inside a single row of the extensions window. `prompt.js` is the extension's **`GPastePrompt` backend** (`GPasteShellPrompt`, `GObject.registerClass({Implements: [GPasteDaemon.Prompt]})` with `vfunc_passphrase`/`vfunc_migration`): `ModalDialog`s built from `St.PasswordEntry`, `CheckBox` and `BarLevel`, which is what lets the in-shell daemon run the storage migration, unlock and re-key in process instead of shelling out. It renders what the C side hands it — the offered backends and their labels, the import/cleanup sensitivity rules, the libpwquality strength rating — so the two prompt backends cannot drift apart. St has no radio button, so the backend choice is a set of `CheckBox`es kept mutually exclusive by hand, and `BarLevel` is normalized to 0..1 so the 0-4 rating is scaled into it. `enable()` creates the indicator and, in the experimental mode, the runner; that runner must survive the screen lock so the clipboard keeps being recorded, so `metadata.json` declares `session-modes: ["user", "unlock-dialog"]` and the extension stays enabled across the lock (`enable`/`disable` are no longer called on lock/unlock). The panel indicator must **not** be reachable from the lock screen, so it is gated on `Main.sessionMode` (`'updated'` signal): created only in `user` mode — and, when the in-shell runner exists, only once `runner.ready` resolves so a fresh start never activates the standalone daemon over D-Bus (without a runner the indicator is added straight away, and that client call is exactly what activates the standalone daemon) — and torn down whenever the mode leaves `user`. `disable()` tears down the indicator and calls `runner.shutdown()` (which flushes, unowns the name, releases the lock); the standalone daemon then resurrects lazily via D-Bus activation if a client calls. **Reporting the extension's state** (the `track-extension-state` key: "stop tracking the clipboard when the extension does", enforced daemon-side in `g_paste_daemon_methods_extension_state_changed`) has to survive an indicator that cannot do it — there is none outside the `user` mode, and one that never reached a daemon has no client to report through. `shutdown(extensionDisabled)` therefore *returns* whether it reported, and `disable()` reports it itself when it did not, so exactly one of the two reports whichever way it went and neither has to guess about the other. **Hosting the daemon ourselves comes before both**, because neither can work there: both are D-Bus calls dispatched a main loop turn later, and `runner.shutdown()` — the next statement — releases the name and drops the daemon they were addressed to, so the policy would silently never be applied on the one setup where we *are* the daemon. `runner.reportExtensionGone()` calls `g_paste_daemon_extension_state_changed (daemon, FALSE)` directly instead (the same `track-extension-state` policy, factored into `g_paste_daemon_methods_extension_state_changed()` so the D-Bus method and the in-process host cannot drift), and reports whether there was a daemon of ours to tell. That fallback (`_reportExtensionGone()`) calls `ReportExtensionState(false)` **straight on the session bus with `DO_NOT_AUTO_START`**, not through a `GPasteClient` (whose proxy is built with `G_DBUS_PROXY_FLAGS_NONE` and would activate a daemon — being disabled must never be what *starts* one) and **never by writing `track-changes` itself**: whether the extension going away should stop tracking is a policy the daemon owns and already implements, and a client second-guessing it duplicates the `track-extension-state` gate. A `G_DBUS_ERROR_SERVICE_UNKNOWN` back means no daemon is running, so nothing is tracking the clipboard and there was nothing to report to; it is swallowed, anything else is logged. The `extensionDisabled` flag is what keeps the lock screen out of it: tearing the indicator down for a non-`user` mode is not the extension going away, and reporting it there would stop recording for the whole locked session — precisely what the `unlock-dialog` session mode exists to prevent.

**History display** (`indicator.js`): the popover history is an `St.ScrollView` (its `max-height` capped at 60% of the monitor work area) wrapping a `PopupMenuSection`, filled lazily. The batch size is computed at runtime to fill the viewport — `_fillBatch()` returns `page_size / average-row-height + 1` (the average derived from the laid-out content, `upper / loaded`), falling back to `_DEFAULT_BATCH` before anything is laid out. `_loadMore()` appends a batch of `GPasteItem` rows, and the scroll view's `vadjustment` drives growth — `changed` keeps appending while the content does not yet overflow the viewport, `notify::value` appends the next batch when the user scrolls to the bottom — so the whole history is reachable by scrolling (no page switcher). St does not recycle actors, so only the scrolled-to depth is ever materialised; `_reloadGeneration` guards against overlapping async reloads, and rows are `destroy()`ed on reload. (We deliberately do *not* use `PopupSubMenu`: it is the same `St.ScrollView` primitive but as a click-to-expand "More…" submenu whose scroll is tied to the top menu's max-height, and it holds every item with no virtualisation — so it would force an expander UX and lose the lazy loading.) Because the section is nested in the scroll view (not the menu's item tree), each row's `activate` is wired to `menu.itemActivated()` by hand to close the menu.

**The footer** (`actions.js`) is three plain `PopupMenuItem`s — Open GPaste, Empty History, Preferences — added under a separator. They used to be `St.Button`s in one *non-reactive* `PopupBaseMenuItem`, through which no key event ever bubbled, so the indicator answered Left, Right and Up itself from the menu actor to move a focus the buttons could not move on their own. Menu items navigate themselves; the one step left by hand is Up from the first of them to the last history row, which is not in the menu's item tree. There is no About: a panel menu is not where an application's about dialog belongs.

**Row previews** (`item.js`): a row shows what it holds next to its text — an image thumbnail, a colour swatch, nothing else — in one `St.Bin` slot added before the pin/delete buttons (an item is never both, and an empty bin takes no room). The image bytes come from `GetImage`, for the reasons the D-Bus section gives; the colour is read straight off the item, its value being the colour itself. That fetch is guarded by the row's `_generation`, exactly as its own content fetch is — St does not recycle actors, but a row *is* refilled in place on reload, and a reply for the item it used to hold must not paint it. A thumbnail is an `St.Icon` over a `Gio.BytesIcon`: `st_icon_theme_lookup_by_gicon_for_scale()` has a `G_IS_LOADABLE_ICON` branch, and the load ends in `gdk_pixbuf_new_from_stream_at_scale (stream, size, size, TRUE)` — scaled into `icon_size` with its aspect ratio kept, so nothing has to be decoded in JS. It is loaded *uncached* (`ST_TEXTURE_CACHE_POLICY_NONE`), because `g_icon_to_string()` cannot serialize a `GBytesIcon` into a cache key — one decode per fill of a row, which is what a handful of image rows costs and the reason not to reach for this for anything bigger. A swatch is an `St.Widget` with an inline `background-color` and an outline spelled out rather than themed: the graphical tool's swatch takes that line from Adwaita's `.frame`, but that class belongs to GTK — the shell stylesheet defines nothing like it, and St's CSS supports neither `currentColor` nor `color-mix()` to rebuild it from. So the two swatches are drawn by different means on purpose, and only the GTK one follows the theme. The shell one interpolates the item's value into an inline style, and that value reached a colour item only because `gdk_rgba_parse()` accepted it — so it is a colour and not arbitrary text. `_showColor()` checks its shape anyway before building the declaration, because what it goes into is a stylesheet and something off the clipboard has no business ending one early. It is outlined in a fixed mid-grey rather than the theme's foreground, which a swatch cannot ask for before it has a theme node, and sized by a constant rather than by `images-preview-size`: turning image previews off must not take the colours with it. The two image-preview settings are watched **once, by the indicator**, which pushes them into every row (`setImagesPreview()`) as it does the element size — one subscription for the menu instead of one per row — and `_createRow()` pushes them into a new row straight away, before its constructor's async fetch can land.

### `data/`

Non-code resources: D-Bus service files (`dbus/`), `.desktop` entries, GSettings schemas (`gsettings/`), systemd user units, AppStream metadata, shell completions (`completions/`).

**The release notes live outside the translated metainfo.** `data/metainfo/org.gnome.GPaste.Ui.metainfo.xml.in` carries `<releases type="external"/>` and the notes themselves sit in `data/metainfo/org.gnome.GPaste.Ui.releases.xml`, installed to `$datadir/metainfo/releases/`, where AppStream looks for `<component-id>.releases.xml`. That file is deliberately absent from `POTFILES.in`: the notes of a version nobody runs are never shown to anyone, yet every one of them sat in the catalog forever and fuzzied across every language the moment an old entry was touched. A new release adds its `<release>` there and nowhere else.

**Shell completions** (`data/completions/gpaste-client` for bash, `_gpaste-client` for zsh, `gpaste-client.fish` for fish — each behind its own `*-completion` meson option) mirror `gpaste-client`'s `commands[]` table, so a new verb or alias has to be added to all three and to `man/1/gpaste-client.1` — `show_help()` is printed from the table itself and needs nothing. `meson test -C build completions` fails until they agree. Two rules every one of them follows:

- **Never activate the daemon from a completion.** Anything that needs the daemon first checks `org.freedesktop.DBus.NameHasOwner org.gnome.GPaste` with `gdbus` and gives up when nothing owns it — otherwise hitting tab would start a daemon behind the user's back and block the shell while it loads the history.
- **History names come off disk**, not from `list-histories`: one file per history, `<name>.{xml,xmls,db,dbs}` under `$XDG_DATA_HOME/GPaste` (falling back to `.../gpaste`, the same `PACKAGE_NAME`/`PACKAGE` rule as `g_paste_util_get_history_dir_path`), so they complete instantly with no daemon at all.

`gpaste-client` reads stdin whenever it is not a tty, so every invocation from a completion redirects `</dev/null` — completion does not always run with a terminal on stdin, and it would otherwise block waiting for EOF. Item arguments complete to uuids, or to indexes when `--use-index` is in effect.

### `po/`

Translations managed via Weblate. Add new strings to the relevant `.c` source with `_()` / `N_()` and update `po/POTFILES.in` if adding a new file — the `potfiles` test (`tests/i18n/test-potfiles.py`) fails when the two drift apart, in either direction: a source carrying a string it does not list, or an entry whose strings have all moved away. Both matter, one because xgettext silently skips what it is not given and nobody notices until a dialog comes out untranslated, the other because a list nobody prunes stops describing anything.

- **A translatable string carries no whitespace of its own**, and nothing is glued onto one: write `g_strconcat (_("[Files]"), " ", rest, NULL)`, never `_("[Files] ")`. A trailing space inside a msgid is invisible in every translation tool there is, and `nl_NL` duly dropped it — the one that had it is the one that broke.
- **One msgid means one thing.** `_("Empty")` was both the button that empties a history and the page saying one is empty; languages that need two words got one. Disambiguate with `C_("verb", "Empty")` rather than rewording around it — the context is in the catalog, so translators see the distinction the code makes. (The status page says "No Items" now, so only the verb is left; it keeps its context, because what it is is still a verb.)
- **Header capitalization for labels, sentence case for sentences.** Buttons, menu items, row and page titles, the headings above them and the short tooltips that name a control are written "Show Only Pinned Items"; descriptions, subtitles and tooltips that are sentences are written "Type the name of a history to switch to it". British spellings ("behaviour", "favourite") are the project's own and stay.
- **The catalogs carry no obsolete entries.** `msgmerge` parks a string that has left the sources at the bottom of every `.po` as a `#~` block, and nothing ever reads one back: Weblate does not show them, `msgfmt` does not compile them, and a translator who needs an old wording has git. `release.sh` runs `msgattrib --no-obsolete` over `po/*.po` right after `GPaste-update-po`, so what a release removes leaves for good instead of silting up.
- **The GSettings schema carries `gettext-domain="GPaste"`** on its `<schemalist>`. Its summaries and descriptions are extracted whether or not it does (they are in `POTFILES.in`, and xgettext knows the schema through GLib's ITS rules), but nothing looks the translations up without it: for a long time every one of those 58 strings was translated and then shown in English by dconf-editor and GNOME Settings alike.

## Key dependencies

- GLib/GObject/Gio ≥ 2.90 (declared as `2.89.0` in `meson.build` so it configures against the development releases)
- GTK4 ≥ 4.24 + libadwaita ≥ 1.10 (UI and preferences; GTK4 declared as `4.23.2` for the same reason)
- GCR (`gcr-4`) ≥ 4.0 (password item storage; also the secure-memory allocator for encryption secrets)
- gjs ≥ 1.78 (GNOME Shell extension runtime)
- gtk4-x11 (the daemon forces the GDK x11 backend at startup)
- libsodium (optional; gated by the `encryption` meson feature, `auto` by default — history-encryption converter)
- libsecret (optional; gated by the `libsecret` meson feature, `auto` by default, and only used together with encryption — remembers the history passphrase in the keyring)
- libpwquality (optional; gated by the `pwquality` meson feature, `auto` by default, and only used together with encryption — rates the passphrase strength in the new-encrypted-history prompt)
- sqlite3 ≥ 3.35 (optional; gated by the `sqlite` meson feature, `auto` by default — the SQLite history storage backend; 3.35 for `RETURNING` and `UPDATE … FROM`)

`meson.build` derives `GLIB_VERSION_MIN_REQUIRED`/`MAX_ALLOWED`, `GDK_VERSION_*` and `ADW_VERSION_*` from those requirements, pinning min and max to the same value so both newer-than-required and newly-deprecated API warn. GLib and GTK only define macros for their stable (even) minors, so an odd minor is rounded up; libadwaita releases every minor as stable and defines a macro for each, so its derivation must **not** be rounded.

Image items use `GdkTexture` from GTK4 directly — there is no longer a GdkPixbuf dependency.

On Fedora: `dnf install meson ninja-build glib2-devel gtk4-devel gcr-devel libadwaita-devel gjs-devel`
