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
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## Unreleased
- No more desktop-file cache: wldash scanned `~/.cache/wldash/desktop.json` (on NFS for shared homes, and stale after every package install) and needed a manual `--desktop-refresh`. Desktop files are now scanned on a background thread at every start, a few milliseconds over local `/usr/share`. `--desktop-refresh` is a no-op kept for key bindings; `simd-json` is gone.
- Calculator history and the Escape-saved prompt moved from `$XDG_CACHE_HOME/wldash` to `$XDG_STATE_HOME/wldash` (`~/.local/state`), where small roaming state belongs; old files are still read.
- Launcher: `Type=Link` entries open their URL with `xdg-open`; `Terminal=true` entries run inside `$TERMINAL -e` when that variable is set.
## 0.4.0 (2026-10-03)
First release of the project.uhhm.no fork, packaged for the `[uhhm]` Arch registry. Numbering: upstream's old code base was tagged up to v0.3.0 and the rewrite called itself 0.2.0; this fork continues above both so pacman sees a monotonic version.
Generated
+1 -216
View File
@@ -8,18 +8,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046"
[[package]]
name = "ahash"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -188,15 +176,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "fontconfig"
version = "0.8.0"
@@ -225,38 +204,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "halfbrown"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5681137554ddff44396e5f149892c769d45301dd9aa19c51602a89ee214cb0ec"
dependencies = [
"hashbrown 0.13.2",
"serde",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.3"
@@ -299,7 +246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
"hashbrown",
]
[[package]]
@@ -334,70 +281,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lexical-core"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46"
dependencies = [
"lexical-parse-float",
"lexical-parse-integer",
"lexical-util",
"lexical-write-float",
"lexical-write-integer",
]
[[package]]
name = "lexical-parse-float"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f"
dependencies = [
"lexical-parse-integer",
"lexical-util",
"static_assertions",
]
[[package]]
name = "lexical-parse-integer"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9"
dependencies = [
"lexical-util",
"static_assertions",
]
[[package]]
name = "lexical-util"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc"
dependencies = [
"static_assertions",
]
[[package]]
name = "lexical-write-float"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862"
dependencies = [
"lexical-util",
"lexical-write-integer",
"static_assertions",
]
[[package]]
name = "lexical-write-integer"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446"
dependencies = [
"lexical-util",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.152"
@@ -693,26 +576,6 @@ dependencies = [
"pest_derive",
]
[[package]]
name = "ref-cast"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "rusttype"
version = "0.9.3"
@@ -764,17 +627,6 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "serde_json"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.31"
@@ -799,28 +651,6 @@ dependencies = [
"digest",
]
[[package]]
name = "simd-json"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2faf8f101b9bc484337a6a6b0409cf76c139f2fb70a9e3aee6b6774be7bfbf76"
dependencies = [
"getrandom",
"halfbrown",
"lexical-core",
"ref-cast",
"serde",
"serde_json",
"simdutf8",
"value-trait",
]
[[package]]
name = "simdutf8"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
[[package]]
name = "slotmap"
version = "1.0.7"
@@ -836,12 +666,6 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "1.0.109"
@@ -930,18 +754,6 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
[[package]]
name = "value-trait"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad8db98c1e677797df21ba03fca7d3bf9bec3ca38db930954e4fe6e1ea27eb4"
dependencies = [
"float-cmp",
"halfbrown",
"itoa",
"ryu",
]
[[package]]
name = "vec_map"
version = "0.8.2"
@@ -964,12 +776,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.90"
@@ -1279,7 +1085,6 @@ dependencies = [
"rusttype",
"serde",
"serde_yaml",
"simd-json",
"unicode-segmentation",
"walkdir",
"wayland-client",
@@ -1316,23 +1121,3 @@ dependencies = [
"once_cell",
"pkg-config",
]
[[package]]
name = "zerocopy"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
-1
View File
@@ -38,5 +38,4 @@ libpulse-binding = "2.28"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
simd-json = "0.13"
+1 -1
View File
@@ -28,7 +28,7 @@ per-host layouts apart on NFS-shared homes.
To generate a new default configuration, run `wldash --write-default-config`. To configure, edit `$XDG_CONFIG_HOME/wldash/config.yml` (or if `XDG_CONFIG_HOME` is not set, `~/.config/wldash/config.yml`). If `/etc/wldash/config.yml` exists it takes precedence over the per-user file; `--config FILE` overrides both.
To regenerate the desktop file cache, run `wldash --desktop-refresh`.
Desktop files are scanned every time wldash starts (on a background thread), so there is no cache to refresh; `--desktop-refresh` is kept as a no-op for old key bindings. Calculator history and the prompt saved on Escape live in `$XDG_STATE_HOME/wldash` (`~/.local/state/wldash`); older installs' files under `~/.cache/wldash` are still read.
For more info, look in `src/config.rs`.
+4 -11
View File
@@ -23,10 +23,7 @@ use event::{Event, Events};
use fonts::{FontMap, MaybeFontMap};
use keyboard::{KeyRepeatSource, RepeatMessage};
use state::State;
use utils::{
desktop::{load_desktop_files, write_desktop_cache},
xdg,
};
use utils::xdg;
use widgets::{Geometry, Widget};
use std::{
@@ -50,7 +47,8 @@ OPTIONS:
else ~/.config/wldash/config.yml)
--write-default-config [v1|v2|toplevel] generate and write a new config file
(v2 by default)
--desktop-refresh refresh desktop file cache
--desktop-refresh no-op kept for old key bindings
(desktop files are scanned at start)
"
);
}
@@ -68,9 +66,7 @@ fn ensure_dir_exists(path: &str) {
fn create_required_directories() {
let dir_config_wldash = format!("{}/wldash", xdg::config_folder());
let dir_cache_wldash = format!("{}/wldash", xdg::cache_folder());
ensure_dir_exists(&dir_config_wldash);
ensure_dir_exists(&dir_cache_wldash);
}
/// Default config path. On a shared (e.g. NFS-roamed) home, a host-specific
@@ -119,10 +115,7 @@ fn main() {
std::process::exit(0);
}
Some(ref s) if s == "--desktop-refresh" => {
let v = load_desktop_files();
if let Err(e) = write_desktop_cache(&v) {
die(format!("could not write desktop cache: {}", e));
}
eprintln!("wldash: desktop files are scanned at every start; nothing to refresh");
std::process::exit(0);
}
Some(ref s) if s == "--help" => {
+9 -19
View File
@@ -2,20 +2,17 @@ use std::{
cmp::Ordering,
collections::HashSet,
error::Error,
fs::{File, read_to_string},
fs::read_to_string,
hash::{Hash, Hasher},
io::{Error as io_error, ErrorKind},
path::Path,
};
use serde::{self, Deserialize, Serialize};
use simd_json;
use walkdir::WalkDir;
use crate::utils::{inish, xdg};
#[derive(Serialize, Deserialize, Clone, Debug, Eq)]
#[serde(rename_all = "snake_case")]
#[derive(Clone, Debug, Eq)]
pub struct Desktop {
pub entry_type: String,
pub name: String,
@@ -83,20 +80,13 @@ impl Hash for Desktop {
}
}
pub fn load_desktop_cache() -> Result<Vec<Desktop>, Box<dyn Error>> {
match File::open(format!("{}/wldash/desktop.json", xdg::cache_folder())) {
Ok(f) => simd_json::from_reader(f).map_err(|e| e.into()),
Err(e) => Err(e.into()),
}
}
pub fn write_desktop_cache(v: &Vec<Desktop>) -> Result<(), Box<dyn Error>> {
match File::create(format!("{}/wldash/desktop.json", xdg::cache_folder())) {
Ok(f) => simd_json::to_writer(f, v).map_err(|e| e.into()),
Err(e) => Err(e.into()),
}
}
/// Scan every XDG applications directory for launchable entries.
///
/// This used to be cached as JSON under XDG_CACHE_HOME and refreshed by
/// hand with `--desktop-refresh`. The scan is a few milliseconds over
/// local /usr/share and runs on a background thread, while the cache sat
/// on the (possibly NFS-mounted) home directory and went stale whenever a
/// package was installed. So: no cache.
pub fn load_desktop_files() -> Vec<Desktop> {
let dirs = xdg::data_folders();
+12
View File
@@ -10,6 +10,18 @@ pub fn cache_folder() -> String {
}
}
/// XDG_STATE_HOME: small persistent state (calculator history, saved
/// prompt). Unlike a cache this is worth roaming with the home directory.
pub fn state_folder() -> String {
match env::var_os("XDG_STATE_HOME") {
Some(s) => s.into_string().unwrap(),
None => {
let home = env::var_os("HOME").unwrap().into_string().unwrap();
format!("{}/.local/state", home)
}
}
}
pub fn config_folder() -> String {
match env::var_os("XDG_CONFIG_HOME") {
Some(s) => s.into_string().unwrap(),
+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);
}
}