feat: Linux/KDE support, nix flake, and distro packaging (v2.4.0)#35
Conversation
Add a Linux platform module that integrates with KDE Plasma via the freedesktop StatusNotifierItem (SNI) protocol over D-Bus (trayicon's Linux backend). CPU usage is read from /proc/stat; settings persist to ~/.config/rustcat; autostart uses a freedesktop autostart .desktop file; theme detection/dialogs/system monitor use kreadconfig/kdialog/ plasma-systemmonitor with graceful fallbacks. Add a Nix flake (crane) with packages, devShells, and apps outputs, wrapping runtime helper tools onto PATH. Scope the crt-static rustflag to Windows only so Linux links dynamically against glibc (it previously forced static-glibc which most distros and Nix don't ship). Add Linux distro packaging: .deb (cargo-deb), .rpm (cargo-generate-rpm), AppImage (linuxdeploy), and a portable .tar.gz, all built in a new build-linux CI job and published on release. Bump version 2.3.0 -> 2.4.0. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces native Linux and KDE support to RustCat, adding a dedicated platform module, CPU usage monitoring via /proc/stat, and packaging configurations for Debian, RPM, and Nix. Feedback on the changes suggests several key improvements: utilizing libc::localtime_r to fetch the local hour efficiently instead of spawning an external date process, waiting on spawned child processes to prevent zombie processes, clamping the calculated CPU usage to ensure it stays within bounds, and installing the application icon to pixmaps/ rather than icons/ for better desktop environment compatibility.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| [target.'cfg(target_os = "linux")'.dependencies] | ||
| dirs = "6.0" |
| fn get_local_hour() -> u32 { | ||
| let output = Command::new("date") | ||
| .arg("+%H") | ||
| .output() | ||
| .unwrap_or_else(|_| std::process::Output { | ||
| status: std::process::ExitStatus::default(), | ||
| stdout: b"0".to_vec(), | ||
| stderr: Vec::new(), | ||
| }); | ||
|
|
||
| String::from_utf8_lossy(&output.stdout) | ||
| .trim() | ||
| .parse() | ||
| .unwrap_or(0) | ||
| } |
There was a problem hiding this comment.
Spawning the external date process every second is highly inefficient and resource-intensive for a lightweight system monitor. Use libc::localtime_r instead, which is extremely fast and has zero process spawning overhead.
fn get_local_hour() -> u32 {
unsafe {
let mut now: libc::time_t = 0;
if libc::time(&mut now) != -1 {
let mut tm = std::mem::zeroed();
if !libc::localtime_r(&now, &mut tm).is_null() {
return tm.tm_hour as u32;
}
}
}
0
}| fn show_dialog(message: &str, title: &str) -> Result<(), Box<dyn std::error::Error>> { | ||
| // Prefer KDE's kdialog, fall back to zenity, then xmessage. | ||
| if Command::new("kdialog").arg("--title").arg(title).arg("--msgbox").arg(message).spawn().is_ok() { | ||
| return Ok(()); | ||
| } | ||
| if Command::new("zenity").args(["--title", title, "--info", "--text", message]).spawn().is_ok() { | ||
| return Ok(()); | ||
| } | ||
| if Command::new("xmessage").args(["-title", title, message]).spawn().is_ok() { | ||
| return Ok(()); | ||
| } | ||
| // Last resort: just print to stderr | ||
| eprintln!("{}: {}", title, message); | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Spawning dialog processes and dropping the Child handle immediately without waiting/reaping causes them to become zombie processes when closed. Spawn a background thread to wait on the child process to prevent zombie accumulation.
fn show_dialog(message: &str, title: &str) -> Result<(), Box<dyn std::error::Error>> {
// Prefer KDE's kdialog, fall back to zenity, then xmessage.
let mut cmd = Command::new("kdialog");
cmd.arg("--title").arg(title).arg("--msgbox").arg(message);
if let Ok(mut child) = cmd.spawn() {
std::thread::spawn(move || {
let _ = child.wait();
});
return Ok(());
}
let mut cmd = Command::new("zenity");
cmd.args(["--title", title, "--info", "--text", message]);
if let Ok(mut child) = cmd.spawn() {
std::thread::spawn(move || {
let _ = child.wait();
});
return Ok(());
}
let mut cmd = Command::new("xmessage");
cmd.args(["-title", title, message]);
if let Ok(mut child) = cmd.spawn() {
std::thread::spawn(move || {
let _ = child.wait();
});
return Ok(());
}
// Last resort: just print to stderr
eprintln!("{}: {}", title, message);
Ok(())
}| fn open_system_monitor() -> Result<(), Box<dyn std::error::Error>> { | ||
| // KDE system monitor (Plasma 5.21+), fall back to older ksysguard / htop. | ||
| for prog in ["plasma-systemmonitor", "ksysguard", "gnome-system-monitor"] { | ||
| if Command::new(prog).spawn().is_ok() { | ||
| return Ok(()); | ||
| } | ||
| } | ||
| if Command::new("xterm").arg("-e").arg("htop").spawn().is_ok() { | ||
| return Ok(()); | ||
| } | ||
| Err("No system monitor found (tried plasma-systemmonitor, ksysguard, gnome-system-monitor, htop)".into()) | ||
| } |
There was a problem hiding this comment.
Spawning system monitor processes and dropping the Child handle immediately without waiting/reaping causes them to become zombie processes when closed. Spawn a background thread to wait on the child process to prevent zombie accumulation.
fn open_system_monitor() -> Result<(), Box<dyn std::error::Error>> {
// KDE system monitor (Plasma 5.21+), fall back to older ksysguard / htop.
for prog in ["plasma-systemmonitor", "ksysguard", "gnome-system-monitor"] {
let mut cmd = Command::new(prog);
if let Ok(mut child) = cmd.spawn() {
std::thread::spawn(move || {
let _ = child.wait();
});
return Ok(());
}
}
let mut cmd = Command::new("xterm");
cmd.arg("-e").arg("htop");
if let Ok(mut child) = cmd.spawn() {
std::thread::spawn(move || {
let _ = child.wait();
});
return Ok(());
}
Err("No system monitor found (tried plasma-systemmonitor, ksysguard, gnome-system-monitor, htop)".into())
}| if total_diff > 0.0 { | ||
| 100.0 - (idle_diff / total_diff * 100.0) | ||
| } else { | ||
| 0.0 | ||
| } |
There was a problem hiding this comment.
Clamp the calculated CPU usage between 0.0 and 100.0 to prevent negative or >100 values in virtualized or unstable environments.
| if total_diff > 0.0 { | |
| 100.0 - (idle_diff / total_diff * 100.0) | |
| } else { | |
| 0.0 | |
| } | |
| if total_diff > 0.0 { | |
| (100.0 - (idle_diff / total_diff * 100.0)).clamp(0.0, 100.0) | |
| } else { | |
| 0.0 | |
| } |
| assets = [ | ||
| ["target/release/rust_cat", "usr/bin/", "755"], | ||
| ["assets/rustcat.desktop", "usr/share/applications/", "644"], | ||
| ["assets/appIcon.ico", "usr/share/icons/rustcat.ico", "644"], |
| assets = [ | ||
| { source = "target/release/rust_cat", dest = "/usr/bin/rust_cat", mode = "755" }, | ||
| { source = "assets/rustcat.desktop", dest = "/usr/share/applications/rustcat.desktop", mode = "644" }, | ||
| { source = "assets/appIcon.ico", dest = "/usr/share/icons/rustcat.ico", mode = "644" }, |
There was a problem hiding this comment.
| rm -rf "${PKG_DIR}" | ||
| mkdir -p "${PKG_DIR}/bin" "${PKG_DIR}/share/applications" "${PKG_DIR}/share/icons" | ||
|
|
||
| cp target/release/${BIN_NAME} "${PKG_DIR}/bin/" | ||
| cp assets/rustcat.desktop "${PKG_DIR}/share/applications/" | ||
| cp assets/appIcon.ico "${PKG_DIR}/share/icons/rustcat.ico" |
There was a problem hiding this comment.
Installing the icon directly to share/icons/ is non-standard and prevents desktop environments from finding it. Move it to share/pixmaps/ so the desktop entry can resolve it correctly.
| rm -rf "${PKG_DIR}" | |
| mkdir -p "${PKG_DIR}/bin" "${PKG_DIR}/share/applications" "${PKG_DIR}/share/icons" | |
| cp target/release/${BIN_NAME} "${PKG_DIR}/bin/" | |
| cp assets/rustcat.desktop "${PKG_DIR}/share/applications/" | |
| cp assets/appIcon.ico "${PKG_DIR}/share/icons/rustcat.ico" | |
| rm -rf "${PKG_DIR}" | |
| mkdir -p "${PKG_DIR}/bin" "${PKG_DIR}/share/applications" "${PKG_DIR}/share/pixmaps" | |
| cp target/release/${BIN_NAME} "${PKG_DIR}/bin/" | |
| cp assets/rustcat.desktop "${PKG_DIR}/share/applications/" | |
| cp assets/appIcon.ico "${PKG_DIR}/share/pixmaps/rustcat.ico" |
|
|
||
| install -Dm755 "${SCRIPT_DIR}/bin/rust_cat" "${PREFIX}/bin/rust_cat" | ||
| install -Dm644 "${SCRIPT_DIR}/share/applications/rustcat.desktop" "${PREFIX}/share/applications/rustcat.desktop" | ||
| install -Dm644 "${SCRIPT_DIR}/share/icons/rustcat.ico" "${PREFIX}/share/icons/rustcat.ico" |
There was a problem hiding this comment.
Installing the icon directly to share/icons/ is non-standard and prevents desktop environments from finding it. Move it to share/pixmaps/ so the desktop entry can resolve it correctly.
| install -Dm644 "${SCRIPT_DIR}/share/icons/rustcat.ico" "${PREFIX}/share/icons/rustcat.ico" | |
| install -Dm644 "${SCRIPT_DIR}/share/pixmaps/rustcat.ico" "${PREFIX}/share/pixmaps/rustcat.ico" |
|
|
||
| # Desktop entry + icon for application launchers. | ||
| install -Dm644 ${self}/assets/rustcat.desktop $out/share/applications/rustcat.desktop | ||
| install -Dm644 ${self}/assets/appIcon.ico $out/share/icons/rustcat.ico |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1d875665ee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| rustCat = craneLib.buildPackage { | ||
| inherit src cargoArtifacts nativeBuildInputs; | ||
| pname = "rustcat"; | ||
| version = "2.3.0"; |
There was a problem hiding this comment.
Keep the Nix package version in sync
When users run nix build .#default for this 2.4.0 release, the derivation is still named/versioned as 2.3.0 even though Cargo.toml, Cargo.lock, and the changelog were bumped to 2.4.0. This produces stale Nix output metadata such as rustcat-2.3.0, which is especially confusing for release artifacts and binary caches; please derive this from the Cargo package version or bump it alongside the release.
Useful? React with 👍 / 👎.
| Type=Application\n\ | ||
| Name=RustCat\n\ | ||
| Comment=CPU usage monitor tray cat\n\ | ||
| Exec={exe_str}\n\ |
There was a problem hiding this comment.
Escape the autostart Exec path
When Run on Start is enabled from a binary installed under a path containing spaces or other desktop Exec metacharacters, this writes the raw absolute path into the autostart file (for example Exec=/home/me/Rust Cat/rust_cat). Freedesktop autostart parses Exec as a command line, so login will try to execute the first token instead of the actual binary and RustCat will not start; quote/escape the path before writing it.
Useful? React with 👍 / 👎.
- Install the icon to share/pixmaps/ (freedesktop location for non-themed icons) instead of share/icons/ across deb, rpm, the tarball, its install.sh, and the nix flake — bare share/icons/rustcat.ico isn't found by most desktop environments. - Derive the nix package version from Cargo.toml (was hardcoded 2.3.0, out of sync after the 2.4.0 bump). - Reap spawned helper processes (kdialog, zenity, xmessage, system monitor) on a background thread so they don't accumulate as zombies during RustCat's long lifetime. - Escape the autostart Exec= value per the desktop entry spec so paths containing spaces or other reserved chars still launch at login. - Clamp /proc/stat-derived CPU usage to [0, 100] to guard against non-monotonic counters (VMs, after suspend). Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c4a0cab48c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if let Ok(exe_path) = std::env::current_exe() { | ||
| let exe_str = exe_path.to_string_lossy().to_string(); |
There was a problem hiding this comment.
Use the stable AppImage path for autostart
When RustCat is launched from the AppImage produced by this PR, current_exe() resolves to the binary inside AppImage's temporary mount (for example /tmp/.mount_.../usr/bin/rust_cat), so enabling Run on Start writes an Exec= path that disappears as soon as the AppImage exits or the user logs out. In that context the autostart entry is left pointing at a non-existent file; prefer the stable AppImage path exposed by the runtime (e.g. $APPIMAGE) before falling back to current_exe().
Useful? React with 👍 / 👎.
Summary
Ports RustCat to Linux/KDE and adds Nix flake + distro packaging (.deb / .rpm / AppImage) in CI. Bumps version to 2.4.0.
Linux/KDE port
src/platform/linux/module mirroring the Windows/macOS architecture:/proc/stat~/.config/rustcat/settings.conf~/.config/autostart/rustcat.desktopkreadconfig6/kreadconfig5(+kdeglobalsfallback)kdialog(→ zenity → xmessage), system monitor viaplasma-systemmonitor(→ ksysguard → gnome-system-monitor → htop), graceful degradation if a tool is missing.icoassets work as-is (backend picks the largest entry).Nix flake
flake.nix(crane) exposingpackages/devShells/apps; runtime helper tools wrapped ontoPATH.Distro packaging (CI)
build-linuxjob produces: portable.tar.gz,.deb(cargo-deb),.rpm(cargo-generate-rpm), and AppImage (linuxdeploy) — uploaded as artifacts and published on release.Cargo.tomlgains[package.metadata.deb]and[package.metadata.generate-rpm]+ standard crates.io metadata.Build fix
.cargo/config.toml'scrt-staticrustflag was global; it was meant for MSVC static linking but forced static-glibc linking on Linux, which most distros (and Nix) don't ship. Scoped it to#[cfg(windows)]; Linux now links dynamically and builds cleanly.Verification
cargo check --target x86_64-unknown-linux-gnu— clean, no warnings.nix build .#default— produces a 1.8 MB dynamically-linked ELF + desktop/icon; binary smoke-tested (loads icons, reads/proc/stat, detects dark theme, builds tray menu).cargo deb— produces a well-formedrustcat_2.4.0_amd64.debwith/usr/bin/rust_cat, desktop entry, icon, andRecommends: kdialog, plasma-systemmonitor.nix flake check --no-build— all checks pass.See
CHANGELOG.mdfor the full 2.4.0 entry.🤖 Generated with Claude Code