From 838a1c2a2d9d1092e7b831220e08e656687275d2 Mon Sep 17 00:00:00 2001 From: Roman Stingler Date: Fri, 21 Aug 2026 19:15:39 +0200 Subject: [PATCH] add do not disturb mode with toggle and suppression --- i18n/de-DE/ashell.ftl | 3 ++ i18n/en-US/ashell.ftl | 3 ++ i18n/fr-FR/ashell.ftl | 3 ++ src/app.rs | 19 ++++++- src/components/icons.rs | 2 + src/config.rs | 2 + src/modules/mod.rs | 9 +++- src/modules/notifications.rs | 49 +++++++++++++++++-- src/modules/settings/mod.rs | 41 +++++++++++++++- website/docs/configuration/full_config.md | 1 + .../configuration/modules/notifications.md | 20 ++++++++ .../docs/configuration/modules/settings.md | 5 ++ 12 files changed, 149 insertions(+), 8 deletions(-) diff --git a/i18n/de-DE/ashell.ftl b/i18n/de-DE/ashell.ftl index c1699078d..3ded567b6 100644 --- a/i18n/de-DE/ashell.ftl +++ b/i18n/de-DE/ashell.ftl @@ -95,6 +95,9 @@ settings-power-status-full = Voll ## Einstellungen — Kein Ruhezustand settings-idle-inhibitor = Kein Ruhezustand +## Einstellungen — Nicht stören +settings-dnd = Nicht stören + ## Einstellungen — tooltips settings-tooltip-empty = Ziemlich leer hier settings-tooltip-empty-audio = Kein aktives Audio-Gerät diff --git a/i18n/en-US/ashell.ftl b/i18n/en-US/ashell.ftl index cd8f47852..33e8ecbe5 100644 --- a/i18n/en-US/ashell.ftl +++ b/i18n/en-US/ashell.ftl @@ -94,6 +94,9 @@ settings-power-status-full = Full ## Settings — idle inhibitor settings-idle-inhibitor = Idle Inhibitor +## Settings — do not disturb +settings-dnd = Do Not Disturb + ## Settings — tooltips settings-tooltip-empty = Nothing to show settings-tooltip-empty-audio = No active audio device diff --git a/i18n/fr-FR/ashell.ftl b/i18n/fr-FR/ashell.ftl index e6bb54fdb..257f24d9f 100644 --- a/i18n/fr-FR/ashell.ftl +++ b/i18n/fr-FR/ashell.ftl @@ -90,6 +90,9 @@ settings-power-status-unknown = Inconnu ## Paramètres — inhibiteur de veille settings-idle-inhibitor = Inhibiteur de veille +## Paramètres — ne pas déranger +settings-dnd = Ne pas déranger + ## Paramètres — infobulles settings-tooltip-empty = Rien à afficher settings-tooltip-empty-audio = Aucun périphérique audio actif diff --git a/src/app.rs b/src/app.rs index 09a946153..c83280a5d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -107,6 +107,8 @@ impl App { let notifications = Notifications::new(config.notifications, config.animations.enabled); + let notifications_module_enabled = config.modules.contains(&ModuleName::Notifications); + let needs_icons = config.modules.contains(&ModuleName::Tray) || config.workspaces.indicator_format == WorkspaceIndicatorFormat::NameAndIcons; let warm_icons = if needs_icons { @@ -136,7 +138,7 @@ impl App { tray: TrayModule::new(config.tray), tempo: Tempo::new(config.tempo), privacy: Privacy::default(), - settings: Settings::new(config.settings), + settings: Settings::new(config.settings, notifications_module_enabled), notifications, media_player: MediaPlayer::new(config.media_player), osd: Osd::new(config.osd), @@ -199,6 +201,11 @@ impl App { self.keyboard_submap = KeyboardSubmap::default(); self.tempo .update(modules::tempo::Message::ConfigReloaded(config.tempo)); + self.settings.set_notifications_module_enabled( + self.general_config + .modules + .contains(&ModuleName::Notifications), + ); self.settings .update(modules::settings::Message::ConfigReloaded(config.settings)); self.media_player @@ -401,6 +408,11 @@ impl App { modules::settings::Action::CloseTooltipMenu(id, menu_type) => self .outputs .close_menu(id, Some(menu_type), self.general_config.enable_esc_key), + modules::settings::Action::SetDnd(dnd) => { + self.settings.set_dnd(dnd); + self.notifications.set_do_not_disturb(dnd); + Task::none() + } }, Message::OutputEvent(event) => match event { OutputEvent::Added(info) => { @@ -502,6 +514,11 @@ impl App { self.outputs .update_toast_input_region(content_size, position) } + modules::notifications::Action::SetDnd(dnd) => { + self.settings.set_dnd(dnd); + self.notifications.set_do_not_disturb(dnd); + Task::none() + } }, Message::IpcOsdCommand(cmd) => { let mut tasks = vec![]; diff --git a/src/components/icons.rs b/src/components/icons.rs index 949b7611c..6dae1a045 100644 --- a/src/components/icons.rs +++ b/src/components/icons.rs @@ -131,6 +131,7 @@ pub enum StaticIcon { Remove, Bell, BellBadge, + BellOff, Delete, } @@ -247,6 +248,7 @@ impl StaticIcon { StaticIcon::Remove => "\u{f0377}", StaticIcon::Bell => "\u{eaa2}", StaticIcon::BellBadge => "\u{eb9a}", + StaticIcon::BellOff => "\u{ec08}", StaticIcon::Delete => "\u{f01b4}", } } diff --git a/src/config.rs b/src/config.rs index 0363b9351..31e17e0a6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -640,6 +640,7 @@ pub struct SettingsModuleConfig { pub bluetooth_more_cmd: Option, pub remove_airplane_btn: bool, pub remove_idle_btn: bool, + pub remove_dnd_btn: bool, pub enable_tooltips: bool, pub indicators: Vec, #[serde(rename = "CustomButton")] @@ -674,6 +675,7 @@ impl Default for SettingsModuleConfig { bluetooth_more_cmd: Default::default(), remove_airplane_btn: Default::default(), remove_idle_btn: Default::default(), + remove_dnd_btn: Default::default(), enable_tooltips: true, indicators: vec![ SettingsIndicator::IdleInhibitor, diff --git a/src/modules/mod.rs b/src/modules/mod.rs index a8a6adb3e..fb20c6498 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -320,7 +320,14 @@ impl App { )), ModuleName::Notifications => Some(( self.notifications.view().map(Message::Notifications), - Some(OnModulePress::ToggleMenu(MenuType::Notifications)), + Some(OnModulePress::ToggleMenuWithExtra { + menu_type: MenuType::Notifications, + on_right_press: Some(Box::new(Message::Notifications( + notifications::Message::ToggleDnd, + ))), + on_scroll_up: None, + on_scroll_down: None, + }), )), } } diff --git a/src/modules/notifications.rs b/src/modules/notifications.rs index 6a7d18b7d..1e3980918 100644 --- a/src/modules/notifications.rs +++ b/src/modules/notifications.rs @@ -121,6 +121,7 @@ pub enum Message { GroupCleared(String, Vec), Event(ServiceEvent), ToggleGroup(String), + ToggleDnd, ExpireToast(u32), DismissToast(u32), StartCollapse(u32), @@ -143,6 +144,7 @@ pub enum Action { Show(Task), Hide(Task), UpdateToastInputRegion(Size), + SetDnd(bool), } // Must match the widget durations so task delays align with animation end. @@ -164,6 +166,7 @@ pub struct Notifications { toasts: VecDeque, dismiss_phases: HashMap, animations_enabled: bool, + dnd: bool, } impl Notifications { @@ -178,6 +181,7 @@ impl Notifications { toasts: VecDeque::new(), dismiss_phases: HashMap::new(), animations_enabled, + dnd: false, } } @@ -185,6 +189,10 @@ impl Notifications { self.animations_enabled = enabled; } + pub fn set_do_not_disturb(&mut self, dnd: bool) { + self.dnd = dnd; + } + fn is_blocklisted(&self, app_name: &str) -> bool { self.blocklist .iter() @@ -338,7 +346,17 @@ impl Notifications { return Action::None; } - let toast_action = self.toast_action_for_update_event(&update_event); + let suppress_toast = self.dnd + && matches!( + &update_event, + NotificationEvent::Received(notification) + if notification.urgency != Urgency::Critical + ); + let toast_action = if suppress_toast { + Action::None + } else { + self.toast_action_for_update_event(&update_event) + }; self.apply_update_event(update_event); toast_action @@ -351,6 +369,10 @@ impl Notifications { Action::Task(invoke_and_close_task(connection, id, action_key)) } Message::NotificationClosed => Action::None, + Message::ToggleDnd => { + self.dnd = !self.dnd; + Action::SetDnd(self.dnd) + } Message::ClearNotifications => { let connection = self.connection.clone(); let notification_ids: Vec = self.notifications.iter().map(|n| n.id).collect(); @@ -627,11 +649,14 @@ impl Notifications { } pub fn view(&'_ self) -> Element<'_, Message> { - if !self.notifications.is_empty() { - icon(StaticIcon::BellBadge).into() + let bell_icon = if self.dnd { + StaticIcon::BellOff + } else if !self.notifications.is_empty() { + StaticIcon::BellBadge } else { - icon(StaticIcon::Bell).into() - } + StaticIcon::Bell + }; + icon(bell_icon).into() } pub fn menu_view<'a>(&'a self) -> Element<'a, Message> { @@ -657,6 +682,20 @@ impl Notifications { .width(Length::Fill) .size(font_size.lg) ) + .push( + icon_button(if self.dnd { + StaticIcon::BellOff + } else { + StaticIcon::Bell + }) + .kind(ButtonKind::Transparent) + .hierarchy(if self.dnd { + ButtonHierarchy::Danger + } else { + ButtonHierarchy::Secondary + }) + .on_press(Message::ToggleDnd) + ) .push((!is_empty).then(|| { icon_button(StaticIcon::Delete).on_press(Message::ClearNotifications) })), diff --git a/src/modules/settings/mod.rs b/src/modules/settings/mod.rs index fdaf703a2..7fbd4756c 100644 --- a/src/modules/settings/mod.rs +++ b/src/modules/settings/mod.rs @@ -47,6 +47,9 @@ pub struct Settings { network: NetworkSettings, bluetooth: BluetoothSettings, idle_inhibitor: Option, + dnd: bool, + remove_dnd_btn: bool, + notifications_module_enabled: bool, sub_menu: Option, network_dialog: Option, network_dialog_show_password: bool, @@ -98,6 +101,7 @@ pub enum Message { Audio(audio::Message), Brightness(brightness::Message), ToggleInhibitIdle, + ToggleDnd, Lock, Power(power::Message), ToggleSubMenu(SubMenu), @@ -124,6 +128,7 @@ pub enum Action { ReleaseKeyboardWithCommand(SurfaceId, Task), OpenTooltipMenu(SurfaceId, MenuType, ButtonUIRef), CloseTooltipMenu(SurfaceId, MenuType), + SetDnd(bool), } #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] @@ -200,7 +205,15 @@ impl Settings { Action::None } - pub fn new(config: SettingsModuleConfig) -> Self { + pub fn set_dnd(&mut self, dnd: bool) { + self.dnd = dnd; + } + + pub fn set_notifications_module_enabled(&mut self, enabled: bool) { + self.notifications_module_enabled = enabled; + } + + pub fn new(config: SettingsModuleConfig, notifications_module_enabled: bool) -> Self { Settings { lock_cmd: config.lock_cmd, power: PowerSettings::new(PowerSettingsConfig::new( @@ -239,6 +252,9 @@ impl Settings { } else { IdleInhibitorManager::new() }, + dnd: false, + remove_dnd_btn: config.remove_dnd_btn, + notifications_module_enabled, sub_menu: None, network_dialog: None, indicators: config.indicators, @@ -366,6 +382,10 @@ impl Settings { } Action::None } + Message::ToggleDnd => { + self.dnd = !self.dnd; + Action::SetDnd(self.dnd) + } Message::Lock => { if let Some(lock_cmd) = &self.lock_cmd { crate::utils::launcher::execute_command(lock_cmd); @@ -546,6 +566,7 @@ impl Settings { } else if self.idle_inhibitor.is_none() { self.idle_inhibitor = IdleInhibitorManager::new(); } + self.remove_dnd_btn = config.remove_dnd_btn; self.indicators = config.indicators; self.custom_buttons = config.custom_buttons; Action::None @@ -660,6 +681,24 @@ impl Settings { None, ) }), + (!self.remove_dnd_btn && self.notifications_module_enabled).then(|| { + ( + quick_setting_button( + if self.dnd { + StaticIcon::BellOff + } else { + StaticIcon::Bell + }, + t!("settings-dnd"), + None, + self.dnd, + Message::ToggleDnd, + None, + None, + ), + None, + ) + }), self.power.quick_setting_button().map(|(button, submenu)| { ( button.map(Message::Power), diff --git a/website/docs/configuration/full_config.md b/website/docs/configuration/full_config.md index 5e1fbcf69..5e296ea81 100644 --- a/website/docs/configuration/full_config.md +++ b/website/docs/configuration/full_config.md @@ -173,6 +173,7 @@ volume_step = 5 # (default) step size for IPC volume up/down, range 1..=50 max_volume = 100 # (default) max volume level, range 1..=200 (>100 enables overdrive) # remove_airplane_btn = false # (default) set true to hide airplane mode button # remove_idle_btn = false # (default) set true to hide idle inhibitor button +# remove_dnd_btn = false # (default) set true to hide do not disturb button indicators = [ "IdleInhibitor", "PowerProfile", "Audio", "Microphone", "Bluetooth", "Network", "Vpn", "Battery", "Brightness" ] # indicators = [ "IdleInhibitor", "PowerProfile", "Audio", "Microphone", "Bluetooth", "Network", "Vpn", "Battery", "PeripheralBattery", "Brightness" ] diff --git a/website/docs/configuration/modules/notifications.md b/website/docs/configuration/modules/notifications.md index 8e6f3e9fb..b3e8e9826 100644 --- a/website/docs/configuration/modules/notifications.md +++ b/website/docs/configuration/modules/notifications.md @@ -25,12 +25,32 @@ The `expire_timeout` hint sent by applications is respected: a value of `-1` fal If you prefer no popups and only the panel indicator, set `toast = false`. +## Do Not Disturb + +Do not disturb suppresses toast popups without dropping notifications: they are +still collected and shown in the menu, and notifications with `Critical` +urgency still pop up as toasts. + +While it is active the bar indicator switches to a crossed-out bell icon. + +You can toggle it in three ways: + +- **Right-click the notification indicator** in the status bar +- **The bell button** in the notifications menu header +- **The "Do Not Disturb" quick setting** in the [Settings](settings.md) menu + (can be hidden with `remove_dnd_btn`; it is only shown when this module is + part of the bar layout) + +The state is not persisted across restarts, ashell always starts with do not +disturb off. + ## Notification Menu Click the notification indicator to open the notifications menu. The menu displays all active notifications with the following features: - **Individual notifications**: Shows the app icon, name, summary, and optional body text and timestamp - **Clear button**: Removes all notifications at once +- **Do not disturb button**: Toggles do not disturb (see above) - **Grouped mode** (optional): Organizes notifications by application with expandable groups - **Clicking a notification**: Invokes its default action (if provided by the app) and closes it diff --git a/website/docs/configuration/modules/settings.md b/website/docs/configuration/modules/settings.md index b6acab234..00f892fef 100644 --- a/website/docs/configuration/modules/settings.md +++ b/website/docs/configuration/modules/settings.md @@ -68,6 +68,11 @@ With the `remove_airplane_btn` option you can remove the airplane mode button. With the `remove_idle_btn` option you can remove the idle inhibitor button. +With the `remove_dnd_btn` option you can remove the do not disturb button. +The button is only shown when the [Notifications](notifications.md) module is +part of the bar layout, since do not disturb only applies while ashell is the +notification daemon. + ## Tooltips By default, hovering over the status bar indicators shows a tooltip describing