git.lucas.co / cce-system-interface
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 }