fix: XDG config, keyring-backed pairing keys, named transport failures — plus configurable keep-awake - #34
Merged
Conversation
emir-hasanbegovic
force-pushed
the
fix/xdg-config-keyring-error-flows
branch
2 times, most recently
from
August 21, 2026 13:51
42f6093 to
16cd474
Compare
…lures
Four related cleanups to how Dish stores its state, reports why a connection
failed, and handles a crash, plus the CMake warnings the build printed on
every configure.
Config: one file, in an XDG location
Every store and repository default-constructed QSettings("Dish","Dish"), a
Windows registry path (HKCU\Software\Dish\Dish) carried over verbatim from the
dish-windows port. On Linux that resolved to ~/.config/Dish/Dish.conf, while
anything that default-constructed QSettings after main() set the organization
landed in ~/.config/TinkerNorth/Dish.conf instead. The app wrote two files, and
the one the README documented held a single UI flag while the other held the
device id, the remembered satellites and the pairing keys.
repository/AppSettings is now the single place that decides the location:
$XDG_CONFIG_HOME/com.tinkernorth.Dish/dish.conf, keyed by the same reverse-DNS
app id as the .desktop entry, the AppStream metainfo and the Flatpak. Both old
files are folded in on first run, key by key, never overwriting one the new
file already has, then renamed .migrated so a bad migration stays recoverable.
Pairing keys move to the desktop keyring
A pairing key authenticates every REST call and is the HKDF input for the
session key, and it sat in plaintext in a file created 0644-minus-umask. It now
lives in the desktop keyring over org.freedesktop.secrets, which gnome-keyring
and KWallet both implement; QtDBus was already linked, so this adds no
dependency. Where no Secret Service answers the config file is still used, now
0600.
Two ways a plaintext copy could have survived the move are closed: the legacy
wifi_shared_key/ entries are removed once migrated rather than left beside the
new namespace, and a key found in the file while the keyring already holds one
is swept instead of being skipped by the early return.
Transport failures are named
RawReply collapsed every pre-HTTP failure into status 0, so host-down, refused,
timed-out and a TLS handshake failure were indistinguishable to both the UI and
the log. TransportFailure carries the cause through to the session FSM. It is
strictly diagnostic: classifyRest still folds every value into Unreachable, and
a test asserts the verdict is identical for all of them, so a cause can never
change a retry decision.
The user-visible effect is that a refused connection now says the satellite is
not running rather than suggesting the machine is powered off. The log effect
is that a remembered-but-absent satellite writes one line per cause CHANGE
instead of one per backoff tick, with per-tick detail behind
QT_LOGGING_RULES="dish.net.debug=true".
This also fixes the "QIODevice::read (QNetworkReplyHttpImpl): device not open"
warning that a remembered satellite printed on every retry: the finished
handler read the reply without checking whether it was still open, which it is
not after an abort or a failed connect.
Crash reports are local and explicit
The Settings toggle said crash reports were shared and helped fix bugs while
nothing was ever transmitted. Collection is local and always was, so the copy
now says that, and UI/CrashReport turns the backtrace into a report the user
reads before deciding to send. redact() strips the home directory, IPv4
literals, .local hostnames and any hex run of 32 characters or more, so a
pairing key on the stack cannot be pasted into a public issue tracker.
Translation contexts
QCoreApplication::translate was called with kTrContext, a variable. lupdate
cannot resolve one, so none of the user-facing strings in WifiConnectionManager
or PairingClient were ever extracted and all of them shipped untranslated in
every language. Using the literal at each call site recovers 12 strings, nine
of which predate this change.
CMake warnings
Configuring printed fourteen warnings.
Thirteen came from qt6_import_qml_plugins, which links QML plugins only when
their target is a STATIC_LIBRARY; against a shared Qt the scan cannot produce
anything linkable. Debian and Ubuntu additionally default
QT_SKIP_AUTO_QML_PLUGIN_INCLUSION to ON, so the plugin targets do not exist and
each one warns. Turning that default back off is not viable: Ubuntu ships no
QmlAssetDownloader module, and auto-inclusion makes Qt6Quick_FOUND false and
fails the configure outright. The scan is now skipped where it cannot do
anything, on a shared Qt, via Qt's own QT_QML_MODULE_NO_IMPORT_SCAN early-out.
A static Qt still scans and still links.
The fourteenth was true, not noise: DISH_UDEV_RULES_DIR was relative, so it was
only correct under the packaging prefix. Under CMake's default prefix it
resolved to /usr/local/lib/udev/rules.d, which udev does not scan, and
`sudo cmake --install` laid the rule down somewhere inert. The default now
depends on the prefix and uses /etc/udev/rules.d off /usr, so the rule installs
somewhere udev reads. The packaging path is unchanged.
rsvg-convert is optional and its absence is a notice rather than a warning, but
it was missing from the documented dependencies; it is listed now.
emir-hasanbegovic
force-pushed
the
fix/xdg-config-keyring-error-flows
branch
from
August 21, 2026 14:08
16cd474 to
ae6350a
Compare
Keep-awake was unconditional, undocumented in Settings, and held the display as well as the machine whenever any slot streamed. Forwarding a gamepad needs the computer awake, not the panel, and a pad left on the desk had no way to let the machine sleep. Settings gains a Power section with three modes — Never, While playing (streaming AND a controller actuated inside a 1..180 minute idle window, 5 by default) and While connected — plus an opt-in that widens the hold to the display. The streaming pill names the reach it actually holds and carries a Configure button through to the setting. The decision is pure and exhaustively tested (core/reducer/KeepAwake.h). The inhibitor moves from Util/ to source/system/ as the Gateway it is, and its surface becomes one idempotent, absolute apply(reach): "system" is logind's idle inhibit, "display" adds the screensaver one. Activity is detected in GamepadInputProcessor::publish, post-deadzone, against a per-device reference that only advances when it trips. That matters because USB-direct slots get no deadzone profile, so their resting sticks dither by a wire LSB every report — plain equality would read an untouched pad as played with forever, while an epsilon against a moving reference would miss a slow deliberate push. A process-wide atomic counter carries it off the hot path; ControllerActivitySource samples it on the GUI tick against an injected clock. The quit confirm now gates on the stream rather than on the hold, so turning keep-awake off does not also remove the guard before a live stream dies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Coverage was reported and never enforced, on the reasoning that extracting a string and translating it are separate acts. In practice that meant 228 of 940 entries shipped in English to every non-English user, silently — an untranslated entry is not a blank, it is the wrong language, and no other gate can see it. All six catalogues are now complete: bs, de, es, fr and pt_BR gained the 228 missing entries each, translated against the terminology already in the catalogue rather than in isolation. dish_en.ts is the case that made a uniform gate awkward. English is the source language, so Qt falls back to <source> and the app has always read correctly — but that fallback is invisible to a coverage count, which would have left the source catalogue permanently "untranslated" and the gate permanently off for the one language nobody has to translate. seed-source-language.py fills its non-plural entries from their own source text, so the count means the same thing in all six. Plural entries are deliberately left for a human: a source string carries one form, and seeding those would ship "1 slots free". check-translations.sh now runs the seeder and fails on any unfinished entry, listing what still needs words. lupdate had also marked 161 entries unfinished where its same-text heuristic or a changed source had left carried-over text behind. Those are retranslated against the current source rather than confirmed blind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
emir-hasanbegovic
added a commit
to TinkerNorth/dish-windows
that referenced
this pull request
Aug 23, 2026
…p-awake (#53) Clicking the `⋯` on a Connections host card, or opening a slot row's context menu on Home, appears to do nothing. The menu is in fact opening. It takes focus and swallows the next click — it just has no width to draw into, which is indistinguishable from a dead button. ## Cause A `Menu` takes its width from its **background**, not from its items. The style's default background carries `implicitWidth: 200`. Both of these menus replace that background with a bare `Rectangle` in order to restyle it, and a bare `Rectangle` has `implicitWidth: 0` — so the menu collapses. Reproduced with the exact declaration from `ConnectionsPage.qml`: ``` opened = true visible = true count = 1 width = 0 height = 32 ``` With a width restored on the background, the same probe reports `width = 180`. ## Scope | Menu | `implicitWidth` on background | | |---|---|---| | `kit/ComboButton.qml` | `Math.max(control.width, 140)` | already correct | | `pages/ConnectionsPage.qml` — host `⋯` | absent | **broken** | | `pages/HomePage.qml` — slot row menu | absent | **broken** | `ComboButton` already restated a width for exactly this reason, which is what makes it the pattern to follow rather than an argument for a constant. ## Fix Both menus are sized to their widest item rather than to a fixed number, so a longer translation is not clipped — *Forget* is one word in English and three in some catalogues — over a shared `Tokens.menuMinWidth` floor so a one-word menu is not a sliver. `menuMinWidth` is a new token in `TokensBridge.h`, since that is where this project's design tokens live. `HomePage`'s menu is `Repeater`-driven, so its widest-item measure is a small loop keyed on `count`, which is the dependency that makes it re-evaluate when the list is rebuilt. `ConnectionsPage` has a single static item and binds straight to it. ## What is and is not verified Verified: the zero width and the fix, reproduced against `QtQuick.Controls` Basic — the style both menus import, whose layout is platform-independent — and `qmllint` is clean on both files. **Not** verified by running the Windows build. I do not have a Windows toolchain here, so the Windows CI on this PR is the first time it compiles. The change is three QML sizing bindings and one `constexpr`-style token getter, so the risk is low, but it deserves a click-through on the smoke checklist entry added here before merge. ## Cross-repo The same defect exists in `dish-linux` — byte-identical QML, same two menus, same already-correct `ComboButton` — and is fixed there in TinkerNorth/dish-linux#34. It came through the original port. `dish-mac` and `dish-android` are worth the same check. --- # Configurable keep-awake Second, unrelated change on this branch, landing in lockstep with dish-linux #34. ## What was wrong Dish asserted `ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED` unconditionally, whenever any slot was streaming. There was no setting for it anywhere — the only surface was a header pill reading *"Streaming · display kept awake"*, and no way to disable it. Forwarding a gamepad needs the computer awake, not the panel, and a pad left on the desk pinned the machine indefinitely. A `keepAwakeCount()` override input existed on `AppModel` as a designed seam with nothing wired into it — permanently zero, and documented as "never bind UI to it". That seam is now the real preference. ## The setting Settings gains a **Power** section: | Mode | Holds while | |---|---| | Never | — | | While playing *(default)* | a slot is streaming **and** a controller was actuated inside the idle window | | While connected | a slot is streaming, however long the pad sits still | plus an idle window of 1–180 minutes (5 by default, shown only in *While playing*) and a **Keep the display awake too** switch, off by default. The switch widens a hold; it never creates one. The streaming pill now names the reach it actually holds — `Streaming`, `· computer kept awake`, or `· display kept awake` — and carries a **Configure** button through to the setting. The default changes behaviour for existing users in two ways, deliberately: the display is no longer held, and an untouched pad releases the machine after five minutes. Both are recoverable from the new section. ## Design The decision is pure and lives in `core/reducer/KeepAwake.h` — mode, idle window, display reach, and the storage-key round trip, all `constexpr`, pinned as a truth table in `test_keep_awake.cpp`. `WakeStateComposer` now folds three upstreams (streaming count, controller activity, preferences) into a `KeepAwakeReach`, and `WakeStateController` drives the inhibitor off it. The inhibitor moves from `src/Util/` to `src/source/system/`: it owns an OS power assertion, which per `ARCHITECTURE.md` makes it a Gateway, not a leaf helper. Its surface collapses from `acquire`/`release` into one idempotent, **absolute** `apply(reach, reason)` — callers pass the reach they want, not a delta. `System` is `ES_CONTINUOUS | ES_SYSTEM_REQUIRED`; `SystemAndDisplay` adds `ES_DISPLAY_REQUIRED`; `None` is `ES_CONTINUOUS` alone, which clears our own flags without disturbing another application's assertion. `ES_AWAYMODE_REQUIRED` stays omitted, as before. ## Detecting "actually playing" Nothing in the codebase knew when input last happened, and the naive answers are both wrong: - **Report rate won't do it.** A USB-direct pad streams at its full poll rate whether or not anyone is holding it — `publish()` bumps its counter on identical reports, which `test_gamepad_input_processor.cpp` already pinned. - **Equality won't do it either.** Deadzones are installed per *SDL device id*, and a USB-direct slot's id is a synthetic `(vid,pid)` key that never appears in the bridge's device list — so those slots are published with **no deadzone at all**, and a resting stick dithers by a wire LSB (257 counts) every report. So `publish()` compares the post-deadzone state against a per-device **reference that only advances when it trips**, with a stick/trigger epsilon. Jitter around a fixed reference never accumulates; a slow deliberate push does. Motion is deliberately excluded — a stationary gyro is never numerically constant — while touchpad samples always count, being genuinely event-driven. The hot path gains one hash lookup and a few integer compares inside the mutex it already holds, and bumps a process-wide atomic. No clock, no Observable, no Qt — `ARCHITECTURE.md`'s hot-path rule is intact. `ControllerActivitySource` samples that counter on the existing 1 Hz GUI tick, against an **injected** clock, so every test is exact rather than timing-dependent. ## Also - The quit confirm now gates on `streamingSlotCount > 0` rather than on the keep-awake hold. Otherwise setting keep-awake to Never would silently remove the "a controller is still streaming" guard too. - `Kit.SliderRow` gains `minValue`, `valueText` and a read-only `displayValue`; its readout was hard-coded to a `%` suffix. Callers format `displayValue` because a drag breaks the inbound `value` binding. - The three new preferences live in `HKCU\Software\TinkerNorth\Dish`, the Windows-desktop-only hive, **not** the cross-client `Software\Dish\Dish` schema that dish-android also defines. ## Testing, and a caveat worth reading **Every file this feature shares with dish-linux is byte-identical** — the reducer, the composer, the controller, the activity source, `GamepadInputProcessor`, `AppSettingsMaps`, `SliderRow.qml`, and all six shared test files. Only the leaf inhibitor, the store's hive, and the AppModel / AppViewModel / QML wiring differ, as they must. That matters because it means dish-linux's verification covers this logic too. There, the full local CI passed end to end and **1767 tests pass, up from 1693** — 74 added — plus clean ASan/UBSan and TSan legs. The heart of it is `test_gamepad_input_processor.cpp`'s activity block: a 1000-report jittery resting pad never trips, and a slow drift against the fixed reference does. **The caveat:** the Windows side could not be compiled or tested locally — it is MSVC + vcpkg + `windows.h` only. What *was* verified locally, on the actual files in this repo: `clang-format --dry-run --Werror` clean; a real `g++ -fsyntax-only` pass over every new/changed header, `WakeInhibitor.cpp`, `AppSettingsMaps.cpp` and all eight wake/keep-awake test translation units, against Qt 6.10 with a Win32 stub — zero diagnostics; `qmllint` silent on all five changed QML files; brace balance; the QML literal scanner; and `lupdate` reproducing all six `.ts` catalogues with zero diff. Windows CI remains the authority for MSVC `/W4 /WX` and clang-tidy. Docs updated: `QML_CONTRACT.md` (the four properties and the new signal), `ARCHITECTURE.md`, `QML_UI_KIT.md`, `PARITY.md`, `README.md`, `CHANGELOG.md`, and a new **Keep awake** section in the manual smoke checklist — `powercfg /requests` is the oracle for the rows CI cannot reach. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four related cleanups to how Dish stores its state, explains a failed
connection, and handles a crash — plus the fourteen CMake warnings the build
printed on every configure.
Config: one file, in an XDG location
Every store and repository default-constructed
QSettings("Dish","Dish")— aWindows registry path (
HKCU\Software\Dish\Dish) carried over verbatim fromthe dish-windows port. On Linux that resolves to
~/.config/Dish/Dish.conf,while anything default-constructing
QSettingsaftermain()set theorganization landed in
~/.config/TinkerNorth/Dish.conf.So the app wrote two files, and the one the README documented held a single
UI flag while the other held the device id, the remembered satellites and the
pairing keys. Someone deleting the documented file to reset Dish would have
deleted almost nothing and left their keys behind.
repository/AppSettingsis now the only place that decides the location:$XDG_CONFIG_HOME/com.tinkernorth.Dish/dish.conf, keyed by the samereverse-DNS app id as the
.desktopentry, the AppStream metainfo and theFlatpak. Both old files are folded in on first run, key by key, never
overwriting one the new file already has, then renamed
.migratedso a badmigration stays recoverable by hand.
Pairing keys move to the desktop keyring
A pairing key authenticates every REST call and is the HKDF input for the
session key that encrypts input packets. It sat in plaintext in a file created
0644-minus-umask — readable by every other account on the machine, and by
anything that backs up or syncs
~/.config.Keys now live in the desktop keyring over
org.freedesktop.secrets, whichgnome-keyring and KWallet both implement. No new dependency: this is QtDBus,
already linked for the screensaver inhibit and the BlueZ probe. Where no Secret
Service answers — headless, bare WMs, some Flatpak sandboxes — the config file
is still used, now 0600.
Two ways a plaintext copy could have outlived the move are closed:
wifi_shared_key/entries are now removed once migrated, ratherthan left sitting beside the current namespace
than skipped by the early return on a successful keyring read
Verified end to end against a real gnome-keyring: after migration the config
file contains no key material at all.
Transport failures are named
RawReplycollapsed every pre-HTTP failure intostatus == 0, so host-down,connection-refused, timed-out and a TLS handshake failure were indistinguishable
to the UI, the log and the retry logic.
TransportFailurecarries the cause through to the session FSM. It isstrictly diagnostic:
classifyReststill folds every value intoUnreachable, andtest_transport_failure.cppasserts the verdict is identicalfor all seven causes, so adding a cause can never quietly change a retry
decision.
What changes for a user is the sentence: a refused connection now says the
satellite is not running on that machine, instead of the generic "check it's
powered on" line that actively points away from the real cause.
What changes in the log is the volume. A remembered-but-absent satellite writes
one line per cause change rather than one per backoff tick:
Per-tick detail stays available behind
QT_LOGGING_RULES="dish.net.debug=true".This also fixes
QIODevice::read (QNetworkReplyHttpImpl): device not open,which a remembered satellite printed on every single retry. The
finishedhandler read the reply without checking whether it was still open — which it is
not after an abort or a failed connect. Traced with
QT_FATAL_WARNINGS=1undergdb rather than guessed at.
Crash reports are local and explicit
The Settings toggle read "Share crash reports — anonymous crash reports help fix
bugs" while nothing was ever transmitted, and the backend logged "no backend
wired", which read as unfinished. Local-only collection is the finished
design, so the copy now says so.
UI/CrashReportturns the backtrace into a report the user reads beforedeciding to send it.
redact()strips the home directory, IPv4 literals,.localhostnames and any hex run of 32 characters or more — so a pairing keythat happened to be on the stack cannot be pasted into a public issue tracker.
The report shown on screen and the report the issue is prefilled with are the
same string; there is no richer version sent behind the user's back.
Translation contexts (pre-existing bug)
QCoreApplication::translatewas called withkTrContext, a variable.lupdate cannot resolve one, so none of the user-facing strings in
WifiConnectionManagerorPairingClientwere ever extracted — every one ofthem shipped untranslated in all six languages, silently, and the translation
gate stayed green because the catalogues matched what lupdate produced.
Using the literal at each call site recovers 12 strings, nine of which predate
this branch. Catalogues go 910 → 922.
They are extracted but not yet translated. Coverage is reported and not
enforced, so this is green, but those nine strings still render in English until
someone translates them.
CMake warnings
Configuring printed fourteen warnings. All fourteen are gone; neither was
silenced by lowering a gate.
Thirteen came from
qt6_import_qml_plugins, which links a QML plugin onlywhen that plugin's target is a
STATIC_LIBRARY— so against a shared Qt thescan can never produce anything linkable. Debian and Ubuntu additionally default
QT_SKIP_AUTO_QML_PLUGIN_INCLUSIONtoON(their own comment inQt6QmlPlugins.cmakereads "Distributions should probably change thisdefault"), so the
Qt6::<plugin>targets are never created and each scannedplugin warns.
Turning that default back off is not viable — Ubuntu ships no
QmlAssetDownloadermodule, so auto-inclusion setsQt6Quick_FOUNDto falseand fails the configure outright. The scan is instead skipped exactly where it
can do nothing, on a shared Qt, via Qt's own
QT_QML_MODULE_NO_IMPORT_SCANearly-out. A static Qt still scans and still links. The property is set after
qt_add_qml_module, which writes it from its own argument and would otherwiseclobber it.
The fourteenth was true, not noise.
DISH_UDEV_RULES_DIRwas relative, soit was only correct under the packaging prefix. Under CMake's default prefix it
resolved to
/usr/local/lib/udev/rules.d, which udev does not scan — meaning aplain
sudo cmake --installlaid the hidraw rule down somewhere inert and everyUSB-direct claim failed
PermissionDeniedafterwards. The default now dependson the prefix and uses
/etc/udev/rules.doff/usr. Confirmed by stagedinstall at both prefixes; the packaging path is byte-for-byte unchanged.
rsvg-convertis optional and its absence is aSTATUSnotice rather than awarning, but it was missing from the documented dependencies. It is listed now.
Also fixed: two pre-existing clang-tidy failures
scripts/ci_local.shdid not pass on Ubuntu 26.04 (clang-tidy 21) before thisbranch — five errors on
main, from checks newer than the CI runner'sclang-tidy. Both are false positives on required idioms and are suppressed with
written rationale rather than by bending the code:
StatusNotifierTrayIcon.cpp, where returning the const referenceparameter is the signature QtDBus requires of a demarshalling operator
HidrawGateway.h, wherefrom_charsis already being passed a(first, last)pair — the check simply does not model it as size-awareTesting
scripts/ci_local.shpasses end to end (exit 0): clang-format, action pin lint,Debug configure/build/test, qmllint, QML literal scanner, translation
catalogues, clang-tidy, Release configure/build/test, hardening flags.
1693 tests pass, up from 1672 — 21 added across four new files covering the
transport-failure invariant, the no-clobber config merge, the keyring round trip
and the crash-report redaction.
The keyring tests run against a real Secret Service and
SKIPwhere noneanswers, so they exercise the D-Bus marshalling on a developer desktop and stay
green on a CI runner. A hand-written
(oayays)struct is exactly the sort ofthing that compiles cleanly and then returns garbage.
Migration notes for reviewers
the pairing keys into it. Old files are renamed, never deleted.
QSettingsgets file-backed behaviour and never touches the developer's loginkeyring — which is why the secret store is injected rather than constructed
inside the repository.
Configurable keep-awake
Second, unrelated change on this branch: keep-awake becomes a user setting, and
stops holding the display by default.
What was wrong
Dish held both the machine and the display awake, unconditionally, whenever
any slot was streaming. There was no setting for it anywhere — the only surface
was a header pill reading "Streaming · display kept awake", and no way to
disable it. Forwarding a gamepad needs the computer awake, not the panel, and a
pad left on the desk pinned the machine indefinitely.
A
keepAwakeCount()override input existed onAppModelas a designed seamwith nothing wired into it — permanently zero, and documented as "never bind UI
to it". That seam is now the real preference.
The setting
Settings gains a Power section:
plus an idle window of 1–180 minutes (5 by default, shown only in While
playing) and a Keep the display awake too switch, off by default. The
switch widens a hold; it never creates one.
The streaming pill now names the reach it actually holds —
Streaming,· computer kept awake, or· display kept awake— and carries aConfigure button through to the setting.
The default changes behaviour for existing users in two ways, deliberately: the
display is no longer held, and an untouched pad releases the machine after five
minutes. Both are recoverable from the new section.
Design
The decision is pure and lives in
core/reducer/KeepAwake.h— mode, idlewindow, display reach, and the storage-key round trip, all
constexpr, pinnedas a truth table in
test_keep_awake.cpp.WakeStateComposernow folds threeupstreams (streaming count, controller activity, preferences) into a
KeepAwakeReach, andWakeStateControllerdrives the inhibitor off it.The inhibitor moves from
src/Util/tosrc/source/system/besideSleepMonitor: it owns two D-Bus handles, which perARCHITECTURE.mdmakes ita Gateway, not a leaf helper. Its surface collapses from
acquire/releaseinto one idempotent, absolute
apply(reach, reason)— callers pass thereach they want, not a delta.
Systemis logind'sidle/blockinhibit alone;SystemAndDisplayaddsorg.freedesktop.ScreenSaver.Inhibit.held()reportsthe greatest reach actually satisfied rather than the one asked for, so a
degraded logind cannot make it overclaim.
Detecting "actually playing"
Nothing in the codebase knew when input last happened, and the naive answers are
both wrong:
whether or not anyone is holding it —
publish()bumps its counter onidentical reports, which
test_gamepad_input_processor.cppalready pinned.and a USB-direct slot's id is a synthetic
(vid,pid)key that never appearsin the bridge's device list — so those slots are published with no deadzone
at all, and a resting stick dithers by a wire LSB (257 counts) every report.
So
publish()compares the post-deadzone state against a per-devicereference that only advances when it trips, with a stick/trigger epsilon.
Jitter around a fixed reference never accumulates; a slow deliberate push does.
Motion is deliberately excluded — a stationary gyro is never numerically
constant — while touchpad samples always count, being genuinely event-driven.
The hot path gains one hash lookup and a few integer compares inside the mutex
it already holds, and bumps a process-wide atomic. No clock, no Observable, no
Qt —
ARCHITECTURE.md's hot-path rule is intact.ControllerActivitySourcesamples that counter on the existing 1 Hz GUI tick, against an injected
clock, so every test is exact rather than timing-dependent.
Also
streamingSlotCount > 0rather than on thekeep-awake hold. Otherwise setting keep-awake to Never would silently remove
the "a controller is still streaming" guard too.
Kit.SliderRowgainsminValue,valueTextand a read-onlydisplayValue;its readout was hard-coded to a
%suffix. Callers formatdisplayValuebecause a drag breaks the inbound
valuebinding.Testing
scripts/ci_local.shpasses end to end (exit 0), every gate includingclang-tidy and both build configurations.
1767 tests pass, up from 1693 — 74 added. The heart of it is
test_gamepad_input_processor.cpp's activity block: a 1000-report jitteryresting pad never trips, and a slow drift against the fixed reference does.
Docs updated:
QML_CONTRACT.md(the four properties and the new signal),ARCHITECTURE.md,QML_UI_KIT.md,PACKAGING.md,PARITY.md,README.md,CHANGELOG.md, and a new Keep awake section in the manual smoke checklist—
systemd-inhibit --listis the oracle for the rows CI cannot reach.The same feature lands in
dish-windows(#53), whereSystemandSystemAndDisplayareES_SYSTEM_REQUIREDand+ES_DISPLAY_REQUIRED. Allshared files are byte-identical across the two repos.