Skip to content

Commit c23b9b4

Browse files
Notifications settings page
Closes: * pop-os/cosmic-notifications#97 * pop-os/cosmic-notifications#83 I implemented a settings page for COSMIC Notifications so that it is easier to edit its config. COSMIC Notification already exposes a config to tweak useful options like the maximum time a notification may stay on the screen. Currently, there isn't a nice way to edit this config which can be confusing to end users.
1 parent 2d25dce commit c23b9b4

8 files changed

Lines changed: 294 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ git = "https://github.com/pop-os/cosmic-idle"
3030
[workspace.dependencies.cosmic-panel-config]
3131
git = "https://github.com/pop-os/cosmic-panel"
3232

33+
[workspace.dependencies.cosmic-notifications-config]
34+
git = "https://github.com/pop-os/cosmic-notifications"
35+
3336
[workspace.dependencies.cosmic-randr-shell]
3437
git = "https://github.com/pop-os/cosmic-randr"
3538

cosmic-settings/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bi
2525
nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
2626
cosmic-idle-config.workspace = true
2727
cosmic-panel-config = { workspace = true, optional = true }
28+
cosmic-notifications-config.workspace = true
2829
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", optional = true }
2930
cosmic-randr-shell.workspace = true
3031
cosmic-randr = { workspace = true, optional = true }

cosmic-settings/src/app.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,12 @@ impl cosmic::Application for SettingsApp {
569569
}
570570
}
571571

572+
crate::pages::Message::Notifications(message) => {
573+
if let Some(page) = self.pages.page_mut::<desktop::notifications::Page>() {
574+
return page.update(message).map(Into::into);
575+
}
576+
}
577+
572578
#[cfg(feature = "page-region")]
573579
crate::pages::Message::Region(message) => {
574580
if let Some(page) = self.pages.page_mut::<time::region::Page>() {

cosmic-settings/src/pages/desktop/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
pub mod appearance;
55
#[cfg(feature = "wayland")]
66
pub mod dock;
7+
pub mod notifications;
78
#[cfg(feature = "wayland")]
89
pub mod panel;
910
pub mod wallpaper;
@@ -37,6 +38,7 @@ impl page::AutoBind<crate::pages::Message> for Page {
3738
) -> page::Insert<crate::pages::Message> {
3839
page = page.sub_page::<wallpaper::Page>();
3940
page = page.sub_page::<appearance::Page>();
41+
page = page.sub_page::<notifications::Page>();
4042

4143
#[cfg(feature = "wayland")]
4244
{
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Copyright 2026 System76 <info@system76.com>
2+
// SPDX-License-Identifier: GPL-3.0-only
3+
4+
//! User configuration for `cosmic-notifications`.
5+
6+
use cosmic::{
7+
Apply, Element, Task,
8+
cosmic_config::{Config, CosmicConfigEntry},
9+
widget::{self, settings},
10+
};
11+
use cosmic_notifications_config::{Anchor, NotificationsConfig};
12+
use cosmic_settings_page::{self as page, AutoBind, Content, Info, Section, section};
13+
use slotmap::SlotMap;
14+
use tracing::{debug, error, instrument, trace, warn};
15+
16+
use crate::{app, pages};
17+
18+
pub struct Page {
19+
entity: page::Entity,
20+
config_helper: Option<Config>,
21+
config: NotificationsConfig,
22+
23+
// Appearance UI helpers
24+
anchor_dropdown: [String; 8],
25+
26+
// Timeout UI helpers
27+
max_notif: String,
28+
}
29+
30+
impl Page {
31+
/// Reload [`NotificationsConfig`] if it exists.
32+
#[inline]
33+
fn refresh(&mut self) {
34+
self.config = load_config(self.config_helper.as_ref());
35+
}
36+
37+
/// View for notification appearance config (e.g. anchor position and others).
38+
fn appearance_view() -> Section<pages::Message> {
39+
crate::slab!(descriptions {
40+
anchor = fl!("notifications", "anchor");
41+
anchor_desc = fl!("notifications", "anchor-desc");
42+
});
43+
44+
Section::default()
45+
.title(fl!("notifications", "appearance"))
46+
.descriptions(descriptions)
47+
.view::<Page>(move |_binder, page, section| {
48+
// XXX: Anchor is trivially copyable but cosmic-notifications doesn't derive Copy.
49+
let anchor_choice = Some(anchor_to_pos(page.config.anchor.clone()));
50+
51+
settings::section()
52+
.title(&*section.title)
53+
.add(
54+
settings::item::builder(&*section.descriptions[anchor])
55+
.description(&*section.descriptions[anchor_desc])
56+
.control(widget::dropdown(
57+
&page.anchor_dropdown,
58+
anchor_choice,
59+
Message::Anchor,
60+
)),
61+
)
62+
.apply(Element::from)
63+
.map(pages::Message::from)
64+
})
65+
}
66+
67+
/// View for notification timeout config (e.g. maximum timeout).
68+
fn timeout_view() -> Section<pages::Message> {
69+
crate::slab!(descriptions {
70+
max_notif = fl!("notifications", "max");
71+
max_notif_desc = fl!("notifications", "max-desc");
72+
max_per_app = fl!("notifications", "max-per-app");
73+
max_per_app_desc = fl!("notifications", "max-per-app-desc");
74+
max_timeout_urgent = fl!("notifications", "max-timeout-urgent");
75+
max_timeout_urgent_desc = fl!("notifications", "max-timeout-urgent-desc");
76+
max_timeout_normal = fl!("notifications", "max-timeout-normal");
77+
max_timeout_normal_desc = fl!("notifications", "max-timeout-normal-desc");
78+
max_timeout_low = fl!("notifications", "max-timeout-low");
79+
max_timeout_low_desc = fl!("notifications", "max-timeout-low-desc");
80+
});
81+
82+
Section::default()
83+
.title(fl!("notifications", "timeout"))
84+
.descriptions(descriptions)
85+
.view::<Page>(move |_binder, page, section| {
86+
settings::section()
87+
.title(&*section.title)
88+
.add(
89+
settings::item::builder(&*section.descriptions[max_notif])
90+
.description(&*section.descriptions[max_notif_desc])
91+
.control(
92+
widget::text_input("", &page.max_notif)
93+
.on_input(Message::MaxNotifications),
94+
),
95+
)
96+
.apply(Element::from)
97+
.map(pages::Message::from)
98+
})
99+
}
100+
101+
// View for per app notification settings.
102+
// pub fn per_app_view(&self) -> Element<'static, pages::Message> {
103+
// unimplemented!()
104+
// }
105+
106+
#[instrument(skip(self), fields(id = %cosmic_notifications_config::ID))]
107+
pub fn update(&mut self, message: Message) -> Task<app::Message> {
108+
match message {
109+
Message::Anchor(i) => {
110+
let anchor = match i {
111+
0 => Anchor::Top,
112+
1 => Anchor::Bottom,
113+
2 => Anchor::Right,
114+
3 => Anchor::Left,
115+
4 => Anchor::TopLeft,
116+
5 => Anchor::TopRight,
117+
6 => Anchor::BottomLeft,
118+
7 => Anchor::BottomRight,
119+
n => unreachable!("Dropdown for 'anchor' returned an out of bounds value: {n}"),
120+
};
121+
122+
if let Some(helper) = self.config_helper.as_ref() {
123+
if let Err(e) = self.config.set_anchor(helper, anchor) {
124+
error!("Failed to set new anchor position: {e}");
125+
}
126+
} else {
127+
warn!("Unable to set new anchor position due to missing config helper");
128+
}
129+
}
130+
Message::MaxNotifications(s) => {
131+
if let Some(helper) = self.config_helper.as_ref()
132+
&& let Ok(max) = s.parse()
133+
&& let Err(e) = self.config.set_max_notifications(helper, max)
134+
{
135+
error!("Failed to set 'max_notifications': {e}");
136+
}
137+
138+
self.max_notif = s;
139+
}
140+
}
141+
142+
Task::none()
143+
}
144+
}
145+
146+
impl Default for Page {
147+
fn default() -> Self {
148+
debug!(id = %cosmic_notifications_config::ID, "Loading Notifications config for the first time this instance");
149+
150+
let config_helper = Config::new(cosmic_notifications_config::ID, 1).ok();
151+
let config = load_config(config_helper.as_ref());
152+
let max_notif = config.max_notifications.to_string();
153+
154+
Self {
155+
entity: Default::default(),
156+
config_helper,
157+
config,
158+
anchor_dropdown: [
159+
fl!("notifications", "anchor-top"),
160+
fl!("notifications", "anchor-bottom"),
161+
fl!("notifications", "anchor-right"),
162+
fl!("notifications", "anchor-left"),
163+
fl!("notifications", "anchor-top-left"),
164+
fl!("notifications", "anchor-top-right"),
165+
fl!("notifications", "anchor-bottom-left"),
166+
fl!("notifications", "anchor-bottom-right"),
167+
],
168+
max_notif,
169+
}
170+
}
171+
}
172+
173+
impl page::Page<pages::Message> for Page {
174+
fn info(&self) -> Info {
175+
Info::new("notifications", "notification-symbolic")
176+
.title(fl!("notifications"))
177+
.description(fl!("notifications", "desc"))
178+
}
179+
180+
fn content(
181+
&self,
182+
sections: &mut SlotMap<section::Entity, Section<pages::Message>>,
183+
) -> Option<Content> {
184+
Some(vec![
185+
sections.insert(Self::appearance_view()),
186+
sections.insert(Self::timeout_view()),
187+
])
188+
}
189+
190+
fn on_enter(&mut self) -> Task<pages::Message> {
191+
self.refresh();
192+
Task::none()
193+
}
194+
195+
fn set_id(&mut self, entity: page::Entity) {
196+
self.entity = entity;
197+
}
198+
}
199+
200+
impl AutoBind<pages::Message> for Page {}
201+
202+
/// Notification [`Page`] message.
203+
#[derive(Clone, Debug)]
204+
pub enum Message {
205+
Anchor(usize),
206+
MaxNotifications(String),
207+
}
208+
209+
impl From<Message> for pages::Message {
210+
fn from(message: Message) -> Self {
211+
pages::Message::Notifications(message)
212+
}
213+
}
214+
215+
/// Load [`NotificationsConfig`] or return the default settings.
216+
#[instrument(skip(helper), fields(id = %cosmic_notifications_config::ID))]
217+
fn load_config(helper: Option<&Config>) -> NotificationsConfig {
218+
trace!("Attempting to load config");
219+
220+
let Some(helper) = helper else {
221+
debug!("Missing config helper; using default settings.");
222+
return Default::default();
223+
};
224+
225+
NotificationsConfig::get_entry(helper).unwrap_or_else(|(errors, config)| {
226+
warn!("Loading config failed with: ");
227+
for error in errors.iter().filter(|e| e.is_err()) {
228+
warn!("\t* {error}");
229+
}
230+
231+
config
232+
})
233+
}
234+
235+
const fn anchor_to_pos(anchor: Anchor) -> usize {
236+
match anchor {
237+
Anchor::Top => 0,
238+
Anchor::Bottom => 1,
239+
Anchor::Right => 2,
240+
Anchor::Left => 3,
241+
Anchor::TopLeft => 4,
242+
Anchor::TopRight => 5,
243+
Anchor::BottomLeft => 6,
244+
Anchor::BottomRight => 7,
245+
}
246+
}

cosmic-settings/src/pages/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub enum Message {
7575
NavShortcuts(input::keyboard::shortcuts::ShortcutMessage),
7676
#[cfg(feature = "page-networking")]
7777
Networking(networking::Message),
78+
Notifications(desktop::notifications::Message),
7879
Page(Entity),
7980
#[cfg(feature = "wayland")]
8081
Panel(desktop::panel::Message),

i18n/en/cosmic_settings.ftl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,31 @@ shadow-and-corners = Window shadow and corners
414414
## Desktop: Notifications
415415

416416
notifications = Notifications
417+
.desc = Do Not Disturb, lockscreen notifications, and per-application settings
418+
.appearance = Appearance
419+
420+
.anchor = Anchor
421+
.anchor-desc = Screen location to display notification
422+
.anchor-top = Top
423+
.anchor-bottom = Bottom
424+
.anchor-right = Right
425+
.anchor-left = Left
426+
.anchor-top-left = Top left
427+
.anchor-top-right = Top right
428+
.anchor-bottom-left = Bottom left
429+
.anchor-bottom-right = Bottom right
430+
431+
.timeout = Timeout
432+
.max = Maximum notifications
433+
.max-desc = Maximum number of notifications that can be displayed at once
434+
.max-per-app = Maximum notifications per app
435+
.max-per-app-desc = Maximum number of notifications that can be displayed per app if not urgent nor constrained by "maximum notifications"
436+
.max-timeout-urgent = Maximum timeout (Urgent)
437+
.max-timeout-urgent-desc = Max time in milliseconds that an urgent notification will be displayed
438+
.max-timeout-normal = Maximum timeout (Normal)
439+
.max-timeout-normal-desc = Max time in milliseconds that a normal notification will be displayed
440+
.max-timeout-low = Maximum timeout (Low)
441+
.max-timeout-low-desc = Max time in milliseconds that a low priority notification will be displayed
417442
418443
## Desktop: Panel
419444

0 commit comments

Comments
 (0)