2 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5.1 4f055935d4 backlight: run without a backlight device instead of panicking (0.4.3)
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 43s
Release / package (aarch64, , aarch64, , , ) (push) Successful in 2m20s
A config carrying a !backlight widget on a machine with nothing under
/sys/class/backlight (a laptop config on a desktop) hit
expect("did not find backlight device") in Backlight::new, which with
panic = "abort" killed wldash before a window appeared.

The device path is now Option<PathBuf>. A missing device, or a
configured `device` name that does not exist, prints one line on stderr
and leaves the widget in place so layout indices stay aligned: it
renders empty, skips sysfs reads and writes, and ignores clicks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 13:54:13 +02:00
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
8 changed files with 152 additions and 52 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## 0.4.3 (2026-10-05)
- Backlight: a config with a `!backlight` widget on a machine without `/sys/class/backlight` entries (or with a `device` name that does not exist) panicked at start ("did not find backlight device"), fatal with `panic = "abort"`. The widget now warns once on stderr, renders empty and ignores clicks, so one config can be shared between a laptop and a desktop.
## 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.
Generated
+1 -1
View File
@@ -1071,7 +1071,7 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "wldash"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"calloop",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "wldash"
version = "0.4.1"
version = "0.4.3"
edition = "2024"
[profile.release]
+1 -1
View File
@@ -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.3
pkgrel=1
pkgdesc="Dashboard/launcher/control-panel for wlroots-style Wayland compositors (layer-shell)"
arch=('x86_64' 'aarch64')
+2
View File
@@ -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`.
+11 -17
View File
@@ -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
+84 -21
View File
@@ -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<String, String> {
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<String, String> {
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
@@ -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<Font<'a>>
pub(crate) fn from_path<'a, P>(path: P) -> Result<Font<'a>, String>
where
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() {
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"));
}
}
+43 -11
View File
@@ -26,7 +26,11 @@ fn write_file_as_u64(path: &Path, value: u64) -> Result<(), Error> {
}
pub struct Backlight {
device_path: PathBuf,
/// `None` when no backlight exists on this machine: the widget then
/// renders empty and ignores clicks instead of taking wldash down.
/// Configs are shared between hosts, so a laptop config running on a
/// desktop must not panic here.
device_path: Option<PathBuf>,
cur: u64,
max: u64,
dirty: bool,
@@ -46,10 +50,13 @@ fn find_device() -> Option<PathBuf> {
impl Backlight {
fn update(&mut self) {
if let Ok(cur) = read_file_as_u64(self.device_path.join("brightness").as_path()) {
let Some(dev) = &self.device_path else {
return;
};
if let Ok(cur) = read_file_as_u64(dev.join("brightness").as_path()) {
self.cur = cur;
}
if let Ok(max) = read_file_as_u64(self.device_path.join("max_brightness").as_path()) {
if let Ok(max) = read_file_as_u64(dev.join("max_brightness").as_path()) {
self.max = max;
}
}
@@ -58,11 +65,30 @@ impl Backlight {
#[allow(clippy::new_ret_no_self)]
pub fn new(path: Option<&str>, fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
let device_path = match path {
Some(path) => Some(Path::new("/sys/class/backlight").join(path)),
None => find_device(),
Some(path) => {
let p = Path::new("/sys/class/backlight").join(path);
if p.is_dir() {
Some(p)
} else {
eprintln!(
"wldash: backlight device {:?} not found, widget disabled",
p
);
None
}
}
None => {
let p = find_device();
if p.is_none() {
eprintln!(
"wldash: no backlight device under /sys/class/backlight, widget disabled"
);
}
p
}
};
let mut dev = Backlight {
device_path: device_path.expect("did not find backlight device"),
device_path,
cur: 0,
max: 0,
dirty: true,
@@ -77,9 +103,13 @@ impl Backlight {
}
pub fn set(&mut self, brightness: f32) {
let Some(dev) = &self.device_path else {
return;
};
let val = (self.max as f32 * brightness.clamp(0.0, 1.0)) as u64;
let _ = write_file_as_u64(self.device_path.join("brightness").as_path(), val);
if let Ok(cur) = read_file_as_u64(self.device_path.join("brightness").as_path()) {
let brightness_path = dev.join("brightness");
let _ = write_file_as_u64(&brightness_path, val);
if let Ok(cur) = read_file_as_u64(&brightness_path) {
self.cur = cur;
}
}
@@ -93,12 +123,14 @@ impl BarWidgetImpl for Backlight {
self.dirty
}
fn value(&mut self) -> f32 {
self.dirty = false;
if self.device_path.is_none() {
return 0.0;
}
if self.max == 0 || self.cur > self.max {
// what.
// Sysfs handed us nonsense; show full rather than a bogus ratio.
return 1.0;
}
self.dirty = false;
self.cur as f32 / self.max as f32
}
fn color(&self) -> Color {