feat(linux): nfpm .deb/.rpm packaging and CI release job#179
Conversation
Greptile SummaryThis PR adds nfpm-based
Confidence Score: 3/5Safe to merge for feature development, but a Linux build failure will block the macOS DMG release until the hard-fail download is switched to best-effort. The publish job uses a hard-fail .github/workflows/release.yml — the publish job artifact download and if-condition; crates/openlogi-agent/src/launch_agent.rs — the idempotency guard in reconcile_linux Important Files Changed
Sequence DiagramsequenceDiagram
participant Tag as Git Tag Push
participant LPkg as linux-packages job
participant MacOS as macos job
participant Pub as publish job
Tag->>LPkg: trigger
Tag->>MacOS: trigger (parallel)
LPkg->>LPkg: cargo run -p xtask -- package-linux
LPkg->>LPkg: nfpm deb + rpm
LPkg->>LPkg: upload-artifact OpenLogi-linux-packages
MacOS->>MacOS: build DMG
MacOS->>MacOS: "upload-artifact OpenLogi-macos-dmg-*"
LPkg-->>Pub: (needs gate)
MacOS-->>Pub: (needs gate)
Pub->>Pub: download-artifact pattern OpenLogi-macos-dmg best-effort
Pub->>Pub: download-artifact pattern OpenLogi-windows best-effort
Pub->>Pub: download-artifact name OpenLogi-linux-packages hard-fail
Note over Pub: if linux-packages failed, download fails, publish blocked
Pub->>Pub: "sha256sum .dmg .exe .msi .deb .rpm > SHA256SUMS"
Pub->>Pub: softprops/action-gh-release publish release
Reviews (17): Last reviewed commit: "Merge remote-tracking branch 'origin/mas..." | Re-trigger Greptile |
- 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>
- 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>
20c8db1 to
742e2d3
Compare
- 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>
742e2d3 to
4c91eeb
Compare
- 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>
4c91eeb to
f715d5e
Compare
- 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>
f715d5e to
5ab635e
Compare
- 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>
5ab635e to
7dbbbb9
Compare
- 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>
7dbbbb9 to
f18f39f
Compare
- 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>
f18f39f to
731bb70
Compare
- 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>
731bb70 to
1141817
Compare
- 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>
1141817 to
1d4ff6c
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
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")]
- 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"
- 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
- 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()
- 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>
1725e8e to
0f49105
Compare
On Windows, PermissionStatus, classify, rgb, and the permissions module are all dead — they only exist for macOS/Linux permission dialogs. Gate each with the appropriate cfg so Windows clippy -D warnings passes.
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")]
- 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
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.
- 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
- 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
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.
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.
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.
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.
- 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)
- 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
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.
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.
…ring - Upgrade all upload-artifact@v7 → @v8 to match pre-existing download- artifact@v8 in publish job (v7/v8 use incompatible artifact backends) - Add linux-packages to publish job needs[] so download never races upload - Add *.deb *.rpm to sha256sum so Linux packages are integrity-checked - Rename preremove.sh → postremove.sh: dpkg/RPM invoke prerm before removing files, so udevadm trigger re-applied the rules instead of revoking them; postrm runs after deletion so the reload actually revokes
- 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>
0f49105 to
dd9644e
Compare
# Conflicts: # crates/openlogi-gui/src/windows/settings.rs
dd9644e to
b81d298
Compare
* feat(linux): launch_at_login via systemd user unit 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 * feat(linux): input device access permission check 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")] * fix(gui): hide macOS-only UI on Linux - 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" * fix(linux): address PR #172 review comments - 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 * fix(linux): address PR #172 round-2 review comments - 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() * i18n(gui): add translations for 'Input device access' Added missing translations for the Linux permission row label introduced in this PR. Follows the same pattern as 'Input Monitoring'. * fix(gui,i18n): add missing locale keys; fix InteractiveElement import 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 * fix(gui): gate dead code; add StatefulInteractiveElement import - Add StatefulInteractiveElement as _ to unconditional gpui import so on_click() resolves on macOS (fixes E0599) - Gate status_badge with any(macos, linux) since both platforms call it (fixes Windows dead_code warning) - Remove unreachable input_device_access stub for non-mac/non-linux targets; nothing calls it there (fixes Windows dead_code warning) * fix(gui): rebase onto upstream; correct cfg gates for macOS-only imports Upstream added Option<bool> accessibility_granted with a 3-state match. Merge that with our Linux cfg structure: - Take upstream's Some(true)/Some(false)/None match for accessibility - Restore #[cfg(target_os = "macos")] on permission_field, permission_item, Permission import, and StatefulInteractiveElement import — all are macOS-only; gating them unconditionally caused dead_code / unused-import errors on Linux * fix(gui): gate platform-specific imports to fix Windows clippy On Windows, PermissionStatus, classify, rgb, and the permissions module are all dead — they only exist for macOS/Linux permission dialogs. Gate each with the appropriate cfg so Windows clippy -D warnings passes. * feat(linux): input device access permission check 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")] * feat(linux): add install artifacts - 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 * docs(linux): add Linux install guide and README section 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. * fix(linux): address PR #173 review comments - 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 * fix(linux): address PR #173 round-2 review comments - 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 * fix(linux): also grant uaccess for Bluetooth-connected Logitech devices 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. * fix(linux): set XDG_RUNTIME_DIR for systemctl --user calls under sudo 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. * refactor(linux): use @bindir@ placeholder in service template 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. * docs(linux): note replug requirement when device connected at install time * feat(linux): add nfpm package descriptor and install scripts 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. * feat(linux): xtask package-linux command and CI release job 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. * refactor(linux): simplify package-linux xtask and CI - 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) * fix(linux): address PR #179 review comments - 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 * fix(ci): pin nfpm to a specific release with SHA256 verification 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. * fix(linux): udevadm settle in package scripts; sign .deb/.rpm 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. * fix(linux): expand @bindir@ in service unit during package postinstall * style(xtask): rustfmt linux.rs args array * fix(ci,pkg): artifact version mismatch, publish race, postremove ordering - Upgrade all upload-artifact@v7 → @v8 to match pre-existing download- artifact@v8 in publish job (v7/v8 use incompatible artifact backends) - Add linux-packages to publish job needs[] so download never races upload - Add *.deb *.rpm to sha256sum so Linux packages are integrity-checked - Rename preremove.sh → postremove.sh: dpkg/RPM invoke prerm before removing files, so udevadm trigger re-applied the rules instead of revoking them; postrm runs after deletion so the reload actually revokes * feat(hid): Unifying receiver — protocol, discovery, enumeration, routing 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. * fix(gui,agent,cli): wire Unifying receiver into the app stack 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 #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). * fix(hid): scope Unifying cache key to receiver; clarify Bolt fallback 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. * fix(hid): safe Clone for UnifyingReceiver; proper Unifying cache key 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. * i18n(gui): add translations for 'Unifying receiver' Added missing translations for the route label introduced in this PR. Placed alongside 'Bolt receiver' following the same pattern. * fix(hidpp): safe Clone for BoltReceiver; share ListenerDropGuard 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. * fix(hid): preserve cached data on Unifying slot probe timeout 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. * fix(hid): gate is_receiver_child_sysfs_path for non-Linux builds 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. * fix(hid): fall back to PID when Unifying receiver UID is unavailable 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. * fix(agent): re-enable systemd unit even when content is already current A manually-disabled unit is not re-enabled on app restart because reconcile exits early when the file content matches. Always call systemctl enable in that arm — it is idempotent and no daemon-reload is needed when the file is unchanged. * fix(gui,xtask): gate platform permissions to macos/linux; drop dead xtask fn PermissionStatus and classify are only meaningful on platforms where permission-check functions exist (macOS and Linux). Gate them so the Windows clippy job no longer reports dead-code warnings. Gate the test module to Linux-only since the tests exercise Linux-specific classify logic. Remove the unused workspace_version helper from xtask/linux.rs (package_linux uses env! instead). --------- Co-authored-by: AprilNEA <github@sku.moe>
* feat(linux): launch_at_login via systemd user unit 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 * feat(linux): input device access permission check 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")] * fix(gui): hide macOS-only UI on Linux - 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" * fix(linux): address PR #172 review comments - 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 * fix(linux): address PR #172 round-2 review comments - 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() * i18n(gui): add translations for 'Input device access' Added missing translations for the Linux permission row label introduced in this PR. Follows the same pattern as 'Input Monitoring'. * fix(gui,i18n): add missing locale keys; fix InteractiveElement import 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 * fix(gui): gate dead code; add StatefulInteractiveElement import - Add StatefulInteractiveElement as _ to unconditional gpui import so on_click() resolves on macOS (fixes E0599) - Gate status_badge with any(macos, linux) since both platforms call it (fixes Windows dead_code warning) - Remove unreachable input_device_access stub for non-mac/non-linux targets; nothing calls it there (fixes Windows dead_code warning) * fix(gui): rebase onto upstream; correct cfg gates for macOS-only imports Upstream added Option<bool> accessibility_granted with a 3-state match. Merge that with our Linux cfg structure: - Take upstream's Some(true)/Some(false)/None match for accessibility - Restore #[cfg(target_os = "macos")] on permission_field, permission_item, Permission import, and StatefulInteractiveElement import — all are macOS-only; gating them unconditionally caused dead_code / unused-import errors on Linux * fix(gui): gate platform-specific imports to fix Windows clippy On Windows, PermissionStatus, classify, rgb, and the permissions module are all dead — they only exist for macOS/Linux permission dialogs. Gate each with the appropriate cfg so Windows clippy -D warnings passes. * feat(linux): input device access permission check 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")] * feat(linux): add install artifacts - 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 * docs(linux): add Linux install guide and README section 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. * fix(linux): address PR #173 review comments - 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 * fix(linux): address PR #173 round-2 review comments - 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 * fix(linux): also grant uaccess for Bluetooth-connected Logitech devices 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. * fix(linux): set XDG_RUNTIME_DIR for systemctl --user calls under sudo 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. * refactor(linux): use @bindir@ placeholder in service template 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. * docs(linux): note replug requirement when device connected at install time * feat(linux): add nfpm package descriptor and install scripts 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. * feat(linux): xtask package-linux command and CI release job 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. * refactor(linux): simplify package-linux xtask and CI - 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) * fix(linux): address PR #179 review comments - 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 * fix(ci): pin nfpm to a specific release with SHA256 verification 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. * fix(linux): udevadm settle in package scripts; sign .deb/.rpm 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. * fix(linux): expand @bindir@ in service unit during package postinstall * style(xtask): rustfmt linux.rs args array * fix(ci,pkg): artifact version mismatch, publish race, postremove ordering - Upgrade all upload-artifact@v7 → @v8 to match pre-existing download- artifact@v8 in publish job (v7/v8 use incompatible artifact backends) - Add linux-packages to publish job needs[] so download never races upload - Add *.deb *.rpm to sha256sum so Linux packages are integrity-checked - Rename preremove.sh → postremove.sh: dpkg/RPM invoke prerm before removing files, so udevadm trigger re-applied the rules instead of revoking them; postrm runs after deletion so the reload actually revokes * feat(hid): Unifying receiver — protocol, discovery, enumeration, routing 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. * fix(gui,agent,cli): wire Unifying receiver into the app stack 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 #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). * fix(hid): scope Unifying cache key to receiver; clarify Bolt fallback 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. * fix(hid): safe Clone for UnifyingReceiver; proper Unifying cache key 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. * i18n(gui): add translations for 'Unifying receiver' Added missing translations for the route label introduced in this PR. Placed alongside 'Bolt receiver' following the same pattern. * fix(hidpp): safe Clone for BoltReceiver; share ListenerDropGuard 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. * fix(hid): preserve cached data on Unifying slot probe timeout 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. * fix(hid): gate is_receiver_child_sysfs_path for non-Linux builds 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. * fix(hid): fall back to PID when Unifying receiver UID is unavailable 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. * fix(agent): re-enable systemd unit even when content is already current A manually-disabled unit is not re-enabled on app restart because reconcile exits early when the file content matches. Always call systemctl enable in that arm — it is idempotent and no daemon-reload is needed when the file is unchanged. * fix(gui,xtask): gate platform permissions to macos/linux; drop dead xtask fn PermissionStatus and classify are only meaningful on platforms where permission-check functions exist (macOS and Linux). Gate them so the Windows clippy job no longer reports dead-code warnings. Gate the test module to Linux-only since the tests exercise Linux-specific classify logic. Remove the unused workspace_version helper from xtask/linux.rs (package_linux uses env! instead). * docs: update README for Linux support - 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 * fix(docs): correct Unifying label; remove duplicate Linux section - '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 * docs(linux): remove stale pre-package and no-Unifying claims - Replace WARNING callout (claimed Unifying unsupported) with NOTE listing both Bolt and Unifying as supported - Replace 'no pre-built packages yet' paragraph with a pointer to the releases page and README install section - Remove Unifying row from known-limitations table (now supported) * fix(gui): restore cfg-gated StatefulInteractiveElement import Rebase dropped the macOS-gated StatefulInteractiveElement import that permission_field's .on_click() requires on Stateful<Div>. * fix(gui): remove duplicate StatefulInteractiveElement import Rebase introduced a second #[cfg(macos)] StatefulInteractiveElement import alongside the one already at the top of the file. --------- Co-authored-by: AprilNEA <github@sku.moe>
PR #179 bumped all upload-artifact steps from v7 to v8 to 'match the pre-existing download-artifact@v8', claiming the v7/v8 artifact backends are incompatible. Neither holds: actions/upload-artifact has no v8 tag (latest is v7.0.1; the two actions' major versions have never been in lockstep), and the v0.6.7 release shipped green on the v7+v8 pair hours earlier. Every job on the v0.6.8 tag run failed at job setup with 'Unable to resolve action actions/upload-artifact@v8', so no GitHub release was published for v0.6.8.
Dependency
Summary
packaging/linux/nfpm.yaml— nfpm package descriptor mapping the three binaries (openlogi,openlogi-gui,openlogi-agent), udev rules, systemd user unit, desktop entry, and icon into.deb/.rpm. The package version is templated via${VERSION}env var set by the xtask at build time.packaging/linux/nfpm-scripts/postinstall.sh— runs after package install: reloads udevadm, refreshes the icon and desktop caches, and prints instructions to enable the background agent.packaging/linux/nfpm-scripts/preremove.sh— runs before package removal: reloads udevadm to revokeuaccesstags immediately.xtask/src/linux.rs— newpackage-linuxxtask command: builds release binaries, reads the workspace version fromCargo.toml, and invokesnfpmfor bothdebandrpm. Accepts--no-buildto skip the cargo step..github/workflows/release.yml— newlinux-packagesjob (runs onubuntu-latest, parallel todmg): installs nfpm via the goreleaser apt repo, runscargo run -p xtask -- package-linux, and uploads.deb/.rpmas a release artifact. Thepublishjob now depends on bothdmgandlinux-packages, downloads both artifact sets, includes.deb/.rpmin the checksums and in the GitHub Release.Test plan
cargo run -p xtask -- package-linuxproducestarget/release/openlogi_*.debandtarget/release/openlogi_*.rpmafter a release builddpkg -c target/release/openlogi_*.debshows all expected paths (/usr/bin/,/etc/udev/rules.d/,/usr/lib/systemd/user/,/usr/share/)rpm -qlp target/release/openlogi_*.rpmshows the same paths.debon a fresh Ubuntu VM — udev rules active, agent unit present, desktop entry visiblelinux-packagesjob passes on a tag push🤖 Generated with Claude Code