Skip to content

fix(linux): make the Linux build actually work; add CI - #9

Merged
PalmarHealer merged 2 commits into
mainfrom
fix/linux-support
Sep 12, 2026
Merged

PalmarHealer merged 2 commits into
mainfrom
fix/linux-support

Conversation

@PalmarHealer

@PalmarHealer PalmarHealer commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

The Linux port had never been compiled by a Linux compiler or run on a Linux desktop. Four things were broken at runtime; this fixes them and adds the CI job that would have caught them.

Blockers

Key simulation was broken for everything except letters and digits. VK codes went straight into enigo's Key::Other, which is a Windows virtual-key code only on Windows — on Linux it is an X11 keysym. The two numbering schemes coincide for ASCII letters/digits and nowhere else:

Key VK Keysym on Linux Result
F1 0x70 p types "p"
Enter 0x0D undefined (Return is 0xFF0D) nothing
Left arrow 0x25 % types "%"
Volume Down 0xAE ® (XF86AudioLowerVolume is 0x1008FF11) nothing
Ctrl / Shift / Alt 0x11 / 0x10 / 0x12 undefined nothing

New keys_vk module with the mapping. A test asserts every entry in keys_library::KEYS has one, so a key cannot be added later without a keysym.

--hidden stranded the app. src/tray.rs is Windows-only, but the settings offered "Start minimized to tray" everywhere, and autostart wrote --hidden into the .desktop file. On Linux that produced a process with no window, no tray, and no close event — kill was the only exit. The flag is now ignored where there is no tray and the two tray rows are hidden.

Serious

All pactl parsing was locale-dependent. Every parser matched English output (Sink Input #, Name:, yes), which pactl translates. On a German desktop the device and process lists came back empty and is_muted always returned false. All invocations now force LC_ALL=C.

Up to 60 fork/exec per second at idle. is_muted / get_volume spawned a pactl per call and the LED engine polls them 10×/s per LED. Volume: and Mute: are already in the pactl list output that gets refreshed anyway, so reads are served from that cache now. Constant ~8 processes/s regardless of LED count.

Smaller

  • friendly() in serial.rs only matched Windows error wording, so both of its Linux branches were dead code. Added the libc spellings and the actual fix (dialout group), plus packaging/99-slidr.rules.
  • Exec= in the .desktop file was unquoted — a path with a space never launched. Also rewrites a stale entry when the binary has moved.
  • libpulse-binding and udev were declared and used nowhere: a native build and runtime dependency each, for nothing.
  • enigo switched from xdo to x11rb. xdo is #[link(name = "xdo")], making libxdo.so.3 a load-time dependency — the shipped binary refused to start without xdotool. DT_NEEDED is now just libudev, libfontconfig, libgcc_s, libm, libc and the loader.
  • /proc/<pid>/comm truncates at 15 chars, so long binaries never matched what PulseAudio reports; recovered from cmdline.
  • deploy.sh published the Linux binary mode 0644.

CI

release.yml only fires on tags, so nothing compiled the Linux target between releases. The new ci.yml builds, tests, and prints the binary's DT_NEEDED so a newly linked native library is visible in the log instead of surfacing as "it won't start". It caught a real error in this changeset on its first run.

The pactl parsers and the keysym table sit in audio/pulse_parse and keys_vk, deliberately not cfg-gated, so the logic most likely to regress is unit-testable on any host.

Verification

Linux CI green on this content: cargo check --all-targets, cargo test (35 passed), cargo build --release. Committed from an isolated worktree and verified to build standalone.

Not verified: no Linux hardware was available, so nothing here was exercised against a real board or desktop session.

Note for the parallel Windows work: this deliberately leaves out the default_output_id() trait method and its pulse implementation — those belong with the output-switch feature branch.

🤖 Generated with Claude Code


This also unblocks CI for every other branch

Verified independently by the parallel Windows-feature branch (feature/app-categories-led-sources, run 34690055598): branched off current main, its Linux CI fails at the link step with

rust-lld: error: unable to find library -lxdo

ci.yml deliberately does not install libxdo-dev, because this PR drops the xdo backend. So the enigo xdo → x11rb change is not merely a runtime packaging improvement — until it lands, any branch off main cannot get past cargo test on Linux. That branch goes green on its own once this merges and it rebases.

Known follow-up

default_output_id() is intentionally not implemented for the pulse backend here: the trait method belongs to the output-switch feature on the other branch, and implementing it needs both that trait method and this PR's rewritten pulse.rs cache. Until the two are combined, the output-switch volume re-apply is Windows-only and no-ops on Linux. The pulse side is a five-line addition once both are on the same branch.

PalmarHealer and others added 2 commits September 12, 2026 12:54
The Linux port was never verified against a Linux compiler or a Linux desktop,
and four things were broken at runtime.

* keys: VK codes were handed to enigo's `Key::Other`, which is a Windows
  virtual-key code only on Windows — on Linux it is an X11 keysym. Letters and
  digits coincided; everything else did not. F1 typed "p", the arrow keys typed
  punctuation, and every media and modifier key was a silent no-op. Adds a
  VK->keysym table in the new `keys_vk` module, with a test asserting every
  entry of `keys_library::KEYS` maps, so a future key cannot be added without
  one. The repeat thread also built a fresh `Enigo` every 33ms, which on Linux
  meant an X11 connection and a keymap upload per tick; it now keeps one.

* enigo: switch from the default `xdo` backend to `x11rb`. `xdo` is
  `#[link(name = "xdo")]`, so libxdo.so.3 was a load-time dependency and the
  shipped binary would not start without xdotool installed. DT_NEEDED is now
  libudev, libfontconfig, libgcc_s, libm, libc and the loader — nothing else.

* main/ui: `--hidden` is ignored where there is no tray, and the two tray
  settings are hidden. `src/tray.rs` is Windows-only, so autostart plus "start
  minimized" left a process with no window, no tray and no close event: `kill`
  was the only way out.

* pulse: force `LC_ALL=C`. Every parser matched English pactl output, so on a
  translated desktop device and process lists came back empty and `is_muted`
  always returned false. Mute and volume are now served from the existing list
  cache instead of spawning a `pactl` per query — the LED engine polls 10x/s
  per LED, which was up to 60 fork/exec per second at idle. Monitor sources are
  filtered out of the microphone picker.

* serial: `friendly()` only matched Windows error wording, so both of its Linux
  branches were unreachable. Adds the libc spellings and names the actual fix
  (the `dialout` group), plus a udev rule in packaging/.

* autostart: quote `Exec=` so a path containing spaces works at all, and
  rewrite a stale entry when the executable has moved.

* Drop libpulse-binding and udev: declared, never used, and each one a native
  build and runtime dependency for nothing.

Add .github/workflows/ci.yml. release.yml only fires on tags, so nothing
compiled the Linux target between releases — and none of the code above is
visible to a Windows developer's compiler. The job builds, tests and prints the
binary's DT_NEEDED so a newly linked native library shows up in the log rather
than as "it won't start" on a user's machine. It caught a real error in this
very changeset on its first run.

The pactl parsers and the keysym table live in `audio/pulse_parse` and
`keys_vk`, deliberately not cfg-gated, so the logic most likely to regress is
unit-testable on any host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`untruncate_comm` splits /proc/<pid>/cmdline on NUL. The literal was written
into the source as an actual 0x00 byte rather than the two-character `\0`
escape. Rust accepts both, so it compiled and the tests passed — but a NUL byte
makes git and grep classify glue.rs as binary, which turns any merge touching
it into a whole-file conflict instead of a three-way merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PalmarHealer
PalmarHealer merged commit 856146a into main Sep 12, 2026
2 checks passed
PalmarHealer added a commit that referenced this pull request Sep 12, 2026
Completes the output-switch volume re-apply on Linux. Until now the trait
method fell back to its `None` default there, so the actuator never noticed a
default-sink change and the feature was Windows-only.

Reads the default sink from the cache `refresh()` already fills from
`pactl info`, so the actuator's 400ms poll costs no extra process.

Written by the session that did the Linux fixes in #9 and handed over for this
branch, since it needs both halves: the trait method from here and the
rewritten pulse.rs from there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant