Skip to content

feat: Linux/KDE support, nix flake, and distro packaging (v2.4.0)#35

Merged
bearice merged 2 commits into
masterfrom
feat/linux-kde-support
Jul 7, 2026
Merged

feat: Linux/KDE support, nix flake, and distro packaging (v2.4.0)#35
bearice merged 2 commits into
masterfrom
feat/linux-kde-support

Conversation

@bearice

@bearice bearice commented Jul 7, 2026

Copy link
Copy Markdown
Owner

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

  • New src/platform/linux/ module mirroring the Windows/macOS architecture:
    • CPU usage from /proc/stat
    • Settings persisted to ~/.config/rustcat/settings.conf
    • Autostart via a freedesktop ~/.config/autostart/rustcat.desktop
    • Theme detection via kreadconfig6/kreadconfig5 (+ kdeglobals fallback)
    • Dialogs via kdialog (→ zenity → xmessage), system monitor via plasma-systemmonitor (→ ksysguard → gnome-system-monitor → htop), graceful degradation if a tool is missing
  • Tray integration uses trayicon's Linux backend = StatusNotifierItem over D-Bus (pure-Rust zbus + ico), so the existing .ico assets work as-is (backend picks the largest entry).

Nix flake

  • flake.nix (crane) exposing packages/devShells/apps; runtime helper tools wrapped onto PATH.

Distro packaging (CI)

  • New build-linux job produces: portable .tar.gz, .deb (cargo-deb), .rpm (cargo-generate-rpm), and AppImage (linuxdeploy) — uploaded as artifacts and published on release.
  • Cargo.toml gains [package.metadata.deb] and [package.metadata.generate-rpm] + standard crates.io metadata.

Build fix

  • .cargo/config.toml's crt-static rustflag 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-formed rustcat_2.4.0_amd64.deb with /usr/bin/rust_cat, desktop entry, icon, and Recommends: kdialog, plasma-systemmonitor.
  • nix flake check --no-build — all checks pass.

See CHANGELOG.md for the full 2.4.0 entry.

🤖 Generated with Claude Code

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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Cargo.toml
Comment on lines +23 to +24
[target.'cfg(target_os = "linux")'.dependencies]
dirs = "6.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Add libc dependency for Linux to enable highly efficient local time querying without spawning external processes.

[target.'cfg(target_os = "linux")'.dependencies]
dirs = "6.0"
libc = "0.2"

Comment on lines +36 to +50
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)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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
    }

Comment on lines +7 to +21
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(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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(())
    }

Comment on lines +23 to +34
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())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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())
    }

Comment on lines +40 to +44
if total_diff > 0.0 {
100.0 - (idle_diff / total_diff * 100.0)
} else {
0.0
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Clamp the calculated CPU usage between 0.0 and 100.0 to prevent negative or >100 values in virtualized or unstable environments.

Suggested change
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
}

Comment thread Cargo.toml Outdated
assets = [
["target/release/rust_cat", "usr/bin/", "755"],
["assets/rustcat.desktop", "usr/share/applications/", "644"],
["assets/appIcon.ico", "usr/share/icons/rustcat.ico", "644"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Installing the icon directly to usr/share/icons/ is non-standard and prevents desktop environments from finding it. Move it to usr/share/pixmaps/ so the desktop entry can resolve it correctly.

    ["assets/appIcon.ico", "usr/share/pixmaps/rustcat.ico", "644"],

Comment thread Cargo.toml Outdated
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" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Installing the icon directly to /usr/share/icons/ is non-standard and prevents desktop environments from finding it. Move it to /usr/share/pixmaps/ so the desktop entry can resolve it correctly.

    { source = "assets/appIcon.ico", dest = "/usr/share/pixmaps/rustcat.ico", mode = "644" },

Comment thread build_linux.sh Outdated
Comment on lines +31 to +36
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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"

Comment thread build_linux.sh Outdated

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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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"

Comment thread flake.nix Outdated

# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 ${self}/assets/appIcon.ico $out/share/pixmaps/rustcat.ico

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread flake.nix Outdated
rustCat = craneLib.buildPackage {
inherit src cargoArtifacts nativeBuildInputs;
pname = "rustcat";
version = "2.3.0";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/platform/linux/settings.rs Outdated
Type=Application\n\
Name=RustCat\n\
Comment=CPU usage monitor tray cat\n\
Exec={exe_str}\n\

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +45 to +46
if let Ok(exe_path) = std::env::current_exe() {
let exe_str = exe_path.to_string_lossy().to_string();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@bearice bearice merged commit 262d223 into master Jul 7, 2026
3 checks passed
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