system settings
git clone https://git.lucas.co/cce-system-interface.git
src/pages/notifications.rs (11.2K)
1 use std::fs;
2 use std::io::Write;
3 use cce_ui::context::UiContext;
4 use cce_ui::widget::Handle;
5 use cce_ui::widget::input::{Toggle, Dropdown, Spinbox};
6 use cce_ui::layout::{PageLayoutBuilder, PageFlow};
7 use crate::app::{AppAction, PageContent};
8 use crate::pages::AppPage;
9
10
11 #[derive(Debug, Clone)]
12 pub struct NotificationsConfig {
13 pub enable: bool,
14 pub bell: String,
15 pub duration: i32,
16 }
17
18 #[derive(Debug, Clone)]
19 pub struct NotificationsState {
20 pub loaded: bool,
21 pub enable: bool,
22 pub enable_toggle: Handle<cce_ui::widget::Adapted<Toggle>>,
23 pub bell: String,
24 pub bell_menu: Handle<cce_ui::widget::Adapted<Dropdown>>,
25 pub duration: i32,
26 pub duration_spinbox: Handle<cce_ui::widget::Adapted<cce_ui::widget::Spinbox>>,
27 }
28
29 impl Default for NotificationsState {
30 fn default() -> Self {
31 Self {
32 loaded: false,
33 enable: true,
34 enable_toggle: Handle::none(),
35 bell: "none".to_string(),
36 bell_menu: Handle::none(),
37 duration: 5,
38 duration_spinbox: Handle::none(),
39 }
40 }
41 }
42
43 impl NotificationsState {
44 /// The page's state, its widgets inserted into `ctx`.
45 pub fn new(ctx: &mut UiContext) -> Self {
46 Self {
47 // Left-aligned: centred, the label straddled the seam at the
48 // toggle's midpoint (see the Browser page's toggles).
49 enable_toggle: ctx.insert(Toggle::new()
50 .with_label("Enable Notifications")
51 .with_left_align(true)
52 .with_config(&get_config_path(), "enable")),
53 bell_menu: ctx.insert(Dropdown::new(
54 vec![
55 "None".to_string(),
56 "Bell".to_string(),
57 "Dialog".to_string(),
58 "Message".to_string(),
59 ],
60 0,
61 ).with_label("Notification Sound")),
62 duration_spinbox: ctx.insert(Spinbox::new(5, 1, 60, 1)
63 .with_label("Notification Duration")
64 .with_unit("s")
65 .with_config(&get_config_path(), "duration")),
66 ..Self::default()
67 }
68 }
69 }
70
71 #[derive(Debug, Clone)]
72 pub enum NotificationsMessage {
73 ToggleNotificationsEnable,
74 SetNotificationsBell(String),
75 SetNotificationsDuration(i32),
76 SendTestNotification,
77 Refreshed(NotificationsConfig),
78 }
79
80 pub fn update(state: &mut NotificationsState, msg: NotificationsMessage, _ctx: &mut UiContext) {
81 match msg {
82 NotificationsMessage::ToggleNotificationsEnable => {
83 state.enable = !state.enable;
84 write_enable_notifications(state.enable);
85 }
86 NotificationsMessage::SetNotificationsBell(sound) => {
87 state.bell = sound.clone();
88 write_config_value("bell", &sound);
89 send_ipc_command("reload");
90 }
91 NotificationsMessage::SetNotificationsDuration(d) => {
92 state.duration = d;
93 write_config_value("duration", &state.duration.to_string());
94 send_ipc_command("reload");
95 }
96 NotificationsMessage::SendTestNotification => {
97 tokio::spawn(async move {
98 if let Ok(connection) = zbus::Connection::session().await {
99 let _ = connection.call_method(
100 Some("org.freedesktop.Notifications"),
101 "/org/freedesktop/Notifications",
102 Some("org.freedesktop.Notifications"),
103 "Notify",
104 &(
105 "cce-system-interface",
106 0u32,
107 "",
108 "Test Notification",
109 "System notifications are working correctly!",
110 Vec::<&str>::new(),
111 std::collections::HashMap::<&str, zbus::zvariant::Value>::new(),
112 -1i32,
113 )
114 ).await;
115 }
116 });
117 }
118 NotificationsMessage::Refreshed(new) => {
119 state.loaded = true;
120 state.enable = new.enable;
121 state.bell = new.bell;
122 state.duration = new.duration;
123 }
124 }
125 }
126
127 fn get_socket_path() -> String {
128 cce_ui::ipc::socket_path("cce")
129 }
130
131 pub fn read_notifications_config() -> NotificationsConfig {
132 let content = fs::read_to_string(get_config_path()).unwrap_or_default();
133 let val = parse_json(&content);
134 let enable = val["notifications"]["enable"].as_bool().unwrap_or(true);
135 let bell = val["notifications"]["bell"].as_str().unwrap_or("none").to_string();
136 let duration = val["notifications"]["duration"].as_i64().map(|v| v as i32).unwrap_or(5);
137 NotificationsConfig {
138 enable,
139 bell,
140 duration,
141 }
142 }
143
144 fn parse_json(content: &str) -> serde_json::Value {
145 cce_ui::config::parse_kdl_to_json(content)
146 }
147
148 #[cfg(test)]
149 fn parse_notifications_enable(content: &str) -> bool {
150 let val = parse_json(content);
151 val["notifications"]["enable"].as_bool().unwrap_or(true)
152 }
153
154 #[cfg(test)]
155 #[allow(dead_code)]
156 fn parse_notifications_bell(content: &str) -> String {
157 let val = parse_json(content);
158 val["notifications"]["bell"].as_str().unwrap_or("none").to_string()
159 }
160
161 #[cfg(test)]
162 fn parse_notifications_duration(content: &str) -> i32 {
163 let val = parse_json(content);
164 val["notifications"]["duration"].as_i64().map(|v| v as i32).unwrap_or(5)
165 }
166
167 fn send_ipc_command(cmd: &str) {
168 if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(get_socket_path()) {
169 let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
170 }
171 }
172
173 fn write_config_value(key: &str, value: &str) {
174 cce_ui::config::write_config_value(&get_config_path(), key, value, "notifications");
175 }
176
177 fn write_enable_notifications(enabled: bool) {
178 write_config_value("enable", &enabled.to_string());
179 send_ipc_command("reload");
180 }
181
182 thread_local! {
183 static TEST_CONFIG_PATH: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
184 }
185
186 fn get_config_path() -> String {
187 #[cfg(test)]
188 {
189 TEST_CONFIG_PATH.with(|p| {
190 if let Some(path) = p.borrow().as_ref() {
191 return path.clone();
192 }
193 cce_ui::config::get_config_path().to_string_lossy().into_owned()
194 })
195 }
196 #[cfg(not(test))]
197 {
198 cce_ui::config::get_config_path().to_string_lossy().into_owned()
199 }
200 }
201
202 impl AppPage for NotificationsState {
203 // Sections: [Notifications Settings]
204 fn section_widgets(&mut self) -> Vec<Vec<cce_ui::widget::WidgetId>> {
205 vec![vec![
206 self.enable_toggle.id(),
207 self.bell_menu.id(),
208 self.duration_spinbox.id(),
209 ]]
210 }
211
212 fn view(
213 &mut self,
214 cx: f32,
215 cy: f32,
216 cw: f32,
217 ch: f32,
218 _root_focused: bool,
219 sec_focused: &[bool],
220 layout: &mut PageFlow,
221 ctx: &mut cce_ui::context::UiContext,
222 ) -> PageContent {
223 let mut final_pc = PageContent::new();
224 let sec_w = 320.0f32;
225 let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(1);
226
227 builder.add_section(&mut final_pc, "Notifications Settings", sec_focused.first().copied().unwrap_or(false), |sec| {
228 ctx[self.enable_toggle].set_toggled(self.enable);
229 ctx[self.bell_menu].selected = match self.bell.as_str() {
230 "bell" => 1,
231 "dialog" => 2,
232 "message" => 3,
233 _ => 0,
234 };
235 ctx[self.duration_spinbox].value = self.duration;
236 ctx[self.duration_spinbox].set_label("Notification Duration");
237
238 let mut form = sec.form();
239 form.column()
240 .widget_h(ctx, self.enable_toggle, cce_ui::layout::toggle_height())
241 .widget_h(ctx, self.bell_menu, cce_ui::layout::dropdown_height())
242 .widget_h(ctx, self.duration_spinbox, cce_ui::layout::spinbox_height())
243 .draw(0.0, cce_ui::layout::button_height(), false, |pc, r, _| {
244 pc.button(
245 "Send Test Notification",
246 r.x,
247 r.y,
248 r.width,
249 r.height,
250 [0.20, 0.40, 0.65, 1.0],
251 [0.28, 0.50, 0.78, 1.0],
252 [1.0, 1.0, 1.0, 1.0],
253 AppAction::Notifications(NotificationsMessage::SendTestNotification),
254 );
255 });
256 sec.place(form, ctx);
257 });
258
259 final_pc
260 }
261
262 fn propagate_widget_changes(&mut self, actions: &mut Vec<AppAction>, ctx: &mut UiContext) {
263 if ctx[self.enable_toggle].take_change() {
264 actions.push(AppAction::Notifications(NotificationsMessage::ToggleNotificationsEnable));
265 }
266 if ctx[self.bell_menu].take_change() {
267 let sound = match ctx[self.bell_menu].selected {
268 0 => "none",
269 1 => "bell",
270 2 => "dialog",
271 3 => "message",
272 _ => "none",
273 }.to_string();
274 actions.push(AppAction::Notifications(NotificationsMessage::SetNotificationsBell(sound)));
275 }
276 if ctx[self.duration_spinbox].take_change() {
277 actions.push(AppAction::Notifications(NotificationsMessage::SetNotificationsDuration(ctx[self.duration_spinbox].value)));
278 }
279 }
280 }
281
282 #[cfg(test)]
283 pub(crate) mod tests {
284 use super::*;
285
286 #[test]
287 fn test_view_layout_grid() {
288 let mut ui = cce_ui::context::UiContext::new();
289 let mut state = NotificationsState::new(&mut ui);
290 let mut layout = cce_ui::layout::PageFlow::new();
291 let sec_focused = vec![false];
292 let pc = state.view(10.0, 20.0, 800.0, 600.0, false, &sec_focused, &mut layout, &mut ui);
293 assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
294 }
295
296 #[test]
297 fn test_parse_notifications_enable_default() {
298 assert!(parse_notifications_enable(""));
299 assert!(parse_notifications_enable("[layout]\ngap = 18\n"));
300 }
301
302 #[test]
303 fn test_parse_notifications_enable_explicit() {
304 let content = "notifications {\n enable (bool)false\n}\n";
305 assert!(!parse_notifications_enable(content));
306
307 let content = "notifications {\n enable (bool)true\n}\n";
308 assert!(parse_notifications_enable(content));
309 }
310
311 #[test]
312 fn test_parse_notifications_enable_other_sections() {
313 let content = "layout {\n enable (bool)false\n}\nnotifications {\n enable (bool)true\n}\ninput {\n enable (bool)false\n}\n";
314 assert!(parse_notifications_enable(content));
315
316 let content = "layout {\n enable (bool)true\n}\nnotifications {\n enable (bool)false\n}\ninput {\n enable (bool)true\n}\n";
317 assert!(!parse_notifications_enable(content));
318 }
319
320 #[test]
321 fn test_parse_notifications_duration_default() {
322 assert_eq!(parse_notifications_duration(""), 5);
323 assert_eq!(parse_notifications_duration("notifications {}"), 5);
324 }
325
326 #[test]
327 fn test_parse_notifications_duration_explicit() {
328 let content = "notifications {\n duration (i64)10\n}\n";
329 assert_eq!(parse_notifications_duration(content), 10);
330 }
331 }