3 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5.1 63179b9c47 fonts: fall back to fontconfig when a pinned font path is missing (0.4.2)
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Failing after 0s
Release / package (aarch64, , aarch64, , , ) (push) Failing after 1s
A config generated on one machine pinned
/usr/share/fonts/google-noto/NotoSans-Regular.ttf; on another host the
same package installs under fonts/noto, so the shared config made the
fontloader thread panic ("Font file not found") and, with panic =
"abort", kill wldash before a window appeared.

resolve_font_path() now uses a pinned path only if the file exists,
otherwise asks fontconfig for the name (what fc-match does) and says so
on stderr. find_font() and FontLoader::from_path() return errors
instead of unwrapping, and an unresolvable font exits with one clear
line. Generated configs leave font_paths unset so names resolve per
machine; pinning remains possible by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:50:02 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 d1df30d6d0 release 0.4.1
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Successful in 42s
Release / package (aarch64, , aarch64, , , ) (push) Successful in 2m14s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:42:49 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 9953865128 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>
2026-10-03 09:33:12 +00:00
11 changed files with 187 additions and 313 deletions
+11
View File
@@ -1,5 +1,16 @@
# Changelog # Changelog
## 0.4.2 (2026-10-03)
- Fonts: a `font_paths` entry pointing at a file that does not exist on this machine made the font-loader thread panic and abort wldash ("Font file not found"). Pinned paths that are missing now fall back to fontconfig by name, with one line on stderr; a font that cannot be resolved at all exits with a clear message instead of a backtrace.
- `--write-default-config` no longer writes absolute font paths. Configs shared between machines with different font package layouts (`fonts/noto` vs `fonts/google-noto`) were breaking on the second host; names are resolved per machine instead. Existing configs keep working, and `font_paths` can still pin a file on purpose.
## 0.4.1 (2026-10-03)
- 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) ## 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. 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
+2 -217
View File
@@ -8,18 +8,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" 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]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@@ -188,15 +176,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 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]] [[package]]
name = "fontconfig" name = "fontconfig"
version = "0.8.0" version = "0.8.0"
@@ -225,38 +204,6 @@ dependencies = [
"version_check", "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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.3" version = "0.14.3"
@@ -299,7 +246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.3", "hashbrown",
] ]
[[package]] [[package]]
@@ -334,70 +281,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 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]] [[package]]
name = "libc" name = "libc"
version = "0.2.152" version = "0.2.152"
@@ -693,26 +576,6 @@ dependencies = [
"pest_derive", "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]] [[package]]
name = "rusttype" name = "rusttype"
version = "0.9.3" version = "0.9.3"
@@ -764,17 +627,6 @@ dependencies = [
"syn 2.0.48", "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]] [[package]]
name = "serde_yaml" name = "serde_yaml"
version = "0.9.31" version = "0.9.31"
@@ -799,28 +651,6 @@ dependencies = [
"digest", "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]] [[package]]
name = "slotmap" name = "slotmap"
version = "1.0.7" version = "1.0.7"
@@ -836,12 +666,6 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@@ -930,18 +754,6 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" 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]] [[package]]
name = "vec_map" name = "vec_map"
version = "0.8.2" version = "0.8.2"
@@ -964,12 +776,6 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.90" version = "0.2.90"
@@ -1265,7 +1071,7 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]] [[package]]
name = "wldash" name = "wldash"
version = "0.4.0" version = "0.4.2"
dependencies = [ dependencies = [
"calloop", "calloop",
"chrono", "chrono",
@@ -1279,7 +1085,6 @@ dependencies = [
"rusttype", "rusttype",
"serde", "serde",
"serde_yaml", "serde_yaml",
"simd-json",
"unicode-segmentation", "unicode-segmentation",
"walkdir", "walkdir",
"wayland-client", "wayland-client",
@@ -1316,23 +1121,3 @@ dependencies = [
"once_cell", "once_cell",
"pkg-config", "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 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "wldash" name = "wldash"
version = "0.4.0" version = "0.4.2"
edition = "2024" edition = "2024"
[profile.release] [profile.release]
@@ -38,5 +38,4 @@ libpulse-binding = "2.28"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9" serde_yaml = "0.9"
simd-json = "0.13"
+1 -1
View File
@@ -4,7 +4,7 @@
# project.uhhm.no (namespace bl). pkgver is the version; the tag has # project.uhhm.no (namespace bl). pkgver is the version; the tag has
# to agree with it and with Cargo.toml. # to agree with it and with Cargo.toml.
pkgname=wldash pkgname=wldash
pkgver=0.4.0 pkgver=0.4.2
pkgrel=1 pkgrel=1
pkgdesc="Dashboard/launcher/control-panel for wlroots-style Wayland compositors (layer-shell)" pkgdesc="Dashboard/launcher/control-panel for wlroots-style Wayland compositors (layer-shell)"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
+3 -1
View File
@@ -28,7 +28,9 @@ 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 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`. Fonts are named (`sans`, `monospace`, or any fontconfig name) and resolved through fontconfig on the machine wldash runs on. `font_paths` in the config can pin a name to a file; if that file does not exist on the current machine, wldash says so on stderr and falls back to fontconfig rather than failing, so one config can roam between hosts with different font package layouts.
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`. For more info, look in `src/config.rs`.
+11 -17
View File
@@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
use crate::{ use crate::{
event::Events, event::Events,
fonts::{FontMap, find_font}, fonts::FontMap,
widgets::{ widgets::{
Backlight, Battery, Calendar, Clock, Date, HorizontalLayout, IndexedLayout, Interface, Backlight, Battery, Calendar, Clock, Date, HorizontalLayout, IndexedLayout, Interface,
InvertedHorizontalLayout, InvertedVerticalLayout, Layout, Line, Margin, PulseAudio, InvertedHorizontalLayout, InvertedVerticalLayout, Layout, Line, Margin, PulseAudio,
@@ -89,8 +89,6 @@ impl Config {
pub fn generate_v1() -> Config { pub fn generate_v1() -> Config {
let has_battery = Battery::detect(); let has_battery = Battery::detect();
let has_backlight = Backlight::detect(); let has_backlight = Backlight::detect();
let sans = find_font("sans");
let monospace = find_font("monospace");
let mut bars = Vec::new(); let mut bars = Vec::new();
if has_battery { if has_battery {
@@ -154,13 +152,12 @@ impl Config {
])), ])),
}; };
let map = HashMap::from([
("sans".to_string(), sans),
("monospace".to_string(), monospace),
]);
Config { Config {
font_paths: Some(map), // Left unset on purpose: "sans"/"monospace" are resolved through
// fontconfig on each machine at start. Pinning absolute paths
// here broke configs shared between hosts with different font
// package layouts. Set font_paths yourself to override.
font_paths: None,
widget, widget,
mode: OperationMode::LayerSurface((1024, 768)), mode: OperationMode::LayerSurface((1024, 768)),
background: Some(0xFAFAFAFA), background: Some(0xFAFAFAFA),
@@ -171,8 +168,6 @@ impl Config {
pub fn generate_v2(clay: bool) -> Config { pub fn generate_v2(clay: bool) -> Config {
let has_battery = Battery::detect(); let has_battery = Battery::detect();
let has_backlight = Backlight::detect(); let has_backlight = Backlight::detect();
let sans = find_font("sans");
let monospace = find_font("monospace");
let mut bars = Vec::new(); let mut bars = Vec::new();
if has_battery { if has_battery {
@@ -244,13 +239,12 @@ impl Config {
]), ]),
]); ]);
let map = HashMap::from([
("sans".to_string(), sans),
("monospace".to_string(), monospace),
]);
Config { Config {
font_paths: Some(map), // Left unset on purpose: "sans"/"monospace" are resolved through
// fontconfig on each machine at start. Pinning absolute paths
// here broke configs shared between hosts with different font
// package layouts. Set font_paths yourself to override.
font_paths: None,
widget, widget,
mode: if clay { mode: if clay {
OperationMode::XdgToplevel OperationMode::XdgToplevel
+84 -21
View File
@@ -9,21 +9,48 @@ use std::{
/// FontRef is used to store Fonts on widgets. /// FontRef is used to store Fonts on widgets.
pub type FontRef<'a> = &'a rusttype::Font<'a>; pub type FontRef<'a> = &'a rusttype::Font<'a>;
/// Ask fontconfig for the file backing a font name ("sans", "monospace",
/// "Noto Sans", ...), the way fc-match does.
#[cfg(feature = "fontconfig")] #[cfg(feature = "fontconfig")]
pub(crate) fn find_font(name: &str) -> String { pub(crate) fn find_font(name: &str) -> Result<String, String> {
use fontconfig::Fontconfig as FontConfig; use fontconfig::Fontconfig as FontConfig;
let fc = FontConfig::new().unwrap(); let fc = FontConfig::new().ok_or("could not initialise fontconfig")?;
fc.find(name, None) let found = fc
.unwrap() .find(name, None)
.ok_or_else(|| format!("fontconfig has no match for {:?}", name))?;
found
.path .path
.to_str() .to_str()
.unwrap() .map(|s| s.to_string())
.to_string() .ok_or_else(|| format!("font path for {:?} is not UTF-8", name))
} }
#[cfg(not(feature = "fontconfig"))] #[cfg(not(feature = "fontconfig"))]
pub(crate) fn find_font(_name: &str) -> String { pub(crate) fn find_font(name: &str) -> Result<String, String> {
panic!("fontconfig not enabled so font search not available"); Err(format!(
"no path configured for font {:?} and wldash was built without fontconfig",
name
))
}
/// Resolve the file to load for `name`: a path pinned in the config if it
/// exists on this machine, otherwise whatever fontconfig picks for the name.
///
/// Configs roam between machines on a shared home, and font packages do
/// not keep the same file layout everywhere (Noto under fonts/noto on one
/// distribution, fonts/google-noto on another), so a pinned path that is
/// missing is a reason to fall back, not to abort.
pub(crate) fn resolve_font_path(name: &str, pinned: Option<&str>) -> Result<String, String> {
if let Some(p) = pinned {
if Path::new(p).is_file() {
return Ok(p.to_string());
}
eprintln!(
"wldash: configured font file for {:?} not found: {}; asking fontconfig instead",
name, p
);
}
find_font(name)
} }
/// FontLoader is a marker struct that is used to load files /// FontLoader is a marker struct that is used to load files
@@ -31,17 +58,20 @@ pub(crate) struct FontLoader;
impl FontLoader { impl FontLoader {
/// Given a path, loads it as a Font, which can be rendered to the screen. /// Given a path, loads it as a Font, which can be rendered to the screen.
pub(crate) fn from_path<'a, P>(path: P) -> Option<Font<'a>> pub(crate) fn from_path<'a, P>(path: P) -> Result<Font<'a>, String>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let mut file = File::open(path).expect("Font file not found"); let path = path.as_ref();
let mut file =
File::open(path).map_err(|e| format!("cannot open {}: {}", path.display(), e))?;
let mut data = match file.metadata() { let mut data = match file.metadata() {
Ok(metadata) => Vec::with_capacity(metadata.len() as usize), Ok(metadata) => Vec::with_capacity(metadata.len() as usize),
Err(_) => vec![], Err(_) => vec![],
}; };
file.read_to_end(&mut data).unwrap(); file.read_to_end(&mut data)
Font::try_from_vec(data) .map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
Font::try_from_vec(data).ok_or_else(|| format!("{} is not a usable font", path.display()))
} }
} }
@@ -102,19 +132,28 @@ impl FontMap {
self.font_paths.insert(font_name, font_path); self.font_paths.insert(font_name, font_path);
} }
/// Load every queued font. Runs on its own thread; a font that cannot
/// be found or read is a configuration problem the user has to see, so
/// it is reported in one line and the process exits instead of
/// aborting with a panic backtrace.
pub fn load_fonts(&mut self) { pub fn load_fonts(&mut self) {
for (font_name, v) in self.required_fonts.iter() { for (font_name, v) in self.required_fonts.iter() {
let path = match self.font_paths.get(font_name) { let pinned = self.font_paths.get(font_name).map(|s| s.as_str());
Some(res) => res, let path = match resolve_font_path(font_name, pinned) {
_ => { Ok(p) => p,
let s = find_font(font_name); Err(e) => {
self.font_paths.insert(font_name, s); eprintln!("wldash: cannot resolve font {:?}: {}", font_name, e);
self.font_paths.get(font_name).unwrap() std::process::exit(1);
} }
}; };
let fontref = Box::leak(Box::new( let font = match FontLoader::from_path(&path) {
FontLoader::from_path(path).expect("unable to load font"), Ok(f) => f,
)); Err(e) => {
eprintln!("wldash: cannot load font {:?}: {}", font_name, e);
std::process::exit(1);
}
};
let fontref = Box::leak(Box::new(font));
for (size, preload) in &v.1 { for (size, preload) in &v.1 {
if let Some(font) = self.fonts.get_mut(&(font_name, ComparableF32(*size))) { if let Some(font) = self.fonts.get_mut(&(font_name, ComparableF32(*size))) {
font.add_str_to_cache(preload); font.add_str_to_cache(preload);
@@ -160,3 +199,27 @@ impl MaybeFontMap {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::resolve_font_path;
#[test]
fn pinned_path_wins_when_present() {
let dir = std::env::temp_dir().join(format!("wldash-font-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("x.ttf");
std::fs::write(&f, b"not really a font").unwrap();
let p = f.to_str().unwrap();
assert_eq!(resolve_font_path("sans", Some(p)).unwrap(), p);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn missing_pinned_path_falls_back_to_lookup() {
// Whatever fontconfig answers (or an error without fontconfig),
// the missing pinned path itself must never be returned.
let r = resolve_font_path("sans", Some("/nonexistent/wldash/font.ttf"));
assert_ne!(r.as_deref().ok(), Some("/nonexistent/wldash/font.ttf"));
}
}
+4 -11
View File
@@ -23,10 +23,7 @@ use event::{Event, Events};
use fonts::{FontMap, MaybeFontMap}; use fonts::{FontMap, MaybeFontMap};
use keyboard::{KeyRepeatSource, RepeatMessage}; use keyboard::{KeyRepeatSource, RepeatMessage};
use state::State; use state::State;
use utils::{ use utils::xdg;
desktop::{load_desktop_files, write_desktop_cache},
xdg,
};
use widgets::{Geometry, Widget}; use widgets::{Geometry, Widget};
use std::{ use std::{
@@ -50,7 +47,8 @@ OPTIONS:
else ~/.config/wldash/config.yml) else ~/.config/wldash/config.yml)
--write-default-config [v1|v2|toplevel] generate and write a new config file --write-default-config [v1|v2|toplevel] generate and write a new config file
(v2 by default) (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() { fn create_required_directories() {
let dir_config_wldash = format!("{}/wldash", xdg::config_folder()); 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_config_wldash);
ensure_dir_exists(&dir_cache_wldash);
} }
/// Default config path. On a shared (e.g. NFS-roamed) home, a host-specific /// Default config path. On a shared (e.g. NFS-roamed) home, a host-specific
@@ -119,10 +115,7 @@ fn main() {
std::process::exit(0); std::process::exit(0);
} }
Some(ref s) if s == "--desktop-refresh" => { Some(ref s) if s == "--desktop-refresh" => {
let v = load_desktop_files(); eprintln!("wldash: desktop files are scanned at every start; nothing to refresh");
if let Err(e) = write_desktop_cache(&v) {
die(format!("could not write desktop cache: {}", e));
}
std::process::exit(0); std::process::exit(0);
} }
Some(ref s) if s == "--help" => { Some(ref s) if s == "--help" => {
+9 -19
View File
@@ -2,20 +2,17 @@ use std::{
cmp::Ordering, cmp::Ordering,
collections::HashSet, collections::HashSet,
error::Error, error::Error,
fs::{File, read_to_string}, fs::read_to_string,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
io::{Error as io_error, ErrorKind}, io::{Error as io_error, ErrorKind},
path::Path, path::Path,
}; };
use serde::{self, Deserialize, Serialize};
use simd_json;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::utils::{inish, xdg}; use crate::utils::{inish, xdg};
#[derive(Serialize, Deserialize, Clone, Debug, Eq)] #[derive(Clone, Debug, Eq)]
#[serde(rename_all = "snake_case")]
pub struct Desktop { pub struct Desktop {
pub entry_type: String, pub entry_type: String,
pub name: String, pub name: String,
@@ -83,20 +80,13 @@ impl Hash for Desktop {
} }
} }
pub fn load_desktop_cache() -> Result<Vec<Desktop>, Box<dyn Error>> { /// Scan every XDG applications directory for launchable entries.
match File::open(format!("{}/wldash/desktop.json", xdg::cache_folder())) { ///
Ok(f) => simd_json::from_reader(f).map_err(|e| e.into()), /// This used to be cached as JSON under XDG_CACHE_HOME and refreshed by
Err(e) => Err(e.into()), /// 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 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()),
}
}
pub fn load_desktop_files() -> Vec<Desktop> { pub fn load_desktop_files() -> Vec<Desktop> {
let dirs = xdg::data_folders(); 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 { pub fn config_folder() -> String {
match env::var_os("XDG_CONFIG_HOME") { match env::var_os("XDG_CONFIG_HOME") {
Some(s) => s.into_string().unwrap(), Some(s) => s.into_string().unwrap(),
+49 -24
View File
@@ -16,7 +16,7 @@ use crate::{
fonts::FontMap, fonts::FontMap,
keyboard::{KeyEvent, keysyms}, keyboard::{KeyEvent, keysyms},
utils::{ utils::{
desktop::{Desktop, load_desktop_cache, load_desktop_files, write_desktop_cache}, desktop::{Desktop, load_desktop_files},
xdg, xdg,
}, },
widgets::{Geometry, Widget}, widgets::{Geometry, Widget},
@@ -225,6 +225,29 @@ fn strip_field_codes(exec: &str) -> String {
out 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 { trait InterfaceWidget {
/// Number of selectable rows (used to clamp the selection). /// Number of selectable rows (used to clamp the selection).
fn rows(&self) -> usize; fn rows(&self) -> usize;
@@ -253,16 +276,8 @@ impl Launcher {
thread::Builder::new() thread::Builder::new()
.name("desktopini".to_string()) .name("desktopini".to_string())
.spawn(move || { .spawn(move || {
let mut options = options.lock().unwrap(); let loaded = load_desktop_files();
*options = match load_desktop_cache() { *options.lock().unwrap() = loaded;
Ok(v) => v,
Err(_) => {
let v = load_desktop_files();
write_desktop_cache(&v).unwrap();
v
}
};
drop(options);
let mut events = events.lock().unwrap(); let mut events = events.lock().unwrap();
events.add_event(Event::LauncherUpdate); events.add_event(Event::LauncherUpdate);
}) })
@@ -306,9 +321,19 @@ impl InterfaceWidget for Launcher {
fn trigger(&mut self, intf: &mut InnerInterface) { fn trigger(&mut self, intf: &mut InnerInterface) {
if self.matches.len() > intf.selection { if self.matches.len() > intf.selection {
let d = &self.matches[intf.selection]; let d = &self.matches[intf.selection];
if let Some(exec) = &d.exec { let cmd = match (&d.exec, &d.url) {
self.exec(&strip_field_codes(exec)); (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() thread::Builder::new()
.name("calc".to_string()) .name("calc".to_string())
.spawn(move || { .spawn(move || {
let s = match read_to_string(format!("{}/wldash/calc", xdg::cache_folder())) { let s = match read_state("calc") {
Ok(s) => s, Some(s) => s,
_ => return, None => return,
}; };
let mut old = old_local.lock().unwrap(); let mut old = old_local.lock().unwrap();
@@ -524,9 +549,9 @@ impl Calc {
} }
fn sync(&self) { fn sync(&self) {
let mut f = match File::create(format!("{}/wldash/calc", xdg::cache_folder())) { let mut f = match create_state("calc") {
Ok(f) => f, Some(f) => f,
_ => return, None => return,
}; };
let old = self.old.lock().unwrap(); let old = self.old.lock().unwrap();
@@ -713,9 +738,9 @@ impl Interface {
std::process::exit(0); std::process::exit(0);
} }
if let Ok(mut f) = File::create(format!("{}/wldash/prompt", xdg::cache_folder())) { if let Some(mut f) = create_state("prompt") {
write!(f, "{}", self.inner.prompt.input).unwrap(); let _ = write!(f, "{}", self.inner.prompt.input);
f.sync_data().unwrap(); let _ = f.sync_data();
} }
std::process::exit(0); std::process::exit(0);
} }
@@ -744,7 +769,7 @@ impl Interface {
widget.update(&self.inner); widget.update(&self.inner);
} }
keysyms::KEY_r if event.modifiers.ctrl => { 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); self.inner.prompt.set(&s);
} }
} }