GNOME HIG AUDIT
Audited against gnome-spec (himachal:~/dev/gnome-spec) v1.1.0 β the
GNOME GUI Specification compiled from HIG v47 plus source audits of 33 GNOME
Core/Circle apps. Section numbers below refer to GNOME-GUI-SPEC.md.
The closest reference app is GNOME Software (audits/gnome-software.md) β
same problem domain (system updates), same shape (hero status + list of
pending changes + detail drill-down).
Scope: static audit of the widget tree as constructed in src/. Screenshot
verification is tracked separately in docs/GUI_TESTING.md.
Summaryβ
| Area | Verdict |
|---|---|
| Window architecture | β Correct |
| Icon style | β Symbolic throughout |
| Empty/status states | β
AdwStatusPage used consistently (15 sites) |
| Destructive-action dialogs | β Correctly reserved for non-undoable actions |
| Transient feedback | β Toasts (20 sites) |
| Adaptive design | β No breakpoints at all |
| Settings storage | β Hand-rolled JSON, not GSettings |
| Navigation pattern | β Hand-rolled gtk::Stack instead of AdwNavigationView |
| Tooltips | β 4 in the entire app; icon-only back button has none |
| Access keys | β 3 use_underline sites, all in the menu |
| Preferences search | β Explicitly disabled |
Status as of 2026-07-26β
| # | Finding | State |
|---|---|---|
| 1 | Adaptive breakpoints | β
fixed β the real cause was gtk::Stack being homogeneous by default, so it requested the largest width of every page including hidden ones; the visible idle page inherited the ~555px minimum of the history/changelog rows. With hhomogeneous/vhomogeneous off, plus AdwBreakpoint, width-request 400β360, and wrapping row labels, the content minimum dropped 579px β 240px and the window renders correctly at 360Γ640. Verified by the narrow screenshot check. |
| 2 | GSettings migration | β
fixed β data/*.gschema.xml.in, templated on application_id so Devel and release get separate schemas and cannot clobber each other's config. An existing settings.json is imported exactly once, guarded by a migrated-from-json key rather than by deleting the file (so a downgrade still works, and a stale file can't later overwrite GSettings changes). Verified end-to-end: a JSON file with distinctive values (weekly, 42, dry_run=true, include_app_updates=false) appeared verbatim in dconf with the guard flipped. Deliberately fail-safe: gio::Settings::new() aborts the process on an unknown schema id, and the schema only exists once installed β so the lookup is checked first and the legacy JSON path remains the fallback. That matters because Settings::load() is what privileged() consults, and a hard dependency would turn "schema missing" into "dry-run silently stops working". The handle is thread-local, since gio::Settings is !Send + !Sync and Settings::load() is called from background threads. |
| 3 | AdwNavigationView | β
fixed β the root of StatusView is now an AdwNavigationView. The three genuine drill-downs (Image Source, Image History, What's New) are AdwNavigationPages pushed by tag; the five state pages (idle/updating/complete/up_to_date/error) correctly stay in a gtk::Stack, since those are mutually exclusive states of one screen rather than places you navigate to. This buys edge-swipe back, Escape / Alt+Left, focus restoration on pop, and per-page titles. Returning home is a pop_to_tag("main") rather than a push, so the stack cannot grow each time the user goes back. |
| 4 | Tooltips | β
fixed β the header-bar back button and the history expander chevron now carry both tooltip-text and an accessible label. A full sweep of the remaining icon-only controls found the rest (pin, pull, roll back, calendar month nav, copy log, what's new, change image) already had tooltips; the original count of "4" came from a per-line grep that missed tooltips set on an adjacent builder line. |
| 5 | Access keys | β
fixed β the preferences dialog had zero use-underline rows; access keys added to all nine focusable rows (Automatic Background Updates, Include App Updates, Configure Automatic Updates, Check Interval, Custom Interval, Pause on Metered Connections, Developer Mode, Enable Hardware Checks, Save to /etc/uupd/config.json). Group and page titles are headings, not focusable controls, so they are deliberately left alone. |
| 6 | Preferences search | β
fixed β set_search_enabled(true). |
Also fixed while auditing, though not HIG findings as such: the dev-mode banner claimed "updates are simulated" during a dry run, which was simply untrue β it now distinguishes the two safety modes.
Six findings, ordered by impact below. Findings 1β3 are architectural and affect the gnome-control-center panel deliverable directly; 4β6 are mechanical.
1. No adaptive breakpoints ββ
Spec Β§8.1, Β§2, Checklist "Adaptive: works at 360px width minimum".
grep -rn "Breakpoint" src/ returns nothing. The window declares:
// src/app.rs:855
adw::ApplicationWindow {
set_default_size: (750, 700),
set_width_request: 400,
set_height_request: 500,
A hard width_request: 400 means the window cannot be narrowed to the
360px the HIG requires, and nothing re-lays-out when it approaches that.
Why it matters more than usual here. This is not only a phone-form-factor
concern. The second deliverable embeds UpdatesPanel into
gnome-control-center, whose content pane is resized by the shell's own
breakpoints β Settings collapses to a single-pane layout on narrow windows.
A panel that refuses to go below 400px will either clip or force the whole
Settings window wider than the user asked for.
Fix. Add an AdwBreakpoint on the window at max-width: 550sp, and have
the rebase dialog's calendar grid + details panel switch from side-by-side to
stacked. rebase_dialog.rs:47 already notes the 720px sizing is a tight fit β
that comment is the same problem observed from the other side.
2. Settings are hand-rolled JSON, not GSettings ββ
Spec Β§9.1 and Anti-Patterns: "Create custom settings storage β Use GSettings/GSchema".
There is no *.gschema.xml anywhere in the tree. src/settings.rs writes
$XDG_CONFIG_HOME/finupdate/settings.json by hand.
Consequences beyond spec compliance:
- No
dconfvisibility β settings can't be inspected, reset, or managed by policy the way every other GNOME app's can. - No change notification. GSettings emits
changed::signals; JSON does not. This is why the code re-readsSettings::load()at call sites (status_view.rs:1920,:2051,:2362,:3324) instead of binding once β and why those sites had to re-checkdry_rundefensively, since another part of the app may have rewritten the file in the meantime. - The control-center panel is the real problem. gnome-control-center panels are expected to expose their settings through GSettings so the Settings search index can find them. A JSON blob is invisible to it.
Fix. Define org.tunaos.finupdate.gschema.xml, migrate the
Settings fields, and use gio::Settings::bind() for the switch/combo/spin
rows β which also removes the manual Rc<RefCell<Settings>> plumbing in
preferences.rs. Keep a one-shot importer for existing settings.json files.
Note this composes cleanly with the new RuntimeOverrides layer: CLI
overrides stay in-memory and GSettings becomes the persistent tier.
3. Navigation is a hand-rolled gtk::Stack ββ
Spec Β§3 "Stack + Back Navigation", Anti-Patterns: "Mix navigation patterns".
The app implements hierarchical drill-down (PageChanged / GoBack,
app.rs:147, :724, :992) over a raw gtk::Stack
(status_view.rs:947), with the back button's visibility toggled by hand
(app.rs:910, status_view.rs:320).
AdwNavigationView exists for exactly this and provides, for free:
- the back button and its visibility,
- edge-swipe back gestures (currently absent β a touch/trackpad regression against every other GNOME app),
- per-page titles wired into
AdwHeaderBar, - correct focus restoration when popping a page,
Escape/Alt+Lefthandling.
Distinguish two uses of gtk::Stack here. Using a stack to switch
visual state (Idle β Updating β Complete β Error, status_view.rs:947) is
legitimate and should stay β that's state, not navigation. The finding is
specifically about page navigation (preferences, changelog, rebase
subpages), which should become AdwNavigationView.
ffi.rs:252 and :277 already describe the panel widgets as destined for an
AdwNavigationView β the control-center side assumes this shape, so the
standalone app is the odd one out.
4. Tooltips are nearly absent ββ
Spec Checklist "All buttons have tooltip-text"; Anti-Patterns: "Skip
tooltips on header bar buttons".
Four set_tooltip_text calls exist in the whole app:
| Site | Widget |
|---|---|
app.rs:871 | Main Menu |
status_view.rs:1111 | Change image variant |
status_view.rs:1218 | What's new |
status_view.rs:1650 | Copy log |
The header bar back button is icon-only with no tooltip (app.rs:910):
let back_btn = gtk::Button::builder()
.icon_name("go-previous-symbolic")
.visible(false)
.build();
An icon-only control with no tooltip and no accessible label is unusable with
a screen reader and unclear to a new user. Every icon-only button needs
tooltip_text (which libadwaita also surfaces as the accessible name).
5. No access keys ββ
Spec Checklist "Access keys on all labeled controls"; Anti-Patterns: "Skip access keys".
Three use_underline sites exist, all in the menu model (_Keyboard Shortcuts, _About Finupdate, _Quit β app.rs:880). None of the
preference rows, dialog buttons, or action buttons declare one.
Fix. Add _ to labels and use-underline: true on AdwSwitchRow,
AdwComboRow, AdwSpinRow, and every AlertDialog response.
6. Preferences search is explicitly disabled ββ
Spec Β§9.4 and Checklist "search-enabled: true".
// src/ui/preferences.rs:55
dialog.set_search_enabled(false);
The preferences dialog has multiple groups plus a nested uupd subpage with eight-plus rows β comfortably past the point where search earns its place. This looks like a deliberate choice made when the dialog was smaller; it should be flipped back on.
What is already rightβ
Worth recording so it doesn't regress:
- Window architecture (Β§2) β
AdwApplicationWindowβAdwToolbarViewβAdwHeaderBar+AdwWindowTitle. Textbook. - Icons (Β§7.4) β every icon reference is
-symbolic. No exceptions found. - Status pages (Β§6.4) β
AdwStatusPageat 15 sites for empty/terminal states. - Dialog discipline (Β§6.3, Anti-Patterns "confirmation dialogs for
undoable actions") β
AdwAlertDialogis used only for genuinely irreversible operations: reboot, powerwash, factory reset, rollback, image switch/pin/unpin, and the NVIDIA-downgrade warning. Everything transient goes to a toast. This is the distinction most apps get wrong, and finupdate gets it right. - Preferences containers (Β§4.1) β correct
AdwPreferencesDialogβAdwPreferencesPageβAdwPreferencesGroupβ row hierarchy.
Suggested orderβ
- GSettings migration (#2) β unblocks the panel deliverable and removes the re-read-settings-everywhere pattern that made dry-run hard to reason about.
AdwNavigationView(#3) β the panel already assumes this shape.- Breakpoints (#1) β needed before the panel can be embedded honestly.
- Tooltips (#4), access keys (#5), preferences search (#6) β mechanical, independently landable, good first commits.