From 77ae14ed1dc601c69ea3b61985c618c4b1fc5ec1 Mon Sep 17 00:00:00 2001 From: elwrcl Date: Tue, 29 Sep 2026 23:29:10 +0300 Subject: [PATCH] fix: doctor lists errors on one line with the hint last --- src/doctor.rs | 30 +++++++++++++++++++++++++++--- src/error.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/doctor.rs b/src/doctor.rs index c8a9462..52dbb08 100644 --- a/src/doctor.rs +++ b/src/doctor.rs @@ -4,6 +4,7 @@ use std::path::Path; use anyhow::{Result, bail}; use crate::config; +use crate::error::one_line; use crate::niri; use crate::wayland::Wayland; @@ -96,7 +97,10 @@ pub fn check_niri(version: Result) -> Check { match version { Err(e) => check( Level::Warn, - format!("niri IPC unavailable ({e:#}); using the first output for `screen`"), + format!( + "niri IPC unavailable ({}); using the first output for `screen`", + one_line(&e) + ), ), Ok(v) => match niri::parse_version(&v) { Some(found) if found == niri::IPC_VERSION => check(Level::Ok, format!("niri {v}")), @@ -117,7 +121,7 @@ pub fn check_config(path: &Path) -> Check { Level::Ok, format!("config {} not found, using defaults", path.display()), ), - Err(e) => check(Level::Fail, format!("config: {e:#}")), + Err(e) => check(Level::Fail, format!("config: {}", one_line(&e))), } } @@ -155,7 +159,7 @@ pub fn run() -> Result<()> { )); } } - Err(e) => checks.push(check(Level::Fail, format!("wayland: {e:#}"))), + Err(e) => checks.push(check(Level::Fail, format!("wayland: {}", one_line(&e)))), } checks.push(check_niri(niri::version())); let config_path = config::default_path(); @@ -246,6 +250,26 @@ mod tests { ); } + #[test] + fn broken_config_fails_with_hint_last() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "[save]\ndirectory = \n").unwrap(); + let c = check_config(&path); + assert_eq!(c.level, Level::Fail); + assert!( + c.text.starts_with("config: invalid config file"), + "{}", + c.text + ); + assert!( + c.text + .ends_with("(hint: fix the file or delete it to use the defaults)"), + "{}", + c.text + ); + } + #[test] fn save_dir_checks() { let dir = tempfile::tempdir().unwrap(); diff --git a/src/error.rs b/src/error.rs index 934b4e0..99b752a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -58,6 +58,22 @@ pub fn render(err: &anyhow::Error, log: Option<&Path>) -> String { out } +/// The error on one line, hint last: `msg: cause (hint: ...)`. For places +/// that list several results, like `valw doctor`. +pub fn one_line(err: &anyhow::Error) -> String { + let hint = err.downcast_ref::().map(|h| h.0.as_str()); + let messages: Vec = err + .chain() + .map(|e| e.to_string()) + .filter(|m| Some(m.as_str()) != hint) + .collect(); + let mut out = messages.join(": "); + if let Some(hint) = hint { + out.push_str(&format!(" (hint: {hint})")); + } + out +} + #[cfg(test)] mod tests { use super::*; @@ -101,6 +117,19 @@ mod tests { ); } + #[test] + fn one_line_puts_hint_last() { + let err = Err::<(), _>(anyhow!("Could not find wayland compositor")) + .context("could not connect to the Wayland compositor") + .hint("valw needs a Wayland session; is WAYLAND_DISPLAY set?") + .unwrap_err(); + assert_eq!( + one_line(&err), + "could not connect to the Wayland compositor: Could not find wayland compositor \ + (hint: valw needs a Wayland session; is WAYLAND_DISPLAY set?)" + ); + } + #[test] fn cancelled_is_detectable() { let err = anyhow::Error::new(Cancelled).context("region selection");