system settings
git clone https://git.lucas.co/cce-system-interface.git
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));
+ }
+}