Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/install-desktop-integration
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Icon=pickgauge
Terminal=false
Categories=Utility;Monitor;
Keywords=usage;quota;codex;claude;
StartupWMClass=pickgauge
StartupWMClass=Pickgauge
EOF

command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$APPS_DIR" || true
Expand Down
10 changes: 5 additions & 5 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ set -eu
REPO="pickforge/pickgauge"
APP_NAME="PickGauge"
BIN_NAME="pickgauge"
# The window's app_id (bundle identifier). The .desktop basename and
# StartupWMClass must equal it or the running window shows a generic icon.
# The window's app_id (bundle identifier) determines the .desktop basename.
APP_ID="pickgauge"
WM_CLASS="Pickgauge"

# Environment overrides:
# PICKGAUGE_INSTALL_DIR Linux AppImage target dir. Default: $HOME/.local/bin.
Expand Down Expand Up @@ -236,8 +236,8 @@ write_desktop_launcher() {
launcher_command=$1
launcher_icon=$2
launcher_dir="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
# Basename and StartupWMClass must equal the window's app_id so the desktop
# environment ties the running window to this entry (and its icon).
# The desktop environment uses the runtime window class to tie the running
# window to this entry (and its icon).
launcher_file="$launcher_dir/$APP_ID.desktop"

mkdir -p "$launcher_dir" 2>/dev/null || return 0
Expand All @@ -249,7 +249,7 @@ write_desktop_launcher() {
printf 'Comment=Local AI usage tray\n'
printf 'Exec="%s"\n' "$launcher_exec"
printf 'Icon=%s\n' "$launcher_icon"
printf 'StartupWMClass=%s\n' "$APP_ID"
printf 'StartupWMClass=%s\n' "$WM_CLASS"
printf 'Terminal=false\n'
printf 'Categories=Development;\n'
printf 'Keywords=pickgauge;usage;codex;claude;\n'
Expand Down
48 changes: 33 additions & 15 deletions src-tauri/src/kwin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ use std::process::Command;

const RULE_GROUP: &str = "pickgauge-float-keep-above";
const FLOAT_TITLE: &str = "PickGauge Float";
const WM_CLASS: &str = "pickgauge";
// KWin wmclass matching is case-sensitive; GTK derives this runtime class from the binary name (hardware-verified on KDE Wayland).
const WM_CLASS: &str = "Pickgauge";

fn find_command(name: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
Expand All @@ -33,7 +34,7 @@ fn find_command(name: &str) -> Option<PathBuf> {
None
}

fn group_has_key(contents: &str, group: &str, key: &str) -> bool {
fn group_value<'a>(contents: &'a str, group: &str, key: &str) -> Option<&'a str> {
let header = format!("[{group}]");
let mut in_group = false;
for line in contents.lines() {
Expand All @@ -42,11 +43,25 @@ fn group_has_key(contents: &str, group: &str, key: &str) -> bool {
in_group = trimmed == header;
continue;
}
if in_group && trimmed.starts_with(key) {
return true;
if in_group {
match trimmed.split_once('=') {
Some((candidate, value)) if candidate.trim() == key => {
return Some(value.trim());
}
_ => {}
}
}
}
false
None
}

fn group_has_key(contents: &str, group: &str, key: &str) -> bool {
group_value(contents, group, key).is_some()
}

fn rule_is_current(contents: &str) -> bool {
group_has_key(contents, RULE_GROUP, "positionrule")
&& group_value(contents, RULE_GROUP, "wmclass") == Some(WM_CLASS)
}

/// Whether the current session is a KDE Wayland compositor session — native
Expand Down Expand Up @@ -81,8 +96,7 @@ pub fn ensure_float_rule() {
let rules_path = Path::new(&home).join(".config/kwinrulesrc");
let existing_rules = std::fs::read_to_string(&rules_path).unwrap_or_default();
let already_registered = existing_rules.contains(RULE_GROUP);
// "positionrule" inside our group marks the current revision; rewrite older ones.
if already_registered && group_has_key(&existing_rules, RULE_GROUP, "positionrule") {
if already_registered && rule_is_current(&existing_rules) {
return;
}

Expand Down Expand Up @@ -113,7 +127,7 @@ pub fn ensure_float_rule() {
};

write("Description", "PickGauge float capsule (managed by PickGauge)");
// Match: window class contains "pickgauge" AND title is exactly the
// Match: window class contains "Pickgauge" AND title is exactly the
// float window title, so the main window is unaffected.
write("wmclass", WM_CLASS);
write("wmclassmatch", "2"); // substring
Expand Down Expand Up @@ -181,6 +195,11 @@ pub fn ensure_float_rule() {
mod tests {
use super::*;

#[test]
fn rule_uses_the_runtime_window_class() {
assert_eq!(WM_CLASS, "Pickgauge");
}

#[test]
fn native_wayland_kde_session_installs_the_rule() {
// Also covers pickforge/pickgauge#49: the predicate takes no
Expand Down Expand Up @@ -234,12 +253,11 @@ mod tests {
}

#[test]
fn group_has_key_finds_a_key_in_the_current_revision() {
let contents = "[pickgauge-float-keep-above]\nskiptaskbar=true\npositionrule=3\n";
assert!(group_has_key(
contents,
"pickgauge-float-keep-above",
"positionrule"
));
fn current_rule_requires_the_exact_runtime_window_class() {
let current = "[pickgauge-float-keep-above]\nwmclass=Pickgauge\npositionrule=3\n";
let stale = "[pickgauge-float-keep-above]\nwmclass=pickgauge\npositionrule=3\n";

assert!(rule_is_current(current));
assert!(!rule_is_current(stale));
}
}
1 change: 1 addition & 0 deletions tests/install-script-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ test("AppImage install writes a FUSE-aware wrapper, desktop entry, and icon", (r
assert.match(readFileSync(command, "utf8"), new RegExp(appImage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(readFileSync(launcher, "utf8"), /Exec=".*\/\.local\/bin\/pickgauge"/);
assert.match(readFileSync(launcher, "utf8"), /Icon=.*pickgauge/);
assert.match(readFileSync(launcher, "utf8"), /^StartupWMClass=Pickgauge$/m);
assert.equal(existsSync(icon), true);
assert.match(output, /Launch with `pickgauge`/);
assertHeadlessForwarding(command, home);
Expand Down