Fixed system shortcuts for appimage version
All checks were successful
CI / linux (push) Successful in 12m9s
CI / windows-cross (push) Successful in 4m43s
CI / release (push) Successful in 13s

This commit is contained in:
Jeremy Karst 2026-09-07 00:12:36 -04:00
parent 658e32159a
commit eb1500324f
4 changed files with 67 additions and 7 deletions

4
Cargo.lock generated
View file

@ -3108,7 +3108,7 @@ dependencies = [
[[package]] [[package]]
name = "quicksearch-core" name = "quicksearch-core"
version = "1.1.7" version = "1.1.8"
dependencies = [ dependencies = [
"argon2", "argon2",
"cfb", "cfb",
@ -3150,7 +3150,7 @@ dependencies = [
[[package]] [[package]]
name = "quicksearch-gui" name = "quicksearch-gui"
version = "1.1.7" version = "1.1.8"
dependencies = [ dependencies = [
"chrono", "chrono",
"eframe", "eframe",

View file

@ -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 rtf-parser = { path = "vendor/rtf-parser" } # Patched a parsing error which occurs on UTF-16 characters
[workspace.package] [workspace.package]
version = "1.1.7" version = "1.1.8"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
authors = ["Jeremy <jeremy@karsttech.com>"] authors = ["Jeremy <jeremy@karsttech.com>"]

View file

@ -107,14 +107,28 @@ pub fn take_token() -> Option<String> {
TOKEN.lock().unwrap_or_else(std::sync::PoisonError::into_inner).take() 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 /// 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 /// run out of `target/` is the common case, and "quicksearch" would be wrong
/// advice there. /// 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 { pub fn command_name() -> String {
let Ok(exe) = std::env::current_exe() else { let Ok(exe) = std::env::current_exe() else {
return "quicksearch".to_string(); 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<PathBuf>, exe: PathBuf) -> String {
if let Some(appimage) = appimage.filter(|p| p.is_absolute()) {
return appimage.display().to_string();
}
let installed = exe let installed = exe
.parent() .parent()
.is_some_and(|dir| matches!(dir.to_str(), Some("/usr/bin") | Some("/usr/local/bin"))); .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); 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 /// The Windows reply parser, which faces whatever a squatting process
/// cares to write into the well-known pipe name. /// cares to write into the well-known pipe name.
#[test] #[test]

View file

@ -399,9 +399,31 @@ mod kde {
binding, holder.action_friendly, holder.component_friendly, binding, holder.action_friendly, holder.component_friendly,
)); ));
} }
// An earlier build's file in the old location would leave a second // A re-install must start from nothing. The daemon's service refresh
// component claiming a key; gone before the new one appears. // only *drops* components whose file vanished — it never re-reads a
let _ = std::fs::remove_file(legacy_desktop_file()); // 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 // The file is the whole registration — key included — and the
// installed marker, so a failure below deletes it again. // installed marker, so a failure below deletes it again.