docs: update README for Linux support#182
Conversation
Greptile SummaryThis PR delivers full Linux support: Unifying receiver enumeration, a systemd user-unit
Confidence Score: 4/5Safe to review further, but the The nfpm SHA-256 hash in the workflow is truncated to 62 hex characters;
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[enumerate_hidpp_devices\nfilters child nodes on Linux] --> B[probe_one per device]
B --> C{receiver::detect}
C -->|Receiver::Bolt| D[probe_bolt_receiver\nget_unique_id\ncount_pairings\ndrain_device_arrival]
C -->|Receiver::Unifying| E[probe_unifying_receiver\nget_unique_id\ncount_pairings\ndrain_device_arrival_unifying]
C -->|None / other| F[probe_direct\nHID++ 0xff self-index]
D --> G[probe_bolt_slot xN\nCacheKey::Bolt]
E --> H[probe_unifying_slot xN\nCacheKey::UnifyingSlot\nper-slot UNIFYING_SLOT_PROBE timeout]
G --> I[DeviceInventory]
H --> I
F --> I
I --> J[device_route_for\npick Bolt / Unifying / Direct\nby UNIFYING_PIDS list]
J --> K[DeviceRoute::Bolt\nDeviceRoute::Unifying\nDeviceRoute::Direct]
Reviews (8): Last reviewed commit: "fix(docs): correct Unifying label; remov..." | Re-trigger Greptile |
c649d2f to
01271a4
Compare
Reconcile the agent's autostart state on Linux by writing/removing a systemd user unit at $XDG_CONFIG_HOME/systemd/user/openlogi-agent.service. Mirrors the macOS LaunchAgent semantics exactly: - Restart=on-failure (crash respawns; clean exit(0) stays stopped) - WantedBy=graphical-session.target (takes effect at next login) - ExecStart escaped for systemd (% doubled, spaces quoted) - Idempotent write/remove — only touches disk when content changes - systemctl --user daemon-reload + enable/disable best-effort Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a Linux-specific permission probe and settings page row: - probe_uinput(): checks write access to /dev/uinput - probe_logitech_hidraw(): iterates /dev/hidraw*, confirms Logitech vendor (HID_ID sysfs field parsed numerically — 0000046D matches 046d) - classify(uinput_ok, hidraw_ok): pure function → Granted/Denied/Unknown - Settings → Permissions shows one "Input device access" row on Linux with description only when access is not yet granted (no noise when everything works) - macOS permission rows and helpers gated #[cfg(target_os = "macos")] Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Accessibility footer in the main window hidden on Linux - "Open" button in permission rows gated to macOS only - Launch-at-login description no longer says "log in to macOS" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove misplaced sysfs comment above the open() call in probe_logitech_hidraw (the sysfs check is in is_logitech_hidraw) - Remove dead #[cfg(not(target_os = "macos"))] suppressor inside the already-macOS-gated permission_field function - Split Denied/Unknown description text: Unknown means uinput is accessible but no Logitech device is connected, so point the user at the device rather than the udev install guide Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- escape_systemd_exec: double $ → $$ to prevent systemd variable substitution in ExecStart paths containing a literal dollar sign - paths: add pub xdg_config_home() that returns the raw XDG config base without the openlogi sub-directory; refactor config_dir() to call it - unit_path: use xdg_config_home() directly instead of relying on the fragile .parent() traversal from config_dir() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added missing translations for the Linux permission row label introduced in this PR. Follows the same pattern as 'Input Monitoring'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
df0799c to
c31703c
Compare
… scope - Add 'Unifying receiver' and 'Input device access' keys to en.yml (all locale files must match en.yml for the i18n test) - Add 'Ricevitore Unifying' to it.yml for key parity - Move InteractiveElement import outside #[cfg(target_os = "macos")] so on_action() calls for CloseWindow/Minimize/Zoom work on Linux - Fix rustfmt line-wrap in launch_agent::unit_path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- udev/70-openlogi.rules: TAG+="uaccess" for hidraw (Logitech VID 046d) and uinput; includes non-systemd GROUP="input" fallback instructions - systemd/openlogi-agent.service: packaged user-unit template for /usr/lib/systemd/user/ (complements the per-user unit written by launch_at_login) - desktop/openlogi.desktop: XDG application launcher - install.sh / uninstall.sh: POSIX sh, set -eu; copies binaries + all artifacts, reloads udevadm, best-effort systemd and icon cache updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs/INSTALL-linux.md covers: prerequisites, build from source, udev rules (uaccess + non-systemd fallback), install.sh usage, autostart via systemctl, verification steps, and known limitations table. README.md gets a Linux subsection under ## Install with the minimal quick-start (build + udev) and a link to the full guide. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- install.sh: rewrite ExecStart in the systemd unit via sed so the installed service always points to $BINDIR/openlogi-agent, not the hardcoded /usr/bin path (which mismatches the default /usr/local prefix) - uninstall.sh: add udevadm trigger after reload so hidraw and uinput nodes lose the uaccess tag immediately, not only after re-plug/reboot Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- install.sh: escape sed replacement string for BINDIR metacharacters (& \ |) so paths like /opt/my&pkg don't corrupt the unit file - install.sh: best-effort daemon-reload via sudo -u $SUDO_USER after writing the unit so reinstalls pick up the new ExecStart immediately - uninstall.sh: use SUDO_USER to target the real user's systemd session when the script is run under sudo, preventing the disable from silently targeting root's session while the user's agent keeps running Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bluetooth HID devices go through the uhid virtual bus, which has no
idVendor sysfs attribute — ATTRS{idVendor}=="046d" doesn't match them.
Add a second rule matching on the HID kernel name format
"bus:VID:PID.iface", whose VID field (046D) covers both BT and USB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sudo -u strips the environment, so systemctl --user cannot locate the user's D-Bus socket without XDG_RUNTIME_DIR. Without it, disable --now in uninstall.sh silently fails (exit code 1, swallowed by || true), leaving the agent enabled even after the binary is removed. daemon-reload in install.sh has the same problem — it would silently skip the reload, requiring a manual reload before the updated unit takes effect. Fix both by computing REAL_UID / INSTALL_USER and passing XDG_RUNTIME_DIR=/run/user/<uid> explicitly to the sudo -u invocation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both install.sh (PREFIX/bin) and nfpm postinstall (/usr/bin) now expand the placeholder explicitly, removing the implicit assumption that the template's hardcoded path matches any particular installer.
nfpm.yaml maps the three binaries + udev rules + systemd user unit + desktop entry + icon into .deb/.rpm. VERSION env var is templated in at build time by the xtask. nfpm-scripts/postinstall.sh: reload udevadm, refresh icon and desktop caches, print enable-agent instructions. nfpm-scripts/preremove.sh: reload udevadm to revoke uaccess tags. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xtask/src/linux.rs: PackageLinux command builds release binaries (unless --no-build), then invokes nfpm for both deb and rpm. Reads the workspace version from Cargo.toml and passes it as VERSION to nfpm. .github/workflows/release.yml: new linux-packages job (ubuntu-latest) installs nfpm via the goreleaser apt repo, runs `cargo run -p xtask -- package-linux`, and uploads .deb/.rpm as an artifact. The publish job now depends on both dmg and linux-packages, downloads both artifact sets, and attaches .deb/.rpm to the GitHub Release alongside the DMGs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace workspace_version() file-I/O + fragile TOML line-scanner with
env!("CARGO_PKG_VERSION") — Cargo bakes the workspace version in at
compile time, zero I/O and always correct
- Merge two separate apt-get update + install steps in the linux-packages
CI job into one (add goreleaser repo first, then single update + install)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- nfpm.yaml: drop v prefix from version field — dpkg/rpm expect bare semver (0.6.0 not v0.6.0); v prefix can cause upgrade ordering issues - nfpm.yaml: fix maintainer to Name <email> format required by Debian - release.yml: replace trusted=yes with a proper GPG key pin for the goreleaser apt repo to prevent supply-chain substitution attacks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the goreleaser apt repo (which has no GPG key and requires trusted=yes) with a direct download of a pinned nfpm release from GitHub. The SHA256 is checked before install, preventing a compromised CDN or MITM from substituting a malicious nfpm binary in the release pipeline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
postinstall.sh / preremove.sh: udevadm trigger is asynchronous — it queues uevent changes but returns before udev finishes processing them. Without udevadm settle, a user who immediately runs `systemctl --user enable --now openlogi-agent` after package install may find /dev/hidraw* and /dev/uinput still denying access because the uaccess tags haven't propagated yet. Add `udevadm settle || true` after the trigger calls in both scripts. release.yml: extend the minisign signing loop from dist/*.dmg to also cover dist/*.deb and dist/*.rpm, so all release artifacts have a .minisig sidecar and SHA256SUMS is consistent across all platforms. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
xtask/src/linux.rs: PackageLinux command builds release binaries (unless --no-build), then invokes nfpm for both deb and rpm. Reads the workspace version from Cargo.toml and passes it as VERSION to nfpm. .github/workflows/release.yml: new linux-packages job (ubuntu-latest) installs nfpm via the goreleaser apt repo, runs `cargo run -p xtask -- package-linux`, and uploads .deb/.rpm as an artifact. The publish job now depends on both dmg and linux-packages, downloads both artifact sets, and attaches .deb/.rpm to the GitHub Release alongside the DMGs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OpenLogi previously only enumerated Logi Bolt receivers (PID 0xC548).
Users with a Unifying receiver (PID 0xC52B / 0xC532) saw no devices.
openlogi-hidpp/receiver/unifying.rs
Add the missing HID++ 1.0 methods needed for device enumeration:
count_pairings(), trigger_device_arrival(), get_device_pairing_information(),
get_unique_id(). Add EventEmitter<Event> + message listener for 0x41
device-connection notifications (same pattern as BoltReceiver). Add
DeviceKind enum (shifted vs Bolt at values 5+: Remote=5, Trackball=6,
Touchpad=7). Update receiver/mod.rs to call get_unique_id() directly.
openlogi-hid/transport.rs
Add is_receiver_child_node() (Linux): the hid-logitech-dj kernel driver
creates a virtual hidraw node per paired Unifying device; these matched
our HID++ filter and caused 5-second probe timeouts every tick. Detect
them by checking whether any known receiver PID appears as a *parent*
directory component in the device's sysfs path, and filter them out
before probing. Extract the path-matching logic to is_receiver_child_sysfs_path()
for testability without filesystem access.
Add BOLT_PIDS and UNIFYING_PIDS as the canonical PID lists; pairing.rs
now derives from them so a new receiver PID needs editing in one place.
openlogi-hid/inventory.rs
probe_one: replace Bolt-only match with explicit Bolt / Unifying / direct
arms. Add probe_unifying_receiver (uses device-arrival events to surface
online paired devices) and probe_unifying_slot (builds PairedDevice from
the event; falls back gracefully on timeout). Add UNIFYING_SLOT_PROBE
(3.5 s) per-slot budget so a slow wireless HID++ 2.0 round-trip on one
device cannot consume the shared PROBE_BUDGET on behalf of others. Add
drain_device_arrival_unifying and map_unifying_kind.
openlogi-hid/route.rs
Add DeviceRoute::Unifying { receiver_uid, slot } — same structure as Bolt
but distinct so the label and write path are correct. Add device_route_for()
constructor that picks Unifying/Bolt/Direct from the receiver's product ID
using the canonical PID lists. Update open_route_channel to handle the
Unifying arm. Add BOLT_PIDS and UNIFYING_PIDS constants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consumers of the HID layer all constructed DeviceRoute::Bolt for any receiver-backed device. This caused three problems for Unifying devices: 1. open_route_channel only tried Receiver::Bolt — writes silently failed. 2. The Device tab showed "Bolt receiver" for Unifying devices. 3. The Buttons tab showed the generic mouse hotspot layout for keyboards (K540 has ReprogControls so capabilities.buttons=true, but no asset). 4. Paired devices without HID++ 2.0 model info were skipped entirely by build_device_list, so the K540 never appeared in the carousel. openlogi-agent-core device_order.rs: DeviceStableId::from_parts folds Unifying into the Bolt variant (same slot-based sort key regardless of receiver family, so the GUI carousel and agent agree on "first device"). orchestrator.rs: call DeviceRoute::device_route_for() directly. openlogi-cli/diag Use DeviceRoute::device_route_for() at both diag call sites. openlogi-gui/app.rs route_label: add "Unifying receiver" arm for DeviceRoute::Unifying. tabs_for: gate the Buttons tab on `can_show_mouse_model` — a pointer-type device (Mouse/Trackball) or one with a resolved asset. A keyboard that exposes ReprogControls but has no asset would otherwise show the generic mouse hotspot layout (Middle Click, DPI Toggle, …) which is wrong. Update tabs_follow_capabilities_not_kind test to use kind=Mouse (the 0x0005 kind-correction fix from AprilNEA#127 corrects mislabeled mice at probe time; the test scenario of kind=Keyboard + mouse-caps no longer arises). Add keyboard_without_asset_hides_buttons_tab test. openlogi-gui/state/devices.rs build_device_list: remove the hard model_info guard. Devices without HID++ 2.0 model info (HID++ 1.0 devices, or probes that timed out) are now surfaced with a wpid-based config_key ("wpid{:04x}") so their settings persist across sessions; slot is the last-resort fallback. Call DeviceRoute::device_route_for() directly (one-liner wrapper removed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
inventory.rs — probe_unifying_slot now receives the receiver's unique_id and incorporates its first 3 ASCII bytes into the CacheKey so two Unifying receivers with a device on the same slot number use distinct cache entries. The previous [0,0,0,slot] key was receiver-agnostic and would hand receiver B's slot-1 query the cached ProbedFeatures built for receiver A's slot-1 device (different model, different capabilities). route.rs — device_route_for: make the Bolt-default design explicit in the doc comment and add a tracing::debug for receivers whose PID is in neither BOLT_PIDS nor UNIFYING_PIDS. Behaviour is unchanged — returning None for unknown PIDs would silently drop writes for future Bolt variants with new PIDs, which is worse than routing them as Bolt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
unifying.rs — #[derive(Clone)] copied msg_listener_hdl (u32) verbatim,
so dropping any clone called remove_msg_listener with the shared handle
and silently deregistered the listener for all surviving clones. The
next drain_device_arrival_unifying would return empty, making Unifying
devices vanish from inventory.
Replace the bare u32 with Arc<ListenerDropGuard>: every Receiver clone
shares the Arc; remove_msg_listener is called exactly once, when the
Arc's refcount hits zero (last clone dropped). The manual Drop impl is
removed — ListenerDropGuard::drop owns the cleanup.
inventory.rs — CacheKey::Bolt { unit_id: [uid[0..3], slot] } used only
3 ASCII bytes of the receiver serial as a prefix, so two receivers whose
serials share the same first three characters (common in Logitech batches,
e.g. "DA2699E1" and "DA2604F2") still collided on the same slot.
Add CacheKey::UnifyingSlot { receiver_uid: String, slot: u8 } keyed on
the full serial string + slot. Two Unifying receivers with a device on
the same slot number now have provably distinct cache entries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added missing translations for the route label introduced in this PR. Placed alongside 'Bolt receiver' following the same pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BoltReceiver had the same Clone+Drop bug fixed for UnifyingReceiver in the previous commit: #[derive(Clone)] copied msg_listener_hdl (u32) verbatim, so the first drop of any clone deregistered the HID++ listener for all surviving copies — subsequent drain_device_arrival calls would return empty and Bolt devices would vanish from inventory. Move ListenerDropGuard to receiver/mod.rs (pub(super)) so it is shared by both bolt.rs and unifying.rs without duplication. Apply the Arc-wrap fix to BoltReceiver identically: store Arc<ListenerDropGuard> instead of a bare u32, remove the manual impl Drop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On timeout the fallback returned ProbedFeatures::default() even when a prior successful probe was in the cache, causing battery, model name, and capability flags to disappear from the carousel for that device. Return the cached probe when available and emit Seen so the entry is not counted toward eviction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
c31703c to
b0c9bd5
Compare
The function is only called from a #[cfg(target_os = "linux")] context, making it dead code on macOS/Windows. Gate it with #[cfg(any(target_os = "linux", test))] so the tests remain cross-platform while silencing the dead_code lint in production builds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
b0c9bd5 to
2a7cbfe
Compare
Empty string fallback causes CacheKey collisions when two receivers share the same slot and both fail get_unique_id(). Product ID is a weaker but non-empty discriminant; a warning is logged so the degraded isolation is visible.
- Remove "Linux... coming soon" — Linux is now fully supported - Roadmap: mark Linux hook, Unifying receivers, button remapping, action catalog, launch-at-login, and localization as ✅ macOS + Linux - DPI and SmartShift are platform-independent writes — drop the macOS qualifier - Add Linux packaging row (udev, systemd, .deb/.rpm) - Per-app profiles: ✅ macOS, 🟡 Linux X11 only (Wayland pending) - Install section: add Linux install steps (.deb/.rpm + systemd enable) and link to docs/INSTALL-linux.md - Update footnote: media actions use D-Bus MPRIS on Linux Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 'Bolt successor' → 'older protocol, replaced by Bolt' (Bolt is the successor to Unifying, not the other way around) - Remove the stale build-from-source Linux section introduced by the install-scripts PR; the new .deb/.rpm section supersedes it - Rebased onto install-script-and-docs so docs/INSTALL-linux.md exists in this branch's tree and the link resolves Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2a7cbfe to
848f351
Compare
| - name: Install system dependencies and nfpm | ||
| env: | ||
| NFPM_VERSION: "2.46.3" | ||
| NFPM_SHA256: "d6417f99d5fa32bba7a4e007084615d3897651498c2e443118c26b9ec3b698a8" |
There was a problem hiding this comment.
Truncated SHA-256 hash breaks nfpm integrity check
The NFPM_SHA256 value is only 62 hex characters — SHA-256 requires exactly 64. sha256sum -c will reject the input entirely (no properly formatted SHA256 checksum lines found), causing the "Install system dependencies and nfpm" step to fail on every run and blocking the entire linux-packages job. Regenerate the hash with sha256sum nfpm_2.46.3_amd64.deb after downloading the file.
Dependencies
This PR must merge after the following PRs land on master, in order:
launch_at_login+ permission check (adds the systemd user unit and the Settings permissions row)docs/INSTALL-linux.mdandpackaging/linux/).deb/.rpmpackaging (adds the package releases referenced in the Linux install instructions)The branch is rebased on top of PR #173 so
docs/INSTALL-linux.mdresolves in the tree.Summary
.deb/.rpmdownload instructions andsystemctl --user enablestepTest plan
.deb/.rpmdocs/INSTALL-linux.mdlink resolves after PR feat(linux): install artifacts and docs #173 merges🤖 Generated with Claude Code