launcher: scan desktop files directly, keep state in XDG_STATE_HOME

The desktop-entry cache lived in $XDG_CACHE_HOME/wldash/desktop.json.
On an NFS-shared home that is a remote read and write on every start
(unless the session happens to export a machine-local XDG_CACHE_HOME),
and it went stale whenever a package was installed until someone ran
--desktop-refresh. Scanning the applications directories takes a few
milliseconds over local /usr/share and already happened on a background
thread, so the cache bought nothing: drop it, along with simd-json and
the serde derives on Desktop. --desktop-refresh stays as a no-op so old
key bindings keep working.

Calculator history and the prompt saved on Escape are state, not cache:
they move to $XDG_STATE_HOME/wldash (~/.local/state), written only on
Enter/Escape, and the old cache location is still read as a fallback.

With the serde derives gone, two Desktop fields were unused; use them:
Type=Link entries now open their URL with xdg-open, and Terminal=true
entries run under $TERMINAL -e when it is set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-10-03 09:33:12 +00:00
co-authored by Claude Fable 5.1
parent 03a3c681b0
commit 9953865128
8 changed files with 82 additions and 272 deletions
+49 -24
View File
@@ -16,7 +16,7 @@ use crate::{
fonts::FontMap,
keyboard::{KeyEvent, keysyms},
utils::{
desktop::{Desktop, load_desktop_cache, load_desktop_files, write_desktop_cache},
desktop::{Desktop, load_desktop_files},
xdg,
},
widgets::{Geometry, Widget},
@@ -225,6 +225,29 @@ fn strip_field_codes(exec: &str) -> String {
out
}
/// Path of a small persistent state file (calc history, saved prompt)
/// under $XDG_STATE_HOME/wldash.
fn state_path(name: &str) -> String {
format!("{}/wldash/{}", xdg::state_folder(), name)
}
/// Read a state file, falling back to where older versions kept it
/// ($XDG_CACHE_HOME/wldash) so nothing is lost on upgrade.
fn read_state(name: &str) -> Option<String> {
read_to_string(state_path(name))
.or_else(|_| read_to_string(format!("{}/wldash/{}", xdg::cache_folder(), name)))
.ok()
}
/// Create (truncate) a state file, making the directory on demand.
fn create_state(name: &str) -> Option<File> {
let path = state_path(name);
if let Some(dir) = std::path::Path::new(&path).parent() {
let _ = std::fs::create_dir_all(dir);
}
File::create(path).ok()
}
trait InterfaceWidget {
/// Number of selectable rows (used to clamp the selection).
fn rows(&self) -> usize;
@@ -253,16 +276,8 @@ impl Launcher {
thread::Builder::new()
.name("desktopini".to_string())
.spawn(move || {
let mut options = options.lock().unwrap();
*options = match load_desktop_cache() {
Ok(v) => v,
Err(_) => {
let v = load_desktop_files();
write_desktop_cache(&v).unwrap();
v
}
};
drop(options);
let loaded = load_desktop_files();
*options.lock().unwrap() = loaded;
let mut events = events.lock().unwrap();
events.add_event(Event::LauncherUpdate);
})
@@ -306,9 +321,19 @@ impl InterfaceWidget for Launcher {
fn trigger(&mut self, intf: &mut InnerInterface) {
if self.matches.len() > intf.selection {
let d = &self.matches[intf.selection];
if let Some(exec) = &d.exec {
self.exec(&strip_field_codes(exec));
}
let cmd = match (&d.exec, &d.url) {
(Some(exec), _) => strip_field_codes(exec),
// Type=Link entries have no Exec; open the URL instead.
(None, Some(url)) => format!("xdg-open '{}'", url.replace('\'', "'\\''")),
(None, None) => return,
};
// Terminal=true: run inside the user's terminal when one is
// configured, otherwise as before.
let cmd = match (d.term, std::env::var("TERMINAL")) {
(true, Ok(term)) if !term.is_empty() => format!("{} -e {}", term, cmd),
_ => cmd,
};
self.exec(&cmd);
}
}
@@ -502,9 +527,9 @@ impl Calc {
thread::Builder::new()
.name("calc".to_string())
.spawn(move || {
let s = match read_to_string(format!("{}/wldash/calc", xdg::cache_folder())) {
Ok(s) => s,
_ => return,
let s = match read_state("calc") {
Some(s) => s,
None => return,
};
let mut old = old_local.lock().unwrap();
@@ -524,9 +549,9 @@ impl Calc {
}
fn sync(&self) {
let mut f = match File::create(format!("{}/wldash/calc", xdg::cache_folder())) {
Ok(f) => f,
_ => return,
let mut f = match create_state("calc") {
Some(f) => f,
None => return,
};
let old = self.old.lock().unwrap();
@@ -713,9 +738,9 @@ impl Interface {
std::process::exit(0);
}
if let Ok(mut f) = File::create(format!("{}/wldash/prompt", xdg::cache_folder())) {
write!(f, "{}", self.inner.prompt.input).unwrap();
f.sync_data().unwrap();
if let Some(mut f) = create_state("prompt") {
let _ = write!(f, "{}", self.inner.prompt.input);
let _ = f.sync_data();
}
std::process::exit(0);
}
@@ -744,7 +769,7 @@ impl Interface {
widget.update(&self.inner);
}
keysyms::KEY_r if event.modifiers.ctrl => {
if let Ok(s) = read_to_string(format!("{}/wldash/prompt", xdg::cache_folder())) {
if let Some(s) = read_state("prompt") {
self.inner.prompt.set(&s);
}
}