From 5250eeca7b8e31020812928e7b3ff219d4b2a055 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 1 Oct 2026 12:06:32 +0100 Subject: [PATCH] Limit status errors to related control popups --- shell/ferese-shell/src/main.rs | 2 +- shell/ferese-shell/src/status.rs | 35 +++++++++++++------ shell/ferese-shell/src/status_ui.rs | 54 ++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/shell/ferese-shell/src/main.rs b/shell/ferese-shell/src/main.rs index 71281e9..3dcd0f4 100644 --- a/shell/ferese-shell/src/main.rs +++ b/shell/ferese-shell/src/main.rs @@ -165,7 +165,7 @@ struct FereseShell { status: status::Snapshot, recorder: recording::Recorder, calendar_offset: i32, - status_error: Option, + status_error: Option, theme_error: Option, menu: Option, system_modal: Option, diff --git a/shell/ferese-shell/src/status.rs b/shell/ferese-shell/src/status.rs index 6b7982f..88dfdbb 100644 --- a/shell/ferese-shell/src/status.rs +++ b/shell/ferese-shell/src/status.rs @@ -84,11 +84,17 @@ impl Action { } } +#[derive(Clone, Debug)] +pub struct ActionError { + pub action: Action, + pub message: String, +} + #[derive(Clone, Debug)] pub struct Update { pub snapshot: Snapshot, pub generation: u64, - pub error: Option, + pub error: Option, } pub struct Service { @@ -123,7 +129,7 @@ impl Updates { } } -type PollState = (Mutex<(u64, bool, Option, bool)>, Condvar); +type PollState = (Mutex<(u64, bool, Option, bool)>, Condvar); fn wait_for_poll(shared: &PollState) -> Option { let state = shared @@ -141,7 +147,7 @@ impl Service { let (updates, rx) = tokio::sync::watch::channel(None); // Polling and writes have separate workers: a missing D-Bus service must // never hold up volume/brightness changes. Publish only coherent polls. - let shared = Arc::new((Mutex::new((0u64, false, None::, false)), Condvar::new())); + let shared = Arc::new((Mutex::new((0u64, false, None::, false)), Condvar::new())); let polling = shared.clone(); thread::spawn(move || { @@ -190,7 +196,9 @@ impl Service { state.1 = true; } let settings = settings.lock().unwrap().clone(); - let error = execute(&action, settings.as_deref()).err(); + let error = execute(&action, settings.as_deref()) + .err() + .map(|message| ActionError { action, message }); let mut state = shared.0.lock().unwrap(); state.1 = false; state.2 = error; @@ -210,11 +218,13 @@ impl Service { } } - pub fn send(&mut self, action: Action) -> Result<(), String> { + pub fn send(&mut self, action: Action) -> Result<(), ActionError> { let generation = self.generation + 1; - self.tx - .try_send((generation, action)) - .map_err(|_| "Controls are busy; please try again".to_owned())?; + let error_action = action.clone(); + self.tx.try_send((generation, action)).map_err(|_| ActionError { + action: error_action, + message: "Controls are busy; please try again".to_owned(), + })?; self.generation = generation; Ok(()) @@ -745,7 +755,10 @@ mod tests { ..Snapshot::default() }, generation, - error: Some("service unavailable".into()), + error: Some(ActionError { + action: Action::Bluetooth(true), + message: "service unavailable".into(), + }), }) }; let (sender, receiver) = tokio::sync::watch::channel(None); @@ -763,7 +776,9 @@ mod tests { }; assert_eq!(update.generation, 2); assert_eq!(update.snapshot.brightness, Some(42)); - assert_eq!(update.error.as_deref(), Some("service unavailable")); + let error = update.error.as_ref().unwrap(); + assert_eq!(error.message, "service unavailable"); + assert!(matches!(error.action, Action::Bluetooth(true))); assert!(stream.as_mut().poll_next(&mut context).is_pending()); let before = wakes.0.load(Ordering::Relaxed); thread::spawn(move || { diff --git a/shell/ferese-shell/src/status_ui.rs b/shell/ferese-shell/src/status_ui.rs index d1282ea..d44c479 100644 --- a/shell/ferese-shell/src/status_ui.rs +++ b/shell/ferese-shell/src/status_ui.rs @@ -24,6 +24,19 @@ pub enum Menu { } impl Menu { + fn action_error(self, error: &status::ActionError) -> Option<&str> { + let relevant = self == Self::System + || matches!( + (self, &error.action), + (Self::Network, Action::Wifi(_)) + | (Self::Bluetooth, Action::Bluetooth(_)) + | (Self::Audio, Action::Volume(_) | Action::Mute(_)) + | (Self::Battery, Action::Brightness(_) | Action::PowerProfile(_)) + | (Self::Notifications, Action::Dnd(_) | Action::Notifications) + ); + relevant.then_some(error.message.as_str()) + } + fn width(self) -> f32 { match self { Self::System => 360.0, @@ -818,7 +831,11 @@ impl FereseShell { } } - if let Some(error) = &self.status_error { + if let Some(error) = self + .status_error + .as_ref() + .and_then(|error| menu.kind.action_error(error)) + { rows = rows.push(text(error).size(12).class(theme::Text::Color(Color { a: p, ..Color::from_rgb8(230, 172, 90) @@ -1271,6 +1288,41 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) { #[cfg(test)] mod tests { + #[test] + fn failed_controls_only_show_errors_in_related_popups() { + for (action, relevant) in [ + (Action::Bluetooth(true), Menu::Bluetooth), + (Action::Wifi(true), Menu::Network), + (Action::Volume(50), Menu::Audio), + (Action::Mute(true), Menu::Audio), + (Action::Brightness(50), Menu::Battery), + (Action::PowerProfile("balanced"), Menu::Battery), + (Action::Dnd(true), Menu::Notifications), + ] { + let error = status::ActionError { + action, + message: "control failed".into(), + }; + for menu in [ + Menu::Network, + Menu::Bluetooth, + Menu::Audio, + Menu::Battery, + Menu::Calendar, + Menu::Recording, + Menu::Notifications, + Menu::System, + ] { + assert_eq!( + menu.action_error(&error), + (menu == relevant || menu == Menu::System).then_some("control failed"), + "{menu:?} showed the wrong control error: {:?}", + error.action + ); + } + } + } + #[test] fn connection_captions_fit_one_short_line() { assert_eq!(super::connection_caption("88%"), "88%");