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

commitf30469d0c5fd7f15eeb234330ddbb39241d1b1db
parent4a28456776
authorLucas Galante <[email protected]>
date2026-05-23 12:24
Add notifications page to clear system interface control panel

 src/app.rs                 |   4 +
 src/main.rs                |  33 +++++-
 src/pages/mod.rs           |   6 +-
 src/pages/notifications.rs | 243 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 284 insertions(+), 2 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 4e98176..d898d6e 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5,6 +5,7 @@ use crate::pages::display;
 use crate::pages::input;
 use crate::pages::layout;
 use crate::pages::network;
+use crate::pages::notifications;
 use crate::pages::power;
 use crate::pages::processors;
 use crate::pages::status;
@@ -24,6 +25,7 @@ pub struct AppState {
     pub system_info: system_info::SystemState,
     pub status: status::StatusState,
     pub storage: storage::StorageState,
+    pub notifications: notifications::NotificationsState,
 }
 
 impl Default for AppState {
@@ -40,6 +42,7 @@ impl Default for AppState {
             system_info: system_info::SystemState::default(),
             status: status::StatusState::default(),
             storage: storage::StorageState::default(),
+            notifications: notifications::read_notifications_config(),
         }
     }
 }
@@ -56,6 +59,7 @@ pub enum AppAction {
     SystemInfo(system_info::SystemMessage),
     Status(status::StatusMessage),
     Storage(storage::StorageMessage),
+    Notifications(notifications::NotificationsMessage),
 }
 
 pub struct PageContent {
diff --git a/src/main.rs b/src/main.rs
index 1f1316b..c430a50 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -125,6 +125,7 @@ struct SystemInterface {
     rx_system: std::sync::mpsc::Receiver<pages::system_info::SystemState>,
     rx_status: std::sync::mpsc::Receiver<pages::status::StatusState>,
     rx_storage: std::sync::mpsc::Receiver<pages::storage::StorageState>,
+    rx_notifications: std::sync::mpsc::Receiver<pages::notifications::NotificationsState>,
     tx_color_selector: std::sync::mpsc::Sender<ColorSelectorAction>,
     rx_color_selector: std::sync::mpsc::Receiver<ColorSelectorAction>,
 
@@ -271,6 +272,17 @@ impl SystemInterface {
         let rx_processors = spawn_bg(3, || pages::processors::fetch_processors_state());
         let rx_status = spawn_bg(10, || pages::status::fetch_status_state());
         let rx_storage = spawn_bg(10, || pages::storage::fetch_storage_state());
+        let rx_notifications = {
+            let (tx, rx) = std::sync::mpsc::channel::<pages::notifications::NotificationsState>();
+            tokio::spawn(async move {
+                loop {
+                    let val = tokio::task::spawn_blocking(|| pages::notifications::read_notifications_config()).await;
+                    if let Ok(val) = val { if tx.send(val).is_err() { break; } }
+                    tokio::time::sleep(std::time::Duration::from_secs(30)).await;
+                }
+            });
+            rx
+        };
 
         let (tx_color_selector, rx_color_selector) = std::sync::mpsc::channel();
         let scale_factor = (window.scale_factor() as f32).max(2.0) as f64;
@@ -284,7 +296,7 @@ impl SystemInterface {
             cursor_x: 0.0, cursor_y: 0.0,
             scale_factor,
             rx_power, rx_audio, rx_display, rx_network, rx_layout, rx_input,
-            rx_processors, rx_system, rx_status, rx_storage,
+            rx_processors, rx_system, rx_status, rx_storage, rx_notifications,
             tx_color_selector, rx_color_selector,
             width: size.width, height: size.height,
             needs_rebuild: true,
@@ -433,6 +445,7 @@ impl SystemInterface {
             Page::System => system_info::view(&self.app.system_info, cx, cy, cw, ch),
             Page::Status => status::view(&self.app.status, cx, cy, cw, ch),
             Page::Storage => storage::view(&self.app.storage, cx, cy, cw, ch),
+            Page::Notifications => notifications::view(&mut self.app.notifications, cx, cy, cw, ch),
         }
     }
 
@@ -535,6 +548,10 @@ impl SystemInterface {
             storage::update(&mut self.app.storage, storage::StorageMessage::Refreshed(s));
             self.needs_rebuild = true;
         }
+        while let Ok(s) = self.rx_notifications.try_recv() {
+            notifications::update(&mut self.app.notifications, notifications::NotificationsMessage::Refreshed(s));
+            self.needs_rebuild = true;
+        }
         while let Ok(action) = self.rx_color_selector.try_recv() {
             match action {
                 ColorSelectorAction::Background(rgb) => {
@@ -595,6 +612,7 @@ impl SystemInterface {
             AppAction::Processors(m) => processors::update(&mut self.app.processors, m.clone()),
             AppAction::Status(m) => status::update(&mut self.app.status, m.clone()),
             AppAction::Storage(m) => storage::update(&mut self.app.storage, m.clone()),
+            AppAction::Notifications(m) => notifications::update(&mut self.app.notifications, m.clone()),
         }
     }
 
@@ -666,6 +684,12 @@ impl SystemInterface {
                         changed = true;
                     }
                 }
+                if self.app.current_page == Page::Notifications {
+                    let s = self.scale_factor as f32;
+                    if self.app.notifications.enable_toggle.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+                        changed = true;
+                    }
+                }
                 if changed { self.needs_rebuild = true; }
                 changed
             }
@@ -895,6 +919,13 @@ impl SystemInterface {
                         actions.push(AppAction::Input(pages::input::InputMessage::ToggleTapToClick));
                     }
                 }
+                if self.app.current_page == Page::Notifications {
+                    let toggle = &mut self.app.notifications.enable_toggle;
+                    toggle.mouse_input(*button, *state, lx, ly);
+                    if toggle.take_click() {
+                        actions.push(AppAction::Notifications(pages::notifications::NotificationsMessage::ToggleEnable));
+                    }
+                }
                 if *state == ElementState::Pressed && self.app.current_page == Page::Audio {
                     for (i, sb) in self.app.audio.sink_spinboxes.iter_mut().enumerate() {
                         if !sb.hit_test(lx, ly) { sb.unfocus(); }
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index bc0be2c..07902e8 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -9,6 +9,7 @@ pub mod keybindings;
 pub mod input;
 pub mod status;
 pub mod processors;
+pub mod notifications;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Page {
@@ -22,14 +23,16 @@ pub enum Page {
     Processors,
     Input,
     Status,
+    Notifications,
 }
 
 impl Page {
-    pub const ALL: [Page; 10] = [
+    pub const ALL: [Page; 11] = [
         Page::Audio,
         Page::Display,
         Page::Input,
         Page::Layout,
+        Page::Notifications,
         Page::Processors,
         Page::Power,
         Page::Radios,
@@ -50,6 +53,7 @@ impl Page {
             Page::Processors => "Processors",
             Page::Input => "Input",
             Page::Status => "Status",
+            Page::Notifications => "Notifications",
         }
     }
 
diff --git a/src/pages/notifications.rs b/src/pages/notifications.rs
new file mode 100644
index 0000000..9fe2d49
--- /dev/null
+++ b/src/pages/notifications.rs
@@ -0,0 +1,243 @@
+use std::fs;
+use std::io::Write;
+
+use crate::app::{AppAction, PageContent};
+use clear_ui::layout::{render_widget, Section};
+use clear_ui::widget::Toggle;
+
+const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
+const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
+
+#[derive(Debug, Clone)]
+pub struct NotificationsState {
+    pub enable: bool,
+    pub enable_toggle: Toggle,
+}
+
+impl Default for NotificationsState {
+    fn default() -> Self {
+        Self {
+            enable: true,
+            enable_toggle: Toggle::new().with_label("Enable Notifications"),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum NotificationsMessage {
+    ToggleEnable,
+    SendTestNotification,
+    Refreshed(NotificationsState),
+}
+
+pub fn read_notifications_config() -> NotificationsState {
+    let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+    let enable = parse_notifications_enable(&content);
+    NotificationsState {
+        enable,
+        enable_toggle: Toggle::new().with_label("Enable Notifications"),
+    }
+}
+
+fn parse_notifications_enable(content: &str) -> bool {
+    let mut in_section = false;
+    for line in content.lines() {
+        let trimmed = line.trim();
+        if trimmed == "[notifications]" {
+            in_section = true;
+            continue;
+        }
+        if trimmed.starts_with('[') && in_section {
+            break;
+        }
+        if in_section && trimmed.starts_with("enable") {
+            if let Some(val) = trimmed.split('=').nth(1) {
+                return val.trim() == "true";
+            }
+        }
+    }
+    true // default to true
+}
+
+fn send_ipc_command(cmd: &str) {
+    if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(CLEARWM_SOCK) {
+        let _ = stream.write_all(format!("{}\n", cmd).as_bytes());
+    }
+}
+
+fn write_config_value(key: &str, value: &str) {
+    let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+    let new_line = format!("{} = {}", key, value);
+
+    let mut found = false;
+    let mut updated_lines = Vec::new();
+    let mut in_section = false;
+
+    for line in content.lines() {
+        let trimmed = line.trim();
+        if trimmed == "[notifications]" {
+            in_section = true;
+            updated_lines.push(line.to_string());
+            continue;
+        }
+        if trimmed.starts_with('[') && in_section {
+            in_section = false;
+        }
+        if in_section && trimmed.starts_with(key) {
+            found = true;
+            updated_lines.push(new_line.clone());
+        } else {
+            updated_lines.push(line.to_string());
+        }
+    }
+
+    let mut updated = updated_lines.join("\n");
+
+    if !found {
+        let mut result = String::new();
+        let has_section = content.lines().any(|l| l.trim() == "[notifications]");
+        if has_section {
+            let mut in_section = false;
+            let mut inserted = false;
+            for line in updated.lines() {
+                if line.trim() == "[notifications]" {
+                    in_section = true;
+                    result.push_str(line);
+                    result.push('\n');
+                    continue;
+                }
+                if line.trim().starts_with('[') && in_section {
+                    if !inserted {
+                        result.push_str(&new_line);
+                        result.push('\n');
+                        inserted = true;
+                    }
+                    in_section = false;
+                }
+                result.push_str(line);
+                result.push('\n');
+            }
+            if !inserted {
+                result.push_str(&new_line);
+                result.push('\n');
+            }
+            updated = result;
+        } else {
+            updated.push_str("\n[notifications]\n");
+            updated.push_str(&new_line);
+            updated.push_str("\n");
+        }
+    }
+    let _ = fs::write(CONFIG_PATH, updated);
+}
+
+fn write_enable_notifications(enabled: bool) {
+    write_config_value("enable", &enabled.to_string());
+    send_ipc_command("reload");
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+
+pub fn view(state: &mut NotificationsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+    let mut pc = PageContent::new();
+    let y = cy + 12.0;
+
+    let mut sec = Section::new(&mut pc, cx, y, cw, "System Notifications");
+
+    let yt = sec.ay();
+    let toggle_w = 48.0;
+    let toggle_h = 24.0;
+    state.enable_toggle.set_toggled(state.enable);
+    render_widget(&mut pc, &mut state.enable_toggle, sec.ax(100.0), yt, toggle_w, toggle_h);
+    sec.content_y += toggle_h + 24.0;
+
+    let btn_w = 160.0;
+    let btn_h = 32.0;
+    let btn_x = sec.ax(0.0);
+    let btn_y = sec.ay();
+    pc.button(
+        "Send Test Notification",
+        btn_x,
+        btn_y,
+        btn_w,
+        btn_h,
+        ACCENT,
+        BTN_HOVER,
+        TEXT_FG,
+        AppAction::Notifications(NotificationsMessage::SendTestNotification),
+    );
+    sec.content_y += btn_h + 12.0;
+
+    sec.finish(&mut pc);
+    pc
+}
+
+pub fn update(state: &mut NotificationsState, msg: NotificationsMessage) {
+    match msg {
+        NotificationsMessage::ToggleEnable => {
+            state.enable = !state.enable;
+            write_enable_notifications(state.enable);
+        }
+        NotificationsMessage::SendTestNotification => {
+            send_ipc_command("notify \"clearwm\" \"System notifications are working correctly!\"");
+        }
+        NotificationsMessage::Refreshed(new) => {
+            *state = new;
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_notifications_enable_default() {
+        assert!(parse_notifications_enable(""));
+        assert!(parse_notifications_enable("[layout]\ngap = 18\n"));
+    }
+
+    #[test]
+    fn test_parse_notifications_enable_explicit() {
+        let content = "\
+[notifications]
+enable = false
+";
+        assert!(!parse_notifications_enable(content));
+
+        let content = "\
+[notifications]
+enable = true
+";
+        assert!(parse_notifications_enable(content));
+    }
+
+    #[test]
+    fn test_parse_notifications_enable_other_sections() {
+        let content = "\
+[layout]
+enable = false
+
+[notifications]
+enable = true
+
+[input]
+enable = false
+";
+        assert!(parse_notifications_enable(content));
+
+        let content = "\
+[layout]
+enable = true
+
+[notifications]
+enable = false
+
+[input]
+enable = true
+";
+        assert!(!parse_notifications_enable(content));
+    }
+}