system settings
git clone https://git.lucas.co/cce-system-interface.git
Move CPU, GPU, governor, battery, notifications, and status interface to System page
src/input_handler.rs | 36 +-
src/main.rs | 12 +-
src/pages/processes.rs | 1046 +-------------------------------------------
src/pages/system_info.rs | 1094 +++++++++++++++++++++++++++++++++++++++++++++-
src/renderer.rs | 47 +-
src/watchers.rs | 12 +-
6 files changed, 1159 insertions(+), 1088 deletions(-)
diff --git a/src/input_handler.rs b/src/input_handler.rs
index a038d4a..2612e5e 100644
--- a/src/input_handler.rs
+++ b/src/input_handler.rs
@@ -531,21 +531,39 @@ impl SystemInterface {
actions.push(AppAction::Display(pages::display::DisplayMessage::SetScreensaverTimeout(self.app.display.screensaver_timeout_spinbox.value)));
}
}
- Page::Processes => {
- if self.app.processes.cpu_gov_menu.take_change() {
- if self.app.processes.cpu_gov_menu.selected == 0 {
- actions.push(AppAction::Processes(pages::processes::ProcessesMessage::SetCpuPerformance));
+ Page::System => {
+ if self.app.system_info.cpu_gov_menu.take_change() {
+ if self.app.system_info.cpu_gov_menu.selected == 0 {
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::SetCpuPerformance));
} else {
- actions.push(AppAction::Processes(pages::processes::ProcessesMessage::SetCpuPowersave));
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::SetCpuPowersave));
}
}
- if self.app.processes.gpu_gov_menu.take_change() {
- if self.app.processes.gpu_gov_menu.selected == 0 {
- actions.push(AppAction::Processes(pages::processes::ProcessesMessage::SetGpuDefault));
+ if self.app.system_info.gpu_gov_menu.take_change() {
+ if self.app.system_info.gpu_gov_menu.selected == 0 {
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::SetGpuDefault));
} else {
- actions.push(AppAction::Processes(pages::processes::ProcessesMessage::SetGpuPowersave));
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::SetGpuPowersave));
}
}
+ if self.app.system_info.notifications_enable_toggle.take_change() {
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::ToggleNotificationsEnable));
+ }
+ if self.app.system_info.notifications_bell_toggle.take_change() {
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::ToggleNotificationsBell));
+ }
+ if self.app.system_info.notifications_duration_spinbox.take_change() {
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::SetNotificationsDuration(self.app.system_info.notifications_duration_spinbox.value)));
+ }
+ if self.app.system_info.status_separators_toggle.take_change() {
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::StatusToggleSeparators));
+ }
+ if self.app.system_info.status_underline_toggle.take_change() {
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::StatusToggleUnderline));
+ }
+ if self.app.system_info.status_padding_spinbox.take_change() {
+ actions.push(AppAction::SystemInfo(pages::system_info::SystemMessage::StatusSetPadding(self.app.system_info.status_padding_spinbox.value as u16)));
+ }
}
Page::Accounts => {
if self.app.accounts.adding_new && self.app.accounts.email_box.take_change() {
diff --git a/src/main.rs b/src/main.rs
index f3b9e2d..f8f502d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -140,9 +140,9 @@ struct SystemInterface {
rx_fingers: std::sync::mpsc::Receiver<Vec<Finger>>,
rx_processes: std::sync::mpsc::Receiver<pages::processes::ProcessesState>,
rx_system: std::sync::mpsc::Receiver<pages::system_info::SystemState>,
- rx_status: std::sync::mpsc::Receiver<pages::processes::StatusData>,
+ rx_status: std::sync::mpsc::Receiver<pages::system_info::StatusData>,
rx_storage: std::sync::mpsc::Receiver<pages::storage::StorageState>,
- rx_notifications: std::sync::mpsc::Receiver<pages::processes::NotificationsConfig>,
+ rx_notifications: std::sync::mpsc::Receiver<pages::system_info::NotificationsConfig>,
rx_typeface: std::sync::mpsc::Receiver<pages::interface::InterfaceState>,
rx_services: std::sync::mpsc::Receiver<Vec<pages::processes::ServiceInfo>>,
rx_interface: std::sync::mpsc::Receiver<pages::interface::InterfaceState>,
@@ -619,8 +619,8 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
}
}
while let Ok(s) = self.rx_status.try_recv() {
- processes::update(&mut self.app.processes, processes::ProcessesMessage::StatusRefreshed(s));
- if self.app.current_page == Page::Processes {
+ system_info::update(&mut self.app.system_info, system_info::SystemMessage::StatusRefreshed(s));
+ if self.app.current_page == Page::System {
self.needs_rebuild = true;
}
}
@@ -631,8 +631,8 @@ fn collect_popover_rects(w: &dyn cce_ui::widget::Element, popovers: &mut Vec<(f3
}
}
while let Ok(s) = self.rx_notifications.try_recv() {
- processes::update(&mut self.app.processes, processes::ProcessesMessage::NotificationsRefreshed(s));
- if self.app.current_page == Page::Processes {
+ system_info::update(&mut self.app.system_info, system_info::SystemMessage::NotificationsRefreshed(s));
+ if self.app.current_page == Page::System {
self.needs_rebuild = true;
}
}
diff --git a/src/pages/processes.rs b/src/pages/processes.rs
index 248d17e..967dd4b 100644
--- a/src/pages/processes.rs
+++ b/src/pages/processes.rs
@@ -1,38 +1,6 @@
-use crate::app::{AppAction, PageContent, SectionContextExt};
+use crate::app::{AppAction, PageContent};
use cce_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy, RenderTarget};
-use cce_ui::widget::{Label, ScrollingList, Dropdown, InfoBox, TextBox, StatusDot, DotStatus, InteractiveListItem, Toggle, Spinbox, Element};
-use crate::pages::interface::parse_u16_from;
-use std::io::Write;
-use std::fs;
-
-#[derive(Debug, Clone, Default)]
-pub struct BatteryInfo {
- pub percentage: f32,
- pub state: String,
- pub energy: f64,
- pub energy_full: f64,
- pub energy_rate: f64,
- pub time_to_empty: i64,
- pub time_to_full: i64,
- pub vendor: String,
- pub model: String,
-}
-
-#[derive(Debug, Clone)]
-pub struct NotificationsConfig {
- pub enable: bool,
- pub bell: bool,
- pub duration: i32,
-}
-
-#[derive(Debug, Clone)]
-pub struct StatusData {
- pub font_size: u16,
- pub padding: u16,
- pub separators: bool,
- pub underline: bool,
- pub running: bool,
-}
+use cce_ui::widget::{ScrollingList, TextBox, StatusDot, DotStatus, InteractiveListItem, Element};
#[derive(Debug, Clone)]
pub struct ServiceInfo {
@@ -57,26 +25,10 @@ impl Default for ServiceTab {
#[derive(Debug, Clone)]
pub struct ProcessesState {
- pub cpu_model: String,
- pub cpu_usage: f32,
- pub cpu_cores: u32,
- pub gpus: Vec<String>,
pub loaded: bool,
- pub cpu_label: Label,
- pub cpu_usage_label: Label,
- pub cpu_temp_label: Label,
- pub gpu_labels: Vec<Label>,
pub processes: Vec<(String, String, String)>, // (pid, cpu, comm)
pub cpu_list_box: ScrollingList,
- // Power-related fields
- pub battery: BatteryInfo,
- pub on_ac: bool,
- pub cpu_powersave: bool,
- pub gpu_powersave: bool,
- pub cpu_gov_menu: Dropdown,
- pub gpu_gov_menu: Dropdown,
-
// Services-related fields
pub services_loaded: bool,
pub services: Vec<ServiceInfo>,
@@ -84,81 +36,21 @@ pub struct ProcessesState {
pub services_search_box: TextBox,
pub services_list_box: ScrollingList,
pub service_items: Vec<InteractiveListItem>,
- pub notifications_loaded: bool,
- pub notifications_enable: bool,
- pub notifications_enable_toggle: Toggle,
- pub notifications_bell: bool,
- pub notifications_bell_toggle: Toggle,
- pub notifications_duration: i32,
- pub notifications_duration_spinbox: Spinbox,
-
- // Status Interface fields
- pub status_loaded: bool,
- pub status_font_size: u16,
- pub status_padding: u16,
- pub status_separators: bool,
- pub status_underline: bool,
- pub status_running: bool,
- pub status_label: Label,
- pub status_separators_toggle: Toggle,
- pub status_underline_toggle: Toggle,
- pub status_padding_spinbox: Spinbox,
}
impl Default for ProcessesState {
fn default() -> Self {
Self {
- cpu_model: String::new(),
- cpu_usage: 0.0,
- cpu_cores: 0,
- gpus: Vec::new(),
loaded: false,
- cpu_label: Label::new("CPU Info"),
- cpu_usage_label: Label::new("CPU Usage"),
- cpu_temp_label: Label::new("CPU Temp"),
- gpu_labels: Vec::new(),
processes: Vec::new(),
cpu_list_box: ScrollingList::new(24.0, 2.0),
- battery: BatteryInfo::default(),
- on_ac: true,
- cpu_powersave: false,
- gpu_powersave: false,
- cpu_gov_menu: Dropdown::new(
- vec!["Performance".to_string(), "Powersave".to_string()],
- 0,
- ).with_label("CPU Governor"),
- gpu_gov_menu: Dropdown::new(
- vec!["Default (80W)".to_string(), "Eco Cap (5W)".to_string()],
- 0,
- ).with_label("GPU Power Limit"),
-
services_loaded: false,
services: Vec::new(),
services_active_tab: ServiceTab::System,
services_search_box: TextBox::new(String::new()).with_label("Filter Services"),
services_list_box: ScrollingList::new(36.0, 6.0),
service_items: Vec::new(),
- notifications_loaded: false,
- notifications_enable: true,
- notifications_enable_toggle: Toggle::new().with_label("Enable Notifications"),
- notifications_bell: false,
- notifications_bell_toggle: Toggle::new().with_label("Play Bell Sound"),
- notifications_duration: 5,
- notifications_duration_spinbox: Spinbox::new(5, 1, 60, 1)
- .with_label("Notification Duration")
- .with_unit("s"),
-
- status_loaded: false,
- status_font_size: 11,
- status_padding: 8,
- status_separators: true,
- status_underline: true,
- status_running: false,
- status_label: Label::new("Status Interface: Stopped").with_font_size(14.0).with_color([170, 51, 51]),
- status_separators_toggle: Toggle::new().with_label("Show Separators"),
- status_underline_toggle: Toggle::new().with_label("Show Underline"),
- status_padding_spinbox: Spinbox::new(8, 0, 32, 1).with_label("Side Padding").with_unit("px"),
}
}
}
@@ -166,10 +58,6 @@ impl Default for ProcessesState {
#[derive(Debug, Clone)]
pub enum ProcessesMessage {
Refreshed(ProcessesState),
- SetCpuPerformance,
- SetCpuPowersave,
- SetGpuDefault,
- SetGpuPowersave,
None,
// Services-related variants
@@ -178,243 +66,9 @@ pub enum ProcessesMessage {
ServicesStart(String, bool),
ServicesStop(String, bool),
ServicesRestart(String, bool),
- ToggleNotificationsEnable,
- ToggleNotificationsBell,
- SetNotificationsDuration(i32),
- SendTestNotification,
- NotificationsRefreshed(NotificationsConfig),
-
- // Status Interface variants
- StatusRefreshed(StatusData),
- StatusToggleSeparators,
- StatusToggleUnderline,
- StatusReload,
- StatusSetPadding(u16),
-}
-
-// ── zbus proxies ────────────────────────────────────────────────────
-
-#[zbus::proxy(
- interface = "org.freedesktop.UPower.Device",
- default_service = "org.freedesktop.UPower",
- default_path = "/org/freedesktop/UPower/devices/battery_BAT0"
-)]
-trait UpowerBattery {
- #[zbus(property)]
- fn percentage(&self) -> zbus::Result<f64>;
- #[zbus(property)]
- fn state(&self) -> zbus::Result<u32>;
- #[zbus(property)]
- fn energy(&self) -> zbus::Result<f64>;
- #[zbus(property)]
- fn energy_full(&self) -> zbus::Result<f64>;
- #[zbus(property)]
- fn energy_rate(&self) -> zbus::Result<f64>;
- #[zbus(property)]
- fn time_to_empty(&self) -> zbus::Result<i64>;
- #[zbus(property)]
- fn time_to_full(&self) -> zbus::Result<i64>;
- #[zbus(property)]
- fn vendor(&self) -> zbus::Result<String>;
- #[zbus(property)]
- fn model(&self) -> zbus::Result<String>;
-}
-
-#[zbus::proxy(
- interface = "org.freedesktop.UPower",
- default_service = "org.freedesktop.UPower",
- default_path = "/org/freedesktop/UPower"
-)]
-trait UpowerDaemon {
- #[zbus(property, name = "OnBattery")]
- fn on_battery(&self) -> zbus::Result<bool>;
-}
-
-// ── Helpers ─────────────────────────────────────────────────────────
-
-fn format_duration(secs: i64) -> String {
- let h = secs / 3600;
- let m = (secs % 3600) / 60;
- if h > 0 { format!("{}h {}m", h, m) } else { format!("{}m", m) }
-}
-
-fn spawn_cpu_power(powersave: bool) {
- let script = if powersave { "cpu-powersave-on" } else { "cpu-powersave-off" };
- let _ = tokio::process::Command::new("pkexec")
- .arg(format!("/home/lsgalante/.local/share/cce-system-interface/helpers/{}", script))
- .spawn();
-}
-
-fn spawn_gpu_power(powersave: bool) {
- let script = if powersave { "gpu-powersave-on" } else { "gpu-powersave-off" };
- let _ = tokio::process::Command::new("pkexec")
- .arg(format!("/home/lsgalante/.local/share/cce-system-interface/helpers/{}", script))
- .spawn();
-}
-
-fn current_cpu_governor() -> String {
- std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
- .unwrap_or_default().trim().to_string()
-}
-
-async fn current_gpu_power_cap() -> bool {
- tokio::process::Command::new("nvidia-smi")
- .args(["--query-gpu=power.limit", "--format=csv,noheader,nounits"])
- .output().await.ok()
- .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse::<f32>().ok())
- .map(|w| w <= 10.0).unwrap_or(false)
-}
-
-async fn fetch_upower() -> (BatteryInfo, bool) {
- let conn = match zbus::Connection::system().await {
- Ok(c) => c,
- Err(_) => return (BatteryInfo::default(), true),
- };
-
- let battery = match UpowerBatteryProxy::new(&conn).await {
- Ok(proxy) => BatteryInfo {
- percentage: proxy.percentage().await.unwrap_or(0.0) as f32,
- state: {
- let s = proxy.state().await.unwrap_or(0);
- match s { 1 => "charging", 2 => "discharging", 4 => "fully-charged", _ => "unknown" }.into()
- },
- energy: proxy.energy().await.unwrap_or(0.0),
- energy_full: proxy.energy_full().await.unwrap_or(0.0),
- energy_rate: proxy.energy_rate().await.unwrap_or(0.0),
- time_to_empty: proxy.time_to_empty().await.unwrap_or(0),
- time_to_full: proxy.time_to_full().await.unwrap_or(0),
- vendor: proxy.vendor().await.unwrap_or_default(),
- model: proxy.model().await.unwrap_or_default(),
- },
- Err(_) => BatteryInfo::default(),
- };
-
- let on_ac = match UpowerDaemonProxy::new(&conn).await {
- Ok(proxy) => !proxy.on_battery().await.unwrap_or(false),
- Err(_) => true,
- };
-
- (battery, on_ac)
-}
-
-fn read_cpu_temp() -> Option<f32> {
- if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") {
- for entry in entries.filter_map(|e| e.ok()) {
- let path = entry.path();
- if let Ok(name) = std::fs::read_to_string(path.join("name")) {
- let name = name.trim();
- if name == "thinkpad" {
- if let Ok(val) = std::fs::read_to_string(path.join("temp1_input")) {
- if let Ok(temp_milli) = val.trim().parse::<f32>() {
- return Some(temp_milli / 1000.0);
- }
- }
- } else if name == "coretemp" {
- if let Ok(val) = std::fs::read_to_string(path.join("temp1_input")) {
- if let Ok(temp_milli) = val.trim().parse::<f32>() {
- return Some(temp_milli / 1000.0);
- }
- }
- }
- }
- }
- }
- None
-}
-
-fn read_thinkpad_gpu_temp() -> Option<f32> {
- if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") {
- for entry in entries.filter_map(|e| e.ok()) {
- let path = entry.path();
- if let Ok(name) = std::fs::read_to_string(path.join("name")) {
- if name.trim() == "thinkpad" {
- for i in 1..=8 {
- let label_path = path.join(format!("temp{}_label", i));
- if let Ok(lbl) = std::fs::read_to_string(&label_path) {
- if lbl.trim() == "GPU" {
- if let Ok(val) = std::fs::read_to_string(path.join(format!("temp{}_input", i))) {
- if let Ok(temp_milli) = val.trim().parse::<f32>() {
- return Some(temp_milli / 1000.0);
- }
- }
- }
- }
- }
- }
- }
- }
- }
- None
-}
-
-async fn read_nvidia_gpu_temp() -> Option<f32> {
- let out = tokio::process::Command::new("nvidia-smi")
- .args(["--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"])
- .output().await.ok()?;
- let val_str = String::from_utf8_lossy(&out.stdout);
- val_str.trim().parse::<f32>().ok()
}
pub async fn fetch_processes_state() -> ProcessesState {
- static CPU_INFO: std::sync::OnceLock<(String, u32)> = std::sync::OnceLock::new();
- let (cpu_model, cpu_cores) = CPU_INFO.get_or_init(|| {
- let output = std::process::Command::new("lscpu")
- .output().ok()
- .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
- .unwrap_or_default();
- let model = output.lines()
- .find(|l| l.contains("Model name"))
- .and_then(|l| l.split(':').nth(1))
- .map(|s| s.trim().to_string())
- .unwrap_or_default();
- let cores = output.lines()
- .find(|l| l.contains("CPU(s)"))
- .and_then(|l| {
- let rest = l.split(':').nth(1).unwrap_or("").trim();
- rest.split_whitespace().next().and_then(|n| n.parse::<u32>().ok())
- })
- .unwrap_or(0);
- (model, cores)
- }).clone();
-
- let cpu_usage = {
- let read_stat = || -> Option<(u64, u64)> {
- let stat = std::fs::read_to_string("/proc/stat").ok()?;
- let first = stat.lines().next()?;
- let vals: Vec<u64> = first.split_whitespace().skip(1).filter_map(|v| v.parse().ok()).collect();
- if vals.len() < 3 { return None; }
- let total: u64 = vals.iter().sum();
- let idle = vals.get(3).copied().unwrap_or(0);
- Some((idle, total))
- };
- let (idle1, total1) = read_stat().unwrap_or((0, 1));
- tokio::time::sleep(std::time::Duration::from_millis(100)).await;
- let (idle2, total2) = read_stat().unwrap_or((0, 1));
- let d_idle = idle2.saturating_sub(idle1);
- let d_total = total2.saturating_sub(total1);
- if d_total > 0 {
- (1.0 - d_idle as f64 / d_total as f64) * 100.0
- } else { 0.0 }
- } as f32;
-
- static GPUS_INFO: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
- let gpus = GPUS_INFO.get_or_init(|| {
- let mut list = Vec::new();
- if let Some(o) = std::process::Command::new("lspci").output().ok() {
- for line in String::from_utf8_lossy(&o.stdout).lines() {
- if line.contains("VGA") || line.contains("3D") {
- if let Some(name) = line.split(':').nth(2) {
- let trimmed = name.trim().to_string();
- if !trimmed.is_empty() {
- list.push(trimmed);
- }
- }
- }
- }
- }
- list
- }).clone();
-
let processes = {
let mut list = Vec::new();
if let Some(o) = tokio::process::Command::new("ps")
@@ -435,116 +89,29 @@ pub async fn fetch_processes_state() -> ProcessesState {
list
};
- let cpu_temp = read_cpu_temp();
- let tp_gpu_temp = read_thinkpad_gpu_temp();
- let nv_gpu_temp = read_nvidia_gpu_temp().await;
-
- let cpu_label_text = format!("CPU {} ({} cores)", cpu_model, cpu_cores);
- let cpu_usage_text = format!("Usage {:.0}%", cpu_usage);
- let cpu_temp_text = cpu_temp.map(|t| format!("Temp {:.0}°C", t)).unwrap_or_else(|| "Temp N/A".to_string());
-
- let gpu_labels = gpus.iter().map(|gpu_name| {
- let temp = if gpu_name.to_lowercase().contains("nvidia") {
- nv_gpu_temp.or(tp_gpu_temp)
- } else {
- tp_gpu_temp
- };
- let temp_str = temp.map(|t| format!(" — {:.0}°C", t)).unwrap_or_default();
- let text = format!("GPU {}{}", gpu_name, temp_str);
- Label::new(&text).with_font_size(12.0).with_color([212, 212, 212])
- }).collect();
-
- let (battery, on_ac) = fetch_upower().await;
- let cpu_powersave = current_cpu_governor() == "powersave";
- let gpu_powersave = current_gpu_power_cap().await;
-
ProcessesState {
- cpu_model,
- cpu_usage,
- cpu_cores,
- gpus,
loaded: true,
- cpu_label: Label::new(&cpu_label_text).with_font_size(12.0).with_color([212, 212, 212]),
- cpu_usage_label: Label::new(&cpu_usage_text).with_font_size(12.0).with_color([212, 212, 212]),
- cpu_temp_label: Label::new(&cpu_temp_text).with_font_size(12.0).with_color([212, 212, 212]),
- gpu_labels,
processes,
cpu_list_box: ScrollingList::new(24.0, 2.0),
- battery,
- on_ac,
- cpu_powersave,
- gpu_powersave,
- cpu_gov_menu: Dropdown::new(
- vec!["Performance".to_string(), "Powersave".to_string()],
- if cpu_powersave { 1 } else { 0 },
- ).with_label("CPU Governor"),
- gpu_gov_menu: Dropdown::new(
- vec!["Default (80W)".to_string(), "Eco Cap (5W)".to_string()],
- if gpu_powersave { 1 } else { 0 },
- ).with_label("GPU Power Limit"),
-
services_loaded: false,
services: Vec::new(),
services_active_tab: ServiceTab::System,
services_search_box: TextBox::new(String::new()).with_label("Filter Services"),
services_list_box: ScrollingList::new(36.0, 6.0),
service_items: Vec::new(),
- notifications_loaded: false,
- notifications_enable: true,
- notifications_enable_toggle: Toggle::new().with_label("Enable Notifications"),
- notifications_bell: false,
- notifications_bell_toggle: Toggle::new().with_label("Play Bell Sound"),
- notifications_duration: 5,
- notifications_duration_spinbox: Spinbox::new(5, 1, 60, 1)
- .with_label("Notification Duration")
- .with_unit("s"),
-
- status_loaded: false,
- status_font_size: 11,
- status_padding: 8,
- status_separators: true,
- status_underline: true,
- status_running: false,
- status_label: Label::new("Status Interface: Stopped").with_font_size(14.0).with_color([170, 51, 51]),
- status_separators_toggle: Toggle::new().with_label("Show Separators"),
- status_underline_toggle: Toggle::new().with_label("Show Underline"),
- status_padding_spinbox: Spinbox::new(8, 0, 32, 1).with_label("Side Padding").with_unit("px"),
}
}
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
-const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
-const ORANGE: [f32; 4] = [1.0, 0.73, 0.20, 1.0];
pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root_focused: bool, sec_focused: &[bool], layout: &mut dyn LayoutStrategy, ctx: &mut cce_ui::context::UiContext) -> PageContent {
let mut final_pc = PageContent::new();
let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(9);
-
- // ── CPU Section ──
- builder.add_section(&mut final_pc, "CPU", false, |sec| {
- if !state.loaded {
- sec.text("Loading CPU model and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
- sec.spacing(10.0);
- } else {
- // CPU Info Label
- sec.widget(&mut state.cpu_label, 12.0, sec.cw - 24.0, 26.0, ctx);
- sec.spacing(12.0);
-
- // CPU Usage Label
- sec.widget(&mut state.cpu_usage_label, 12.0, sec.cw - 24.0, 26.0, ctx);
- sec.spacing(12.0);
-
- // CPU Temp Label
- sec.widget(&mut state.cpu_temp_label, 12.0, sec.cw - 24.0, 26.0, ctx);
- sec.spacing(12.0);
- }
- });
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
// ── Processes Section ──
- builder.add_section(&mut final_pc, "Processes", root_focused || sec_focused.get(1).copied().unwrap_or(false), |sec| {
+ builder.add_section(&mut final_pc, "Processes", root_focused || sec_focused.first().copied().unwrap_or(false), |sec| {
let rx = sec.left;
if !state.loaded {
sec.text("Loading processes...", 12.0, 0.0, 12.0, TEXT_FG);
@@ -604,139 +171,8 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
}
});
- // ── GPU Section ──
- builder.add_section(&mut final_pc, "GPU", false, |sec_gpu| {
- if !state.loaded {
- sec_gpu.text("Loading GPU models...", 12.0, 0.0, 12.0, TEXT_FG);
- sec_gpu.spacing(10.0);
- } else {
- for (i, gpu_lbl) in state.gpu_labels.iter_mut().enumerate() {
- if i > 0 { sec_gpu.spacing(12.0); }
- sec_gpu.widget(gpu_lbl, 12.0, sec_gpu.cw - 24.0, 26.0, ctx);
- }
- }
- });
-
- // ── Battery Section ──
- builder.add_section(&mut final_pc, "Battery", false, |sec_bat| {
- if !state.loaded {
- sec_bat.text("Loading battery status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_bat.spacing(18.0);
- } else {
- let bat = &state.battery;
- let bat_icon = match bat.state.as_str() {
- "charging" => "+",
- "fully-charged" => "=",
- _ => "",
- };
-
- let pct_color = if bat.percentage < 20.0 { RED }
- else if bat.percentage < 50.0 { ORANGE }
- else { ACCENT };
-
- let pct_str = format!("{} {:.0}%", bat_icon, bat.percentage);
- sec_bat.text(&pct_str, 12.0, 0.0, 24.0, pct_color);
- sec_bat.spacing(30.0);
-
- let state_str = format!("{} • {:.1}W • {:.1}/{:.1} Wh",
- bat.state, bat.energy_rate, bat.energy, bat.energy_full);
- sec_bat.text(&state_str, 12.0, 0.0, 12.0, TEXT_DIM);
- sec_bat.spacing(18.0);
-
- let time_str = if bat.time_to_empty > 0 {
- format!("Time remaining: {}", format_duration(bat.time_to_empty))
- } else if bat.time_to_full > 0 {
- format!("Time to full: {}", format_duration(bat.time_to_full))
- } else { String::new() };
- if !time_str.is_empty() {
- sec_bat.text(&time_str, 12.0, 0.0, 12.0, TEXT_DIM);
- sec_bat.spacing(18.0);
- }
-
- let detail_str = format!("{} {}", bat.vendor, bat.model);
- sec_bat.text(&detail_str, 12.0, 0.0, 11.0, TEXT_DIM);
- sec_bat.spacing(20.0);
-
- let ac_str = if state.on_ac { "On AC Power" } else { "On Battery" };
- sec_bat.text(ac_str, 12.0, 0.0, 14.0, TEXT_FG);
- sec_bat.spacing(20.0);
- }
- });
-
- // ── CPU Governor section ──
- builder.add_section(&mut final_pc, "CPU Governor", false, |sec_gov| {
- let rx = sec_gov.left;
- if !state.loaded {
- sec_gov.text("Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_gov.spacing(18.0);
- } else {
- sec_gov.widget(&mut state.cpu_gov_menu, 12.0, sec_gov.cw - 24.0, 26.0, ctx);
- sec_gov.spacing(12.0);
-
- let (info_title, info_lines) = if state.cpu_powersave {
- (
- "CPU Governor: Powersave",
- vec![
- "• Active: powersave".to_string(),
- "• Governor set to powersave — lower power, slower burst".to_string(),
- ],
- )
- } else {
- (
- "CPU Governor: Performance",
- vec![
- "• Active: performance".to_string(),
- "• Governor set to performance".to_string(),
- ],
- )
- };
-
- let mut info_box = InfoBox::new(info_title, info_lines);
- let info_h = 80.0;
- let info_y = sec_gov.ay();
- render_widget(sec_gov.pc, &mut info_box, rx + 12.0, info_y, sec_gov.cw - 24.0, info_h, ctx);
- sec_gov.spacing(info_h + 12.0);
- }
- });
-
- // ── GPU Power section ──
- builder.add_section(&mut final_pc, "GPU Power", false, |sec_gpow| {
- let rx = sec_gpow.left;
- if !state.loaded {
- sec_gpow.text("Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec_gpow.spacing(18.0);
- } else {
- sec_gpow.widget(&mut state.gpu_gov_menu, 12.0, sec_gpow.cw - 24.0, 26.0, ctx);
- sec_gpow.spacing(12.0);
-
- let (info_title, info_lines) = if state.gpu_powersave {
- (
- "GPU Power Limit: Eco Cap",
- vec![
- "• Mode: 5W Cap".to_string(),
- "• NVIDIA power limit capped at 5W — minimal draw".to_string(),
- ],
- )
- } else {
- (
- "GPU Power Limit: Default",
- vec![
- "• Mode: 80W Default".to_string(),
- "• NVIDIA running at default power limit".to_string(),
- ],
- )
- };
-
- let mut info_box = InfoBox::new(info_title, info_lines);
- let info_h = 80.0;
- let info_y = sec_gpow.ay();
- render_widget(sec_gpow.pc, &mut info_box, rx + 12.0, info_y, sec_gpow.cw - 24.0, info_h, ctx);
- sec_gpow.spacing(info_h + 12.0);
- }
- });
-
// ── Services Section ──
- builder.add_section_spanned(&mut final_pc, "Services", 2, sec_focused.get(6).copied().unwrap_or(false), |sec| {
+ builder.add_section_spanned(&mut final_pc, "Services", 2, sec_focused.get(1).copied().unwrap_or(false), |sec| {
let sec_w = sec.cw;
if !state.services_loaded {
sec.text("Loading systemd services...", 12.0, 0.0, 12.0, TEXT_DIM);
@@ -787,7 +223,7 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
let search_h = 46.0;
state.services_search_box.set_row_rect(sec.left + 12.0, search_w);
- cce_ui::layout::render_widget(
+ render_widget(
sec.pc,
&mut state.services_search_box,
sec.left + 12.0,
@@ -804,7 +240,7 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
let list_box_w = sec_w - 24.0;
let list_box_h = 360.0;
- cce_ui::layout::render_widget(sec.pc, &mut state.services_list_box, list_box_x, list_box_y, list_box_w, list_box_h, ctx);
+ render_widget(sec.pc, &mut state.services_list_box, list_box_x, list_box_y, list_box_w, list_box_h, ctx);
// Filter services
let query = if state.services_search_box.editing {
@@ -862,7 +298,7 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
let item_btn = &mut state.service_items[idx];
item_btn.title = service.name.clone();
item_btn.subtitle = Some(desc_truncated);
- cce_ui::layout::render_widget(sec.pc, item_btn, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h, ctx);
+ render_widget(sec.pc, item_btn, list_box_x + 24.0, draw_y, list_box_w - 44.0, item_h, ctx);
// Render StatusDot
let status_dot_state = if service.active_state == "failed" {
@@ -873,7 +309,7 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
DotStatus::Inactive
};
let mut dot = StatusDot::new(status_dot_state);
- cce_ui::layout::render_widget(sec.pc, &mut dot, list_box_x + 10.0, draw_y + (item_h - 10.0) / 2.0, 10.0, 10.0, ctx);
+ render_widget(sec.pc, &mut dot, list_box_x + 10.0, draw_y + (item_h - 10.0) / 2.0, 10.0, 10.0, ctx);
let active_txt = [0.90, 0.90, 0.95, 1.0];
let disabled_txt = [0.40, 0.40, 0.45, 1.0];
@@ -932,96 +368,6 @@ pub fn view(state: &mut ProcessesState, cx: f32, cy: f32, cw: f32, ch: f32, root
}
});
- // ── System Notifications ──
- builder.add_section(&mut final_pc, "System Notifications", sec_focused.get(7).copied().unwrap_or(false), |sec2| {
- let sec_w = sec2.cw;
- state.notifications_enable_toggle.set_toggled(state.notifications_enable);
- sec2.widget_full(&mut state.notifications_enable_toggle, cce_ui::layout::toggle_height(), ctx);
- sec2.spacing(8.0);
-
- state.notifications_bell_toggle.set_toggled(state.notifications_bell);
- sec2.widget_full(&mut state.notifications_bell_toggle, cce_ui::layout::toggle_height(), ctx);
- sec2.spacing(16.0);
-
- state.notifications_duration_spinbox.value = state.notifications_duration;
- state.notifications_duration_spinbox.set_label("Notification Duration");
- sec2.widget(&mut state.notifications_duration_spinbox, 14.0, sec_w - 28.0, 44.0, ctx);
- sec2.spacing(16.0);
-
- let btn_h = 32.0;
- let btn_y = sec2.ay();
- let white_color = [1.0, 1.0, 1.0, 1.0];
- let btn_bg = [0.20, 0.40, 0.65, 1.0];
- let btn_hover = [0.28, 0.50, 0.78, 1.0];
-
- let cols = sec2.row_layout(1, 0.0);
- if let Some(&(x, w)) = cols.first() {
- sec2.button(
- "Send Test Notification",
- x,
- btn_y,
- w,
- btn_h,
- btn_bg,
- btn_hover,
- white_color,
- AppAction::Processes(ProcessesMessage::SendTestNotification),
- );
- }
- sec2.spacing(12.0);
- });
-
- // ── Status Interface ──
- builder.add_section(&mut final_pc, "Status Interface", sec_focused.get(8).copied().unwrap_or(false), |sec3| {
- let sec_w = sec3.cw;
- if !state.status_loaded {
- sec3.text("Loading Status Interface status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec3.spacing(18.0);
- } else {
- // Status
- let status_text = if state.status_running { "Status Interface: Running" } else { "Status Interface: Stopped" };
- let status_color = if state.status_running { [92, 143, 97] } else { [170, 51, 51] };
- state.status_label.set_text(status_text);
- state.status_label.set_color(status_color);
- sec3.widget(&mut state.status_label, 12.0, sec_w - 24.0, 20.0, ctx);
- sec3.spacing(12.0);
-
- sec3.spacing(4.0);
-
- // Separators toggle
- state.status_separators_toggle.set_toggled(state.status_separators);
- sec3.widget_full(&mut state.status_separators_toggle, cce_ui::layout::toggle_height(), ctx);
- sec3.spacing(16.0);
-
- // Underline toggle
- state.status_underline_toggle.set_toggled(state.status_underline);
- sec3.widget_full(&mut state.status_underline_toggle, cce_ui::layout::toggle_height(), ctx);
- sec3.spacing(16.0);
-
- // Padding spinbox
- state.status_padding_spinbox.value = state.status_padding as i32;
- sec3.widget(&mut state.status_padding_spinbox, 12.0, sec_w - 24.0, 44.0, ctx);
- sec3.spacing(16.0);
-
- // Reload button
- let yt_reload = sec3.ay();
- let btn_w = sec_w - 24.0;
- let button_x = sec3.left + 12.0;
- sec3.button(
- "Reload Status Interface",
- button_x,
- yt_reload,
- btn_w,
- 32.0,
- [0.13, 0.18, 0.14, 1.0],
- [0.25, 0.30, 0.26, 1.0],
- [1.0, 1.0, 1.0, 1.0],
- AppAction::Processes(ProcessesMessage::StatusReload),
- );
- sec3.spacing(12.0);
- }
- });
-
final_pc
}
@@ -1029,45 +375,10 @@ pub fn update(state: &mut ProcessesState, msg: ProcessesMessage) {
match msg {
ProcessesMessage::Refreshed(new) => {
state.loaded = new.loaded;
- state.cpu_model = new.cpu_model;
- state.cpu_usage = new.cpu_usage;
- state.cpu_cores = new.cpu_cores;
- state.gpus = new.gpus;
- state.cpu_label = new.cpu_label;
- state.cpu_usage_label = new.cpu_usage_label;
- state.cpu_temp_label = new.cpu_temp_label;
- state.gpu_labels = new.gpu_labels;
state.processes = new.processes;
let old_scroll = state.cpu_list_box.scroll_y();
state.cpu_list_box = new.cpu_list_box;
state.cpu_list_box.set_scroll_y(old_scroll);
-
- state.battery = new.battery;
- state.on_ac = new.on_ac;
- state.cpu_powersave = new.cpu_powersave;
- state.gpu_powersave = new.gpu_powersave;
- state.cpu_gov_menu.selected = new.cpu_gov_menu.selected;
- state.gpu_gov_menu.selected = new.gpu_gov_menu.selected;
- }
- ProcessesMessage::SetCpuPerformance => {
- state.cpu_powersave = false;
- state.cpu_gov_menu.selected = 0;
- spawn_cpu_power(false);
- }
- ProcessesMessage::SetCpuPowersave => {
- state.cpu_powersave = true;
- state.cpu_gov_menu.selected = 1;
- spawn_cpu_power(true);
- }
- ProcessesMessage::SetGpuDefault => {
- state.gpu_powersave = false;
- state.gpu_gov_menu.selected = 0;
- spawn_gpu_power(false);
- }
- ProcessesMessage::SetGpuPowersave => {
- state.gpu_powersave = true;
- state.gpu_gov_menu.selected = 1;
- spawn_gpu_power(true);
}
ProcessesMessage::ServicesRefreshed(new_services) => {
state.services_loaded = true;
@@ -1100,80 +411,6 @@ pub fn update(state: &mut ProcessesState, msg: ProcessesMessage) {
}
service_action(&name, "restart", is_system);
}
- ProcessesMessage::ToggleNotificationsEnable => {
- state.notifications_enable = !state.notifications_enable;
- write_enable_notifications(state.notifications_enable);
- }
- ProcessesMessage::ToggleNotificationsBell => {
- state.notifications_bell = !state.notifications_bell;
- write_config_value("bell", &state.notifications_bell.to_string());
- }
- ProcessesMessage::SetNotificationsDuration(d) => {
- state.notifications_duration = d;
- write_config_value("duration", &state.notifications_duration.to_string());
- }
- ProcessesMessage::SendTestNotification => {
- tokio::spawn(async move {
- if let Ok(connection) = zbus::Connection::session().await {
- let _ = connection.call_method(
- Some("org.freedesktop.Notifications"),
- "/org/freedesktop/Notifications",
- Some("org.freedesktop.Notifications"),
- "Notify",
- &(
- "cce-client",
- 0u32,
- "",
- "System notifications are working correctly!",
- "",
- Vec::<&str>::new(),
- std::collections::HashMap::<&str, zbus::zvariant::Value>::new(),
- -1i32,
- )
- ).await;
- }
- });
- }
- ProcessesMessage::NotificationsRefreshed(new) => {
- state.notifications_loaded = true;
- state.notifications_enable = new.enable;
- state.notifications_bell = new.bell;
- state.notifications_duration = new.duration;
- }
- ProcessesMessage::StatusRefreshed(new) => {
- let was_status_hovered = state.status_label.hovered();
- let was_separators_hovered = state.status_separators_toggle.hovered();
- let was_underline_hovered = state.status_underline_toggle.hovered();
-
- state.status_loaded = true;
- state.status_font_size = new.font_size;
- state.status_padding = new.padding;
- state.status_separators = new.separators;
- state.status_underline = new.underline;
- state.status_running = new.running;
-
- state.status_label.set_hovered(was_status_hovered);
- state.status_separators_toggle.set_hovered(was_separators_hovered);
- state.status_underline_toggle.set_hovered(was_underline_hovered);
- }
- ProcessesMessage::StatusToggleSeparators => {
- state.status_separators = !state.status_separators;
- write_status_separators(state.status_separators);
- status_interface_reload();
- }
- ProcessesMessage::StatusToggleUnderline => {
- state.status_underline = !state.status_underline;
- write_status_underline(state.status_underline);
- status_interface_reload();
- }
- ProcessesMessage::StatusSetPadding(val) => {
- state.status_padding = val;
- write_status_padding(val);
- status_interface_reload();
- }
- ProcessesMessage::StatusReload => {
- status_interface_reload();
- }
ProcessesMessage::None => {}
}
}
@@ -1253,171 +490,6 @@ fn service_action(name: &str, action: &str, is_system: bool) {
}
}
-// ── Notifications Configuration Reader & Writer ──
-
-const CONFIG_PATH: &str = "/home/lsgalante/.config/cce/config.json";
-
-fn get_socket_path() -> String {
- match std::env::var("WAYLAND_DISPLAY") {
- Ok(display) => format!("/tmp/cce-{}.sock", display),
- Err(_) => "/tmp/cce.sock".to_string(),
- }
-}
-
-pub fn read_notifications_config() -> NotificationsConfig {
- let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
- let enable = parse_notifications_enable(&content);
- let bell = parse_notifications_bell(&content);
- let duration = parse_notifications_duration(&content);
- NotificationsConfig {
- enable,
- bell,
- duration,
- }
-}
-
-fn parse_json(content: &str) -> serde_json::Value {
- serde_json::from_str(content).unwrap_or_default()
-}
-
-fn parse_notifications_enable(content: &str) -> bool {
- let val = parse_json(content);
- val["notifications"]["enable"].as_bool().unwrap_or(true)
-}
-
-fn parse_notifications_bell(content: &str) -> bool {
- let val = parse_json(content);
- val["notifications"]["bell"].as_bool().unwrap_or(false)
-}
-
-fn parse_notifications_duration(content: &str) -> i32 {
- let val = parse_json(content);
- val["notifications"]["duration"].as_i64().map(|v| v as i32).unwrap_or(5)
-}
-
-fn send_ipc_command(cmd: &str) {
- if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(get_socket_path()) {
- 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 mut val = parse_json(&content);
- let j_val = if let Ok(b) = value.parse::<bool>() {
- serde_json::json!(b)
- } else if let Ok(n) = value.parse::<i64>() {
- serde_json::json!(n)
- } else if let Ok(f) = value.parse::<f64>() {
- serde_json::json!(f)
- } else {
- serde_json::json!(value)
- };
- if let Some(notifications) = val.get_mut("notifications").and_then(|n| n.as_object_mut()) {
- notifications.insert(key.to_string(), j_val);
- } else {
- let mut map = serde_json::Map::new();
- map.insert(key.to_string(), j_val);
- if let Some(obj) = val.as_object_mut() {
- obj.insert("notifications".to_string(), serde_json::Value::Object(map));
- }
- }
- if let Ok(updated_str) = serde_json::to_string_pretty(&val) {
- let _ = fs::write(CONFIG_PATH, updated_str);
- }
-}
-
-fn write_enable_notifications(enabled: bool) {
- write_config_value("enable", &enabled.to_string());
- send_ipc_command("reload");
-}
-
-
-thread_local! {
- static TEST_CONFIG_PATH: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
-}
-
-fn get_config_path() -> String {
- #[cfg(test)]
- {
- TEST_CONFIG_PATH.with(|p| {
- if let Some(path) = p.borrow().as_ref() {
- return path.clone();
- }
- "/home/lsgalante/.config/cce/config.json".to_string()
- })
- }
- #[cfg(not(test))]
- {
- "/home/lsgalante/.config/cce/config.json".to_string()
- }
-}
-
-fn write_status_value(key: &str, value: &str) {
- crate::pages::interface::write_config_value_path(&get_config_path(), key, value);
-}
-
-fn read_status_font_size() -> Option<u16> {
- let content = std::fs::read_to_string(&get_config_path()).ok()?;
- Some(parse_u16_from(&content, "status_font_size", 11))
-}
-
-
-fn read_status_padding() -> Option<u16> {
- let content = std::fs::read_to_string(&get_config_path()).ok()?;
- Some(parse_u16_from(&content, "status_padding", 8))
-}
-
-fn write_status_padding(padding: u16) {
- write_status_value("status_padding", &padding.to_string());
-}
-
-fn read_status_separators() -> Option<bool> {
- let content = std::fs::read_to_string(&get_config_path()).ok()?;
- Some(crate::pages::interface::parse_bool_from(&content, "status_separators", true))
-}
-
-fn write_status_separators(val: bool) {
- write_status_value("status_separators", &val.to_string());
-}
-
-fn read_status_underline() -> Option<bool> {
- let content = std::fs::read_to_string(&get_config_path()).ok()?;
- Some(crate::pages::interface::parse_bool_from(&content, "status_underline", true))
-}
-
-fn write_status_underline(val: bool) {
- write_status_value("status_underline", &val.to_string());
-}
-
-fn status_interface_reload() {
- let _ = std::process::Command::new("pkill")
- .args(["-f", "cce-status-interface"])
- .status();
- std::thread::sleep(std::time::Duration::from_millis(150));
- send_ipc_command("spawn cce-status-interface");
-}
-
-pub async fn fetch_status_state() -> StatusData {
- let running = tokio::process::Command::new("pgrep")
- .args(["-f", "cce-status-interface"]).output().await.ok()
- .map(|o| !o.stdout.is_empty())
- .unwrap_or(false);
-
- let font_size = read_status_font_size().unwrap_or(11);
- let padding = read_status_padding().unwrap_or(8);
- let separators = read_status_separators().unwrap_or(true);
- let underline = read_status_underline().unwrap_or(true);
-
- StatusData {
- font_size,
- padding,
- separators,
- underline,
- running,
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -1426,107 +498,9 @@ mod tests {
fn test_view_layout_grid() {
let mut state = ProcessesState::default();
let mut layout = cce_ui::layout::ColumnLayout::new(20.0);
- let sec_focused = vec![false, false, false, false, false, false, false, false, false];
+ let sec_focused = vec![false, false];
let mut ctx = cce_ui::context::UiContext::new();
let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, false, &sec_focused, &mut layout, &mut ctx);
assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
}
-
- #[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 = r#"{
- "layout": {"enable": false},
- "notifications": {"enable": true},
- "input": {"enable": false}
- }"#;
- assert!(parse_notifications_enable(content));
-
- let content = r#"{
- "layout": {"enable": true},
- "notifications": {"enable": false},
- "input": {"enable": true}
- }"#;
- assert!(!parse_notifications_enable(content));
- }
-
- #[test]
- fn test_parse_notifications_duration_default() {
- assert_eq!(parse_notifications_duration(""), 5);
- assert_eq!(parse_notifications_duration("{\"notifications\": {}}"), 5);
- }
-
- #[test]
- fn test_parse_notifications_duration_explicit() {
- let content = "{\"notifications\": {\"duration\": 10}}";
- assert_eq!(parse_notifications_duration(content), 10);
- }
-
- #[test]
- fn test_read_write_separators() {
- let dir = std::env::temp_dir();
- let path = dir.join("test_status_separators.json");
- let path_str = path.to_str().unwrap().to_string();
-
- let _ = fs::write(&path_str, "{\"layout\": {\"status_separators\": true, \"status_padding\": 8}}");
- TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
-
- let original = read_status_separators().unwrap_or(true);
- write_status_separators(!original);
- assert_eq!(read_status_separators(), Some(!original));
- write_status_separators(original);
- assert_eq!(read_status_separators(), Some(original));
-
- let _ = fs::remove_file(path);
- }
-
- #[test]
- fn test_read_write_padding() {
- let dir = std::env::temp_dir();
- let path = dir.join("test_status_padding.json");
- let path_str = path.to_str().unwrap().to_string();
-
- let _ = fs::write(&path_str, "{\"layout\": {\"status_separators\": true, \"status_padding\": 8}}");
- TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
-
- let original = read_status_padding().unwrap_or(8);
- write_status_padding(12);
- assert_eq!(read_status_padding(), Some(12));
- write_status_padding(original);
- assert_eq!(read_status_padding(), Some(original));
-
- let _ = fs::remove_file(path);
- }
-
- #[test]
- fn test_read_write_underline() {
- let dir = std::env::temp_dir();
- let path = dir.join("test_status_underline.json");
- let path_str = path.to_str().unwrap().to_string();
-
- let _ = fs::write(&path_str, "{\"layout\": {\"status_underline\": true, \"status_padding\": 8}}");
- TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
-
- let original = read_status_underline().unwrap_or(true);
- write_status_underline(!original);
- assert_eq!(read_status_underline(), Some(!original));
- write_status_underline(original);
- assert_eq!(read_status_underline(), Some(original));
-
- let _ = fs::remove_file(path);
- }
}
diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index 17196c4..5743917 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -1,12 +1,139 @@
use crate::app::{AppAction, PageContent, SectionContextExt};
-use cce_ui::layout::{PageLayoutBuilder, LayoutStrategy};
+use cce_ui::layout::{render_widget, PageLayoutBuilder, LayoutStrategy};
+use cce_ui::widget::{Label, Dropdown, InfoBox, Toggle, Spinbox, Element};
+use crate::pages::interface::parse_u16_from;
+use crate::config_manager::CONFIG_PATH;
+use std::io::Write;
+use std::fs;
#[derive(Debug, Clone, Default)]
+pub struct BatteryInfo {
+ pub percentage: f32,
+ pub state: String,
+ pub energy: f64,
+ pub energy_full: f64,
+ pub energy_rate: f64,
+ pub time_to_empty: i64,
+ pub time_to_full: i64,
+ pub vendor: String,
+ pub model: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct NotificationsConfig {
+ pub enable: bool,
+ pub bell: bool,
+ pub duration: i32,
+}
+
+#[derive(Debug, Clone)]
+pub struct StatusData {
+ pub font_size: u16,
+ pub padding: u16,
+ pub separators: bool,
+ pub underline: bool,
+ pub running: bool,
+}
+
+#[derive(Debug, Clone)]
pub struct SystemState {
pub hostname: String,
pub kernel: String,
pub uptime: String,
pub loaded: bool,
+
+ // Hardware status
+ pub cpu_model: String,
+ pub cpu_usage: f32,
+ pub cpu_cores: u32,
+ pub gpus: Vec<String>,
+ pub cpu_label: Label,
+ pub cpu_usage_label: Label,
+ pub cpu_temp_label: Label,
+ pub gpu_labels: Vec<Label>,
+
+ // Power-related fields
+ pub battery: BatteryInfo,
+ pub on_ac: bool,
+ pub cpu_powersave: bool,
+ pub gpu_powersave: bool,
+ pub cpu_gov_menu: Dropdown,
+ pub gpu_gov_menu: Dropdown,
+
+ // Notifications
+ pub notifications_loaded: bool,
+ pub notifications_enable: bool,
+ pub notifications_enable_toggle: Toggle,
+ pub notifications_bell: bool,
+ pub notifications_bell_toggle: Toggle,
+ pub notifications_duration: i32,
+ pub notifications_duration_spinbox: Spinbox,
+
+ // Status Interface fields
+ pub status_loaded: bool,
+ pub status_font_size: u16,
+ pub status_padding: u16,
+ pub status_separators: bool,
+ pub status_underline: bool,
+ pub status_running: bool,
+ pub status_label: Label,
+ pub status_separators_toggle: Toggle,
+ pub status_underline_toggle: Toggle,
+ pub status_padding_spinbox: Spinbox,
+}
+
+impl Default for SystemState {
+ fn default() -> Self {
+ Self {
+ hostname: String::new(),
+ kernel: String::new(),
+ uptime: String::new(),
+ loaded: false,
+
+ cpu_model: String::new(),
+ cpu_usage: 0.0,
+ cpu_cores: 0,
+ gpus: Vec::new(),
+ cpu_label: Label::new("CPU Info"),
+ cpu_usage_label: Label::new("CPU Usage"),
+ cpu_temp_label: Label::new("CPU Temp"),
+ gpu_labels: Vec::new(),
+
+ battery: BatteryInfo::default(),
+ on_ac: true,
+ cpu_powersave: false,
+ gpu_powersave: false,
+ cpu_gov_menu: Dropdown::new(
+ vec!["Performance".to_string(), "Powersave".to_string()],
+ 0,
+ ).with_label("CPU Governor"),
+ gpu_gov_menu: Dropdown::new(
+ vec!["Default (80W)".to_string(), "Eco Cap (5W)".to_string()],
+ 0,
+ ).with_label("GPU Power Limit"),
+
+ notifications_loaded: false,
+ notifications_enable: true,
+ notifications_enable_toggle: Toggle::new().with_label("Enable Notifications"),
+ notifications_bell: false,
+ notifications_bell_toggle: Toggle::new().with_label("Play Bell Sound"),
+ notifications_duration: 5,
+ notifications_duration_spinbox: Spinbox::new(5, 1, 60, 1)
+ .with_label("Notification Duration")
+ .with_unit("s"),
+
+ status_loaded: false,
+ status_font_size: 11,
+ status_padding: 8,
+ status_separators: true,
+ status_underline: true,
+ status_running: false,
+ status_label: Label::new("Status Interface: Stopped").with_font_size(14.0).with_color([170, 51, 51]),
+ status_separators_toggle: Toggle::new().with_label("Show Separators"),
+ status_underline_toggle: Toggle::new().with_label("Show Underline"),
+ status_padding_spinbox: Spinbox::new(8, 0, 32, 1).with_label("Side Padding").with_unit("px"),
+ }
+ }
}
#[derive(Debug, Clone)]
@@ -16,6 +143,189 @@ pub enum SystemMessage {
Hibernate,
Reboot,
PowerOff,
+
+ // Moved variants
+ SetCpuPerformance,
+ SetCpuPowersave,
+ SetGpuDefault,
+ SetGpuPowersave,
+ ToggleNotificationsEnable,
+ ToggleNotificationsBell,
+ SetNotificationsDuration(i32),
+ SendTestNotification,
+ NotificationsRefreshed(NotificationsConfig),
+ StatusRefreshed(StatusData),
+ StatusToggleSeparators,
+ StatusToggleUnderline,
+ StatusReload,
+ StatusSetPadding(u16),
+}
+
+// ── zbus proxies ────────────────────────────────────────────────────
+
+#[zbus::proxy(
+ interface = "org.freedesktop.UPower.Device",
+ default_service = "org.freedesktop.UPower",
+ default_path = "/org/freedesktop/UPower/devices/battery_BAT0"
+)]
+trait UpowerBattery {
+ #[zbus(property)]
+ fn percentage(&self) -> zbus::Result<f64>;
+ #[zbus(property)]
+ fn state(&self) -> zbus::Result<u32>;
+ #[zbus(property)]
+ fn energy(&self) -> zbus::Result<f64>;
+ #[zbus(property)]
+ fn energy_full(&self) -> zbus::Result<f64>;
+ #[zbus(property)]
+ fn energy_rate(&self) -> zbus::Result<f64>;
+ #[zbus(property)]
+ fn time_to_empty(&self) -> zbus::Result<i64>;
+ #[zbus(property)]
+ fn time_to_full(&self) -> zbus::Result<i64>;
+ #[zbus(property)]
+ fn vendor(&self) -> zbus::Result<String>;
+ #[zbus(property)]
+ fn model(&self) -> zbus::Result<String>;
+}
+
+#[zbus::proxy(
+ interface = "org.freedesktop.UPower",
+ default_service = "org.freedesktop.UPower",
+ default_path = "/org/freedesktop/UPower"
+)]
+trait UpowerDaemon {
+ #[zbus(property, name = "OnBattery")]
+ fn on_battery(&self) -> zbus::Result<bool>;
+}
+
+// ── Helpers ─────────────────────────────────────────────────────────
+
+fn format_duration(secs: i64) -> String {
+ let h = secs / 3600;
+ let m = (secs % 3600) / 60;
+ if h > 0 { format!("{}h {}m", h, m) } else { format!("{}m", m) }
+}
+
+fn spawn_cpu_power(powersave: bool) {
+ let script = if powersave { "cpu-powersave-on" } else { "cpu-powersave-off" };
+ let _ = std::process::Command::new("pkexec")
+ .arg(format!("/home/lsgalante/.local/share/cce-system-interface/helpers/{}", script))
+ .spawn();
+}
+
+fn spawn_gpu_power(powersave: bool) {
+ let script = if powersave { "gpu-powersave-on" } else { "gpu-powersave-off" };
+ let _ = std::process::Command::new("pkexec")
+ .arg(format!("/home/lsgalante/.local/share/cce-system-interface/helpers/{}", script))
+ .spawn();
+}
+
+fn current_cpu_governor() -> String {
+ std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
+ .unwrap_or_default().trim().to_string()
+}
+
+async fn current_gpu_power_cap() -> bool {
+ tokio::process::Command::new("nvidia-smi")
+ .args(["--query-gpu=power.limit", "--format=csv,noheader,nounits"])
+ .output().await.ok()
+ .and_then(|o| String::from_utf8_lossy(&o.stdout).trim().parse::<f32>().ok())
+ .map(|w| w <= 10.0).unwrap_or(false)
+}
+
+async fn fetch_upower() -> (BatteryInfo, bool) {
+ let conn = match zbus::Connection::system().await {
+ Ok(c) => c,
+ Err(_) => return (BatteryInfo::default(), true),
+ };
+
+ let battery = match UpowerBatteryProxy::new(&conn).await {
+ Ok(proxy) => BatteryInfo {
+ percentage: proxy.percentage().await.unwrap_or(0.0) as f32,
+ state: {
+ let s = proxy.state().await.unwrap_or(0);
+ match s { 1 => "charging", 2 => "discharging", 4 => "fully-charged", _ => "unknown" }.into()
+ },
+ energy: proxy.energy().await.unwrap_or(0.0),
+ energy_full: proxy.energy_full().await.unwrap_or(0.0),
+ energy_rate: proxy.energy_rate().await.unwrap_or(0.0),
+ time_to_empty: proxy.time_to_empty().await.unwrap_or(0),
+ time_to_full: proxy.time_to_full().await.unwrap_or(0),
+ vendor: proxy.vendor().await.unwrap_or_default(),
+ model: proxy.model().await.unwrap_or_default(),
+ },
+ Err(_) => BatteryInfo::default(),
+ };
+
+ let on_ac = match UpowerDaemonProxy::new(&conn).await {
+ Ok(proxy) => !proxy.on_battery().await.unwrap_or(false),
+ Err(_) => true,
+ };
+
+ (battery, on_ac)
+}
+
+fn read_cpu_temp() -> Option<f32> {
+ if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") {
+ for entry in entries.filter_map(|e| e.ok()) {
+ let path = entry.path();
+ if let Ok(name) = std::fs::read_to_string(path.join("name")) {
+ let name = name.trim();
+ if name == "thinkpad" {
+ if let Ok(val) = std::fs::read_to_string(path.join("temp1_input")) {
+ if let Ok(temp_milli) = val.trim().parse::<f32>() {
+ return Some(temp_milli / 1000.0);
+ }
+ }
+ } else if name == "coretemp" {
+ if let Ok(val) = std::fs::read_to_string(path.join("temp1_input")) {
+ if let Ok(temp_milli) = val.trim().parse::<f32>() {
+ return Some(temp_milli / 1000.0);
+ }
+ }
+ }
+ }
+ }
+ }
+ None
+}
+
+fn read_thinkpad_gpu_temp() -> Option<f32> {
+ if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") {
+ for entry in entries.filter_map(|e| e.ok()) {
+ let path = entry.path();
+ if let Ok(name) = std::fs::read_to_string(path.join("name")) {
+ if name.trim() == "thinkpad" {
+ for i in 1..=8 {
+ let label_path = path.join(format!("temp{}_label", i));
+ if let Ok(lbl) = std::fs::read_to_string(&label_path) {
+ if lbl.trim() == "GPU" {
+ if let Ok(val) = std::fs::read_to_string(path.join(format!("temp{}_input", i))) {
+ if let Ok(temp_milli) = val.trim().parse::<f32>() {
+ return Some(temp_milli / 1000.0);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ None
+}
+
+async fn read_nvidia_gpu_temp() -> Option<f32> {
+ let out = tokio::process::Command::new("nvidia-smi")
+ .args(["--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"])
+ .output().await.ok()?;
+ let val_str = String::from_utf8_lossy(&out.stdout);
+ val_str.trim().parse::<f32>().ok()
+}
+
+fn spawn_systemctl(action: &str) {
+ let _ = std::process::Command::new("systemctl").arg(action).spawn();
}
pub async fn fetch_system_state() -> SystemState {
@@ -34,26 +344,155 @@ pub async fn fetch_system_state() -> SystemState {
.map(|o| String::from_utf8_lossy(&o.stdout).trim().trim_start_matches("up ").to_string())
.unwrap_or_default();
- SystemState { hostname, kernel, uptime, loaded: true }
+ static CPU_INFO: std::sync::OnceLock<(String, u32)> = std::sync::OnceLock::new();
+ let (cpu_model, cpu_cores) = CPU_INFO.get_or_init(|| {
+ let output = std::process::Command::new("lscpu")
+ .output().ok()
+ .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
+ .unwrap_or_default();
+ let model = output.lines()
+ .find(|l| l.contains("Model name"))
+ .and_then(|l| l.split(':').nth(1))
+ .map(|s| s.trim().to_string())
+ .unwrap_or_default();
+ let cores = output.lines()
+ .find(|l| l.contains("CPU(s)"))
+ .and_then(|l| {
+ let rest = l.split(':').nth(1).unwrap_or("").trim();
+ rest.split_whitespace().next().and_then(|n| n.parse::<u32>().ok())
+ })
+ .unwrap_or(0);
+ (model, cores)
+ }).clone();
+
+ let cpu_usage = {
+ let read_stat = || -> Option<(u64, u64)> {
+ let stat = std::fs::read_to_string("/proc/stat").ok()?;
+ let first = stat.lines().next()?;
+ let vals: Vec<u64> = first.split_whitespace().skip(1).filter_map(|v| v.parse().ok()).collect();
+ if vals.len() < 3 { return None; }
+ let total: u64 = vals.iter().sum();
+ let idle = vals.get(3).copied().unwrap_or(0);
+ Some((idle, total))
+ };
+ let (idle1, total1) = read_stat().unwrap_or((0, 1));
+ tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+ let (idle2, total2) = read_stat().unwrap_or((0, 1));
+ let d_idle = idle2.saturating_sub(idle1);
+ let d_total = total2.saturating_sub(total1);
+ if d_total > 0 {
+ (1.0 - d_idle as f64 / d_total as f64) * 100.0
+ } else { 0.0 }
+ } as f32;
+
+ static GPUS_INFO: std::sync::OnceLock<Vec<String>> = std::sync::OnceLock::new();
+ let gpus = GPUS_INFO.get_or_init(|| {
+ let mut list = Vec::new();
+ if let Some(o) = std::process::Command::new("lspci").output().ok() {
+ for line in String::from_utf8_lossy(&o.stdout).lines() {
+ if line.contains("VGA") || line.contains("3D") {
+ if let Some(name) = line.split(':').nth(2) {
+ let trimmed = name.trim().to_string();
+ if !trimmed.is_empty() {
+ list.push(trimmed);
+ }
+ }
+ }
+ }
+ }
+ list
+ }).clone();
+
+ let cpu_temp = read_cpu_temp();
+ let tp_gpu_temp = read_thinkpad_gpu_temp();
+ let nv_gpu_temp = read_nvidia_gpu_temp().await;
+
+ let cpu_label_text = format!("CPU {} ({} cores)", cpu_model, cpu_cores);
+ let cpu_usage_text = format!("Usage {:.0}%", cpu_usage);
+ let cpu_temp_text = cpu_temp.map(|t| format!("Temp {:.0}°C", t)).unwrap_or_else(|| "Temp N/A".to_string());
+
+ let gpu_labels = gpus.iter().map(|gpu_name| {
+ let temp = if gpu_name.to_lowercase().contains("nvidia") {
+ nv_gpu_temp.or(tp_gpu_temp)
+ } else {
+ tp_gpu_temp
+ };
+ let temp_str = temp.map(|t| format!(" — {:.0}°C", t)).unwrap_or_default();
+ let text = format!("GPU {}{}", gpu_name, temp_str);
+ Label::new(&text).with_font_size(12.0).with_color([212, 212, 212])
+ }).collect();
+
+ let (battery, on_ac) = fetch_upower().await;
+ let cpu_powersave = current_cpu_governor() == "powersave";
+ let gpu_powersave = current_gpu_power_cap().await;
+
+ SystemState {
+ hostname,
+ kernel,
+ uptime,
+ loaded: true,
+
+ cpu_model,
+ cpu_usage,
+ cpu_cores,
+ gpus,
+ cpu_label: Label::new(&cpu_label_text).with_font_size(12.0).with_color([212, 212, 212]),
+ cpu_usage_label: Label::new(&cpu_usage_text).with_font_size(12.0).with_color([212, 212, 212]),
+ cpu_temp_label: Label::new(&cpu_temp_text).with_font_size(12.0).with_color([212, 212, 212]),
+ gpu_labels,
+
+ battery,
+ on_ac,
+ cpu_powersave,
+ gpu_powersave,
+ cpu_gov_menu: Dropdown::new(
+ vec!["Performance".to_string(), "Powersave".to_string()],
+ if cpu_powersave { 1 } else { 0 },
+ ).with_label("CPU Governor"),
+ gpu_gov_menu: Dropdown::new(
+ vec!["Default (80W)".to_string(), "Eco Cap (5W)".to_string()],
+ if gpu_powersave { 1 } else { 0 },
+ ).with_label("GPU Power Limit"),
+
+ notifications_loaded: false,
+ notifications_enable: true,
+ notifications_enable_toggle: Toggle::new().with_label("Enable Notifications"),
+ notifications_bell: false,
+ notifications_bell_toggle: Toggle::new().with_label("Play Bell Sound"),
+ notifications_duration: 5,
+ notifications_duration_spinbox: Spinbox::new(5, 1, 60, 1)
+ .with_label("Notification Duration")
+ .with_unit("s"),
+
+ status_loaded: false,
+ status_font_size: 11,
+ status_padding: 8,
+ status_separators: true,
+ status_underline: true,
+ status_running: false,
+ status_label: Label::new("Status Interface: Stopped").with_font_size(14.0).with_color([170, 51, 51]),
+ status_separators_toggle: Toggle::new().with_label("Show Separators"),
+ status_underline_toggle: Toggle::new().with_label("Show Underline"),
+ status_padding_spinbox: Spinbox::new(8, 0, 32, 1).with_label("Side Padding").with_unit("px"),
+ }
}
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
+const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
+const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
+const ORANGE: [f32; 4] = [1.0, 0.73, 0.20, 1.0];
const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
const DANGER_BG: [f32; 4] = [0.67, 0.20, 0.20, 1.0];
const SAFE_BG: [f32; 4] = [0.20, 0.33, 0.22, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-fn spawn_systemctl(action: &str) {
- let _ = tokio::process::Command::new("systemctl").arg(action).spawn();
-}
-
-pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mut dyn LayoutStrategy, _ctx: &mut cce_ui::context::UiContext) -> PageContent {
+pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_focused: bool, sec_focused: &[bool], layout: &mut dyn LayoutStrategy, ctx: &mut cce_ui::context::UiContext) -> PageContent {
let mut final_pc = PageContent::new();
let sec_w = 320.0f32;
- let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(2);
+ let mut builder = PageLayoutBuilder::new(layout, cx, cy, cw, ch, sec_w).with_section_count(9);
- // 1. System Section
+ // ── 1. System Section ──
builder.add_section(&mut final_pc, "System", false, |sec| {
if !state.loaded {
sec.text("Loading system information...", 12.0, 0.0, 14.0, TEXT_FG);
@@ -65,7 +504,7 @@ pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mu
}
});
- // 2. System Actions Section
+ // ── 2. System Actions Section ──
builder.add_section(&mut final_pc, "System Actions", false, |sec| {
let yt = sec.ay();
let act_btn_h = 32.0;
@@ -95,15 +534,646 @@ pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, ch: f32, layout: &mu
sec.spacing(12.0);
});
+ // ── 3. CPU Section ──
+ builder.add_section(&mut final_pc, "CPU", false, |sec| {
+ if !state.loaded {
+ sec.text("Loading CPU model and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(10.0);
+ } else {
+ // CPU Info Label
+ sec.widget(&mut state.cpu_label, 12.0, sec.cw - 24.0, 26.0, ctx);
+ sec.spacing(12.0);
+
+ // CPU Usage Label
+ sec.widget(&mut state.cpu_usage_label, 12.0, sec.cw - 24.0, 26.0, ctx);
+ sec.spacing(12.0);
+
+ // CPU Temp Label
+ sec.widget(&mut state.cpu_temp_label, 12.0, sec.cw - 24.0, 26.0, ctx);
+ sec.spacing(12.0);
+ }
+ });
+
+ // ── 4. GPU Section ──
+ builder.add_section(&mut final_pc, "GPU", false, |sec_gpu| {
+ if !state.loaded {
+ sec_gpu.text("Loading GPU models...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec_gpu.spacing(10.0);
+ } else {
+ for (i, gpu_lbl) in state.gpu_labels.iter_mut().enumerate() {
+ if i > 0 { sec_gpu.spacing(12.0); }
+ sec_gpu.widget(gpu_lbl, 12.0, sec_gpu.cw - 24.0, 26.0, ctx);
+ }
+ }
+ });
+
+ // ── 5. CPU Governor Section ──
+ builder.add_section(&mut final_pc, "CPU Governor", sec_focused.get(4).copied().unwrap_or(false), |sec_gov| {
+ let rx = sec_gov.left;
+ if !state.loaded {
+ sec_gov.text("Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_gov.spacing(18.0);
+ } else {
+ sec_gov.widget(&mut state.cpu_gov_menu, 12.0, sec_gov.cw - 24.0, 26.0, ctx);
+ sec_gov.spacing(12.0);
+
+ let (info_title, info_lines) = if state.cpu_powersave {
+ (
+ "CPU Governor: Powersave",
+ vec![
+ "• Active: powersave".to_string(),
+ "• Governor set to powersave — lower power, slower burst".to_string(),
+ ],
+ )
+ } else {
+ (
+ "CPU Governor: Performance",
+ vec![
+ "• Active: performance".to_string(),
+ "• Governor set to performance".to_string(),
+ ],
+ )
+ };
+
+ let mut info_box = InfoBox::new(info_title, info_lines);
+ let info_h = 80.0;
+ let info_y = sec_gov.ay();
+ render_widget(sec_gov.pc, &mut info_box, rx + 12.0, info_y, sec_gov.cw - 24.0, info_h, ctx);
+ sec_gov.spacing(info_h + 12.0);
+ }
+ });
+
+ // ── 6. GPU Power Section ──
+ builder.add_section(&mut final_pc, "GPU Power", sec_focused.get(5).copied().unwrap_or(false), |sec_gpow| {
+ let rx = sec_gpow.left;
+ if !state.loaded {
+ sec_gpow.text("Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_gpow.spacing(18.0);
+ } else {
+ sec_gpow.widget(&mut state.gpu_gov_menu, 12.0, sec_gpow.cw - 24.0, 26.0, ctx);
+ sec_gpow.spacing(12.0);
+
+ let (info_title, info_lines) = if state.gpu_powersave {
+ (
+ "GPU Power Limit: Eco Cap",
+ vec![
+ "• Mode: 5W Cap".to_string(),
+ "• NVIDIA power limit capped at 5W — minimal draw".to_string(),
+ ],
+ )
+ } else {
+ (
+ "GPU Power Limit: Default",
+ vec![
+ "• Mode: 80W Default".to_string(),
+ "• NVIDIA running at default power limit".to_string(),
+ ],
+ )
+ };
+
+ let mut info_box = InfoBox::new(info_title, info_lines);
+ let info_h = 80.0;
+ let info_y = sec_gpow.ay();
+ render_widget(sec_gpow.pc, &mut info_box, rx + 12.0, info_y, sec_gpow.cw - 24.0, info_h, ctx);
+ sec_gpow.spacing(info_h + 12.0);
+ }
+ });
+
+ // ── 7. Battery Section ──
+ builder.add_section(&mut final_pc, "Battery", false, |sec_bat| {
+ if !state.loaded {
+ sec_bat.text("Loading battery status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_bat.spacing(18.0);
+ } else {
+ let bat = &state.battery;
+ let bat_icon = match bat.state.as_str() {
+ "charging" => "+",
+ "fully-charged" => "=",
+ _ => "",
+ };
+
+ let pct_color = if bat.percentage < 20.0 { RED }
+ else if bat.percentage < 50.0 { ORANGE }
+ else { ACCENT };
+
+ let pct_str = format!("{} {:.0}%", bat_icon, bat.percentage);
+ sec_bat.text(&pct_str, 12.0, 0.0, 24.0, pct_color);
+ sec_bat.spacing(30.0);
+
+ let state_str = format!("{} • {:.1}W • {:.1}/{:.1} Wh",
+ bat.state, bat.energy_rate, bat.energy, bat.energy_full);
+ sec_bat.text(&state_str, 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_bat.spacing(18.0);
+
+ let time_str = if bat.time_to_empty > 0 {
+ format!("Time remaining: {}", format_duration(bat.time_to_empty))
+ } else if bat.time_to_full > 0 {
+ format!("Time to full: {}", format_duration(bat.time_to_full))
+ } else { String::new() };
+ if !time_str.is_empty() {
+ sec_bat.text(&time_str, 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_bat.spacing(18.0);
+ }
+
+ let detail_str = format!("{} {}", bat.vendor, bat.model);
+ sec_bat.text(&detail_str, 12.0, 0.0, 11.0, TEXT_DIM);
+ sec_bat.spacing(20.0);
+
+ let ac_str = if state.on_ac { "On AC Power" } else { "On Battery" };
+ sec_bat.text(ac_str, 12.0, 0.0, 14.0, TEXT_FG);
+ sec_bat.spacing(20.0);
+ }
+ });
+
+ // ── 8. System Notifications Section ──
+ builder.add_section(&mut final_pc, "System Notifications", sec_focused.get(7).copied().unwrap_or(false), |sec2| {
+ let sec_w = sec2.cw;
+ state.notifications_enable_toggle.set_toggled(state.notifications_enable);
+ sec2.widget_full(&mut state.notifications_enable_toggle, cce_ui::layout::toggle_height(), ctx);
+ sec2.spacing(8.0);
+
+ state.notifications_bell_toggle.set_toggled(state.notifications_bell);
+ sec2.widget_full(&mut state.notifications_bell_toggle, cce_ui::layout::toggle_height(), ctx);
+ sec2.spacing(16.0);
+
+ state.notifications_duration_spinbox.value = state.notifications_duration;
+ state.notifications_duration_spinbox.set_label("Notification Duration");
+ sec2.widget(&mut state.notifications_duration_spinbox, 14.0, sec_w - 28.0, 44.0, ctx);
+ sec2.spacing(16.0);
+
+ let btn_h = 32.0;
+ let btn_y = sec2.ay();
+ let white_color = [1.0, 1.0, 1.0, 1.0];
+ let btn_bg = [0.20, 0.40, 0.65, 1.0];
+ let btn_hover = [0.28, 0.50, 0.78, 1.0];
+
+ let cols = sec2.row_layout(1, 0.0);
+ if let Some(&(x, w)) = cols.first() {
+ sec2.button(
+ "Send Test Notification",
+ x,
+ btn_y,
+ w,
+ btn_h,
+ btn_bg,
+ btn_hover,
+ white_color,
+ AppAction::SystemInfo(SystemMessage::SendTestNotification),
+ );
+ }
+ sec2.spacing(12.0);
+ });
+
+ // ── 9. Status Interface Section ──
+ builder.add_section(&mut final_pc, "Status Interface", sec_focused.get(8).copied().unwrap_or(false), |sec3| {
+ let sec_w = sec3.cw;
+ if !state.status_loaded {
+ sec3.text("Loading Status Interface status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec3.spacing(18.0);
+ } else {
+ // Status
+ let status_text = if state.status_running { "Status Interface: Running" } else { "Status Interface: Stopped" };
+ let status_color = if state.status_running { [92, 143, 97] } else { [170, 51, 51] };
+ state.status_label.set_text(status_text);
+ state.status_label.set_color(status_color);
+ sec3.widget(&mut state.status_label, 12.0, sec_w - 24.0, 20.0, ctx);
+ sec3.spacing(12.0);
+
+ sec3.spacing(4.0);
+
+ // Separators toggle
+ state.status_separators_toggle.set_toggled(state.status_separators);
+ sec3.widget_full(&mut state.status_separators_toggle, cce_ui::layout::toggle_height(), ctx);
+ sec3.spacing(16.0);
+
+ // Underline toggle
+ state.status_underline_toggle.set_toggled(state.status_underline);
+ sec3.widget_full(&mut state.status_underline_toggle, cce_ui::layout::toggle_height(), ctx);
+ sec3.spacing(16.0);
+
+ // Padding spinbox
+ state.status_padding_spinbox.value = state.status_padding as i32;
+ sec3.widget(&mut state.status_padding_spinbox, 12.0, sec_w - 24.0, 44.0, ctx);
+ sec3.spacing(16.0);
+
+ // Reload button
+ let yt_reload = sec3.ay();
+ let btn_w = sec_w - 24.0;
+ let button_x = sec3.left + 12.0;
+ sec3.button(
+ "Reload Status Interface",
+ button_x,
+ yt_reload,
+ btn_w,
+ 32.0,
+ [0.13, 0.18, 0.14, 1.0],
+ [0.25, 0.30, 0.26, 1.0],
+ [1.0, 1.0, 1.0, 1.0],
+ AppAction::SystemInfo(SystemMessage::StatusReload),
+ );
+ sec3.spacing(12.0);
+ }
+ });
+
final_pc
}
-pub fn update(_state: &mut SystemState, msg: SystemMessage) {
+pub fn update(state: &mut SystemState, msg: SystemMessage) {
match msg {
- SystemMessage::Refreshed(new) => { *_state = new; }
+ SystemMessage::Refreshed(new) => {
+ state.hostname = new.hostname;
+ state.kernel = new.kernel;
+ state.uptime = new.uptime;
+ state.loaded = new.loaded;
+
+ state.cpu_model = new.cpu_model;
+ state.cpu_usage = new.cpu_usage;
+ state.cpu_cores = new.cpu_cores;
+ state.gpus = new.gpus;
+ state.cpu_label = new.cpu_label;
+ state.cpu_usage_label = new.cpu_usage_label;
+ state.cpu_temp_label = new.cpu_temp_label;
+ state.gpu_labels = new.gpu_labels;
+
+ state.battery = new.battery;
+ state.on_ac = new.on_ac;
+ state.cpu_powersave = new.cpu_powersave;
+ state.gpu_powersave = new.gpu_powersave;
+ state.cpu_gov_menu.selected = new.cpu_gov_menu.selected;
+ state.gpu_gov_menu.selected = new.gpu_gov_menu.selected;
+ }
SystemMessage::Suspend => spawn_systemctl("suspend"),
SystemMessage::Hibernate => spawn_systemctl("hibernate"),
SystemMessage::Reboot => spawn_systemctl("reboot"),
SystemMessage::PowerOff => spawn_systemctl("poweroff"),
+
+ SystemMessage::SetCpuPerformance => {
+ state.cpu_powersave = false;
+ state.cpu_gov_menu.selected = 0;
+ spawn_cpu_power(false);
+ }
+ SystemMessage::SetCpuPowersave => {
+ state.cpu_powersave = true;
+ state.cpu_gov_menu.selected = 1;
+ spawn_cpu_power(true);
+ }
+ SystemMessage::SetGpuDefault => {
+ state.gpu_powersave = false;
+ state.gpu_gov_menu.selected = 0;
+ spawn_gpu_power(false);
+ }
+ SystemMessage::SetGpuPowersave => {
+ state.gpu_powersave = true;
+ state.gpu_gov_menu.selected = 1;
+ spawn_gpu_power(true);
+ }
+ SystemMessage::ToggleNotificationsEnable => {
+ state.notifications_enable = !state.notifications_enable;
+ write_enable_notifications(state.notifications_enable);
+ }
+ SystemMessage::ToggleNotificationsBell => {
+ state.notifications_bell = !state.notifications_bell;
+ write_config_value("bell", &state.notifications_bell.to_string());
+ }
+ SystemMessage::SetNotificationsDuration(d) => {
+ state.notifications_duration = d;
+ write_config_value("duration", &state.notifications_duration.to_string());
+ }
+ SystemMessage::SendTestNotification => {
+ tokio::spawn(async move {
+ if let Ok(connection) = zbus::Connection::session().await {
+ let _ = connection.call_method(
+ Some("org.freedesktop.Notifications"),
+ "/org/freedesktop/Notifications",
+ Some("org.freedesktop.Notifications"),
+ "Notify",
+ &(
+ "cce-client",
+ 0u32,
+ "",
+ "System notifications are working correctly!",
+ "",
+ Vec::<&str>::new(),
+ std::collections::HashMap::<&str, zbus::zvariant::Value>::new(),
+ -1i32,
+ )
+ ).await;
+ }
+ });
+ }
+ SystemMessage::NotificationsRefreshed(new) => {
+ state.notifications_loaded = true;
+ state.notifications_enable = new.enable;
+ state.notifications_bell = new.bell;
+ state.notifications_duration = new.duration;
+ }
+ SystemMessage::StatusRefreshed(new) => {
+ let was_status_hovered = state.status_label.hovered();
+ let was_separators_hovered = state.status_separators_toggle.hovered();
+ let was_underline_hovered = state.status_underline_toggle.hovered();
+
+ state.status_loaded = true;
+ state.status_font_size = new.font_size;
+ state.status_padding = new.padding;
+ state.status_separators = new.separators;
+ state.status_underline = new.underline;
+ state.status_running = new.running;
+
+ state.status_label.set_hovered(was_status_hovered);
+ state.status_separators_toggle.set_hovered(was_separators_hovered);
+ state.status_underline_toggle.set_hovered(was_underline_hovered);
+ }
+ SystemMessage::StatusToggleSeparators => {
+ state.status_separators = !state.status_separators;
+ write_status_separators(state.status_separators);
+ status_interface_reload();
+ }
+ SystemMessage::StatusToggleUnderline => {
+ state.status_underline = !state.status_underline;
+ write_status_underline(state.status_underline);
+ status_interface_reload();
+ }
+ SystemMessage::StatusSetPadding(val) => {
+ state.status_padding = val;
+ write_status_padding(val);
+ status_interface_reload();
+ }
+ SystemMessage::StatusReload => {
+ status_interface_reload();
+ }
+ }
+}
+
+// ── Notifications Configuration Reader & Writer ──
+
+fn get_socket_path() -> String {
+ match std::env::var("WAYLAND_DISPLAY") {
+ Ok(display) => format!("/tmp/cce-{}.sock", display),
+ Err(_) => "/tmp/cce.sock".to_string(),
+ }
+}
+
+pub fn read_notifications_config() -> NotificationsConfig {
+ let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let enable = parse_notifications_enable(&content);
+ let bell = parse_notifications_bell(&content);
+ let duration = parse_notifications_duration(&content);
+ NotificationsConfig {
+ enable,
+ bell,
+ duration,
+ }
+}
+
+fn parse_json(content: &str) -> serde_json::Value {
+ serde_json::from_str(content).unwrap_or_default()
+}
+
+fn parse_notifications_enable(content: &str) -> bool {
+ let val = parse_json(content);
+ val["notifications"]["enable"].as_bool().unwrap_or(true)
+}
+
+fn parse_notifications_bell(content: &str) -> bool {
+ let val = parse_json(content);
+ val["notifications"]["bell"].as_bool().unwrap_or(false)
+}
+
+fn parse_notifications_duration(content: &str) -> i32 {
+ let val = parse_json(content);
+ val["notifications"]["duration"].as_i64().map(|v| v as i32).unwrap_or(5)
+}
+
+fn send_ipc_command(cmd: &str) {
+ if let Ok(mut stream) = std::os::unix::net::UnixStream::connect(get_socket_path()) {
+ 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 mut val = parse_json(&content);
+ let j_val = if let Ok(b) = value.parse::<bool>() {
+ serde_json::json!(b)
+ } else if let Ok(n) = value.parse::<i64>() {
+ serde_json::json!(n)
+ } else if let Ok(f) = value.parse::<f64>() {
+ serde_json::json!(f)
+ } else {
+ serde_json::json!(value)
+ };
+ if let Some(notifications) = val.get_mut("notifications").and_then(|n| n.as_object_mut()) {
+ notifications.insert(key.to_string(), j_val);
+ } else {
+ let mut map = serde_json::Map::new();
+ map.insert(key.to_string(), j_val);
+ if let Some(obj) = val.as_object_mut() {
+ obj.insert("notifications".to_string(), serde_json::Value::Object(map));
+ }
+ }
+ if let Ok(updated_str) = serde_json::to_string_pretty(&val) {
+ let _ = fs::write(CONFIG_PATH, updated_str);
+ }
+}
+
+fn write_enable_notifications(enabled: bool) {
+ write_config_value("enable", &enabled.to_string());
+ send_ipc_command("reload");
+}
+
+thread_local! {
+ static TEST_CONFIG_PATH: std::cell::RefCell<Option<String>> = std::cell::RefCell::new(None);
+}
+
+fn get_config_path() -> String {
+ #[cfg(test)]
+ {
+ TEST_CONFIG_PATH.with(|p| {
+ if let Some(path) = p.borrow().as_ref() {
+ return path.clone();
+ }
+ "/home/lsgalante/.config/cce/config.json".to_string()
+ })
+ }
+ #[cfg(not(test))]
+ {
+ "/home/lsgalante/.config/cce/config.json".to_string()
+ }
+}
+
+fn write_status_value(key: &str, value: &str) {
+ crate::pages::interface::write_config_value_path(&get_config_path(), key, value);
+}
+
+fn read_status_font_size() -> Option<u16> {
+ let content = std::fs::read_to_string(&get_config_path()).ok()?;
+ Some(parse_u16_from(&content, "status_font_size", 11))
+}
+
+fn read_status_padding() -> Option<u16> {
+ let content = std::fs::read_to_string(&get_config_path()).ok()?;
+ Some(parse_u16_from(&content, "status_padding", 8))
+}
+
+fn write_status_padding(padding: u16) {
+ write_status_value("status_padding", &padding.to_string());
+}
+
+fn read_status_separators() -> Option<bool> {
+ let content = std::fs::read_to_string(&get_config_path()).ok()?;
+ Some(crate::pages::interface::parse_bool_from(&content, "status_separators", true))
+}
+
+fn write_status_separators(val: bool) {
+ write_status_value("status_separators", &val.to_string());
+}
+
+fn read_status_underline() -> Option<bool> {
+ let content = std::fs::read_to_string(&get_config_path()).ok()?;
+ Some(crate::pages::interface::parse_bool_from(&content, "status_underline", true))
+}
+
+fn write_status_underline(val: bool) {
+ write_status_value("status_underline", &val.to_string());
+}
+
+fn status_interface_reload() {
+ let _ = std::process::Command::new("pkill")
+ .args(["-f", "cce-status-interface"])
+ .status();
+ std::thread::sleep(std::time::Duration::from_millis(150));
+ send_ipc_command("spawn cce-status-interface");
+}
+
+pub async fn fetch_status_state() -> StatusData {
+ let running = tokio::process::Command::new("pgrep")
+ .args(["-f", "cce-status-interface"]).output().await.ok()
+ .map(|o| !o.stdout.is_empty())
+ .unwrap_or(false);
+
+ let font_size = read_status_font_size().unwrap_or(11);
+ let padding = read_status_padding().unwrap_or(8);
+ let separators = read_status_separators().unwrap_or(true);
+ let underline = read_status_underline().unwrap_or(true);
+
+ StatusData {
+ font_size,
+ padding,
+ separators,
+ underline,
+ running,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_view_layout_grid() {
+ let mut state = SystemState::default();
+ let mut layout = cce_ui::layout::ColumnLayout::new(20.0);
+ let sec_focused = vec![false, false, false, false, false, false, false, false, false];
+ let mut ctx = cce_ui::context::UiContext::new();
+ let pc = view(&mut state, 10.0, 20.0, 800.0, 600.0, false, &sec_focused, &mut layout, &mut ctx);
+ assert!(!pc.rects.is_empty() || !pc.texts.is_empty());
+ }
+
+ #[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 = r#"{
+ "layout": {"enable": false},
+ "notifications": {"enable": true},
+ "input": {"enable": false}
+ }"#;
+ assert!(parse_notifications_enable(content));
+
+ let content = r#"{
+ "layout": {"enable": true},
+ "notifications": {"enable": false},
+ "input": {"enable": true}
+ }"#;
+ assert!(!parse_notifications_enable(content));
+ }
+
+ #[test]
+ fn test_parse_notifications_duration_default() {
+ assert_eq!(parse_notifications_duration(""), 5);
+ assert_eq!(parse_notifications_duration("{\"notifications\": {}}"), 5);
+ }
+
+ #[test]
+ fn test_parse_notifications_duration_explicit() {
+ let content = "{\"notifications\": {\"duration\": 10}}";
+ assert_eq!(parse_notifications_duration(content), 10);
+ }
+
+ #[test]
+ fn test_read_write_separators() {
+ let dir = std::env::temp_dir();
+ let path = dir.join("test_status_separators.json");
+ let path_str = path.to_str().unwrap().to_string();
+
+ let _ = fs::write(&path_str, "{\"layout\": {\"status_separators\": true, \"status_padding\": 8}}");
+ TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
+
+ let original = read_status_separators().unwrap_or(true);
+ write_status_separators(!original);
+ assert_eq!(read_status_separators(), Some(!original));
+ write_status_separators(original);
+ assert_eq!(read_status_separators(), Some(original));
+
+ let _ = fs::remove_file(path);
+ }
+
+ #[test]
+ fn test_read_write_padding() {
+ let dir = std::env::temp_dir();
+ let path = dir.join("test_status_padding.json");
+ let path_str = path.to_str().unwrap().to_string();
+
+ let _ = fs::write(&path_str, "{\"layout\": {\"status_separators\": true, \"status_padding\": 8}}");
+ TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
+
+ let original = read_status_padding().unwrap_or(8);
+ write_status_padding(12);
+ assert_eq!(read_status_padding(), Some(12));
+ write_status_padding(original);
+ assert_eq!(read_status_padding(), Some(original));
+
+ let _ = fs::remove_file(path);
+ }
+
+ #[test]
+ fn test_read_write_underline() {
+ let dir = std::env::temp_dir();
+ let path = dir.join("test_status_underline.json");
+ let path_str = path.to_str().unwrap().to_string();
+
+ let _ = fs::write(&path_str, "{\"layout\": {\"status_underline\": true, \"status_padding\": 8}}");
+ TEST_CONFIG_PATH.with(|p| *p.borrow_mut() = Some(path_str));
+
+ let original = read_status_underline().unwrap_or(true);
+ write_status_underline(!original);
+ assert_eq!(read_status_underline(), Some(!original));
+ write_status_underline(original);
+ assert_eq!(read_status_underline(), Some(original));
+
+ let _ = fs::remove_file(path);
}
}
diff --git a/src/renderer.rs b/src/renderer.rs
index 74d2558..6a63e99 100644
--- a/src/renderer.rs
+++ b/src/renderer.rs
@@ -155,9 +155,14 @@ impl SystemInterface {
self.app.interface.terminal_menu.clear_children(&mut self.ui_context); self.app.interface.terminal_menu.set_parent(None, &mut self.ui_context);
self.app.interface.terminal_box.clear_children(&mut self.ui_context); self.app.interface.terminal_box.set_parent(None, &mut self.ui_context);
- self.app.processes.notifications_enable_toggle.clear_children(&mut self.ui_context); self.app.processes.notifications_enable_toggle.set_parent(None, &mut self.ui_context);
- self.app.processes.notifications_bell_toggle.clear_children(&mut self.ui_context); self.app.processes.notifications_bell_toggle.set_parent(None, &mut self.ui_context);
- self.app.processes.notifications_duration_spinbox.clear_children(&mut self.ui_context); self.app.processes.notifications_duration_spinbox.set_parent(None, &mut self.ui_context);
+ self.app.system_info.cpu_gov_menu.clear_children(&mut self.ui_context); self.app.system_info.cpu_gov_menu.set_parent(None, &mut self.ui_context);
+ self.app.system_info.gpu_gov_menu.clear_children(&mut self.ui_context); self.app.system_info.gpu_gov_menu.set_parent(None, &mut self.ui_context);
+ self.app.system_info.notifications_enable_toggle.clear_children(&mut self.ui_context); self.app.system_info.notifications_enable_toggle.set_parent(None, &mut self.ui_context);
+ self.app.system_info.notifications_bell_toggle.clear_children(&mut self.ui_context); self.app.system_info.notifications_bell_toggle.set_parent(None, &mut self.ui_context);
+ self.app.system_info.notifications_duration_spinbox.clear_children(&mut self.ui_context); self.app.system_info.notifications_duration_spinbox.set_parent(None, &mut self.ui_context);
+ self.app.system_info.status_separators_toggle.clear_children(&mut self.ui_context); self.app.system_info.status_separators_toggle.set_parent(None, &mut self.ui_context);
+ self.app.system_info.status_underline_toggle.clear_children(&mut self.ui_context); self.app.system_info.status_underline_toggle.set_parent(None, &mut self.ui_context);
+ self.app.system_info.status_padding_spinbox.clear_children(&mut self.ui_context); self.app.system_info.status_padding_spinbox.set_parent(None, &mut self.ui_context);
self.app.input.rate_spinbox.clear_children(&mut self.ui_context); self.app.input.rate_spinbox.set_parent(None, &mut self.ui_context);
self.app.input.delay_spinbox.clear_children(&mut self.ui_context); self.app.input.delay_spinbox.set_parent(None, &mut self.ui_context);
@@ -209,9 +214,7 @@ impl SystemInterface {
scale_lbl.clear_children(&mut self.ui_context); scale_lbl.set_parent(None, &mut self.ui_context);
}
}
- self.app.processes.status_separators_toggle.clear_children(&mut self.ui_context); self.app.processes.status_separators_toggle.set_parent(None, &mut self.ui_context);
- self.app.processes.status_underline_toggle.clear_children(&mut self.ui_context); self.app.processes.status_underline_toggle.set_parent(None, &mut self.ui_context);
- self.app.processes.status_padding_spinbox.clear_children(&mut self.ui_context); self.app.processes.status_padding_spinbox.set_parent(None, &mut self.ui_context);
+
for menu in &mut self.app.interface.windows.tag_layout_menus {
menu.clear_children(&mut self.ui_context);
@@ -246,22 +249,28 @@ impl SystemInterface {
}
}
- Page::Processes => {
+ Page::System => {
self.page_sec_containers.resize_with(9, cce_ui::widget::Container::new);
for i in 0..9 {
link_parent_child(page_root, &mut self.page_sec_containers[i], &mut self.ui_context);
}
- link_parent_child(&mut self.page_sec_containers[1], &mut self.app.processes.cpu_list_box.scroll_box, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[4], &mut self.app.processes.cpu_gov_menu, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[5], &mut self.app.processes.gpu_gov_menu, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[6], &mut self.app.processes.services_search_box, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[6], &mut self.app.processes.services_list_box.scroll_box, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[7], &mut self.app.processes.notifications_enable_toggle, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[7], &mut self.app.processes.notifications_bell_toggle, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[7], &mut self.app.processes.notifications_duration_spinbox, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[8], &mut self.app.processes.status_separators_toggle, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[8], &mut self.app.processes.status_underline_toggle, &mut self.ui_context);
- link_parent_child(&mut self.page_sec_containers[8], &mut self.app.processes.status_padding_spinbox, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[4], &mut self.app.system_info.cpu_gov_menu, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[5], &mut self.app.system_info.gpu_gov_menu, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[7], &mut self.app.system_info.notifications_enable_toggle, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[7], &mut self.app.system_info.notifications_bell_toggle, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[7], &mut self.app.system_info.notifications_duration_spinbox, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.system_info.status_separators_toggle, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.system_info.status_underline_toggle, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[8], &mut self.app.system_info.status_padding_spinbox, &mut self.ui_context);
+ }
+ Page::Processes => {
+ self.page_sec_containers.resize_with(2, cce_ui::widget::Container::new);
+ for i in 0..2 {
+ link_parent_child(page_root, &mut self.page_sec_containers[i], &mut self.ui_context);
+ }
+ link_parent_child(&mut self.page_sec_containers[0], &mut self.app.processes.cpu_list_box.scroll_box, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.processes.services_search_box, &mut self.ui_context);
+ link_parent_child(&mut self.page_sec_containers[1], &mut self.app.processes.services_list_box.scroll_box, &mut self.ui_context);
}
Page::Radios => {
self.page_sec_containers.resize_with(2, cce_ui::widget::Container::new);
@@ -822,7 +831,7 @@ impl SystemInterface {
Page::Radios => network::view(&mut self.app.network, cx, cy, cw, ch, root_focused, &mut layout, &mut self.ui_context),
Page::Processes => processes::view(&mut self.app.processes, cx, cy, cw, ch, root_focused, &sec_focused, &mut layout, &mut self.ui_context),
Page::Input => input::view(&mut self.app.input, cx, cy, cw, ch, &sec_focused, &mut layout, &mut self.ui_context),
- Page::System => system_info::view(&self.app.system_info, cx, cy, cw, ch, &mut layout, &mut self.ui_context),
+ Page::System => system_info::view(&mut self.app.system_info, cx, cy, cw, ch, root_focused, &sec_focused, &mut layout, &mut self.ui_context),
Page::Storage => storage::view(&self.app.storage, cx, cy, cw, ch, &mut layout, &mut self.ui_context),
Page::Interface => interface::view(&mut self.app.interface, cx, cy, cw, ch, &sec_focused, &mut layout, &mut self.ui_context),
Page::Packages => packages::view(&mut self.app.packages, cx, cy, cw, ch, &sec_focused, &mut layout, &mut self.ui_context),
diff --git a/src/watchers.rs b/src/watchers.rs
index f038872..280a2e6 100644
--- a/src/watchers.rs
+++ b/src/watchers.rs
@@ -14,9 +14,9 @@ pub struct Watchers {
pub rx_fingers: Receiver<Vec<Finger>>,
pub rx_processes: Receiver<processes::ProcessesState>,
pub rx_system: Receiver<system_info::SystemState>,
- pub rx_status: Receiver<processes::StatusData>,
+ pub rx_status: Receiver<system_info::StatusData>,
pub rx_storage: Receiver<storage::StorageState>,
- pub rx_notifications: Receiver<processes::NotificationsConfig>,
+ pub rx_notifications: Receiver<system_info::NotificationsConfig>,
pub rx_typeface: Receiver<interface::InterfaceState>,
pub rx_services: Receiver<Vec<processes::ServiceInfo>>,
pub rx_interface: Receiver<interface::InterfaceState>,
@@ -193,23 +193,23 @@ pub fn spawn_all(
let rx_system = spawn_bg_active(current_page_shared.clone(), 9, 5, || system_info::fetch_system_state());
let rx_processes = spawn_bg_active(current_page_shared.clone(), 6, 3, || processes::fetch_processes_state());
- let rx_status = spawn_bg_active(current_page_shared.clone(), 6, 10, || processes::fetch_status_state());
+ let rx_status = spawn_bg_active(current_page_shared.clone(), 9, 10, || system_info::fetch_status_state());
let rx_storage = spawn_bg_active(current_page_shared.clone(), 8, 10, || storage::fetch_storage_state());
let rx_notifications = {
- let (tx, rx) = channel::<processes::NotificationsConfig>();
+ let (tx, rx) = channel::<system_info::NotificationsConfig>();
let current_page_shared = current_page_shared.clone();
tokio::spawn(async move {
let mut last_fetch: Option<std::time::Instant> = None;
loop {
let current_page = current_page_shared.load(Ordering::SeqCst);
- if current_page == 6 { // Processes is index 6
+ if current_page == 9 { // System is index 9
let should_fetch = match last_fetch {
None => true,
Some(t) => t.elapsed() >= std::time::Duration::from_secs(30),
};
if should_fetch {
- let val = tokio::task::spawn_blocking(|| processes::read_notifications_config()).await;
+ let val = tokio::task::spawn_blocking(|| system_info::read_notifications_config()).await;
if let Ok(val) = val {
if tx.send(val).is_err() { break; }
}