From eb1500324f882d5d80c3c2d4b7f9a0a6f41c0491 Mon Sep 17 00:00:00 2001 From: Jeremy Karst Date: Mon, 7 Sep 2026 00:12:36 -0400 Subject: [PATCH] Fixed system shortcuts for appimage version --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/quicksearch-gui/src/activate.rs | 40 +++++++++++++++++++- crates/quicksearch-gui/src/shortcut_setup.rs | 28 ++++++++++++-- 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf239ea..1e69c8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3108,7 +3108,7 @@ dependencies = [ [[package]] name = "quicksearch-core" -version = "1.1.7" +version = "1.1.8" dependencies = [ "argon2", "cfb", @@ -3150,7 +3150,7 @@ dependencies = [ [[package]] name = "quicksearch-gui" -version = "1.1.7" +version = "1.1.8" dependencies = [ "chrono", "eframe", diff --git a/Cargo.toml b/Cargo.toml index e22d3f0..23cf8c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ pdf-extract = { path = "vendor/pdf-extract" } # Patched unbounded reads which co rtf-parser = { path = "vendor/rtf-parser" } # Patched a parsing error which occurs on UTF-16 characters [workspace.package] -version = "1.1.7" +version = "1.1.8" edition = "2021" license = "GPL-3.0-or-later" authors = ["Jeremy "] diff --git a/crates/quicksearch-gui/src/activate.rs b/crates/quicksearch-gui/src/activate.rs index 13cd88f..aedf6f1 100644 --- a/crates/quicksearch-gui/src/activate.rs +++ b/crates/quicksearch-gui/src/activate.rs @@ -107,14 +107,28 @@ pub fn take_token() -> Option { TOKEN.lock().unwrap_or_else(std::sync::PoisonError::into_inner).take() } -/// What to tell the user to bind, as they would type it. The installed name +/// What to tell the user (or a desktop binding) to run. The installed name /// when we are on the path under it, and the full path otherwise — a build /// run out of `target/` is the common case, and "quicksearch" would be wrong /// advice there. +/// +/// An AppImage needs its own rule: `current_exe` there points into the +/// runtime's FUSE mount (`/tmp/.mount_…`), which vanishes with the process, +/// so a binding made from it dies the moment QuickSearch closes. The +/// runtime exports the durable path of the `.AppImage` file itself as +/// `$APPIMAGE`; that is the thing to run. pub fn command_name() -> String { let Ok(exe) = std::env::current_exe() else { return "quicksearch".to_string(); }; + durable_command(std::env::var_os("APPIMAGE").map(std::path::PathBuf::from), exe) +} + +/// The rule itself, split from the environment for the tests. +fn durable_command(appimage: Option, exe: PathBuf) -> String { + if let Some(appimage) = appimage.filter(|p| p.is_absolute()) { + return appimage.display().to_string(); + } let installed = exe .parent() .is_some_and(|dir| matches!(dir.to_str(), Some("/usr/bin") | Some("/usr/local/bin"))); @@ -584,6 +598,30 @@ mod tests { assert_ne!(a, b); } + /// An AppImage's mount path dies with the process; a binding must run + /// the image file itself. Everything else keeps the old rule. + #[test] + fn the_bound_command_survives_the_appimage_exiting() { + let mount = PathBuf::from("/tmp/.mount_quicksjBMkDo/usr/bin/quicksearch"); + assert_eq!( + durable_command(Some(PathBuf::from("/home/u/Apps/QuickSearch.AppImage")), mount.clone()), + "/home/u/Apps/QuickSearch.AppImage" + ); + // A relative or empty APPIMAGE is somebody playing games; ignored. + assert_eq!( + durable_command(Some(PathBuf::from("games")), mount.clone()), + mount.display().to_string() + ); + assert_eq!( + durable_command(None, PathBuf::from("/usr/bin/quicksearch")), + "quicksearch" + ); + assert_eq!( + durable_command(None, PathBuf::from("/opt/qs/quicksearch")), + "/opt/qs/quicksearch" + ); + } + /// The Windows reply parser, which faces whatever a squatting process /// cares to write into the well-known pipe name. #[test] diff --git a/crates/quicksearch-gui/src/shortcut_setup.rs b/crates/quicksearch-gui/src/shortcut_setup.rs index ed8f239..e81a048 100644 --- a/crates/quicksearch-gui/src/shortcut_setup.rs +++ b/crates/quicksearch-gui/src/shortcut_setup.rs @@ -399,9 +399,31 @@ mod kde { binding, holder.action_friendly, holder.component_friendly, )); } - // An earlier build's file in the old location would leave a second - // component claiming a key; gone before the new one appears. - let _ = std::fs::remove_file(legacy_desktop_file()); + // A re-install must start from nothing. The daemon's service refresh + // only *drops* components whose file vanished — it never re-reads a + // changed one, and a component surviving `unregister` empty blocks + // re-detection by name — so an install over an existing binding + // would keep serving the old Exec line (fatal for an AppImage, + // whose old mount path died with the last run). Tear down like + // `remove` does and let the daemon notice before rebuilding. + if desktop_file().is_file() || legacy_desktop_file().is_file() { + let _ = call("unregister", &[COMPONENT, ACTION]); + let _ = std::fs::remove_file(desktop_file()); + let _ = std::fs::remove_file(legacy_desktop_file()); + if run("kbuildsycoca6", &[]).or_else(|_| run("kbuildsycoca5", &[])).is_ok() { + // Gone when getComponent stops answering; bounded, and a + // timeout just falls through to the rebuild below. + for _ in 0..10 { + if call("getComponent", &[COMPONENT]).is_err() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(200)); + } + } + } else { + // No marker file, but an earlier claim may still be registered. + let _ = call("unregister", &[COMPONENT, ACTION]); + } // The file is the whole registration — key included — and the // installed marker, so a failure below deletes it again.