git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit4a28456776660b3a60e3e9d45681385e50ff19e3
parent8df70d5b22
authorLucas Galante <[email protected]>
date2026-05-22 15:22
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; }
+    }
+}