system settings
git clone https://git.lucas.co/cce-system-interface.git
Merge power page into hardware page and move system actions to system page
src/app.rs | 4 -
src/main.rs | 15 +-
src/pages/hardware.rs | 300 ++++++++++++++++++++++++++++++++++++++-
src/pages/mod.rs | 6 +-
src/pages/power.rs | 357 -----------------------------------------------
src/pages/system_info.rs | 56 +++++++-
6 files changed, 350 insertions(+), 388 deletions(-)
diff --git a/src/app.rs b/src/app.rs
index 0647030..f6c97b2 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -6,7 +6,6 @@ use crate::pages::input;
use crate::pages::layout;
use crate::pages::network;
use crate::pages::notifications;
-use crate::pages::power;
use crate::pages::hardware;
use crate::pages::status;
use crate::pages::storage;
@@ -19,7 +18,6 @@ use crate::pages::Page;
pub struct AppState {
pub current_page: Page,
- pub power: power::PowerState,
pub audio: audio::AudioState,
pub display: display::DisplayState,
pub network: network::NetworkState,
@@ -40,7 +38,6 @@ impl Default for AppState {
fn default() -> Self {
Self {
current_page: Page::ALL[0],
- power: power::PowerState::default(),
audio: audio::AudioState::default(),
display: display::DisplayState::default(),
network: network::NetworkState::default(),
@@ -61,7 +58,6 @@ impl Default for AppState {
#[derive(Debug, Clone)]
pub enum AppAction {
- Power(power::PowerMessage),
Audio(audio::AudioMessage),
Display(display::DisplayMessage),
Radios(network::NetworkMessage),
diff --git a/src/main.rs b/src/main.rs
index c457c19..cc16a3a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -211,7 +211,6 @@ struct SystemInterface {
cursor_x: f32,
cursor_y: f32,
- rx_power: std::sync::mpsc::Receiver<pages::power::PowerState>,
rx_audio: std::sync::mpsc::Receiver<pages::audio::AudioState>,
rx_display: std::sync::mpsc::Receiver<pages::display::DisplayState>,
rx_network: std::sync::mpsc::Receiver<pages::network::NetworkState>,
@@ -369,7 +368,6 @@ impl SystemInterface {
rx
}
- let rx_power = spawn_bg(5, || pages::power::fetch_power_state());
let rx_audio = spawn_bg(3, || pages::audio::fetch_audio_state());
let rx_display = spawn_bg(10, || pages::display::fetch_display_state());
let rx_network = spawn_bg(5, || pages::network::fetch_network_state());
@@ -462,7 +460,7 @@ impl SystemInterface {
sidebar_width: 140.0, header_height: 0.0, status_height: 0.0,
cursor_x: 0.0, cursor_y: 0.0,
scale_factor,
- rx_power, rx_audio, rx_display, rx_network, rx_layout, rx_input, rx_fingers,
+ rx_audio, rx_display, rx_network, rx_layout, rx_input, rx_fingers,
rx_hardware, rx_system, rx_status, rx_storage, rx_notifications,
rx_backup_state, rx_typeface, rx_services, rx_colors, tx_backup, rx_backup,
tx_color_selector, rx_color_selector,
@@ -861,7 +859,6 @@ impl SystemInterface {
.map(|c| clear_ui::widget::focus::is_focused(c))
.collect();
match self.app.current_page {
- Page::Power => power::view(&self.app.power, cx, cy, cw, ch),
Page::Audio => audio::view(&mut self.app.audio, cx, cy, cw, ch, &sec_focused),
Page::Display => display::view(&mut self.app.display, cx, cy, cw, ch),
Page::Radios => network::view(&mut self.app.network, cx, cy, cw, ch, root_focused),
@@ -938,10 +935,6 @@ impl SystemInterface {
fn poll_background_updates(&mut self) {
use pages::*;
- while let Ok(s) = self.rx_power.try_recv() {
- power::update(&mut self.app.power, power::PowerMessage::Refreshed(s));
- self.needs_rebuild = true;
- }
while let Ok(s) = self.rx_audio.try_recv() {
audio::update(&mut self.app.audio, audio::AudioMessage::Refreshed(s));
self.needs_rebuild = true;
@@ -1029,7 +1022,6 @@ impl SystemInterface {
fn handle_action(&mut self, action: &AppAction) {
use pages::*;
match action {
- AppAction::Power(m) => power::update(&mut self.app.power, m.clone()),
AppAction::Audio(m) => audio::update(&mut self.app.audio, m.clone()),
AppAction::Display(m) => display::update(&mut self.app.display, m.clone()),
AppAction::Radios(m) => network::update(&mut self.app.network, m.clone()),
@@ -2344,7 +2336,6 @@ struct App {
surface: Option<wl_surface::WlSurface>,
state: Option<SystemInterface>,
- initial_page: Page,
exit: bool,
redraw: bool,
ctrl_pressed: bool,
@@ -2756,7 +2747,6 @@ fn main() {
window: None,
surface: None,
state: None,
- initial_page,
exit: false,
redraw: true,
ctrl_pressed: false,
@@ -2772,7 +2762,7 @@ fn main() {
let pw = (820.0 * scale) as u32;
let ph = (680.0 * scale) as u32;
- let state = pollster::block_on(SystemInterface::new(
+ let mut state = pollster::block_on(SystemInterface::new(
&conn,
&qh,
&app.compositor_state,
@@ -2781,6 +2771,7 @@ fn main() {
ph,
scale,
));
+ state.app.current_page = initial_page;
app.window = Some(state.window.clone());
app.surface = Some(state.surface.clone());
app.state = Some(state);
diff --git a/src/pages/hardware.rs b/src/pages/hardware.rs
index 7b0d1f4..53d73d6 100644
--- a/src/pages/hardware.rs
+++ b/src/pages/hardware.rs
@@ -1,7 +1,20 @@
-use crate::app::PageContent;
+use crate::app::{AppAction, PageContent};
use clear_ui::layout::Section;
use clear_ui::widget::{Label, ScrollingList};
+#[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 HardwareState {
pub cpu_model: String,
@@ -13,6 +26,12 @@ pub struct HardwareState {
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,
}
impl Default for HardwareState {
@@ -27,6 +46,11 @@ impl Default for HardwareState {
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,
}
}
}
@@ -34,9 +58,118 @@ impl Default for HardwareState {
#[derive(Debug, Clone)]
pub enum HardwareMessage {
Refreshed(HardwareState),
+ SetCpuPerformance,
+ SetCpuPowersave,
+ SetGpuDefault,
+ SetGpuPowersave,
None,
}
+// ── 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/clear-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/clear-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()) {
@@ -188,6 +321,10 @@ pub async fn fetch_hardware_state() -> HardwareState {
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;
+
HardwareState {
cpu_model,
cpu_usage,
@@ -198,11 +335,22 @@ pub async fn fetch_hardware_state() -> HardwareState {
gpu_labels,
processes,
cpu_list_box: ScrollingList::new(24.0, 2.0),
+ battery,
+ on_ac,
+ cpu_powersave,
+ gpu_powersave,
}
}
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 BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
+const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
+const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
+const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 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 HardwareState, cx: f32, cy: f32, cw: f32, _ch: f32, root_focused: bool) -> PageContent {
let mut pc = PageContent::new();
@@ -237,12 +385,10 @@ pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, _ch: f32, root
pc.text("CPU %", list_box_x + list_box_w - 60.0, list_box_y + 5.0, 11.0, [0.53, 0.53, 0.60, 1.0]);
let row_h = 24.0;
- let row_gap = 2.0;
// Update ScrollingList bounds for the scrollable viewport (which starts below the header)
state.cpu_list_box.update_bounds(state.processes.len(), list_box_y + header_h, list_box_h - header_h - 6.0);
// Visible process rows rendering (virtualized/clipped)
-
for (idx, (pid, cpu, comm)) in state.processes.iter().enumerate() {
if let Some(draw_y) = state.cpu_list_box.get_item_draw_y(idx, 4.0) {
// Standard row action button (transparent background, highlights on hover)
@@ -255,7 +401,7 @@ pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, _ch: f32, root
[0.0, 0.0, 0.0, 0.0],
[1.0, 1.0, 1.0, 0.06],
[0.0, 0.0, 0.0, 0.0],
- crate::app::AppAction::Hardware(HardwareMessage::None),
+ AppAction::Hardware(HardwareMessage::None),
);
pc.text(pid, list_box_x + 12.0, draw_y + 6.0, 12.0, [0.80, 0.80, 0.85, 1.0]);
@@ -283,7 +429,130 @@ pub fn view(state: &mut HardwareState, cx: f32, cy: f32, cw: f32, _ch: f32, root
sec_gpu.widget(&mut pc, gpu_lbl, 12.0, cw - 24.0, 26.0);
}
}
- sec_gpu.finish(&mut pc);
+ y = sec_gpu.finish(&mut pc);
+
+ // ── Battery Section ──
+ let mut sec_bat = Section::new(&mut pc, cx, y, cw, "Battery");
+ if !state.loaded {
+ sec_bat.text(&mut pc, "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(&mut pc, &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(&mut pc, &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(&mut pc, &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(&mut pc, &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(&mut pc, ac_str, 12.0, 0.0, 14.0, TEXT_FG);
+ }
+ y = sec_bat.finish(&mut pc);
+
+ // ── CPU Governor section ──
+ let mut sec_gov = Section::new(&mut pc, cx, y, cw, "CPU Governor");
+ if !state.loaded {
+ sec_gov.text(&mut pc, "Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_gov.spacing(18.0);
+ } else {
+ let btn_h = 44.0;
+ let yt = sec_gov.ay();
+
+ sec_gov.row(2, 8.0, btn_h, |i, x, w| {
+ if i == 0 {
+ let perf_active = !state.cpu_powersave;
+ let (perf_bg, perf_desc, perf_desc_color) = if perf_active {
+ (BTN_ACTIVE, "Governor set to performance", ACCENT)
+ } else {
+ (BTN_INACTIVE, "Switch to performance governor", TEXT_DIM)
+ };
+
+ pc.button("Performance", x, yt, w, btn_h,
+ perf_bg, BTN_HOVER, WHITE,
+ AppAction::Hardware(HardwareMessage::SetCpuPerformance));
+ pc.text(perf_desc, x + 4.0, yt + 26.0, 10.0, perf_desc_color);
+ } else {
+ let (save_bg, save_desc, save_desc_color) = if state.cpu_powersave {
+ (BTN_ACTIVE, "Governor set to powersave — lower power, slower burst", ACCENT)
+ } else {
+ (BTN_INACTIVE, "Switch to powersave governor (requires auth)", TEXT_DIM)
+ };
+
+ pc.button("Powersave", x, yt, w, btn_h,
+ save_bg, BTN_HOVER, WHITE,
+ AppAction::Hardware(HardwareMessage::SetCpuPowersave));
+ pc.text(save_desc, x + 4.0, yt + 26.0, 10.0, save_desc_color);
+ }
+ });
+ sec_gov.spacing(12.0);
+ }
+ y = sec_gov.finish(&mut pc);
+
+ // ── GPU Power section ──
+ let mut sec_gpow = Section::new(&mut pc, cx, y, cw, "GPU Power");
+ if !state.loaded {
+ sec_gpow.text(&mut pc, "Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec_gpow.spacing(18.0);
+ } else {
+ let btn_h = 44.0;
+ let yt = sec_gpow.ay();
+
+ sec_gpow.row(2, 8.0, btn_h, |i, x, w| {
+ if i == 0 {
+ let gpu_def_active = !state.gpu_powersave;
+ let (gpu_def_bg, gpu_def_desc, gpu_def_desc_c) = if gpu_def_active {
+ (BTN_ACTIVE, "NVIDIA running at default power limit", ACCENT)
+ } else {
+ (BTN_INACTIVE, "Restore default power limit (requires auth)", TEXT_DIM)
+ };
+
+ pc.button("80W Default", x, yt, w, btn_h,
+ gpu_def_bg, BTN_HOVER, WHITE,
+ AppAction::Hardware(HardwareMessage::SetGpuDefault));
+ pc.text(gpu_def_desc, x + 4.0, yt + 26.0, 10.0, gpu_def_desc_c);
+ } else {
+ let (gpu_cap_bg, gpu_cap_desc, gpu_cap_desc_c) = if state.gpu_powersave {
+ (BTN_ACTIVE, "NVIDIA power limit capped at 5W — minimal draw", ACCENT)
+ } else {
+ (BTN_INACTIVE, "Cap NVIDIA to 5W power limit (requires auth)", TEXT_DIM)
+ };
+
+ pc.button("5W Cap", x, yt, w, btn_h,
+ gpu_cap_bg, BTN_HOVER, WHITE,
+ AppAction::Hardware(HardwareMessage::SetGpuPowersave));
+ pc.text(gpu_cap_desc, x + 4.0, yt + 26.0, 10.0, gpu_cap_desc_c);
+ }
+ });
+ sec_gpow.spacing(12.0);
+ }
+ sec_gpow.finish(&mut pc);
pc
}
@@ -302,6 +571,27 @@ pub fn update(state: &mut HardwareState, msg: HardwareMessage) {
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;
+ }
+ HardwareMessage::SetCpuPerformance => {
+ state.cpu_powersave = false;
+ spawn_cpu_power(false);
+ }
+ HardwareMessage::SetCpuPowersave => {
+ state.cpu_powersave = true;
+ spawn_cpu_power(true);
+ }
+ HardwareMessage::SetGpuDefault => {
+ state.gpu_powersave = false;
+ spawn_gpu_power(false);
+ }
+ HardwareMessage::SetGpuPowersave => {
+ state.gpu_powersave = true;
+ spawn_gpu_power(true);
}
HardwareMessage::None => {}
}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index ce677a1..c1c82e4 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -1,4 +1,3 @@
-pub mod power;
pub mod audio;
pub mod network;
pub mod display;
@@ -17,7 +16,6 @@ pub mod colors;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Page {
- Power,
Audio,
Radios,
Services,
@@ -35,7 +33,7 @@ pub enum Page {
}
impl Page {
- pub const ALL: [Page; 15] = [
+ pub const ALL: [Page; 14] = [
Page::Audio,
Page::Backup,
Page::Colors,
@@ -44,7 +42,6 @@ impl Page {
Page::Layout,
Page::Notifications,
Page::Hardware,
- Page::Power,
Page::Radios,
Page::Services,
Page::Status,
@@ -55,7 +52,6 @@ impl Page {
pub fn label(self) -> &'static str {
match self {
- Page::Power => "Power",
Page::Audio => "Audio",
Page::Radios => "Radios",
Page::Services => "Services",
diff --git a/src/pages/power.rs b/src/pages/power.rs
deleted file mode 100644
index 0f37df6..0000000
--- a/src/pages/power.rs
+++ /dev/null
@@ -1,357 +0,0 @@
-use crate::app::{AppAction, PageContent};
-use clear_ui::layout::Section;
-
-#[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, Default)]
-pub struct PowerState {
- pub loaded: bool,
- pub battery: BatteryInfo,
- pub on_ac: bool,
- pub cpu_powersave: bool,
- pub gpu_powersave: bool,
-}
-
-#[derive(Debug, Clone)]
-pub enum PowerMessage {
- Refreshed(PowerState),
- SetCpuPerformance,
- SetCpuPowersave,
- SetGpuDefault,
- SetGpuPowersave,
- Suspend,
- Hibernate,
- Reboot,
- PowerOff,
- Tick,
-}
-
-// ── 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_systemctl(action: &str) {
- let _ = tokio::process::Command::new("systemctl").arg(action).spawn();
-}
-
-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/clear-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/clear-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)
-}
-
-pub async fn fetch_power_state() -> PowerState {
- let (battery, on_ac) = fetch_upower().await;
- let cpu_powersave = current_cpu_governor() == "powersave";
- let gpu_powersave = current_gpu_power_cap().await;
- PowerState { loaded: true, battery, on_ac, cpu_powersave, gpu_powersave }
-}
-
-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)
-}
-
-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 BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
-const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 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];
-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: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
- let mut pc = PageContent::new();
- let mut y = cy + 12.0;
-
- // ── Battery section ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "Battery");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading battery status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.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.text(&mut pc, &pct_str, 12.0, 0.0, 24.0, pct_color);
- sec.spacing(30.0);
-
- let state_str = format!("{} • {:.1}W • {:.1}/{:.1} Wh",
- bat.state, bat.energy_rate, bat.energy, bat.energy_full);
- sec.text(&mut pc, &state_str, 12.0, 0.0, 12.0, TEXT_DIM);
- sec.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.text(&mut pc, &time_str, 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- }
-
- let detail_str = format!("{} {}", bat.vendor, bat.model);
- sec.text(&mut pc, &detail_str, 12.0, 0.0, 11.0, TEXT_DIM);
- sec.spacing(20.0);
-
- let ac_str = if state.on_ac { "On AC Power" } else { "On Battery" };
- sec.text(&mut pc, ac_str, 12.0, 0.0, 14.0, TEXT_FG);
- }
-
- y = sec.finish(&mut pc);
-
- // ── CPU Governor section ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "CPU Governor");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let btn_h = 44.0;
- let yt = sec.ay();
-
- sec.row(2, 8.0, btn_h, |i, x, w| {
- if i == 0 {
- let perf_active = !state.cpu_powersave;
- let (perf_bg, perf_desc, perf_desc_color) = if perf_active {
- (BTN_ACTIVE, "Governor set to performance", ACCENT)
- } else {
- (BTN_INACTIVE, "Switch to performance governor", TEXT_DIM)
- };
-
- pc.button("Performance", x, yt, w, btn_h,
- perf_bg, BTN_HOVER, WHITE,
- AppAction::Power(PowerMessage::SetCpuPerformance));
- pc.text(perf_desc, x + 4.0, yt + 26.0, 10.0, perf_desc_color);
- } else {
- let (save_bg, save_desc, save_desc_color) = if state.cpu_powersave {
- (BTN_ACTIVE, "Governor set to powersave — lower power, slower burst", ACCENT)
- } else {
- (BTN_INACTIVE, "Switch to powersave governor (requires auth)", TEXT_DIM)
- };
-
- pc.button("Powersave", x, yt, w, btn_h,
- save_bg, BTN_HOVER, WHITE,
- AppAction::Power(PowerMessage::SetCpuPowersave));
- pc.text(save_desc, x + 4.0, yt + 26.0, 10.0, save_desc_color);
- }
- });
- sec.spacing(12.0);
- }
- y = sec.finish(&mut pc);
-
- // ── GPU Power section ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "GPU Power");
-
- if !state.loaded {
- sec.text(&mut pc, "Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(18.0);
- } else {
- let btn_h = 44.0;
- let yt = sec.ay();
-
- sec.row(2, 8.0, btn_h, |i, x, w| {
- if i == 0 {
- let gpu_def_active = !state.gpu_powersave;
- let (gpu_def_bg, gpu_def_desc, gpu_def_desc_c) = if gpu_def_active {
- (BTN_ACTIVE, "NVIDIA running at default power limit", ACCENT)
- } else {
- (BTN_INACTIVE, "Restore default power limit (requires auth)", TEXT_DIM)
- };
-
- pc.button("80W Default", x, yt, w, btn_h,
- gpu_def_bg, BTN_HOVER, WHITE,
- AppAction::Power(PowerMessage::SetGpuDefault));
- pc.text(gpu_def_desc, x + 4.0, yt + 26.0, 10.0, gpu_def_desc_c);
- } else {
- let (gpu_cap_bg, gpu_cap_desc, gpu_cap_desc_c) = if state.gpu_powersave {
- (BTN_ACTIVE, "NVIDIA power limit capped at 5W — minimal draw", ACCENT)
- } else {
- (BTN_INACTIVE, "Cap NVIDIA to 5W power limit (requires auth)", TEXT_DIM)
- };
-
- pc.button("5W Cap", x, yt, w, btn_h,
- gpu_cap_bg, BTN_HOVER, WHITE,
- AppAction::Power(PowerMessage::SetGpuPowersave));
- pc.text(gpu_cap_desc, x + 4.0, yt + 26.0, 10.0, gpu_cap_desc_c);
- }
- });
- sec.spacing(12.0);
- }
- y = sec.finish(&mut pc);
-
- // ── System Actions section ──
- let mut sec = Section::new(&mut pc, cx, y, cw, "System Actions");
-
- let yt = sec.ay();
- let act_btn_h = 32.0;
-
- sec.row(4, 8.0, act_btn_h, |i, x, w| {
- match i {
- 0 => {
- pc.button("Suspend", x, yt, w, act_btn_h,
- SAFE_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Suspend));
- }
- 1 => {
- pc.button("Hibernate", x, yt, w, act_btn_h,
- SAFE_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Hibernate));
- }
- 2 => {
- pc.button("Reboot", x, yt, w, act_btn_h,
- DANGER_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Reboot));
- }
- 3 => {
- pc.button("Power Off", x, yt, w, act_btn_h,
- DANGER_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::PowerOff));
- }
- _ => {}
- }
- });
- sec.spacing(12.0);
- sec.finish(&mut pc);
-
- pc
-}
-
-pub fn update(state: &mut PowerState, msg: PowerMessage) {
- match msg {
- PowerMessage::Refreshed(new) => { *state = new; }
- PowerMessage::SetCpuPerformance => {
- state.cpu_powersave = false;
- spawn_cpu_power(false);
- }
- PowerMessage::SetCpuPowersave => {
- state.cpu_powersave = true;
- spawn_cpu_power(true);
- }
- PowerMessage::SetGpuDefault => {
- state.gpu_powersave = false;
- spawn_gpu_power(false);
- }
- PowerMessage::SetGpuPowersave => {
- state.gpu_powersave = true;
- spawn_gpu_power(true);
- }
- PowerMessage::Suspend => spawn_systemctl("suspend"),
- PowerMessage::Hibernate => spawn_systemctl("hibernate"),
- PowerMessage::Reboot => spawn_systemctl("reboot"),
- PowerMessage::PowerOff => spawn_systemctl("poweroff"),
- PowerMessage::Tick => {} // handled externally
- }
-}
diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index 3e7929e..1b9e1cf 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -1,4 +1,4 @@
-use crate::app::PageContent;
+use crate::app::{AppAction, PageContent};
use clear_ui::layout::Section;
#[derive(Debug, Clone, Default)]
@@ -12,6 +12,10 @@ pub struct SystemState {
#[derive(Debug, Clone)]
pub enum SystemMessage {
Refreshed(SystemState),
+ Suspend,
+ Hibernate,
+ Reboot,
+ PowerOff,
}
pub async fn fetch_system_state() -> SystemState {
@@ -35,10 +39,18 @@ pub async fn fetch_system_state() -> SystemState {
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 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) -> PageContent {
let mut pc = PageContent::new();
- let y = cy + 12.0;
+ let mut y = cy + 12.0;
let mut sec = Section::new(&mut pc, cx, y, cw, "System");
if !state.loaded {
@@ -49,13 +61,47 @@ pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCon
sec.spacing(10.0);
sec.text(&mut pc, &format!("Uptime: {}", state.uptime), 12.0, 0.0, 12.0, TEXT_DIM);
}
- sec.finish(&mut pc);
+ y = sec.finish(&mut pc);
+
+ // ── System Actions section ──
+ let mut sec_act = Section::new(&mut pc, cx, y, cw, "System Actions");
+
+ let yt = sec_act.ay();
+ let act_btn_h = 32.0;
+
+ sec_act.row(4, 8.0, act_btn_h, |i, x, w| {
+ match i {
+ 0 => {
+ pc.button("Suspend", x, yt, w, act_btn_h,
+ SAFE_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Suspend));
+ }
+ 1 => {
+ pc.button("Hibernate", x, yt, w, act_btn_h,
+ SAFE_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Hibernate));
+ }
+ 2 => {
+ pc.button("Reboot", x, yt, w, act_btn_h,
+ DANGER_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::Reboot));
+ }
+ 3 => {
+ pc.button("Power Off", x, yt, w, act_btn_h,
+ DANGER_BG, BTN_HOVER, WHITE, AppAction::SystemInfo(SystemMessage::PowerOff));
+ }
+ _ => {}
+ }
+ });
+ sec_act.spacing(12.0);
+ sec_act.finish(&mut pc);
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 = new; }
+ SystemMessage::Suspend => spawn_systemctl("suspend"),
+ SystemMessage::Hibernate => spawn_systemctl("hibernate"),
+ SystemMessage::Reboot => spawn_systemctl("reboot"),
+ SystemMessage::PowerOff => spawn_systemctl("poweroff"),
}
}