git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

src/pages/notifications.rs (10.9K)

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