Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ the repos share a version number.

### Added

- Configurable keep-awake, under *Power* in Settings. **Never**, **While
playing** (the default: hold only while a bound controller has actually been
actuated inside a 1–180 minute idle window, 5 by default) or **While
connected** (hold for as long as a slot streams, however long the pad sits
still). The hold now covers the **machine** by default and the display only on
request, because forwarding a pad needs the computer, not the panel. Activity
is measured post-deadzone against a reference that only advances when it
moves, so a drifting stick cannot pin the machine awake and a slow deliberate
push still registers. The streaming pill names the reach it actually holds and
carries a **Configure** button through to the setting.
- Running in the background. Closing the window leaves Dish streaming behind a
StatusNotifierItem tray icon whose menu is the way back and the way out, and a
one-time desktop notification says so the first time it happens. The hide is
Expand Down Expand Up @@ -106,6 +116,25 @@ the repos share a version number.

### Changed

- Settings live in one XDG-shaped file, `$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. Every store used to default-construct
`QSettings("Dish","Dish")` — a Windows registry path carried over from the
dish-windows port — so the app wrote both `~/.config/Dish/Dish.conf` and
`~/.config/TinkerNorth/Dish.conf`, and the documented one held a single UI flag
while the other held the device id, the remembered satellites and the pairing
keys. Both are folded into the new file on first launch, key by key and never
over one already present, then renamed `.migrated`.
- Crash reporting is described as what it is. Collection has always been local
and nothing has ever been transmitted, so the Settings toggle no longer says
reports are shared, and a crash now surfaces a report the user reads and sends
themselves. The text has the home directory, IPv4 literals, `.local` hostnames
and any hex run of 32 characters or more removed before it is shown, so a
pairing key on the stack cannot be pasted into a public issue.
- A satellite that cannot be reached says why. Refused, timed out and a TLS
failure were all one sentence about checking the power; a refused connection
now points at the satellite not running instead. The cause is diagnostic only
— the retry verdict is unchanged for every one of them.
- The Qt floor moved from 6.2 to 6.7, which the Qt Quick UI requires. Ubuntu
24.04 LTS ships 6.4 and now needs a backport or a Flatpak build; see
[`docs/PACKAGING.md`](docs/PACKAGING.md).
Expand All @@ -124,6 +153,42 @@ the repos share a version number.

### Fixed

- Pairing keys are stored in the desktop keyring over `org.freedesktop.secrets`
rather than as plaintext hex in a world-readable config file. gnome-keyring
and KWallet both implement it and QtDBus was already linked, so this adds no
dependency; where no Secret Service answers the config file is still used, now
0600. Two paths that would have left a plaintext copy behind are closed: the
legacy `wifi_shared_key/` entries are removed once migrated, and a key still in
the file while the keyring already holds one is swept rather than skipped.
- A remembered satellite that is switched off no longer floods the log. The REST
gateway read replies that were never opened, printing
`QIODevice::read (QNetworkReplyHttpImpl): device not open` on every backoff
tick; it now writes one line per cause change, with per-tick detail behind
`QT_LOGGING_RULES="dish.net.debug=true"`.
- `sudo cmake --install` puts the hidraw rule somewhere udev reads.
`DISH_UDEV_RULES_DIR` was relative, so off the packaging prefix it resolved to
`/usr/local/lib/udev/rules.d`, which udev does not scan — the rule installed
silently and every USB-direct claim failed `PermissionDenied` afterwards. The
default now depends on the prefix; the packaged path is unchanged.
- Error messages in `WifiConnectionManager` and `PairingClient` can be
translated. `QCoreApplication::translate` was called with a variable context,
which lupdate cannot resolve, so none of those strings were ever extracted and
all of them shipped in English regardless of locale. Twelve strings are
recovered, nine of which predate this change.
- The `⋯` overflow menus open. A Menu takes its width from its background, and
both the Connections host menu and the Home row menu restyled that background
without restating a width, so the menu opened at zero width — it took focus
and drew nothing, which is indistinguishable from a dead button. Both are now
sized to their widest item over a shared `Tokens.menuMinWidth` floor, the same
way `ComboButton` already did it.
- A skipped test no longer reads as a failed one. Catch2 exits 4 from `SKIP()`
and CTest was never told what that means, so the keyring round-trip — which
skips wherever no Secret Service answers, including every CI runner — turned
the whole suite red on any machine without a desktop keyring.
- Configuring is warning-free. Qt's QML import scanner links plugins only for a
static Qt, and Debian and Ubuntu ship the plugin targets undefined, so a shared
build printed thirteen "link target does not exist" warnings that no
configuration could satisfy; the scan is now skipped where it cannot act.
- The installed `.deb` renders its brand glyphs. Every SVG failed to decode
because Debian splits `imageformats/libqsvg.so` out of `libqt6svg6` into
`qt6-svg-plugins`, which nothing depended on; the plugin is opened by name at
Expand Down
66 changes: 55 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,25 @@ set(DISH_SANITIZER "" CACHE STRING
# lands in /usr/etc/udev/rules.d under the usual -DCMAKE_INSTALL_PREFIX=/usr.
# Not CMAKE_INSTALL_LIBDIR either — Debian multiarch turns that into
# lib/x86_64-linux-gnu, which udev does not scan.
set(DISH_UDEV_RULES_DIR "lib/udev/rules.d" CACHE STRING
"Directory the hidraw udev rule is installed into, relative to the prefix")
#
# The default has to depend on the prefix, because a RELATIVE path is only
# correct under the packaging prefix. "lib/udev/rules.d" resolves to
# /usr/lib/udev/rules.d under -DCMAKE_INSTALL_PREFIX=/usr, which is what the .deb
# and .rpm build with and what udev scans. Under CMake's DEFAULT prefix it
# resolves to /usr/local/lib/udev/rules.d, which udev does not scan — so a plain
# `sudo cmake --install` silently laid the rule down somewhere inert and every
# USB-direct claim failed PermissionDenied afterwards.
#
# Off the packaging prefix, use the one directory that is correct from any
# prefix: /etc/udev/rules.d, absolute so it is not joined onto the prefix again.
# It is also the path README tells people to install the rule into by hand.
if(CMAKE_INSTALL_PREFIX STREQUAL "/usr")
set(_dish_udev_rules_default "lib/udev/rules.d")
else()
set(_dish_udev_rules_default "/etc/udev/rules.d")
endif()
set(DISH_UDEV_RULES_DIR "${_dish_udev_rules_default}" CACHE STRING
"Directory the hidraw udev rule is installed into; a relative path is joined onto the prefix")
# A Flatpak cannot own a host rules directory and an AppImage installs nothing
# outside itself; both turn this off and degrade to the SDL path.
option(DISH_INSTALL_UDEV_RULES "Install the hidraw udev rule" ON)
Expand Down Expand Up @@ -143,6 +160,7 @@ set(DISH_CORE_SOURCES
src/core/reducer/RestOutcome.h
src/core/reducer/Backoff.h
src/core/reducer/BackgroundMode.h
src/core/reducer/KeepAwake.h
src/core/reducer/Reconcile.h
src/core/reducer/CloseNotify.h
src/core/reducer/PathChoice.h
Expand Down Expand Up @@ -202,8 +220,6 @@ set(DISH_CORE_SOURCES
src/Util/Endian.h
src/Util/HostBattery.h
src/Util/HostBattery.cpp
src/Util/DisplaySleepInhibitor.h
src/Util/DisplaySleepInhibitor.cpp
src/Util/UnixSignalWatcher.h
src/Util/UnixSignalWatcher.cpp
# In dish_core so test_translations exercises the real locale-fallback path
Expand Down Expand Up @@ -259,6 +275,7 @@ set(DISH_CORE_SOURCES
src/source/store/TouchpadModeStore.h
src/source/store/UiPreferenceStore.h
src/source/store/BackgroundPreferenceStore.h
src/source/store/KeepAwakePreferenceStore.h
src/source/store/UpdatePreferenceStore.h
# xpad publishes no hidraw node for Xbox pads, so this path serves
# DualSense/DS4/Switch Pro/8BitDo and Xbox pads stay on SDL.
Expand All @@ -273,12 +290,17 @@ set(DISH_CORE_SOURCES
src/source/notification/DesktopNotifier.h
src/source/notification/DesktopNotifier.cpp
src/source/inputrate/SlotInputRates.h
src/source/input/ControllerActivitySource.h
src/source/inputrate/InputRateStore.h
src/source/inputrate/InputRateStore.cpp
src/source/system/BluetoothRadioProbe.h
src/source/system/BluetoothRadioProbe.cpp
src/source/system/SecretServiceStore.h
src/source/system/SleepMonitor.h
src/source/system/WakeInhibitor.h
src/source/system/SecretServiceStore.cpp
src/source/system/SleepMonitor.cpp
src/source/system/WakeInhibitor.cpp
src/source/tray/TrayIcon.h
src/source/tray/StatusNotifierTrayIcon.h
src/source/tray/StatusNotifierTrayIcon.cpp
Expand All @@ -289,6 +311,8 @@ set(DISH_CORE_SOURCES
src/update/HttpGateways.cpp
src/update/UpdateChecker.h
src/update/UpdateChecker.cpp
src/UI/CrashReport.h
src/repository/AppSettings.h
src/repository/SettingsKeys.h
src/repository/RememberedWifiEq.h
src/repository/SatellitePinRepository.h
Expand All @@ -298,6 +322,8 @@ set(DISH_CORE_SOURCES
src/repository/RememberedSatelliteRepository.h
src/repository/RememberedSatelliteRepository.cpp
src/repository/ConnectionStore.h
src/UI/CrashReport.cpp
src/repository/AppSettings.cpp
src/repository/ConnectionStore.cpp
src/repository/DeadzoneRepository.h
src/repository/DeadzoneRepository.cpp
Expand Down Expand Up @@ -426,13 +452,25 @@ if(QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif()

# Configuring prints ~13 "qml plugin X is a dependency but the link target does
# not exist" warnings. They come from qt6_import_qml_plugins, which Qt runs at
# finalization to STATICALLY link QML plugins; against a shared Qt there is
# nothing to link and the modules load from the import path at run time. Adding
# the plugins' packages does not create the targets and neither does remapping
# the imported config — the warning is Qt's, not this project's, and the app
# runs clean. Do not chase it; do read any warning that is NOT one of these.
# Qt runs qt6_import_qml_plugins at finalization to STATICALLY link the QML
# plugins an import scan turns up. It links a plugin only when that plugin's
# target is a STATIC_LIBRARY, so against a SHARED Qt the scan can never produce
# anything linkable — the modules load from the import path at run time instead.
#
# The scan is not merely useless there, it is noisy: Debian and Ubuntu default
# QT_SKIP_AUTO_QML_PLUGIN_INCLUSION to ON (see Qt6QmlPlugins.cmake, "Distributions
# should probably change this default"), so the Qt6::<plugin> targets are never
# created and every scanned plugin prints "the link target it defines does not
# exist in the current scope" — ~13 warnings on every configure.
#
# Turning that default back OFF is NOT the fix: Ubuntu ships no QmlAssetDownloader
# module, so auto-including every QML plugin config makes Qt6Quick_FOUND false and
# the configure fails outright.
#
# So skip the scan exactly where it cannot do anything: a shared Qt. A static Qt
# still scans and still links, which is the only configuration that needs it.
# QT_QML_MODULE_NO_IMPORT_SCAN is Qt's own early-out, read at the top of
# qt6_import_qml_plugins.
qt_add_qml_module(Dish
URI Dish.Chrome
VERSION 1.0
Expand Down Expand Up @@ -468,6 +506,12 @@ qt_add_qml_module(Dish
src/qml/wizard/WizardFeelPage.qml
src/qml/wizard/WizardReviewPage.qml)

# Set AFTER qt_add_qml_module, which writes this property from its own
# NO_IMPORT_SCAN argument and would otherwise clobber the value.
if(QT6_IS_SHARED_LIBS_BUILD)
set_target_properties(Dish PROPERTIES QT_QML_MODULE_NO_IMPORT_SCAN ON)
endif()

# qmltyperegistrar's generated .cpp #includes the bridge headers by BASENAME,
# so their directory has to be on the include path for it to compile.
target_include_directories(Dish PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/qml/chrome)
Expand Down
16 changes: 12 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,18 @@ back out as `WifiConnectionManager`, which is not the context `moc` hands
`tr()` at run time, so the entry it writes could never be looked up. The script
refuses to run on an older one rather than let that land.

Coverage is reported, never enforced: translating a string is a separate act
from extracting it. English is a real catalogue rather than the untranslated
fallback, because a `%n` message carries one source string but needs one plural
form per category and Bosnian has three.
Coverage is **enforced**. `check-translations.sh` fails when any catalogue has
an unfinished entry, because a missing translation is not a blank — it is
English shown to someone who does not read English, and nothing else in the
suite can see that. Extracting a string and translating it are one commit, not
two.

English is a real catalogue rather than the untranslated fallback, because a
`%n` message carries one source string but needs one plural form per category
and Bosnian has three. Its non-plural entries are filled from their own source
text by `scripts/seed-source-language.py`, which the gate runs for you — so the
count means the same thing in all six catalogues, and the only English entries
a person writes are the plural forms that genuinely need a decision.

## License headers

Expand Down
6 changes: 3 additions & 3 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Persisted with `QSettings`, which on Linux writes INI files under
`$XDG_CONFIG_HOME` (`~/.config` by default). The cross-client settings schema
lands in `~/.config/Dish/Dish.conf`. Preferences that only exist on the desktop
clients are written through the app's default organisation name and land in
`~/.config/TinkerNorth/Dish.conf` instead: the navigation preference
`~/.config/com.tinkernorth.Dish/dish.conf` instead: the navigation preference
`ui_rail_collapsed` and the update settings listed after the table.

| Setting | Holds | Used for |
Expand All @@ -71,7 +71,7 @@ clients are written through the app's default organisation name and land in
Legacy values `wifi_list` and `wifi_shared_key/<id>` from older builds are
migrated in place on first run so you do not have to re-pair.

These three values live in `~/.config/TinkerNorth/Dish.conf` and cover the
These three values live in `~/.config/com.tinkernorth.Dish/dish.conf` and cover the
update check described in section 2.4. They record your choice and the app's
own bookkeeping. None of them is transmitted anywhere.

Expand Down Expand Up @@ -276,7 +276,7 @@ SDL.
pairings and crash log are deliberately left behind so that reinstalling
restores your setup; the *Wipe everything* step below removes those too.
- **Wipe everything.** Delete `~/.config/Dish/Dish.conf`,
`~/.config/TinkerNorth/Dish.conf` and `~/.local/state/dish/`. That removes
`~/.config/com.tinkernorth.Dish/dish.conf` and `~/.local/state/dish/`. That removes
every remembered server, pairing key, certificate pin, preference, update
setting, and crash artifact. There is no server-side record to delete,
because there is no TinkerNorth server.
Expand Down
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ Physical controllers only. There is no on-screen touch gamepad; that belongs to
down by the host
- Several satellites side by side, with per-slot controller binding
- Per-device deadzones, button remapping, and a guided setup wizard
- Holds the display awake while a slot is streaming, releases it on the last
unbind
- Keeps the computer awake while a slot is streaming — never, only while a
controller is actually being used, or for as long as the link is up — with an
optional idle timeout and an opt-in for the display too
- Keeps streaming with the window closed, behind a tray icon that quits it
- Closes its sessions on suspend and re-opens them on resume
- Light and dark themes that follow the desktop, six UI languages
Expand All @@ -58,7 +59,7 @@ Ubuntu 24.04 LTS ships Qt 6.4, below this project's 6.7 floor, so there is no
[`docs/PACKAGING.md`](docs/PACKAGING.md) has the full per-distro table and what
each package lays down.

Settings persist under `~/.config/TinkerNorth/Dish.conf`; a crash writes a
Settings persist under `~/.config/com.tinkernorth.Dish/dish.conf`; a crash writes a
backtrace to `$XDG_STATE_HOME/dish/crash.log`.

### USB-direct needs a udev rule
Expand Down Expand Up @@ -94,14 +95,18 @@ update-related network request when off. What the check sends is spelled out in
- Qt 6.7+ (Core, Gui, Network, DBus, Svg, Quick, Qml, QuickControls2; Linguist
tools for the translation catalogues)
- libsodium, SDL2, Catch2 v3
- Optional: `rsvg-convert`, which renders the rest of the launcher-icon ladder
from the SVG. Without it the build says so and installs only the scalable and
512x512 icons, which is enough for a working menu entry.

On Debian and Ubuntu:

```sh
sudo apt install build-essential cmake ninja-build pkg-config \
qt6-base-dev qt6-base-dev-tools qt6-declarative-dev qt6-svg-dev \
qt6-tools-dev qt6-l10n-tools \
libsodium-dev libsdl2-dev libdbus-1-dev catch2
libsodium-dev libsdl2-dev libdbus-1-dev catch2 \
librsvg2-bin
```

Then:
Expand Down Expand Up @@ -201,8 +206,9 @@ choice in their power settings. Docked already works untouched:
Three portal-backed facts, each with a documented fallback so a minimal desktop
degrades rather than breaks: light/dark from the XDG appearance portal (falling
back to dark), reduced motion from the XDG settings portal then `kdeglobals`
(falling back to motion allowed), and keep-awake from
`org.freedesktop.ScreenSaver.Inhibit` (falling back to a silent no-op).
(falling back to motion allowed), and keep-awake from logind's `idle` inhibit
plus, when the display opt-in is on, `org.freedesktop.ScreenSaver.Inhibit`
(each falling back to a silent no-op).
Bluetooth presence and power come from sysfs and BlueZ directly, because the
wizard needs to tell "no adapter" from "adapter off".

Expand Down
6 changes: 4 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ layer together.
| `src/composer/` | Composers (pure derive), Controllers (side effects), Coordinators (command services) | `core/`, `source/`, `repository/` | yes (QObject) |
| `src/qml/` | QML bridge: the `AppViewModel` facade, role models, chrome singletons, and the `.qml` tree | everything | yes (Quick) |
| `src/Models/` | Shared value types (`models::ControllerSlot`, `DiscoveredServer`, notifications) | `core/` | yes (QString) |
| `src/Util/` | Leaf helpers with no domain state: endian, hex, host battery, display-sleep inhibitor, locale install | nothing | mixed |
| `src/Util/` | Leaf helpers with no domain state: endian, hex, host battery, locale install | nothing | mixed |
| `src/UI/` | The design-token palette (`Theme`), the font-family probes (`FontStacks`), crash handling, the `SlotLiveStats` mapper, `common/ExternalLink`, `licenses/LicenseManifest` | `core/` | yes (Gui) |
| `src/Input/` | The SDL bridge, the input processor, joystick mapping, the output command queue | `core/` | yes |
| `src/Network/` | Sockets and the REST control plane: `SatelliteClient`, `ConnectionHub`, `WifiConnectionManager`, `HTTPClient`, `PairingClient` | `core/` | yes |
Expand Down Expand Up @@ -474,7 +474,9 @@ CI), so each needs a device-in-the-loop test pass.
- **Translations are tested.** `test_translations` pins locale fallback, the
per-language `numerusform` order, and placeholder integrity across every
catalogue. `scripts/check-translations.sh` re-runs `lupdate` in CI and fails
on any diff, so a new string cannot land without its catalogue entry.
on any diff, so a new string cannot land without its catalogue entry — and
fails again if any catalogue still has an unfinished entry, so it cannot land
without its words either.
- **The update checker is tested the same way.** Test names are prefixed so
`ctest -R update` selects them: the exhaustive reducer table for
`UpdateMachine`, the manifest grammar, the preference schema, and
Expand Down
Loading
Loading