Skip to content

Commit 3f5df2e

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 3f5df2e

9 files changed

Lines changed: 415 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: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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,
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, warn};
15+
16+
use crate::{app, pages, set_max_timeout};
17+
18+
mod helpers;
19+
use helpers::{anchor_to_pos, load_config};
20+
21+
pub struct Page {
22+
entity: page::Entity,
23+
config_helper: Option<Config>,
24+
config: NotificationsConfig,
25+
26+
// Appearance UI helpers
27+
anchor_dropdown: [String; 8],
28+
29+
// Timeout UI helpers
30+
max_notif: String,
31+
max_per_app: String,
32+
max_timeout_urgent: String,
33+
max_timeout_normal: String,
34+
max_timeout_low: String,
35+
}
36+
37+
impl Page {
38+
/// Reload [`NotificationsConfig`] if it exists.
39+
#[inline]
40+
fn refresh(&mut self) {
41+
self.config = load_config(self.config_helper.as_ref());
42+
}
43+
44+
/// View for notification appearance config (e.g. anchor position and others).
45+
fn appearance_view() -> Section<pages::Message> {
46+
crate::slab!(descriptions {
47+
anchor = fl!("notifications", "anchor");
48+
anchor_desc = fl!("notifications", "anchor-desc");
49+
});
50+
51+
Section::default()
52+
.title(fl!("notifications", "appearance"))
53+
.descriptions(descriptions)
54+
.view::<Page>(move |_binder, page, section| {
55+
// XXX: Anchor is trivially copyable but cosmic-notifications doesn't derive Copy.
56+
let anchor_choice = Some(anchor_to_pos(page.config.anchor.clone()));
57+
58+
settings::section()
59+
.title(&*section.title)
60+
.add(
61+
settings::item::builder(&*section.descriptions[anchor])
62+
.description(&*section.descriptions[anchor_desc])
63+
.control(widget::dropdown(
64+
&page.anchor_dropdown,
65+
anchor_choice,
66+
Message::Anchor,
67+
)),
68+
)
69+
.apply(Element::from)
70+
.map(pages::Message::from)
71+
})
72+
}
73+
74+
/// View for notification timeout config (e.g. maximum timeout).
75+
fn timeout_view() -> Section<pages::Message> {
76+
crate::slab!(descriptions {
77+
max_notif = fl!("notifications", "max");
78+
max_notif_desc = fl!("notifications", "max-desc");
79+
max_per_app = fl!("notifications", "max-per-app");
80+
max_per_app_desc = fl!("notifications", "max-per-app-desc");
81+
max_timeout_urgent = fl!("notifications", "max-timeout-urgent");
82+
max_timeout_urgent_desc = fl!("notifications", "max-timeout-urgent-desc");
83+
max_timeout_normal = fl!("notifications", "max-timeout-normal");
84+
max_timeout_normal_desc = fl!("notifications", "max-timeout-normal-desc");
85+
max_timeout_low = fl!("notifications", "max-timeout-low");
86+
max_timeout_low_desc = fl!("notifications", "max-timeout-low-desc");
87+
});
88+
89+
Section::default()
90+
.title(fl!("notifications", "timeout"))
91+
.descriptions(descriptions)
92+
.view::<Page>(move |_binder, page, section| {
93+
settings::section()
94+
.title(&*section.title)
95+
.add(
96+
settings::item::builder(&*section.descriptions[max_notif])
97+
.description(&*section.descriptions[max_notif_desc])
98+
.control(
99+
widget::text_input("", &page.max_notif)
100+
.on_input(Message::MaxNotifications),
101+
),
102+
)
103+
.add(
104+
settings::item::builder(&*section.descriptions[max_per_app])
105+
.description(&*section.descriptions[max_per_app_desc])
106+
.control(
107+
widget::text_input("", &page.max_per_app)
108+
.on_input(Message::MaxPerApp),
109+
),
110+
)
111+
.add(
112+
settings::item::builder(&*section.descriptions[max_timeout_urgent])
113+
.description(&*section.descriptions[max_timeout_urgent_desc])
114+
.control(
115+
widget::text_input("", &page.max_timeout_urgent)
116+
.on_input(Message::MaxTimeoutUrgent),
117+
),
118+
)
119+
.add(
120+
settings::item::builder(&*section.descriptions[max_timeout_normal])
121+
.description(&*section.descriptions[max_timeout_normal_desc])
122+
.control(
123+
widget::text_input("", &page.max_timeout_normal)
124+
.on_input(Message::MaxTimeoutNormal),
125+
),
126+
)
127+
.add(
128+
settings::item::builder(&*section.descriptions[max_timeout_low])
129+
.description(&*section.descriptions[max_timeout_low_desc])
130+
.control(
131+
widget::text_input("", &page.max_timeout_low)
132+
.on_input(Message::MaxTimeoutLow),
133+
),
134+
)
135+
.apply(Element::from)
136+
.map(pages::Message::from)
137+
})
138+
}
139+
140+
// View for per app notification settings.
141+
// pub fn per_app_view(&self) -> Element<'static, pages::Message> {
142+
// unimplemented!()
143+
// }
144+
145+
#[instrument(skip(self), fields(id = %cosmic_notifications_config::ID))]
146+
pub fn update(&mut self, message: Message) -> Task<app::Message> {
147+
match message {
148+
Message::Anchor(i) => {
149+
let anchor = match i {
150+
0 => Anchor::Top,
151+
1 => Anchor::Bottom,
152+
2 => Anchor::Right,
153+
3 => Anchor::Left,
154+
4 => Anchor::TopLeft,
155+
5 => Anchor::TopRight,
156+
6 => Anchor::BottomLeft,
157+
7 => Anchor::BottomRight,
158+
n => unreachable!("Dropdown for 'anchor' returned an out of bounds value: {n}"),
159+
};
160+
161+
if let Some(helper) = self.config_helper.as_ref() {
162+
if let Err(e) = self.config.set_anchor(helper, anchor) {
163+
error!("Failed to set new anchor position: {e}");
164+
}
165+
} else {
166+
warn!("Unable to set new anchor position due to missing config helper");
167+
}
168+
}
169+
Message::MaxNotifications(s) => {
170+
set_max_timeout!(
171+
self,
172+
"max_notifications",
173+
max_notifications,
174+
set_max_notifications,
175+
s
176+
);
177+
self.max_notif = s;
178+
}
179+
Message::MaxPerApp(s) => {
180+
set_max_timeout!(self, "max_per_app", max_per_app, set_max_per_app, s);
181+
self.max_per_app = s;
182+
}
183+
Message::MaxTimeoutUrgent(s) => {
184+
set_max_timeout!(
185+
self,
186+
"max_timeout_urgent",
187+
max_timeout_urgent,
188+
set_max_timeout_urgent,
189+
s
190+
);
191+
self.max_timeout_urgent = s;
192+
}
193+
Message::MaxTimeoutLow(s) => {
194+
set_max_timeout!(
195+
self,
196+
"max_timeout_low",
197+
max_timeout_low,
198+
set_max_timeout_low,
199+
s
200+
);
201+
self.max_timeout_low = s;
202+
}
203+
Message::MaxTimeoutNormal(s) => {
204+
set_max_timeout!(
205+
self,
206+
"max_timeout_normal",
207+
max_timeout_normal,
208+
set_max_timeout_normal,
209+
s
210+
);
211+
self.max_timeout_normal = s;
212+
}
213+
}
214+
215+
Task::none()
216+
}
217+
}
218+
219+
impl Default for Page {
220+
fn default() -> Self {
221+
debug!(id = %cosmic_notifications_config::ID, "Loading Notifications config for the first time this instance");
222+
223+
let config_helper = Config::new(cosmic_notifications_config::ID, 1).ok();
224+
let config = load_config(config_helper.as_ref());
225+
let max_notif = config.max_notifications.to_string();
226+
let max_per_app = config.max_per_app.to_string();
227+
let max_timeout_urgent = config
228+
.max_timeout_urgent
229+
.map(|i| i.to_string())
230+
.unwrap_or_default();
231+
let max_timeout_normal = config
232+
.max_timeout_normal
233+
.map(|i| i.to_string())
234+
.unwrap_or_default();
235+
let max_timeout_low = config
236+
.max_timeout_low
237+
.map(|i| i.to_string())
238+
.unwrap_or_default();
239+
240+
Self {
241+
entity: Default::default(),
242+
config_helper,
243+
config,
244+
anchor_dropdown: [
245+
fl!("notifications", "anchor-top"),
246+
fl!("notifications", "anchor-bottom"),
247+
fl!("notifications", "anchor-right"),
248+
fl!("notifications", "anchor-left"),
249+
fl!("notifications", "anchor-top-left"),
250+
fl!("notifications", "anchor-top-right"),
251+
fl!("notifications", "anchor-bottom-left"),
252+
fl!("notifications", "anchor-bottom-right"),
253+
],
254+
max_notif,
255+
max_per_app,
256+
max_timeout_urgent,
257+
max_timeout_normal,
258+
max_timeout_low,
259+
}
260+
}
261+
}
262+
263+
impl page::Page<pages::Message> for Page {
264+
fn info(&self) -> Info {
265+
Info::new("notifications", "notification-symbolic")
266+
.title(fl!("notifications"))
267+
.description(fl!("notifications", "desc"))
268+
}
269+
270+
fn content(
271+
&self,
272+
sections: &mut SlotMap<section::Entity, Section<pages::Message>>,
273+
) -> Option<Content> {
274+
Some(vec![
275+
sections.insert(Self::appearance_view()),
276+
sections.insert(Self::timeout_view()),
277+
])
278+
}
279+
280+
fn on_enter(&mut self) -> Task<pages::Message> {
281+
self.refresh();
282+
Task::none()
283+
}
284+
285+
fn set_id(&mut self, entity: page::Entity) {
286+
self.entity = entity;
287+
}
288+
}
289+
290+
impl AutoBind<pages::Message> for Page {}
291+
292+
/// Notification [`Page`] message.
293+
#[derive(Clone, Debug)]
294+
pub enum Message {
295+
Anchor(usize),
296+
MaxNotifications(String),
297+
MaxPerApp(String),
298+
MaxTimeoutUrgent(String),
299+
MaxTimeoutNormal(String),
300+
MaxTimeoutLow(String),
301+
}
302+
303+
impl From<Message> for pages::Message {
304+
fn from(message: Message) -> Self {
305+
pages::Message::Notifications(message)
306+
}
307+
}

0 commit comments

Comments
 (0)