system settings
git clone https://git.lucas.co/cce-system-interface.git
feat: separate UI processes from startup polling and add offset, edge gap, and top gap controls to Cascade section
src/main.rs | 106 +++++++++++++++++++-------
src/pages/audio.rs | 169 ++++++++++++++++++++++--------------------
src/pages/display.rs | 83 ++++++++++++---------
src/pages/layout.rs | 66 ++++++++++++++++-
src/pages/network.rs | 122 ++++++++++++++++--------------
src/pages/power.rs | 188 ++++++++++++++++++++++++++---------------------
src/pages/processors.rs | 26 +++++--
src/pages/status.rs | 64 ++++++++--------
src/pages/storage.rs | 88 ++++++++++++----------
src/pages/system_info.rs | 20 +++--
10 files changed, 568 insertions(+), 364 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index ceb52e1..1f1316b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -218,33 +218,11 @@ impl SystemInterface {
mapped_at_creation: false,
});
- // ── Initial state (fetch all concurrently) ──
- let (power, audio, display, network, system_info, status, storage, processors) = tokio::join!(
- pages::power::fetch_power_state(),
- pages::audio::fetch_audio_state(),
- pages::display::fetch_display_state(),
- pages::network::fetch_network_state(),
- pages::system_info::fetch_system_state(),
- pages::status::fetch_status_state(),
- pages::storage::fetch_storage_state(),
- pages::processors::fetch_processors_state(),
- );
- let mut app = AppState {
- power,
- audio,
- display,
- network,
+ let app = AppState {
layout: pages::layout::read_layout_config(),
input: pages::input::read_input_config(),
- processors,
- system_info,
- status,
- storage,
- current_page: Page::ALL[0],
+ ..Default::default()
};
- // Init spinbox vectors to match fetched sinks/sources
- app.audio.sink_spinboxes.resize_with(app.audio.sinks.len(), || Spinbox::new(50, 0, 100, 1));
- app.audio.source_spinboxes.resize_with(app.audio.sources.len(), || Spinbox::new(50, 0, 100, 1));
// ── Background refresh channels ──
fn spawn_bg<T, F>(period_secs: u64, f: fn() -> F) -> std::sync::mpsc::Receiver<T>
@@ -255,9 +233,9 @@ impl SystemInterface {
let (tx, rx) = std::sync::mpsc::channel::<T>();
tokio::spawn(async move {
loop {
- tokio::time::sleep(std::time::Duration::from_secs(period_secs)).await;
let val = f().await;
if tx.send(val).is_err() { break; }
+ tokio::time::sleep(std::time::Duration::from_secs(period_secs)).await;
}
});
rx
@@ -271,9 +249,9 @@ impl SystemInterface {
let (tx, rx) = std::sync::mpsc::channel::<pages::layout::LayoutState>();
tokio::spawn(async move {
loop {
- tokio::time::sleep(std::time::Duration::from_secs(30)).await;
let val = tokio::task::spawn_blocking(|| pages::layout::read_layout_config()).await;
if let Ok(val) = val { if tx.send(val).is_err() { break; } }
+ tokio::time::sleep(std::time::Duration::from_secs(30)).await;
}
});
rx
@@ -282,9 +260,9 @@ impl SystemInterface {
let (tx, rx) = std::sync::mpsc::channel::<pages::input::InputState>();
tokio::spawn(async move {
loop {
- tokio::time::sleep(std::time::Duration::from_secs(30)).await;
let val = tokio::task::spawn_blocking(|| pages::input::read_input_config()).await;
if let Ok(val) = val { if tx.send(val).is_err() { break; } }
+ tokio::time::sleep(std::time::Duration::from_secs(30)).await;
}
});
rx
@@ -642,6 +620,15 @@ impl SystemInterface {
changed = true;
}
}
+ if self.app.layout.cascade_offset_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
+ if self.app.layout.edge_gap_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
+ if self.app.layout.top_gap_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
for cp in &mut self.app.layout.color_selectors {
if cp.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
changed = true;
@@ -686,11 +673,50 @@ impl SystemInterface {
if self.app.current_page == Page::Layout {
let mut changed = false;
let mut actions = Vec::new();
- for sb in &mut self.app.layout.spinboxes {
+ for (i, sb) in self.app.layout.spinboxes.iter_mut().enumerate() {
+ let old = sb.value;
if sb.keyboard_input(event) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetWidth(
+ pages::layout::WidthParam::ALL[i],
+ sb.value as u16,
+ )
+ ));
+ }
changed = true;
}
}
+ let sb = &mut self.app.layout.cascade_offset_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetCascadeOffset(sb.value as u16)
+ ));
+ }
+ changed = true;
+ }
+ let sb = &mut self.app.layout.edge_gap_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetEdgeGap(sb.value as u16)
+ ));
+ }
+ changed = true;
+ }
+ let sb = &mut self.app.layout.top_gap_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetTopGap(sb.value as u16)
+ ));
+ }
+ changed = true;
+ }
for (i, cp) in self.app.layout.color_selectors.iter_mut().enumerate() {
let old = cp.color;
if cp.keyboard_input(event) {
@@ -806,6 +832,30 @@ impl SystemInterface {
));
}
}
+ let sb = &mut self.app.layout.cascade_offset_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetCascadeOffset(sb.value as u16)
+ ));
+ }
+ let sb = &mut self.app.layout.edge_gap_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetEdgeGap(sb.value as u16)
+ ));
+ }
+ let sb = &mut self.app.layout.top_gap_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Layout(
+ pages::layout::LayoutMessage::SetTopGap(sb.value as u16)
+ ));
+ }
for (i, cp) in self.app.layout.color_selectors.iter_mut().enumerate() {
let old = cp.color;
if !cp.hit_test(lx, ly) { cp.unfocus(); }
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index 4af8283..bfac176 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -22,6 +22,7 @@ pub struct AudioSource {
#[derive(Debug, Clone, Default)]
pub struct AudioState {
+ pub loaded: bool,
pub sinks: Vec<AudioSink>,
pub sources: Vec<AudioSource>,
pub sink_spinboxes: Vec<Spinbox>,
@@ -117,7 +118,7 @@ pub async fn fetch_audio_state() -> AudioState {
let connected = drm_connected_ports();
let sinks = fetch_sinks(&connected).await;
let sources = fetch_sources(&connected).await;
- AudioState { sinks, sources, sink_spinboxes: Vec::new(), source_spinboxes: Vec::new() }
+ AudioState { loaded: true, sinks, sources, sink_spinboxes: Vec::new(), source_spinboxes: Vec::new() }
}
async fn fetch_sinks(connected_ports: &[String]) -> Vec<AudioSink> {
@@ -213,49 +214,54 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Page
// ── Output section ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Output");
- if state.sinks.is_empty() {
+ if !state.loaded {
+ sec.text(&mut pc, "Loading output devices...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else if state.sinks.is_empty() {
sec.text(&mut pc, "No output devices found", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
}
- for (idx, sink) in state.sinks.iter().enumerate() {
- let label = if !sink.active {
- format!("{} (inactive)", sink.name)
- } else if sink.muted {
- format!("{} {:.0}% (muted)", sink.name, sink.volume * 100.0)
- } else {
- format!("{} {:.0}%", sink.name, sink.volume * 100.0)
- };
- let lc = if sink.muted { RED } else { TEXT_FG };
- sec.text(&mut pc, &label, 14.0, 0.0, 13.0, lc);
- sec.spacing(18.0);
-
- if sink.active {
- let bar_w = cw - 100.0;
- let bar_x = 14.0;
- let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * sink.volume, 8.0);
- sec.text(&mut pc, &format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
-
- let row_y = sec.ay() + 12.0;
- let sb_w = 100.0;
- let sb_h = 26.0;
- let mute_w = 60.0;
- let gap = 8.0;
-
- state.sink_spinboxes[idx].value = (sink.volume * 100.0).round() as i32;
- render_widget(&mut pc, &mut state.sink_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
-
- let mute_label = if sink.muted { "Unmute" } else { "Mute" };
- let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
- pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
- mute_col, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SinkMute(sink.id)));
-
- sec.content_y += 12.0 + sb_h + 6.0;
- } else {
- sec.content_y += 6.0;
+ if state.loaded {
+ for (idx, sink) in state.sinks.iter().enumerate() {
+ let label = if !sink.active {
+ format!("{} (inactive)", sink.name)
+ } else if sink.muted {
+ format!("{} {:.0}% (muted)", sink.name, sink.volume * 100.0)
+ } else {
+ format!("{} {:.0}%", sink.name, sink.volume * 100.0)
+ };
+ let lc = if sink.muted { RED } else { TEXT_FG };
+ sec.text(&mut pc, &label, 14.0, 0.0, 13.0, lc);
+ sec.spacing(18.0);
+
+ if sink.active {
+ let bar_w = cw - 100.0;
+ let bar_x = 14.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * sink.volume, 8.0);
+ sec.text(&mut pc, &format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
+
+ let row_y = sec.ay() + 12.0;
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ let mute_w = 60.0;
+ let gap = 8.0;
+
+ state.sink_spinboxes[idx].value = (sink.volume * 100.0).round() as i32;
+ render_widget(&mut pc, &mut state.sink_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
+
+ let mute_label = if sink.muted { "Unmute" } else { "Mute" };
+ let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
+ pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
+ mute_col, BTN_HOVER, WHITE,
+ AppAction::Audio(AudioMessage::SinkMute(sink.id)));
+
+ sec.content_y += 12.0 + sb_h + 6.0;
+ } else {
+ sec.content_y += 6.0;
+ }
}
}
@@ -264,49 +270,54 @@ pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Page
// ── Input section ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Input");
- if state.sources.is_empty() {
+ if !state.loaded {
+ sec.text(&mut pc, "Loading input devices...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else if state.sources.is_empty() {
sec.text(&mut pc, "No input devices found", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
}
- for (idx, src) in state.sources.iter().enumerate() {
- let label = if !src.active {
- format!("{} (inactive)", src.name)
- } else if src.muted {
- format!("{} {:.0}% (muted)", src.name, src.volume * 100.0)
- } else {
- format!("{} {:.0}%", src.name, src.volume * 100.0)
- };
- let lc = if src.muted { RED } else { TEXT_FG };
- sec.text(&mut pc, &label, 14.0, 0.0, 13.0, lc);
- sec.spacing(18.0);
-
- if src.active {
- let bar_w = cw - 100.0;
- let bar_x = 14.0;
- let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * src.volume, 8.0);
- sec.text(&mut pc, &format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
-
- let row_y = sec.ay() + 12.0;
- let sb_w = 100.0;
- let sb_h = 26.0;
- let mute_w = 60.0;
- let gap = 8.0;
-
- state.source_spinboxes[idx].value = (src.volume * 100.0).round() as i32;
- render_widget(&mut pc, &mut state.source_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
-
- let mute_label = if src.muted { "Unmute" } else { "Mute" };
- let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
- pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
- mute_col, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SourceMute(src.id)));
-
- sec.content_y += 12.0 + sb_h + 6.0;
- } else {
- sec.content_y += 6.0;
+ if state.loaded {
+ for (idx, src) in state.sources.iter().enumerate() {
+ let label = if !src.active {
+ format!("{} (inactive)", src.name)
+ } else if src.muted {
+ format!("{} {:.0}% (muted)", src.name, src.volume * 100.0)
+ } else {
+ format!("{} {:.0}%", src.name, src.volume * 100.0)
+ };
+ let lc = if src.muted { RED } else { TEXT_FG };
+ sec.text(&mut pc, &label, 14.0, 0.0, 13.0, lc);
+ sec.spacing(18.0);
+
+ if src.active {
+ let bar_w = cw - 100.0;
+ let bar_x = 14.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * src.volume, 8.0);
+ sec.text(&mut pc, &format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
+
+ let row_y = sec.ay() + 12.0;
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ let mute_w = 60.0;
+ let gap = 8.0;
+
+ state.source_spinboxes[idx].value = (src.volume * 100.0).round() as i32;
+ render_widget(&mut pc, &mut state.source_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
+
+ let mute_label = if src.muted { "Unmute" } else { "Mute" };
+ let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
+ pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
+ mute_col, BTN_HOVER, WHITE,
+ AppAction::Audio(AudioMessage::SourceMute(src.id)));
+
+ sec.content_y += 12.0 + sb_h + 6.0;
+ } else {
+ sec.content_y += 6.0;
+ }
}
}
diff --git a/src/pages/display.rs b/src/pages/display.rs
index cf47cdd..aaeac19 100644
--- a/src/pages/display.rs
+++ b/src/pages/display.rs
@@ -13,6 +13,7 @@ pub struct DisplayOutput {
#[derive(Debug, Clone)]
pub struct DisplayState {
+ pub loaded: bool,
pub brightness: f32,
pub max_brightness: f32,
pub outputs: Vec<DisplayOutput>,
@@ -23,6 +24,7 @@ pub struct DisplayState {
impl Default for DisplayState {
fn default() -> Self {
Self {
+ loaded: false,
brightness: 0.0,
max_brightness: 0.0,
outputs: Vec::new(),
@@ -46,6 +48,7 @@ pub async fn fetch_display_state() -> DisplayState {
(brightness / max_brightness * 100.0).round() as i32
} else { 50 };
DisplayState {
+ loaded: true,
brightness, max_brightness, outputs, night_light,
brightness_spinbox: Spinbox::new(pct, 0, 100, 5),
}
@@ -128,49 +131,63 @@ pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pa
// ── Brightness ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Brightness");
- let bright_pct = if state.max_brightness > 0.0 {
- (state.brightness / state.max_brightness * 100.0).round() as i32
- } else { 0 };
-
- let bar_w = cw - 100.0;
- let yt = sec.ay();
- pc.rect(BLANK_BAR, sec.ax(12.0), yt, bar_w, 8.0);
- pc.rect(FILL_BAR, sec.ax(12.0), yt, bar_w * bright_pct as f32 / 100.0, 8.0);
- pc.text(&format!("{}%", bright_pct), sec.ax(16.0 + bar_w), yt - 2.0, 11.0, TEXT_DIM);
- sec.content_y += 14.0;
-
- let yt = sec.ay();
- let sb_w = 100.0;
- let sb_h = 26.0;
- state.brightness_spinbox.value = bright_pct;
- render_widget(&mut pc, &mut state.brightness_spinbox, sec.ax(12.0), yt, sb_w, sb_h);
- sec.content_y += sb_h + 12.0;
+ if !state.loaded {
+ sec.text(&mut pc, "Loading display settings...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
+ } else {
+ let bright_pct = if state.max_brightness > 0.0 {
+ (state.brightness / state.max_brightness * 100.0).round() as i32
+ } else { 0 };
+
+ let bar_w = cw - 100.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(12.0), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(12.0), yt, bar_w * bright_pct as f32 / 100.0, 8.0);
+ pc.text(&format!("{}%", bright_pct), sec.ax(16.0 + bar_w), yt - 2.0, 11.0, TEXT_DIM);
+ sec.content_y += 14.0;
+
+ let yt = sec.ay();
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ state.brightness_spinbox.value = bright_pct;
+ render_widget(&mut pc, &mut state.brightness_spinbox, sec.ax(12.0), yt, sb_w, sb_h);
+ sec.content_y += sb_h + 12.0;
+ }
y = sec.finish(&mut pc);
// ── Night Light ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Night Light");
- let nl_label = if state.night_light { "Night Light: ON" } else { "Night Light: OFF" };
- sec.text(&mut pc, nl_label, 12.0, 0.0, 13.0, TEXT_FG);
+ if !state.loaded {
+ sec.text(&mut pc, "Loading...", 12.0, 0.0, 12.0, TEXT_DIM);
+ } else {
+ let nl_label = if state.night_light { "Night Light: ON" } else { "Night Light: OFF" };
+ sec.text(&mut pc, nl_label, 12.0, 0.0, 13.0, TEXT_FG);
+ }
y = sec.finish(&mut pc);
// ── Outputs ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Outputs");
- for out in &state.outputs {
- if out.connected {
- let scale_info = if out.scale > 1.0 {
- if let Some((w_str, h_str)) = out.resolution.rsplit_once('x') {
- if let (Ok(w), Ok(h)) = (w_str.parse::<u32>(), h_str.parse::<u32>()) {
- format!(" logical {:.0}x{:.0} | scale {:.0}x", w as f32 / out.scale, h as f32 / out.scale, out.scale)
- } else { format!(" scale {:.0}x", out.scale) }
- } else { String::new() }
- } else { String::new() };
- sec.text(&mut pc, &format!("{} {} @ {}Hz{}", out.name, out.resolution, out.refresh, scale_info),
- 14.0, 0.0, 12.0, TEXT_FG);
- } else {
- sec.text(&mut pc, &format!("{} (disconnected)", out.name), 14.0, 0.0, 12.0, TEXT_DIM);
- }
+ if !state.loaded {
+ sec.text(&mut pc, "Loading outputs...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
+ } else {
+ for out in &state.outputs {
+ if out.connected {
+ let scale_info = if out.scale > 1.0 {
+ if let Some((w_str, h_str)) = out.resolution.rsplit_once('x') {
+ if let (Ok(w), Ok(h)) = (w_str.parse::<u32>(), h_str.parse::<u32>()) {
+ format!(" logical {:.0}x{:.0} | scale {:.0}x", w as f32 / out.scale, h as f32 / out.scale, out.scale)
+ } else { format!(" scale {:.0}x", out.scale) }
+ } else { String::new() }
+ } else { String::new() };
+ sec.text(&mut pc, &format!("{} {} @ {}Hz{}", out.name, out.resolution, out.refresh, scale_info),
+ 14.0, 0.0, 12.0, TEXT_FG);
+ } else {
+ sec.text(&mut pc, &format!("{} (disconnected)", out.name), 14.0, 0.0, 12.0, TEXT_DIM);
+ }
+ sec.spacing(18.0);
+ }
}
sec.finish(&mut pc);
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index bf1c279..cfd0d2a 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -61,8 +61,14 @@ pub struct LayoutState {
pub vsplit_border_width: u16,
pub hsplit_border_width: u16,
pub floating_border_width: u16,
+ pub cascade_offset: u16,
+ pub edge_gap: u16,
+ pub top_gap: u16,
pub color_options: Vec<(&'static str, [u8; 3])>,
pub spinboxes: Vec<Spinbox>,
+ pub cascade_offset_spinbox: Spinbox,
+ pub edge_gap_spinbox: Spinbox,
+ pub top_gap_spinbox: Spinbox,
pub color_selectors: Vec<ColorSelector>,
}
@@ -77,8 +83,14 @@ impl Default for LayoutState {
vsplit_border_width: 6,
hsplit_border_width: 6,
floating_border_width: 6,
+ cascade_offset: 20,
+ edge_gap: 48,
+ top_gap: 48,
color_options: preset_colors(),
spinboxes: make_spinboxes(0, 6, 6, 6, 6, 6),
+ cascade_offset_spinbox: Spinbox::new(20, 0, 200, 1),
+ edge_gap_spinbox: Spinbox::new(48, 0, 200, 1),
+ top_gap_spinbox: Spinbox::new(48, 0, 200, 1),
color_selectors: vec![
ColorSelector::new([0x0a, 0x1a, 0x0e]).with_label("Desktop Background"),
ColorSelector::new([0x3e, 0x3e, 0x3e]).with_label("Border Color"),
@@ -94,6 +106,9 @@ pub enum LayoutMessage {
PickBackgroundColor,
PickBorderColor,
SetWidth(WidthParam, u16),
+ SetCascadeOffset(u16),
+ SetEdgeGap(u16),
+ SetTopGap(u16),
Refreshed(LayoutState),
}
@@ -122,6 +137,9 @@ pub fn read_layout_config() -> LayoutState {
let v = parse_u16_from(&content, "vsplit_border_width", 6);
let h = parse_u16_from(&content, "hsplit_border_width", 6);
let fl = parse_u16_from(&content, "floating_border_width", 6);
+ let co = parse_u16_from(&content, "cascade_offset", 20);
+ let gl = parse_u16_from(&content, "gap_left", 48);
+ let gt = parse_u16_from(&content, "gap_top", 48);
LayoutState {
background_color: parse_color_from_key(&content, "background_color", [0x0a, 0x1a, 0x0e]),
border_color: parse_color_from_key(&content, "border_color", [0x3e, 0x3e, 0x3e]),
@@ -131,8 +149,14 @@ pub fn read_layout_config() -> LayoutState {
vsplit_border_width: v,
hsplit_border_width: h,
floating_border_width: fl,
+ cascade_offset: co,
+ edge_gap: gl,
+ top_gap: gt,
color_options: preset_colors(),
spinboxes: make_spinboxes(fs, ca, g, v, h, fl),
+ cascade_offset_spinbox: Spinbox::new(co as i32, 0, 200, 1),
+ edge_gap_spinbox: Spinbox::new(gl as i32, 0, 200, 1),
+ top_gap_spinbox: Spinbox::new(gt as i32, 0, 200, 1),
color_selectors: vec![
ColorSelector::new(parse_color_from_key(&content, "background_color", [0x0a, 0x1a, 0x0e]))
.with_label("Desktop Background"),
@@ -230,6 +254,11 @@ fn apply_all_widths(s: &LayoutState) {
w("vsplit_border_width", s.vsplit_border_width);
w("hsplit_border_width", s.hsplit_border_width);
w("floating_border_width", s.floating_border_width);
+ w("cascade_offset", s.cascade_offset);
+ w("gap_left", s.edge_gap);
+ w("gap_right", s.edge_gap);
+ w("gap_bottom", s.edge_gap);
+ w("gap_top", s.top_gap);
}
pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
@@ -246,14 +275,25 @@ pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pag
sec.widget(&mut pc, &mut state.color_selectors[1], 12.0, 220.0, 22.0);
y = sec.finish(&mut pc);
- let mut sec = Section::new(&mut pc, cx, y, cw, "Border Width");
- sec.spacing(8.0);
for (i, param) in WidthParam::ALL.iter().enumerate() {
- state.spinboxes[i].set_label(param.label());
+ let mut sec = Section::new(&mut pc, cx, y, cw, param.label());
+ sec.spacing(8.0);
+ state.spinboxes[i].set_label("Border Width");
sec.widget(&mut pc, &mut state.spinboxes[i], 14.0, 200.0, 26.0);
+ if *param == WidthParam::Cascade {
+ sec.spacing(8.0);
+ state.cascade_offset_spinbox.set_label("Offset");
+ sec.widget(&mut pc, &mut state.cascade_offset_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ state.edge_gap_spinbox.set_label("Edge Gap");
+ sec.widget(&mut pc, &mut state.edge_gap_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ state.top_gap_spinbox.set_label("Top Gap");
+ sec.widget(&mut pc, &mut state.top_gap_spinbox, 14.0, 200.0, 26.0);
+ }
sec.spacing(8.0);
+ y = sec.finish(&mut pc);
}
- sec.finish(&mut pc);
pc
}
@@ -295,6 +335,24 @@ pub fn update(state: &mut LayoutState, msg: LayoutMessage) {
}
LayoutMessage::PickBackgroundColor | LayoutMessage::PickBorderColor => {}
LayoutMessage::SetWidth(p, v) => set_width(state, p, v),
+ LayoutMessage::SetCascadeOffset(v) => {
+ let val = v.min(200);
+ state.cascade_offset = val;
+ state.cascade_offset_spinbox.value = val as i32;
+ apply_all_widths(state);
+ }
+ LayoutMessage::SetEdgeGap(v) => {
+ let val = v.min(200);
+ state.edge_gap = val;
+ state.edge_gap_spinbox.value = val as i32;
+ apply_all_widths(state);
+ }
+ LayoutMessage::SetTopGap(v) => {
+ let val = v.min(200);
+ state.top_gap = val;
+ state.top_gap_spinbox.value = val as i32;
+ apply_all_widths(state);
+ }
LayoutMessage::Refreshed(new) => { *state = new; }
}
}
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 8e8d31a..9e1dcdd 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -19,6 +19,7 @@ pub struct BluetoothDevice {
#[derive(Debug, Clone, Default)]
pub struct NetworkState {
+ pub loaded: bool,
pub wifi_enabled: bool,
pub connected_ssid: String,
pub signal_strength: u8,
@@ -85,6 +86,7 @@ pub async fn fetch_network_state() -> NetworkState {
let (bt_enabled, bt_devices) = fetch_bluetooth_state().await;
NetworkState {
+ loaded: true,
wifi_enabled, connected_ssid, signal_strength: signal,
ip_address, device, available,
bt_enabled, bt_devices, bt_scanning: false,
@@ -210,35 +212,40 @@ pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
// ── WiFi ──
let mut sec = Section::new(&mut pc, cx, y, cw, "WiFi");
- let yt = sec.ay();
- pc.button(if state.wifi_enabled { "ON" } else { "OFF" },
- sec.ax(cw - 80.0), yt, 60.0, 28.0,
- if state.wifi_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
- AppAction::Radios(NetworkMessage::ToggleWifi));
- sec.content_y += 34.0;
-
- if !state.connected_ssid.is_empty() {
- sec.text(&mut pc, &format!("Connected: {}", state.connected_ssid), 14.0, 0.0, 13.0, ACCENT);
+ if !state.loaded {
+ sec.text(&mut pc, "Loading WiFi interfaces...", 12.0, 0.0, 12.0, TEXT_DIM);
sec.spacing(18.0);
- sec.text(&mut pc, &format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
- 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(16.0);
- } else if state.wifi_enabled {
- sec.text(&mut pc, "Not connected", 14.0, 0.0, 12.0, TEXT_DIM);
- sec.spacing(16.0);
- }
+ } else {
+ let yt = sec.ay();
+ pc.button(if state.wifi_enabled { "ON" } else { "OFF" },
+ sec.ax(cw - 80.0), yt, 60.0, 28.0,
+ if state.wifi_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
+ AppAction::Radios(NetworkMessage::ToggleWifi));
+ sec.content_y += 34.0;
+
+ if !state.connected_ssid.is_empty() {
+ sec.text(&mut pc, &format!("Connected: {}", state.connected_ssid), 14.0, 0.0, 13.0, ACCENT);
+ sec.spacing(18.0);
+ sec.text(&mut pc, &format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
+ 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(16.0);
+ } else if state.wifi_enabled {
+ sec.text(&mut pc, "Not connected", 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(16.0);
+ }
- if state.wifi_enabled && !state.available.is_empty() {
- for net in &state.available {
- let prefix = if net.in_use { ">" } else { " " };
- let label = format!("{} {} ({}%)", prefix, net.ssid, net.signal);
- let active = net.in_use;
- let yt = sec.ay();
- pc.button(&label, sec.ax(14.0), yt, cw - 28.0, 26.0,
- if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
- if active { ACCENT } else { TEXT_FG },
- AppAction::Radios(NetworkMessage::ConnectWifi(net.ssid.clone())));
- sec.content_y += 30.0;
+ if state.wifi_enabled && !state.available.is_empty() {
+ for net in &state.available {
+ let prefix = if net.in_use { ">" } else { " " };
+ let label = format!("{} {} ({}%)", prefix, net.ssid, net.signal);
+ let active = net.in_use;
+ let yt = sec.ay();
+ pc.button(&label, sec.ax(14.0), yt, cw - 28.0, 26.0,
+ if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
+ if active { ACCENT } else { TEXT_FG },
+ AppAction::Radios(NetworkMessage::ConnectWifi(net.ssid.clone())));
+ sec.content_y += 30.0;
+ }
}
}
@@ -247,35 +254,40 @@ pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
// ── Bluetooth ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Bluetooth");
- let yt = sec.ay();
- pc.button(if state.bt_enabled { "ON" } else { "OFF" },
- sec.ax(cw - 140.0), yt, 60.0, 28.0,
- if state.bt_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
- AppAction::Radios(NetworkMessage::ToggleBluetooth));
- pc.button("Scan", sec.ax(cw - 72.0), yt, 52.0, 28.0,
- TOGGLE_OFF, BTN_HOVER, WHITE,
- AppAction::Radios(NetworkMessage::BtScan));
- sec.content_y += 34.0;
-
- if state.bt_devices.is_empty() {
- if state.bt_enabled {
- sec.text(&mut pc, "No paired devices found", 14.0, 0.0, 12.0, TEXT_DIM);
- }
+ if !state.loaded {
+ sec.text(&mut pc, "Loading Bluetooth status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
} else {
- for dev in &state.bt_devices {
- let status = if dev.connected { ">" } else { " " };
- let label = format!("{} {} ({})", status, dev.name, dev.mac);
- let action_label = if dev.connected { "Disconnect" } else { "Connect" };
- let yt = sec.ay();
- sec.text(&mut pc, &label, 14.0, 0.0, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
- pc.button(action_label, sec.ax(cw - 90.0), yt - 2.0, 70.0, 22.0,
- if dev.connected { TOGGLE_OFF } else { TOGGLE_ON }, BTN_HOVER, WHITE,
- if dev.connected {
- AppAction::Radios(NetworkMessage::BtDisconnect(dev.mac.clone()))
- } else {
- AppAction::Radios(NetworkMessage::BtConnect(dev.mac.clone()))
- });
- sec.content_y += 24.0;
+ let yt = sec.ay();
+ pc.button(if state.bt_enabled { "ON" } else { "OFF" },
+ sec.ax(cw - 140.0), yt, 60.0, 28.0,
+ if state.bt_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
+ AppAction::Radios(NetworkMessage::ToggleBluetooth));
+ pc.button("Scan", sec.ax(cw - 72.0), yt, 52.0, 28.0,
+ TOGGLE_OFF, BTN_HOVER, WHITE,
+ AppAction::Radios(NetworkMessage::BtScan));
+ sec.content_y += 34.0;
+
+ if state.bt_devices.is_empty() {
+ if state.bt_enabled {
+ sec.text(&mut pc, "No paired devices found", 14.0, 0.0, 12.0, TEXT_DIM);
+ }
+ } else {
+ for dev in &state.bt_devices {
+ let status = if dev.connected { ">" } else { " " };
+ let label = format!("{} {} ({})", status, dev.name, dev.mac);
+ let action_label = if dev.connected { "Disconnect" } else { "Connect" };
+ let yt = sec.ay();
+ sec.text(&mut pc, &label, 14.0, 0.0, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
+ pc.button(action_label, sec.ax(cw - 90.0), yt - 2.0, 70.0, 22.0,
+ if dev.connected { TOGGLE_OFF } else { TOGGLE_ON }, BTN_HOVER, WHITE,
+ if dev.connected {
+ AppAction::Radios(NetworkMessage::BtDisconnect(dev.mac.clone()))
+ } else {
+ AppAction::Radios(NetworkMessage::BtConnect(dev.mac.clone()))
+ });
+ sec.content_y += 24.0;
+ }
}
}
sec.finish(&mut pc);
diff --git a/src/pages/power.rs b/src/pages/power.rs
index 9dcab48..a38b7f5 100644
--- a/src/pages/power.rs
+++ b/src/pages/power.rs
@@ -16,6 +16,7 @@ pub struct BatteryInfo {
#[derive(Debug, Clone, Default)]
pub struct PowerState {
+ pub loaded: bool,
pub battery: BatteryInfo,
pub on_ac: bool,
pub cpu_powersave: bool,
@@ -117,7 +118,7 @@ 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 { battery, on_ac, cpu_powersave, gpu_powersave }
+ PowerState { loaded: true, battery, on_ac, cpu_powersave, gpu_powersave }
}
async fn fetch_upower() -> (BatteryInfo, bool) {
@@ -171,105 +172,124 @@ pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
// ── Battery section ──
let mut sec = Section::new(&mut pc, cx, y, cw, "Battery");
- 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);
+ 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 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);
+ 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");
- let btn_w = (cw - 40.0) / 2.0;
- let btn_h = 44.0;
- let yt = sec.ay();
-
- 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", sec.ax(12.0), yt, btn_w, btn_h,
- perf_bg, BTN_HOVER, WHITE,
- AppAction::Power(PowerMessage::SetCpuPerformance));
- sec.text(&mut pc, perf_desc, 16.0, 26.0, 10.0, perf_desc_color);
-
- let (save_bg, save_desc, save_desc_color) = if state.cpu_powersave {
- (BTN_ACTIVE, "Governor set to powersave — lower power, slower burst", ACCENT)
+ if !state.loaded {
+ sec.text(&mut pc, "Loading CPU governor...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
} else {
- (BTN_INACTIVE, "Switch to powersave governor (requires auth)", TEXT_DIM)
- };
-
- let save_x = 16.0 + btn_w;
- pc.button("Powersave", sec.ax(save_x), yt, btn_w, btn_h,
- save_bg, BTN_HOVER, WHITE,
- AppAction::Power(PowerMessage::SetCpuPowersave));
- sec.text(&mut pc, save_desc, save_x + 4.0, 26.0, 10.0, save_desc_color);
- sec.content_y += btn_h + 12.0;
+ let btn_w = (cw - 40.0) / 2.0;
+ let btn_h = 44.0;
+ let yt = sec.ay();
+
+ 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", sec.ax(12.0), yt, btn_w, btn_h,
+ perf_bg, BTN_HOVER, WHITE,
+ AppAction::Power(PowerMessage::SetCpuPerformance));
+ sec.text(&mut pc, perf_desc, 16.0, 26.0, 10.0, perf_desc_color);
+
+ 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)
+ };
+
+ let save_x = 16.0 + btn_w;
+ pc.button("Powersave", sec.ax(save_x), yt, btn_w, btn_h,
+ save_bg, BTN_HOVER, WHITE,
+ AppAction::Power(PowerMessage::SetCpuPowersave));
+ sec.text(&mut pc, save_desc, save_x + 4.0, 26.0, 10.0, save_desc_color);
+ sec.content_y += btn_h + 12.0;
+ }
y = sec.finish(&mut pc);
// ── GPU Power section ──
let mut sec = Section::new(&mut pc, cx, y, cw, "GPU Power");
- let yt = sec.ay();
- 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", sec.ax(12.0), yt, btn_w, btn_h,
- gpu_def_bg, BTN_HOVER, WHITE,
- AppAction::Power(PowerMessage::SetGpuDefault));
- sec.text(&mut pc, gpu_def_desc, 16.0, 26.0, 10.0, gpu_def_desc_c);
-
- 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)
+ if !state.loaded {
+ sec.text(&mut pc, "Loading GPU power status...", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
} else {
- (BTN_INACTIVE, "Cap NVIDIA to 5W power limit (requires auth)", TEXT_DIM)
- };
-
- pc.button("5W Cap", sec.ax(save_x), yt, btn_w, btn_h,
- gpu_cap_bg, BTN_HOVER, WHITE,
- AppAction::Power(PowerMessage::SetGpuPowersave));
- sec.text(&mut pc, gpu_cap_desc, save_x + 4.0, 26.0, 10.0, gpu_cap_desc_c);
- sec.content_y += btn_h + 12.0;
+ let btn_w = (cw - 40.0) / 2.0;
+ let btn_h = 44.0;
+ let yt = sec.ay();
+ let save_x = 16.0 + btn_w;
+
+ 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", sec.ax(12.0), yt, btn_w, btn_h,
+ gpu_def_bg, BTN_HOVER, WHITE,
+ AppAction::Power(PowerMessage::SetGpuDefault));
+ sec.text(&mut pc, gpu_def_desc, 16.0, 26.0, 10.0, gpu_def_desc_c);
+
+ 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", sec.ax(save_x), yt, btn_w, btn_h,
+ gpu_cap_bg, BTN_HOVER, WHITE,
+ AppAction::Power(PowerMessage::SetGpuPowersave));
+ sec.text(&mut pc, gpu_cap_desc, save_x + 4.0, 26.0, 10.0, gpu_cap_desc_c);
+ sec.content_y += btn_h + 12.0;
+ }
y = sec.finish(&mut pc);
// ── System Actions section ──
diff --git a/src/pages/processors.rs b/src/pages/processors.rs
index ec5963f..4d45c9b 100644
--- a/src/pages/processors.rs
+++ b/src/pages/processors.rs
@@ -7,6 +7,7 @@ pub struct ProcessorsState {
pub cpu_usage: f32,
pub cpu_cores: u32,
pub gpu: String,
+ pub loaded: bool,
}
#[derive(Debug, Clone)]
@@ -65,7 +66,7 @@ pub async fn fetch_processors_state() -> ProcessorsState {
})
.unwrap_or_default();
- ProcessorsState { cpu_model, cpu_usage, cpu_cores, gpu }
+ ProcessorsState { cpu_model, cpu_usage, cpu_cores, gpu, loaded: true }
}
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
@@ -75,15 +76,24 @@ pub fn view(state: &ProcessorsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> Pag
let y = cy + 12.0;
let mut sec = Section::new(&mut pc, cx, y, cw, "Processors");
- sec.text(&mut pc,
- &format!("CPU {} ({} cores) — {:.0}%", state.cpu_model, state.cpu_cores, state.cpu_usage),
- 12.0, 0.0, 12.0, TEXT_FG,
- );
- sec.spacing(10.0);
- sec.text(&mut pc, &format!("GPU {}", state.gpu), 12.0, 0.0, 12.0, TEXT_FG);
+ if !state.loaded {
+ sec.text(&mut pc, "Loading processor models and utilization...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(10.0);
+ } else {
+ sec.text(&mut pc,
+ &format!("CPU {} ({} cores) — {:.0}%", state.cpu_model, state.cpu_cores, state.cpu_usage),
+ 12.0, 0.0, 12.0, TEXT_FG,
+ );
+ sec.spacing(10.0);
+ sec.text(&mut pc, &format!("GPU {}", state.gpu), 12.0, 0.0, 12.0, TEXT_FG);
+ }
sec.finish(&mut pc);
pc
}
-pub fn update(_state: &mut ProcessorsState, _msg: ProcessorsMessage) {}
+pub fn update(state: &mut ProcessorsState, msg: ProcessorsMessage) {
+ match msg {
+ ProcessorsMessage::Refreshed(new) => { *state = new; }
+ }
+}
diff --git a/src/pages/status.rs b/src/pages/status.rs
index 117c1de..353c07a 100644
--- a/src/pages/status.rs
+++ b/src/pages/status.rs
@@ -5,6 +5,7 @@ use clear_ui::layout::Section;
pub struct StatusState {
pub font_size: u16,
pub running: bool,
+ pub loaded: bool,
}
#[derive(Debug, Clone)]
@@ -22,7 +23,7 @@ pub async fn fetch_status_state() -> StatusState {
.unwrap_or(false);
let font_size = read_waybar_font_size().unwrap_or(13);
- StatusState { font_size, running }
+ StatusState { font_size, running, loaded: true }
}
fn read_waybar_font_size() -> Option<u16> {
@@ -74,34 +75,39 @@ pub fn view(state: &StatusState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCon
let mut sec = Section::new(&mut pc, cx, y, cw, "Waybar");
- // Status
- let status_color = if state.running { ACCENT } else { [0.67, 0.20, 0.20, 1.0] };
- let status_text = if state.running { "Running" } else { "Stopped" };
- sec.text(&mut pc, "Waybar", 12.0, 0.0, 14.0, TEXT_FG);
- sec.text(&mut pc, status_text, 80.0, 0.0, 14.0, status_color);
- sec.spacing(22.0);
-
- // Font size
- sec.text(&mut pc, &format!("Font size: {}px", state.font_size), 12.0, 0.0, 13.0, TEXT_FG);
- sec.spacing(20.0);
-
- let btn_h = 28.0;
- let yt = sec.ay();
- pc.button("-1", sec.ax(12.0), yt, 36.0, btn_h,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::FontSizeDown));
- pc.text(&format!(" {}px ", state.font_size), sec.ax(56.0), yt + 7.0, 13.0, TEXT_FG);
- pc.button("+1", sec.ax(12.0 + 36.0 + 8.0), yt, 36.0, btn_h,
- BTN_ACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::FontSizeUp));
- sec.content_y += btn_h + 12.0;
-
- // Reload button
- let yt = sec.ay();
- let btn_w = (cw - 24.0).min(200.0);
- pc.button("Reload Waybar", cx + cw / 2.0 - btn_w / 2.0, yt, btn_w, 32.0,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Status(StatusMessage::ReloadWaybar));
+ if !state.loaded {
+ sec.text(&mut pc, "Loading Waybar status...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
+ } else {
+ // Status
+ let status_color = if state.running { ACCENT } else { [0.67, 0.20, 0.20, 1.0] };
+ let status_text = if state.running { "Running" } else { "Stopped" };
+ sec.text(&mut pc, "Waybar", 12.0, 0.0, 14.0, TEXT_FG);
+ sec.text(&mut pc, status_text, 80.0, 0.0, 14.0, status_color);
+ sec.spacing(22.0);
+
+ // Font size
+ sec.text(&mut pc, &format!("Font size: {}px", state.font_size), 12.0, 0.0, 13.0, TEXT_FG);
+ sec.spacing(20.0);
+
+ let btn_h = 28.0;
+ let yt = sec.ay();
+ pc.button("-1", sec.ax(12.0), yt, 36.0, btn_h,
+ BTN_INACTIVE, BTN_HOVER, WHITE,
+ AppAction::Status(StatusMessage::FontSizeDown));
+ pc.text(&format!(" {}px ", state.font_size), sec.ax(56.0), yt + 7.0, 13.0, TEXT_FG);
+ pc.button("+1", sec.ax(12.0 + 36.0 + 8.0), yt, 36.0, btn_h,
+ BTN_ACTIVE, BTN_HOVER, WHITE,
+ AppAction::Status(StatusMessage::FontSizeUp));
+ sec.content_y += btn_h + 12.0;
+
+ // Reload button
+ let yt = sec.ay();
+ let btn_w = (cw - 24.0).min(200.0);
+ pc.button("Reload Waybar", cx + cw / 2.0 - btn_w / 2.0, yt, btn_w, 32.0,
+ BTN_INACTIVE, BTN_HOVER, WHITE,
+ AppAction::Status(StatusMessage::ReloadWaybar));
+ }
sec.finish(&mut pc);
pc
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
index 604caf8..1e3c6ed 100644
--- a/src/pages/storage.rs
+++ b/src/pages/storage.rs
@@ -7,6 +7,7 @@ pub struct StorageState {
pub disk_used: f64,
pub ram_total: f64,
pub ram_used: f64,
+ pub loaded: bool,
}
#[derive(Debug, Clone)]
@@ -29,7 +30,7 @@ pub async fn fetch_storage_state() -> StorageState {
.unwrap_or_default();
let (ram_total, ram_used) = parse_mem(&mem_output);
- StorageState { disk_total, disk_used, ram_total, ram_used }
+ StorageState { disk_total, disk_used, ram_total, ram_used, loaded: true }
}
fn parse_disk(info: &str) -> (f64, f64) {
@@ -67,48 +68,57 @@ pub fn view(state: &StorageState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
let mut sec = Section::new(&mut pc, cx, y, cw, "Local Storage");
- let disk_pct = if state.disk_total > 0.0 {
- state.disk_used / state.disk_total * 100.0
+ if !state.loaded {
+ sec.text(&mut pc, "Loading storage and memory usage...", 12.0, 0.0, 12.0, TEXT_FG);
+ sec.spacing(18.0);
} else {
- 0.0
- };
-
- sec.text(&mut pc, "Disk", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(&mut pc,
- &format!("{:.0} / {:.0} GiB ({:.0}%)", state.disk_used, state.disk_total, disk_pct),
- 100.0, 0.0, 12.0, TEXT_FG,
- );
- sec.spacing(18.0);
-
- let bar_w = cw - 24.0;
- let yt = sec.ay();
- pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
- if disk_pct > 0.0 {
- pc.rect([0.36, 0.60, 0.36, 1.0], sec.ax(12.0), yt, bar_w * (disk_pct as f32 / 100.0).min(1.0), 8.0);
- }
- sec.content_y += 20.0;
-
- let ram_pct = if state.ram_total > 0.0 {
- state.ram_used / state.ram_total * 100.0
- } else {
- 0.0
- };
-
- sec.text(&mut pc, "RAM", 12.0, 0.0, 12.0, LABEL_FG);
- sec.text(&mut pc,
- &format!("{:.1} / {:.1} GiB ({:.0}%)", state.ram_used, state.ram_total, ram_pct),
- 100.0, 0.0, 12.0, TEXT_FG,
- );
- sec.spacing(18.0);
-
- let yt = sec.ay();
- pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
- if ram_pct > 0.0 {
- pc.rect([0.50, 0.50, 0.65, 1.0], sec.ax(12.0), yt, bar_w * (ram_pct as f32 / 100.0).min(1.0), 8.0);
+ let disk_pct = if state.disk_total > 0.0 {
+ state.disk_used / state.disk_total * 100.0
+ } else {
+ 0.0
+ };
+
+ sec.text(&mut pc, "Disk", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(&mut pc,
+ &format!("{:.0} / {:.0} GiB ({:.0}%)", state.disk_used, state.disk_total, disk_pct),
+ 100.0, 0.0, 12.0, TEXT_FG,
+ );
+ sec.spacing(18.0);
+
+ let bar_w = cw - 24.0;
+ let yt = sec.ay();
+ pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
+ if disk_pct > 0.0 {
+ pc.rect([0.36, 0.60, 0.36, 1.0], sec.ax(12.0), yt, bar_w * (disk_pct as f32 / 100.0).min(1.0), 8.0);
+ }
+ sec.content_y += 20.0;
+
+ let ram_pct = if state.ram_total > 0.0 {
+ state.ram_used / state.ram_total * 100.0
+ } else {
+ 0.0
+ };
+
+ sec.text(&mut pc, "RAM", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(&mut pc,
+ &format!("{:.1} / {:.1} GiB ({:.0}%)", state.ram_used, state.ram_total, ram_pct),
+ 100.0, 0.0, 12.0, TEXT_FG,
+ );
+ sec.spacing(18.0);
+
+ let yt = sec.ay();
+ pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
+ if ram_pct > 0.0 {
+ pc.rect([0.50, 0.50, 0.65, 1.0], sec.ax(12.0), yt, bar_w * (ram_pct as f32 / 100.0).min(1.0), 8.0);
+ }
}
sec.finish(&mut pc);
pc
}
-pub fn update(_state: &mut StorageState, _msg: StorageMessage) {}
+pub fn update(state: &mut StorageState, msg: StorageMessage) {
+ match msg {
+ StorageMessage::Refreshed(new) => { *state = new; }
+ }
+}
diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index eb20492..3e7929e 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -6,6 +6,7 @@ pub struct SystemState {
pub hostname: String,
pub kernel: String,
pub uptime: String,
+ pub loaded: bool,
}
#[derive(Debug, Clone)]
@@ -29,7 +30,7 @@ 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 }
+ SystemState { hostname, kernel, uptime, loaded: true }
}
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
@@ -40,12 +41,21 @@ pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCon
let y = cy + 12.0;
let mut sec = Section::new(&mut pc, cx, y, cw, "System");
- sec.text(&mut pc, &format!("{} — Linux {}", state.hostname, state.kernel), 12.0, 0.0, 14.0, TEXT_FG);
- sec.spacing(10.0);
- sec.text(&mut pc, &format!("Uptime: {}", state.uptime), 12.0, 0.0, 12.0, TEXT_DIM);
+ if !state.loaded {
+ sec.text(&mut pc, "Loading system information...", 12.0, 0.0, 14.0, TEXT_FG);
+ sec.spacing(10.0);
+ } else {
+ sec.text(&mut pc, &format!("{} — Linux {}", state.hostname, state.kernel), 12.0, 0.0, 14.0, TEXT_FG);
+ sec.spacing(10.0);
+ sec.text(&mut pc, &format!("Uptime: {}", state.uptime), 12.0, 0.0, 12.0, TEXT_DIM);
+ }
sec.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; }
+ }
+}