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 }