system settings
git clone https://git.lucas.co/cce-system-interface.git
Add Screensaver settings page and options tab to clear-system-interface
src/app.rs | 5 +
src/main.rs | 96 +++++++++++++-
src/pages/mod.rs | 7 +-
src/pages/screensaver.rs | 327 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 432 insertions(+), 3 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index f6c97b2..d17cdeb 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -14,6 +14,7 @@ use crate::pages::backup;
use crate::pages::typeface;
use crate::pages::services;
use crate::pages::colors;
+use crate::pages::screensaver;
use crate::pages::Page;
pub struct AppState {
@@ -32,6 +33,7 @@ pub struct AppState {
pub typeface: typeface::TypefaceState,
pub services: services::ServicesState,
pub colors: colors::ColorsState,
+ pub screensaver: screensaver::ScreensaverState,
}
impl Default for AppState {
@@ -52,6 +54,7 @@ impl Default for AppState {
typeface: typeface::TypefaceState::default(),
services: services::ServicesState::default(),
colors: colors::ColorsState::default(),
+ screensaver: screensaver::read_screensaver_config(),
}
}
}
@@ -72,8 +75,10 @@ pub enum AppAction {
Typeface(typeface::TypefaceMessage),
Services(services::ServicesMessage),
Colors(colors::ColorsMessage),
+ Screensaver(screensaver::ScreensaverMessage),
}
+
pub struct PageContent {
pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>)>,
diff --git a/src/main.rs b/src/main.rs
index 08a10cb..c2dd1b7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -202,6 +202,7 @@ struct SystemInterface {
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>,
+ rx_screensaver: std::sync::mpsc::Receiver<pages::screensaver::ScreensaverState>,
rx_backup_state: std::sync::mpsc::Receiver<pages::backup::BackupState>,
rx_typeface: std::sync::mpsc::Receiver<pages::typeface::TypefaceState>,
rx_services: std::sync::mpsc::Receiver<Vec<pages::services::ServiceInfo>>,
@@ -348,6 +349,17 @@ impl clear_ui::engine::Application for SystemInterface {
});
rx
};
+ let rx_screensaver = {
+ let (tx, rx) = std::sync::mpsc::channel::<pages::screensaver::ScreensaverState>();
+ tokio::spawn(async move {
+ loop {
+ let val = tokio::task::spawn_blocking(|| pages::screensaver::read_screensaver_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 rx_backup_state = spawn_bg(30, || pages::backup::fetch_backup_state());
let rx_typeface = spawn_bg(30, || pages::typeface::fetch_typeface_state());
let rx_services = spawn_bg(3, || pages::services::fetch_services());
@@ -400,6 +412,7 @@ impl clear_ui::engine::Application for SystemInterface {
rx_status,
rx_storage,
rx_notifications,
+ rx_screensaver,
rx_backup_state,
rx_typeface,
rx_services,
@@ -461,6 +474,7 @@ impl clear_ui::engine::Application for SystemInterface {
self.width = width as u32;
self.height = height as u32;
self.scale_factor = scale;
+ self.paginator.set_scale_factor(scale as f32);
self.rebuild_layout(width, height);
}
for w in &self.widgets {
@@ -473,12 +487,16 @@ impl clear_ui::engine::Application for SystemInterface {
}
fn clear_color(&self) -> [f32; 4] {
- [
+ let mut color = [
self.app.colors.page_low_color[0] as f32 / 255.0,
self.app.colors.page_low_color[1] as f32 / 255.0,
self.app.colors.page_low_color[2] as f32 / 255.0,
1.0,
- ]
+ ];
+ if let Some(opacity) = clear_ui::color::read_opacity_if_configured() {
+ color[3] = opacity;
+ }
+ color
}
fn handle_pointer_move(&mut self, pos: clear_ui::engine::LogicalPosition, needs_rebuild: &mut bool) {
@@ -722,6 +740,12 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
link_parent_child(&mut self.page_root_container, &mut self.app.notifications.duration_spinbox);
link_parent_child(&mut self.page_root_container, &mut self.app.notifications.opacity_slider);
}
+ Page::Screensaver => {
+ link_parent_child(&mut self.page_root_container, &mut self.app.screensaver.enable_toggle);
+ link_parent_child(&mut self.page_root_container, &mut self.app.screensaver.lock_screen_toggle);
+ link_parent_child(&mut self.page_root_container, &mut self.app.screensaver.timeout_spinbox);
+ link_parent_child(&mut self.page_root_container, &mut self.app.screensaver.style_menu);
+ }
Page::Input => {
self.page_sec_containers.resize_with(5, clear_ui::widget::Container::new);
link_parent_child(&mut self.page_root_container, &mut self.page_sec_containers[0]);
@@ -972,6 +996,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
Page::Storage => storage::view(&self.app.storage, cx, cy, cw, ch),
Page::Notifications => notifications::view(&mut self.app.notifications, cx, cy, cw, ch),
Page::Backup => backup::view(&self.app.backup, cx, cy, cw, ch),
+ Page::Screensaver => screensaver::view(&mut self.app.screensaver, cx, cy, cw, ch),
Page::Typefaces => typeface::view(&mut self.app.typeface, cx, cy, cw, ch, &sec_focused),
Page::Services => services::view(&mut self.app.services, cx, cy, cw, ch, root_focused),
Page::Colors => colors::view(&mut self.app.colors, cx, cy, cw, ch),
@@ -1104,6 +1129,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
notifications::update(&mut self.app.notifications, notifications::NotificationsMessage::Refreshed(s));
self.needs_rebuild = true;
}
+ while let Ok(s) = self.rx_screensaver.try_recv() {
+ screensaver::update(&mut self.app.screensaver, pages::screensaver::ScreensaverMessage::Refreshed(s));
+ self.needs_rebuild = true;
+ }
while let Ok(s) = self.rx_backup_state.try_recv() {
pages::backup::update(&mut self.app.backup, pages::backup::BackupMessage::Refreshed(s));
self.needs_rebuild = true;
@@ -1161,6 +1190,7 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
AppAction::Services(m) => services::update(&mut self.app.services, m.clone()),
AppAction::Colors(m) => colors::update(&mut self.app.colors, m.clone()),
+ AppAction::Screensaver(m) => screensaver::update(&mut self.app.screensaver, m.clone()),
AppAction::Backup(m) => match m {
pages::backup::BackupMessage::StartBackup => {
pages::backup::update(&mut self.app.backup, pages::backup::BackupMessage::StartBackup);
@@ -1332,6 +1362,20 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
changed = true;
}
}
+ if self.app.current_page == Page::Screensaver {
+ if self.app.screensaver.enable_toggle.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.screensaver.lock_screen_toggle.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.screensaver.timeout_spinbox.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ if self.app.screensaver.style_menu.cursor_moved(lx, ly) {
+ changed = true;
+ }
+ }
if self.app.current_page == Page::Hardware {
if self.app.hardware.cpu_label.cursor_moved(lx, ly) {
changed = true;
@@ -1514,6 +1558,10 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
if self.app.notifications.duration_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
if self.app.notifications.opacity_slider.hit_test(lx, ly) { clicked_any_focusable = true; }
}
+ Page::Screensaver => {
+ if self.app.screensaver.timeout_spinbox.hit_test(lx, ly) { clicked_any_focusable = true; }
+ if self.app.screensaver.style_menu.hit_test(lx, ly) { clicked_any_focusable = true; }
+ }
Page::Audio => {
for sb in &mut self.app.audio.sink_spinboxes {
if sb.hit_test(lx, ly) { clicked_any_focusable = true; }
@@ -1825,6 +1873,37 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
}
}
}
+ if self.app.current_page == Page::Screensaver {
+ let toggle = &mut self.app.screensaver.enable_toggle;
+ toggle.mouse_input(button, state, lx, ly);
+ if toggle.take_click() {
+ actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::ToggleEnable));
+ }
+
+ let toggle = &mut self.app.screensaver.lock_screen_toggle;
+ toggle.mouse_input(button, state, lx, ly);
+ if toggle.take_click() {
+ actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::ToggleLockScreen));
+ }
+
+ if state == clear_ui::widget::ElementState::Pressed {
+ let sb = &mut self.app.screensaver.timeout_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(button, state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::SetTimeout(sb.value)));
+ }
+ }
+
+ let menu = &mut self.app.screensaver.style_menu;
+ if state == clear_ui::widget::ElementState::Pressed && !menu.hit_test(lx, ly) { menu.unfocus(); }
+ if menu.mouse_input(button, state, lx, ly) {
+ self.needs_rebuild = true;
+ }
+ if state == clear_ui::widget::ElementState::Pressed && menu.take_change() {
+ actions.push(AppAction::Screensaver(pages::screensaver::ScreensaverMessage::SetStyle(menu.selected)));
+ }
+ }
if state == clear_ui::widget::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(); }
@@ -2353,6 +2432,19 @@ fn collect_popover_rects(w: &dyn clear_ui::widget::Widget, popovers: &mut Vec<(f
return true;
}
}
+ if self.app.current_page == Page::Screensaver {
+ let sb = &mut self.app.screensaver.timeout_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ drop(sb);
+ if new_val != old {
+ self.handle_action(&AppAction::Screensaver(pages::screensaver::ScreensaverMessage::SetTimeout(new_val)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
if self.app.current_page == Page::Notifications {
let sb = &mut self.app.notifications.duration_spinbox;
let old = sb.value;
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index c1c82e4..5921d4c 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -13,6 +13,7 @@ pub mod backup;
pub mod typeface;
pub mod services;
pub mod colors;
+pub mod screensaver;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Page {
@@ -30,10 +31,11 @@ pub enum Page {
Backup,
Typefaces,
Colors,
+ Screensaver,
}
impl Page {
- pub const ALL: [Page; 14] = [
+ pub const ALL: [Page; 15] = [
Page::Audio,
Page::Backup,
Page::Colors,
@@ -48,6 +50,7 @@ impl Page {
Page::Storage,
Page::System,
Page::Typefaces,
+ Page::Screensaver,
];
pub fn label(self) -> &'static str {
@@ -66,6 +69,7 @@ impl Page {
Page::Backup => "Backup",
Page::Typefaces => "Typefaces",
Page::Colors => "Colors",
+ Page::Screensaver => "Screensaver",
}
}
@@ -73,3 +77,4 @@ impl Page {
""
}
}
+
diff --git a/src/pages/screensaver.rs b/src/pages/screensaver.rs
new file mode 100644
index 0000000..6f14b46
--- /dev/null
+++ b/src/pages/screensaver.rs
@@ -0,0 +1,327 @@
+use std::fs;
+use crate::app::{AppAction, PageContent};
+use clear_ui::layout::Section;
+use clear_ui::widget::{Toggle, Spinbox, Dropdown};
+
+const CONFIG_PATH: &str = "/home/lsgalante/.config/ccec/config.toml";
+
+#[derive(Debug, Clone)]
+pub struct ScreensaverState {
+ pub enable: bool,
+ pub enable_toggle: Toggle,
+ pub timeout: i32,
+ pub timeout_spinbox: Spinbox,
+ pub lock_screen: bool,
+ pub lock_screen_toggle: Toggle,
+ pub style: String,
+ pub style_menu: Dropdown,
+}
+
+impl Default for ScreensaverState {
+ fn default() -> Self {
+ Self {
+ enable: true,
+ enable_toggle: Toggle::new().with_label("Enable Screensaver"),
+ timeout: 10,
+ timeout_spinbox: Spinbox::new(10, 1, 120, 1)
+ .with_label("Screensaver Timeout")
+ .with_unit("m"),
+ lock_screen: true,
+ lock_screen_toggle: Toggle::new().with_label("Lock Screen on Activation"),
+ style: "starfield".to_string(),
+ style_menu: Dropdown::new(
+ vec!["Blank".to_string(), "Starfield".to_string(), "Matrix Rain".to_string()],
+ 1,
+ ).with_label("Screensaver Style"),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum ScreensaverMessage {
+ ToggleEnable,
+ ToggleLockScreen,
+ SetTimeout(i32),
+ SetStyle(usize),
+ StartPreview,
+ Refreshed(ScreensaverState),
+}
+
+pub fn read_screensaver_config() -> ScreensaverState {
+ let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let enable = parse_screensaver_enable(&content);
+ let timeout = parse_screensaver_timeout(&content);
+ let lock_screen = parse_screensaver_lock_screen(&content);
+ let style = parse_screensaver_style(&content);
+
+ let style_idx = match style.to_lowercase().as_str() {
+ "blank" => 0,
+ "starfield" => 1,
+ "matrix" => 2,
+ _ => 1, // default to Starfield
+ };
+
+ ScreensaverState {
+ enable,
+ enable_toggle: Toggle::new().with_label("Enable Screensaver"),
+ timeout,
+ timeout_spinbox: Spinbox::new(timeout, 1, 120, 1)
+ .with_label("Screensaver Timeout")
+ .with_unit("m"),
+ lock_screen,
+ lock_screen_toggle: Toggle::new().with_label("Lock Screen on Activation"),
+ style: style.clone(),
+ style_menu: Dropdown::new(
+ vec!["Blank".to_string(), "Starfield".to_string(), "Matrix Rain".to_string()],
+ style_idx,
+ ).with_label("Screensaver Style"),
+ }
+}
+
+fn parse_screensaver_enable(content: &str) -> bool {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ 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 parse_screensaver_lock_screen(content: &str) -> bool {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("lock_screen") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ return val.trim() == "true";
+ }
+ }
+ }
+ true // default to true
+}
+
+fn parse_screensaver_timeout(content: &str) -> i32 {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("timeout") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ if let Ok(t) = val.trim().parse::<i32>() {
+ return t;
+ }
+ }
+ }
+ }
+ 10 // default to 10 minutes
+}
+
+fn parse_screensaver_style(content: &str) -> String {
+ let mut in_section = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[screensaver]" {
+ in_section = true;
+ continue;
+ }
+ if trimmed.starts_with('[') && in_section {
+ break;
+ }
+ if in_section && trimmed.starts_with("style") {
+ if let Some(val) = trimmed.split('=').nth(1) {
+ return val.trim().trim_matches('"').to_string();
+ }
+ }
+ }
+ "starfield".to_string() // default to starfield
+}
+
+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 == "[screensaver]" {
+ 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() == "[screensaver]");
+ if has_section {
+ let mut in_section = false;
+ let mut inserted = false;
+ for line in updated.lines() {
+ if line.trim() == "[screensaver]" {
+ 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[screensaver]\n");
+ updated.push_str(&new_line);
+ updated.push_str("\n");
+ }
+ }
+ let _ = fs::write(CONFIG_PATH, updated);
+}
+
+const BTN_BG: [f32; 4] = [0.20, 0.40, 0.65, 1.0];
+const BTN_HOVER: [f32; 4] = [0.28, 0.50, 0.78, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
+
+pub fn view(state: &mut ScreensaverState, 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, "Screensaver Settings");
+
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+
+ state.enable_toggle.set_toggled(state.enable);
+ sec.widget(&mut pc, &mut state.enable_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(8.0);
+
+ state.lock_screen_toggle.set_toggled(state.lock_screen);
+ sec.widget(&mut pc, &mut state.lock_screen_toggle, 14.0, toggle_w, toggle_h);
+ sec.spacing(16.0);
+
+ state.timeout_spinbox.value = state.timeout;
+ sec.widget(&mut pc, &mut state.timeout_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(16.0);
+
+ sec.widget(&mut pc, &mut state.style_menu, 14.0, 200.0, 26.0);
+ sec.spacing(24.0);
+
+ let btn_w = 160.0;
+ let btn_h = 32.0;
+ let btn_y = sec.ay();
+ sec.row(1, 0.0, btn_h, |_, x, _| {
+ pc.button(
+ "Preview Screensaver",
+ x,
+ btn_y,
+ btn_w,
+ btn_h,
+ BTN_BG,
+ BTN_HOVER,
+ WHITE,
+ AppAction::Screensaver(ScreensaverMessage::StartPreview),
+ );
+ });
+ sec.spacing(12.0);
+ sec.finish(&mut pc);
+
+ pc
+}
+
+pub fn update(state: &mut ScreensaverState, msg: ScreensaverMessage) {
+ match msg {
+ ScreensaverMessage::ToggleEnable => {
+ state.enable = !state.enable;
+ write_config_value("enable", &state.enable.to_string());
+ }
+ ScreensaverMessage::ToggleLockScreen => {
+ state.lock_screen = !state.lock_screen;
+ write_config_value("lock_screen", &state.lock_screen.to_string());
+ }
+ ScreensaverMessage::SetTimeout(t) => {
+ state.timeout = t;
+ write_config_value("timeout", &state.timeout.to_string());
+ }
+ ScreensaverMessage::SetStyle(idx) => {
+ state.style_menu.selected = idx;
+ let val = match idx {
+ 0 => "blank",
+ 1 => "starfield",
+ 2 => "matrix",
+ _ => "starfield",
+ };
+ state.style = val.to_string();
+ write_config_value("style", &format!("\"{}\"", val));
+ }
+ ScreensaverMessage::StartPreview => {
+ let style_flag = match state.style_menu.selected {
+ 0 => "blank",
+ 1 => "starfield",
+ 2 => "matrix",
+ _ => "starfield",
+ };
+
+ // Spawn screensaver tool from PATH or local directory
+ std::process::Command::new("/home/lsgalante/Dropbox/Clear/cce-screenaver/target/debug/cce-screenaver")
+ .arg(style_flag)
+ .spawn()
+ .or_else(|_| {
+ std::process::Command::new("cce-screenaver")
+ .arg(style_flag)
+ .spawn()
+ })
+ .ok();
+ }
+ ScreensaverMessage::Refreshed(new) => {
+ *state = new;
+ }
+ }
+}