diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a5d621..9c9f55d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 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. diff --git a/Cargo.lock b/Cargo.lock index 6a32445..8889aff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1071,7 +1071,7 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "wldash" -version = "0.4.1" +version = "0.4.2" dependencies = [ "calloop", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 1c6df30..75f5b2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wldash" -version = "0.4.1" +version = "0.4.2" edition = "2024" [profile.release] diff --git a/PKGBUILD b/PKGBUILD index 88d1850..41695ed 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -4,7 +4,7 @@ # project.uhhm.no (namespace bl). pkgver is the version; the tag has # to agree with it and with Cargo.toml. pkgname=wldash -pkgver=0.4.1 +pkgver=0.4.2 pkgrel=1 pkgdesc="Dashboard/launcher/control-panel for wlroots-style Wayland compositors (layer-shell)" arch=('x86_64' 'aarch64') diff --git a/README.md b/README.md index 5ed6758..8b82c9f 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ 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. +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`. diff --git a/src/config.rs b/src/config.rs index f985773..ea6288e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex}; use crate::{ event::Events, - fonts::{FontMap, find_font}, + fonts::FontMap, widgets::{ Backlight, Battery, Calendar, Clock, Date, HorizontalLayout, IndexedLayout, Interface, InvertedHorizontalLayout, InvertedVerticalLayout, Layout, Line, Margin, PulseAudio, @@ -89,8 +89,6 @@ impl Config { pub fn generate_v1() -> Config { let has_battery = Battery::detect(); let has_backlight = Backlight::detect(); - let sans = find_font("sans"); - let monospace = find_font("monospace"); let mut bars = Vec::new(); if has_battery { @@ -154,13 +152,12 @@ impl Config { ])), }; - let map = HashMap::from([ - ("sans".to_string(), sans), - ("monospace".to_string(), monospace), - ]); - 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, mode: OperationMode::LayerSurface((1024, 768)), background: Some(0xFAFAFAFA), @@ -171,8 +168,6 @@ impl Config { pub fn generate_v2(clay: bool) -> Config { let has_battery = Battery::detect(); let has_backlight = Backlight::detect(); - let sans = find_font("sans"); - let monospace = find_font("monospace"); let mut bars = Vec::new(); if has_battery { @@ -244,13 +239,12 @@ impl Config { ]), ]); - let map = HashMap::from([ - ("sans".to_string(), sans), - ("monospace".to_string(), monospace), - ]); - 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, mode: if clay { OperationMode::XdgToplevel diff --git a/src/fonts.rs b/src/fonts.rs index 8da2dcc..2587163 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -9,21 +9,48 @@ use std::{ /// FontRef is used to store Fonts on widgets. 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")] -pub(crate) fn find_font(name: &str) -> String { +pub(crate) fn find_font(name: &str) -> Result { use fontconfig::Fontconfig as FontConfig; - let fc = FontConfig::new().unwrap(); - fc.find(name, None) - .unwrap() + let fc = FontConfig::new().ok_or("could not initialise fontconfig")?; + let found = fc + .find(name, None) + .ok_or_else(|| format!("fontconfig has no match for {:?}", name))?; + found .path .to_str() - .unwrap() - .to_string() + .map(|s| s.to_string()) + .ok_or_else(|| format!("font path for {:?} is not UTF-8", name)) } #[cfg(not(feature = "fontconfig"))] -pub(crate) fn find_font(_name: &str) -> String { - panic!("fontconfig not enabled so font search not available"); +pub(crate) fn find_font(name: &str) -> Result { + 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 { + 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 @@ -31,17 +58,20 @@ pub(crate) struct FontLoader; impl FontLoader { /// 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> + pub(crate) fn from_path<'a, P>(path: P) -> Result, String> where P: AsRef, { - 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() { Ok(metadata) => Vec::with_capacity(metadata.len() as usize), Err(_) => vec![], }; - file.read_to_end(&mut data).unwrap(); - Font::try_from_vec(data) + file.read_to_end(&mut 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); } + /// 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) { for (font_name, v) in self.required_fonts.iter() { - let path = match self.font_paths.get(font_name) { - Some(res) => res, - _ => { - let s = find_font(font_name); - self.font_paths.insert(font_name, s); - self.font_paths.get(font_name).unwrap() + let pinned = self.font_paths.get(font_name).map(|s| s.as_str()); + let path = match resolve_font_path(font_name, pinned) { + Ok(p) => p, + Err(e) => { + eprintln!("wldash: cannot resolve font {:?}: {}", font_name, e); + std::process::exit(1); } }; - let fontref = Box::leak(Box::new( - FontLoader::from_path(path).expect("unable to load font"), - )); + let font = match FontLoader::from_path(&path) { + 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 { if let Some(font) = self.fonts.get_mut(&(font_name, ComparableF32(*size))) { 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")); + } +}