From 430d92a0eee388f8ba4b661458f1e0ce844ac0db Mon Sep 17 00:00:00 2001 From: adrerl Date: Wed, 11 Mar 2026 08:28:19 +0100 Subject: [PATCH 01/12] Refactor input handling and improve surface blitting for better performance and clarity --- compd/src/islands/input.rs | 14 +--- compd/src/islands/renderer.rs | 101 ++++++++++++++++++----- compd/src/islands/surface_mgr.rs | 17 ++-- hwinit/src/process/schedular/wait.rs | 74 ++++++++++------- persistent/src/pe/embedded_reloc_data.rs | 88 ++++++++++---------- tests/system-visualizer/src/state.rs | 53 ++++++++---- 6 files changed, 220 insertions(+), 127 deletions(-) diff --git a/compd/src/islands/input.rs b/compd/src/islands/input.rs index dd872ef2..a513de4e 100644 --- a/compd/src/islands/input.rs +++ b/compd/src/islands/input.rs @@ -1,6 +1,6 @@ extern crate alloc; -use crate::islands::{CompState, HitRegion, MouseCapture, BORDER, MAX_WINDOWS, PANEL_H, TITLE_H}; +use crate::islands::{CompState, HitRegion, MouseCapture, BORDER, MAX_WINDOWS, TITLE_H}; use crate::messages::InputMsg; use libmorpheus::{compositor as compsys, hw, io, process}; @@ -83,16 +83,6 @@ fn poll_mouse(state: &mut CompState) { } if left_pressed { - // panel area is always handled by shelld — intercept BEFORE window hit-test. - // this ensures the taskbar is always clickable even with overlapping windows. - let panel_top = (state.fb_h as i32).saturating_sub(PANEL_H as i32); - if state.mouse_y >= panel_top { - // re-forward with actual button state so shelld registers the click - forward_to_desktop(state, 0, 0, ms.buttons); - state.last_buttons = ms.buttons; - return; - } - if let Some((idx, region)) = hit_test(state, state.mouse_x, state.mouse_y) { state.focused = Some(idx); match region { @@ -128,7 +118,7 @@ fn poll_mouse(state: &mut CompState) { HitRegion::Content => {} } } else { - // click on empty desktop — forward buttons to shelld + // no window hit: panel area goes to shelld, else desktop click also goes to shelld. forward_to_desktop(state, 0, 0, ms.buttons); state.last_buttons = ms.buttons; return; diff --git a/compd/src/islands/renderer.rs b/compd/src/islands/renderer.rs index a63794c9..d3f4fda1 100644 --- a/compd/src/islands/renderer.rs +++ b/compd/src/islands/renderer.rs @@ -266,9 +266,8 @@ fn draw_window(state: &mut CompState, idx: usize, focused: bool) { } } -/// 1:1 blit from surface buffer to framebuffer. no scaling. clips to both window and FB bounds. -/// the window is a viewport into the top-left of the app's full-resolution surface. -/// apps render at native FB res. we show as many pixels as fit. text stays sharp. +/// Scale full source surface into the window rectangle. +/// Bilinear sampling keeps resized windows from looking like crunchy nearest-neighbor soup. fn blit_surface( state: &CompState, fb_ptr: *mut u32, @@ -276,13 +275,17 @@ fn blit_surface( dst_y: i32, dst_w: u32, dst_h: u32, - _src_w: u32, - _src_h: u32, + src_w: u32, + src_h: u32, src_stride: u32, surface_ptr: *const u32, surface_pages: u64, ) { - let src_stride = src_stride.max(1); + let src_w = src_w.max(1); + let src_h = src_h.max(1); + let src_stride = src_stride.max(src_w).max(1); + let dst_w = dst_w.max(1); + let dst_h = dst_h.max(1); let fb_stride = state.fb_stride; let mapped_pixels = (surface_pages as usize).saturating_mul(4096 / 4); @@ -295,28 +298,88 @@ fn blit_surface( return; } - // source offset: if dst_x < 0 we skip into the source by that many pixels - let src_x_off = (x0 as i32 - dst_x) as u32; - let src_y_off = (y0 as i32 - dst_y) as u32; + #[inline(always)] + fn lerp8(a: u32, b: u32, t: u32) -> u32 { + // fixed-point blend, t in [0, 65535] + ((a * (65535 - t)) + (b * t)) >> 16 + } + + #[inline(always)] + fn bilerp(c00: u32, c10: u32, c01: u32, c11: u32, tx: u32, ty: u32) -> u32 { + let b00 = c00 & 0x0000_00FF; + let g00 = (c00 >> 8) & 0xFF; + let r00 = (c00 >> 16) & 0xFF; + + let b10 = c10 & 0x0000_00FF; + let g10 = (c10 >> 8) & 0xFF; + let r10 = (c10 >> 16) & 0xFF; + + let b01 = c01 & 0x0000_00FF; + let g01 = (c01 >> 8) & 0xFF; + let r01 = (c01 >> 16) & 0xFF; + + let b11 = c11 & 0x0000_00FF; + let g11 = (c11 >> 8) & 0xFF; + let r11 = (c11 >> 16) & 0xFF; + + let bx0 = lerp8(b00, b10, tx); + let gx0 = lerp8(g00, g10, tx); + let rx0 = lerp8(r00, r10, tx); + + let bx1 = lerp8(b01, b11, tx); + let gx1 = lerp8(g01, g11, tx); + let rx1 = lerp8(r01, r11, tx); + + let b = lerp8(bx0, bx1, ty); + let g = lerp8(gx0, gx1, ty); + let r = lerp8(rx0, rx1, ty); - // 1:1 copy. one source pixel = one destination pixel. the compiler can auto-vectorize this. + b | (g << 8) | (r << 16) + } + + // map each destination pixel into source space so windows resize live. unsafe { for dy in y0..y1 { - let sy = src_y_off + (dy - y0); let dst_row = dy * fb_stride; - let src_row = sy * src_stride; - if (src_row + src_x_off) as usize >= mapped_pixels { - break; + let local_y = (dy as i32 - dst_y) as u64; + let sy_fp = (local_y << 16).saturating_mul(src_h as u64) / dst_h as u64; + let sy0 = ((sy_fp >> 16) as u32).min(src_h - 1); + let sy1 = (sy0 + 1).min(src_h - 1); + let ty = (sy_fp as u32) & 0xFFFF; + + let src_row0 = sy0 * src_stride; + let src_row1 = sy1 * src_stride; + if src_row0 as usize >= mapped_pixels || src_row1 as usize >= mapped_pixels { + continue; } for dx in x0..x1 { - let sx = src_x_off + (dx - x0); - let src_off = (src_row + sx) as usize; - if src_off >= mapped_pixels { - break; + let local_x = (dx as i32 - dst_x) as u64; + let sx_fp = (local_x << 16).saturating_mul(src_w as u64) / dst_w as u64; + let sx0 = ((sx_fp >> 16) as u32).min(src_w - 1); + let sx1 = (sx0 + 1).min(src_w - 1); + let tx = (sx_fp as u32) & 0xFFFF; + + let off00 = (src_row0 + sx0) as usize; + let off10 = (src_row0 + sx1) as usize; + let off01 = (src_row1 + sx0) as usize; + let off11 = (src_row1 + sx1) as usize; + if off00 >= mapped_pixels + || off10 >= mapped_pixels + || off01 >= mapped_pixels + || off11 >= mapped_pixels + { + continue; } + + let c00 = *surface_ptr.add(off00); + let c10 = *surface_ptr.add(off10); + let c01 = *surface_ptr.add(off01); + let c11 = *surface_ptr.add(off11); + let out = bilerp(c00, c10, c01, c11, tx, ty); + let dst_off = (dst_row + dx) as usize; - *fb_ptr.add(dst_off) = *surface_ptr.add(src_off); + *fb_ptr.add(dst_off) = out; } } } diff --git a/compd/src/islands/surface_mgr.rs b/compd/src/islands/surface_mgr.rs index 6987863a..b0295734 100644 --- a/compd/src/islands/surface_mgr.rs +++ b/compd/src/islands/surface_mgr.rs @@ -67,6 +67,8 @@ pub fn update(state: &mut CompState) { win.surface_pages = entry.pages; win.src_w = entry.width; win.src_h = entry.height; + // Keep historical behavior: entry stride comes from shared + // metadata path in bytes, convert to pixels for blit math. win.src_stride = (entry.stride / 4).max(entry.width.max(1)); win.mapped = true; } @@ -114,12 +116,15 @@ pub fn update(state: &mut CompState) { state.desktop_idx = Some(idx); } else { let step = CASCADE_STEP * (state.cascade_n % 5); - let max_w = state.fb_w.saturating_sub(40); - let max_h = state.fb_h.saturating_sub(TITLE_H + PANEL_H + 40); - let w = ((state.fb_w as u64 * 58) / 100) as u32; - let h = ((state.fb_h as u64 * 58) / 100) as u32; - let w = w.clamp(320, max_w.max(320)); - let h = h.clamp(220, max_h.max(220)); + // Open at source size when possible, but clamp to visible work area + // so move/resize affordances remain reachable. + let max_w = state.fb_w.saturating_sub(40).max(160); + let max_h = state + .fb_h + .saturating_sub(TITLE_H + PANEL_H + 40) + .max(120); + let w = entry.width.max(1).min(max_w); + let h = entry.height.max(1).min(max_h); let cx = (20 + step).clamp(0, (state.fb_w as i32 - w as i32).max(0)); let cy = (TITLE_H as i32 + 20 + step).clamp( diff --git a/hwinit/src/process/schedular/wait.rs b/hwinit/src/process/schedular/wait.rs index c4ac2409..72a1f58e 100644 --- a/hwinit/src/process/schedular/wait.rs +++ b/hwinit/src/process/schedular/wait.rs @@ -32,48 +32,51 @@ pub unsafe fn block_sleep(deadline: u64) -> u64 { pub unsafe fn wait_for_child(child_pid: u32) -> u64 { let current = this_core_pid(); - PROCESS_TABLE_LOCK.lock(); + loop { + PROCESS_TABLE_LOCK.lock(); + + let (child_parent, child_state) = match PROCESS_TABLE + .get(child_pid as usize) + .and_then(|s| s.as_ref()) + { + Some(p) => (p.parent_pid, p.state), + None => { + PROCESS_TABLE_LOCK.unlock(); + return u64::MAX - 3; + } + }; - let (child_parent, child_state) = match PROCESS_TABLE - .get(child_pid as usize) - .and_then(|s| s.as_ref()) - { - Some(p) => (p.parent_pid, p.state), - None => { + if child_parent != current { PROCESS_TABLE_LOCK.unlock(); return u64::MAX - 3; } - }; - if child_parent != current { - PROCESS_TABLE_LOCK.unlock(); - return u64::MAX - 3; - } + if matches!(child_state, ProcessState::Terminated) { + PROCESS_TABLE_LOCK.unlock(); + return u64::MAX - 10; + } - if matches!(child_state, ProcessState::Terminated) { - PROCESS_TABLE_LOCK.unlock(); - return u64::MAX - 10; - } + if child_state == ProcessState::Zombie { + let result = reap_child(child_pid); + PROCESS_TABLE_LOCK.unlock(); + // Zombie observed but still running on some core; wait until + // scheduler deschedules it before freeing CR3/page tables. + if result == u64::MAX - 11 { + core::arch::asm!("sti", "hlt", "cli", options(nostack, nomem)); + continue; + } + return result; + } + + let cur = current as usize; + if let Some(Some(proc)) = PROCESS_TABLE.get_mut(cur) { + proc.state = ProcessState::Blocked(BlockReason::WaitChild(child_pid)); + } - if child_state == ProcessState::Zombie { - let result = reap_child(child_pid); PROCESS_TABLE_LOCK.unlock(); - return result; - } - let cur = current as usize; - if let Some(Some(proc)) = PROCESS_TABLE.get_mut(cur) { - proc.state = ProcessState::Blocked(BlockReason::WaitChild(child_pid)); + core::arch::asm!("sti", "hlt", "cli", options(nostack, nomem)); } - - PROCESS_TABLE_LOCK.unlock(); - - core::arch::asm!("sti", "hlt", "cli", options(nostack, nomem)); - - PROCESS_TABLE_LOCK.lock(); - let result = reap_child(child_pid); - PROCESS_TABLE_LOCK.unlock(); - result } pub unsafe fn try_wait_child(child_pid: u32) -> u64 { @@ -114,6 +117,13 @@ pub unsafe fn try_wait_child(child_pid: u32) -> u64 { unsafe fn reap_child(pid: u32) -> u64 { if let Some(Some(child)) = PROCESS_TABLE.get_mut(pid as usize) { + // A process may be Zombie but still executing on another core until + // that core takes a scheduler tick and switches away. Reaping early + // would free its page tables while CR3 still points at them. + if child.running_on != u32::MAX { + return u64::MAX - 11; + } + let code = child.exit_code.unwrap_or(-1); free_process_resources(child); diff --git a/persistent/src/pe/embedded_reloc_data.rs b/persistent/src/pe/embedded_reloc_data.rs index 1a1d625d..0d51e504 100644 --- a/persistent/src/pe/embedded_reloc_data.rs +++ b/persistent/src/pe/embedded_reloc_data.rs @@ -11,15 +11,15 @@ pub const RELOC_RVA: u32 = 0x00611000; /// Original .reloc section size -pub const RELOC_SIZE: u32 = 0x00000260; +pub const RELOC_SIZE: u32 = 0x0000025c; /// Original ImageBase from linker script pub const ORIGINAL_IMAGE_BASE: u64 = 0x0000004001000000; -/// Hardcoded .reloc section data (608 bytes) -/// Extracted from morpheus-bootloader.efi at file offset 0x001d3c00 +/// Hardcoded .reloc section data (604 bytes) +/// Extracted from morpheus-bootloader.efi at file offset 0x001d3a00 #[allow(dead_code)] -pub const RELOC_DATA: [u8; 608] = [ +pub const RELOC_DATA: [u8; 604] = [ 0x00, 0xf0, 0x04, 0x00, 0x34, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xf8, 0xa6, 0x10, 0xa7, 0x28, 0xa7, 0xb8, 0xa7, 0xc0, 0xa8, 0xc8, 0xaa, 0xe0, 0xaa, 0xe8, 0xab, @@ -29,47 +29,47 @@ pub const RELOC_DATA: [u8; 608] = [ 0xf8, 0xa4, 0x10, 0xa5, 0x28, 0xa5, 0x40, 0xa5, 0x58, 0xa5, 0x70, 0xa5, 0xa8, 0xa5, 0xc0, 0xa5, 0x38, 0xa6, 0x70, 0xa6, 0x80, 0xa7, 0x98, 0xa7, 0xe8, 0xa7, 0x00, 0xa8, 0x18, 0xa8, 0x50, 0xa8, 0x80, 0xa8, 0x00, 0x00, - 0x00, 0x10, 0x05, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x50, 0xaa, + 0x00, 0x10, 0x05, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x50, 0xaa, 0x68, 0xaa, 0xf0, 0xaa, 0x08, 0xab, 0xa0, 0xab, 0xb8, 0xab, 0xd0, 0xab, 0xe8, 0xab, 0x00, 0xac, 0x10, 0xad, 0x70, 0xad, 0xe8, 0xad, 0x28, 0xaf, - 0x48, 0xaf, 0xd8, 0xaf, 0xf0, 0xaf, 0x00, 0x00, 0x00, 0x20, 0x05, 0x00, - 0x34, 0x00, 0x00, 0x00, 0x08, 0xa0, 0x10, 0xa2, 0x28, 0xa2, 0x40, 0xa2, - 0x58, 0xa2, 0x70, 0xa2, 0x88, 0xa2, 0xa0, 0xa2, 0x60, 0xa3, 0xc8, 0xa3, - 0xe8, 0xa3, 0x00, 0xa4, 0x18, 0xa4, 0x30, 0xa4, 0x48, 0xa4, 0xa0, 0xa4, - 0xb8, 0xa4, 0xd0, 0xa4, 0xe8, 0xa4, 0x00, 0xa5, 0x40, 0xa5, 0xd0, 0xa5, - 0x00, 0x30, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, 0x60, 0xa0, 0x50, 0xa5, - 0x68, 0xa5, 0x80, 0xa5, 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, 0x18, 0xa6, - 0xb0, 0xa6, 0x20, 0xa9, 0x78, 0xaa, 0x88, 0xaa, 0x98, 0xaa, 0xa8, 0xaa, - 0xb8, 0xaa, 0xc8, 0xaa, 0xd8, 0xaa, 0xe8, 0xaa, 0xf8, 0xaa, 0x08, 0xab, - 0x18, 0xab, 0x28, 0xab, 0x38, 0xab, 0x48, 0xab, 0x58, 0xab, 0x68, 0xab, - 0x78, 0xab, 0x88, 0xab, 0x98, 0xab, 0xa8, 0xab, 0xb8, 0xab, 0xc8, 0xab, - 0xd8, 0xab, 0xe8, 0xab, 0xf8, 0xab, 0x08, 0xac, 0x18, 0xac, 0x28, 0xac, - 0x38, 0xac, 0x48, 0xac, 0x58, 0xac, 0x68, 0xac, 0x48, 0xae, 0xe0, 0xae, - 0x38, 0xaf, 0x00, 0x00, 0x00, 0x40, 0x05, 0x00, 0x28, 0x00, 0x00, 0x00, - 0xa8, 0xa0, 0xc0, 0xa0, 0x10, 0xa1, 0x20, 0xa1, 0x30, 0xa1, 0x40, 0xa1, - 0x50, 0xa1, 0x80, 0xa1, 0xb8, 0xa1, 0x00, 0xa2, 0x50, 0xa2, 0x88, 0xa2, - 0xa0, 0xa2, 0xe8, 0xa2, 0x70, 0xa3, 0x00, 0x00, 0x00, 0x50, 0x05, 0x00, - 0x54, 0x00, 0x00, 0x00, 0x28, 0xa0, 0x40, 0xa0, 0x78, 0xa0, 0xa8, 0xa0, - 0xc0, 0xa0, 0xd8, 0xa0, 0xf0, 0xa0, 0x08, 0xa1, 0x20, 0xa1, 0x38, 0xa1, - 0x50, 0xa1, 0x90, 0xa1, 0xa8, 0xa1, 0xc0, 0xa1, 0xd8, 0xa1, 0xf0, 0xa1, - 0x28, 0xa2, 0x58, 0xa2, 0x70, 0xa2, 0x88, 0xa2, 0xa0, 0xa2, 0xd8, 0xa2, - 0xf0, 0xa2, 0x08, 0xa3, 0x20, 0xa3, 0x38, 0xa3, 0x50, 0xa3, 0xe0, 0xa3, - 0x28, 0xaa, 0x38, 0xaa, 0x68, 0xaa, 0x80, 0xaa, 0xc8, 0xaa, 0xd8, 0xaa, - 0xe8, 0xaa, 0x10, 0xab, 0x48, 0xab, 0x00, 0x00, 0x00, 0x60, 0x05, 0x00, - 0x6c, 0x00, 0x00, 0x00, 0x88, 0xa0, 0x98, 0xa0, 0xa8, 0xa0, 0xb8, 0xa0, - 0x08, 0xa1, 0x18, 0xa1, 0x28, 0xa1, 0x38, 0xa1, 0x48, 0xa1, 0x70, 0xa1, - 0x80, 0xa1, 0x90, 0xa1, 0xf8, 0xa1, 0x08, 0xa2, 0x18, 0xa2, 0x60, 0xa2, - 0x70, 0xa2, 0xa8, 0xa2, 0xb8, 0xa2, 0xe0, 0xa2, 0xf0, 0xa2, 0x28, 0xa3, - 0x40, 0xa3, 0x98, 0xa3, 0x08, 0xaa, 0x20, 0xaa, 0x58, 0xaa, 0xa8, 0xaa, - 0xf0, 0xaa, 0x00, 0xab, 0x00, 0xac, 0x18, 0xac, 0x50, 0xac, 0x68, 0xac, - 0x80, 0xac, 0x98, 0xac, 0x08, 0xad, 0x00, 0xae, 0x18, 0xae, 0x30, 0xae, - 0x48, 0xae, 0x60, 0xae, 0xb8, 0xae, 0xd0, 0xae, 0xe8, 0xae, 0x68, 0xaf, - 0x98, 0xaf, 0xb0, 0xaf, 0xe0, 0xaf, 0x00, 0x00, 0x00, 0x70, 0x05, 0x00, - 0x18, 0x00, 0x00, 0x00, 0x38, 0xa0, 0x50, 0xa0, 0x68, 0xa0, 0x80, 0xa0, - 0x98, 0xa0, 0xb0, 0xa0, 0xc8, 0xa0, 0x00, 0x00, 0x00, 0x80, 0x05, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x40, 0xa3, 0x48, 0xa3, 0x00, 0x20, 0x1c, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x80, 0xa3, 0xc0, 0xae, 0x00, 0x50, 0x1d, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0xf8, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x61, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, + 0x48, 0xaf, 0xc8, 0xaf, 0x00, 0x20, 0x05, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x30, 0xa0, 0x48, 0xa0, 0x60, 0xa0, 0x68, 0xa2, 0x80, 0xa2, 0x98, 0xa2, + 0xb0, 0xa2, 0xc8, 0xa2, 0xe0, 0xa2, 0xf8, 0xa2, 0xb8, 0xa3, 0x20, 0xa4, + 0x40, 0xa4, 0x58, 0xa4, 0x70, 0xa4, 0x88, 0xa4, 0xa0, 0xa4, 0xf8, 0xa4, + 0x10, 0xa5, 0x28, 0xa5, 0x40, 0xa5, 0x58, 0xa5, 0x98, 0xa5, 0x28, 0xa6, + 0x00, 0x30, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, 0xb8, 0xa0, 0xa8, 0xa5, + 0xc0, 0xa5, 0xd8, 0xa5, 0xf0, 0xa5, 0x08, 0xa6, 0x20, 0xa6, 0x70, 0xa6, + 0x08, 0xa7, 0x78, 0xa9, 0xd0, 0xaa, 0xe0, 0xaa, 0xf0, 0xaa, 0x00, 0xab, + 0x10, 0xab, 0x20, 0xab, 0x30, 0xab, 0x40, 0xab, 0x50, 0xab, 0x60, 0xab, + 0x70, 0xab, 0x80, 0xab, 0x90, 0xab, 0xa0, 0xab, 0xb0, 0xab, 0xc0, 0xab, + 0xd0, 0xab, 0xe0, 0xab, 0xf0, 0xab, 0x00, 0xac, 0x10, 0xac, 0x20, 0xac, + 0x30, 0xac, 0x40, 0xac, 0x50, 0xac, 0x60, 0xac, 0x70, 0xac, 0x80, 0xac, + 0x90, 0xac, 0xa0, 0xac, 0xb0, 0xac, 0xc0, 0xac, 0xa0, 0xae, 0x38, 0xaf, + 0x90, 0xaf, 0x00, 0x00, 0x00, 0x40, 0x05, 0x00, 0x28, 0x00, 0x00, 0x00, + 0x00, 0xa1, 0x18, 0xa1, 0x68, 0xa1, 0x78, 0xa1, 0x88, 0xa1, 0x98, 0xa1, + 0xa8, 0xa1, 0xd8, 0xa1, 0x10, 0xa2, 0x58, 0xa2, 0xa8, 0xa2, 0xe0, 0xa2, + 0xf8, 0xa2, 0x40, 0xa3, 0xc8, 0xa3, 0x00, 0x00, 0x00, 0x50, 0x05, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x80, 0xa0, 0x98, 0xa0, 0xd0, 0xa0, 0x00, 0xa1, + 0x18, 0xa1, 0x30, 0xa1, 0x48, 0xa1, 0x60, 0xa1, 0x78, 0xa1, 0x90, 0xa1, + 0xa8, 0xa1, 0xe8, 0xa1, 0x00, 0xa2, 0x18, 0xa2, 0x30, 0xa2, 0x48, 0xa2, + 0x80, 0xa2, 0xb0, 0xa2, 0xc8, 0xa2, 0xe0, 0xa2, 0xf8, 0xa2, 0x30, 0xa3, + 0x48, 0xa3, 0x60, 0xa3, 0x78, 0xa3, 0x90, 0xa3, 0xa8, 0xa3, 0x38, 0xa4, + 0x80, 0xaa, 0x90, 0xaa, 0xc0, 0xaa, 0xd8, 0xaa, 0x20, 0xab, 0x30, 0xab, + 0x40, 0xab, 0x68, 0xab, 0xa0, 0xab, 0x00, 0x00, 0x00, 0x60, 0x05, 0x00, + 0x68, 0x00, 0x00, 0x00, 0xe0, 0xa0, 0xf0, 0xa0, 0x00, 0xa1, 0x10, 0xa1, + 0x60, 0xa1, 0x70, 0xa1, 0x80, 0xa1, 0x90, 0xa1, 0xa0, 0xa1, 0xc8, 0xa1, + 0xd8, 0xa1, 0xe8, 0xa1, 0x50, 0xa2, 0x60, 0xa2, 0x70, 0xa2, 0xb8, 0xa2, + 0xc8, 0xa2, 0x00, 0xa3, 0x10, 0xa3, 0x38, 0xa3, 0x48, 0xa3, 0x80, 0xa3, + 0x98, 0xa3, 0xf0, 0xa3, 0x60, 0xaa, 0x78, 0xaa, 0xb0, 0xaa, 0x00, 0xab, + 0x48, 0xab, 0x58, 0xab, 0x58, 0xac, 0x70, 0xac, 0xa8, 0xac, 0xc0, 0xac, + 0xd8, 0xac, 0xf0, 0xac, 0x20, 0xae, 0x50, 0xae, 0x68, 0xae, 0xc0, 0xae, + 0xd8, 0xae, 0xf0, 0xae, 0x08, 0xaf, 0x20, 0xaf, 0x78, 0xaf, 0x90, 0xaf, + 0xa8, 0xaf, 0xd8, 0xaf, 0x00, 0x70, 0x05, 0x00, 0x18, 0x00, 0x00, 0x00, + 0x30, 0xa0, 0x48, 0xa0, 0x60, 0xa0, 0x78, 0xa0, 0x90, 0xa0, 0xa8, 0xa0, + 0xc0, 0xa0, 0x00, 0x00, 0x00, 0x80, 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x40, 0xa3, 0x48, 0xa3, 0x00, 0x20, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x80, 0xa3, 0xc0, 0xae, 0x00, 0x50, 0x1d, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0xf8, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x61, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x20, 0xa0, 0x00, 0x00, ]; diff --git a/tests/system-visualizer/src/state.rs b/tests/system-visualizer/src/state.rs index 6b9f6f0d..ca645db9 100644 --- a/tests/system-visualizer/src/state.rs +++ b/tests/system-visualizer/src/state.rs @@ -156,8 +156,10 @@ impl SystemState { let now = time::clock_gettime(); let _elapsed_ns = now.saturating_sub(self.prev_poll_ns).max(1); - let mut info = SysInfo::zeroed(); - let _ = sys::sysinfo(&mut info); + let mut info = Box::new(SysInfo::zeroed()); + if sys::sysinfo(&mut *info).is_err() { + return; + } self.total_mem = info.total_mem; self.free_mem = info.free_mem; self.heap_total = info.heap_total; @@ -177,7 +179,15 @@ impl SystemState { // -- accumulate this poll's deltas into the 1-second window -- for i in 0..count { let r = &raw[i]; - let prev_tsc = self.find_prev_cpu_tsc(r.pid); + let prev_tsc = match self.find_prev_cpu_tsc(r.pid) { + Some(v) => v, + None => { + // First sample for this PID: seed baseline and skip this poll + // so we don't treat lifetime cpu_tsc as a 250ms delta. + self.set_prev_cpu_tsc(r.pid, r.cpu_tsc); + continue; + } + }; let delta_tsc = r.cpu_tsc.wrapping_sub(prev_tsc); let slot = self.acc_slot_for_pid(r.pid); self.acc_cpu_tsc[slot] = self.acc_cpu_tsc[slot].saturating_add(delta_tsc); @@ -235,15 +245,8 @@ impl SystemState { }; } - self.prev_cpu_tsc_count = 0; for i in 0..count { - if self.prev_cpu_tsc_count >= MAX_PROCS { - break; - } - let s = self.prev_cpu_tsc_count; - self.prev_cpu_tsc_pids[s] = self.procs[i].pid; - self.prev_cpu_tsc_vals[s] = self.procs[i].cpu_tsc; - self.prev_cpu_tsc_count += 1; + self.set_prev_cpu_tsc(self.procs[i].pid, self.procs[i].cpu_tsc); } self.prev_uptime_ticks = info.uptime_ticks; @@ -311,13 +314,35 @@ impl SystemState { .position(|p| p.pid == pid) } - fn find_prev_cpu_tsc(&self, pid: u32) -> u64 { + fn find_prev_cpu_tsc(&self, pid: u32) -> Option { + for i in 0..self.prev_cpu_tsc_count { + if self.prev_cpu_tsc_pids[i] == pid { + return Some(self.prev_cpu_tsc_vals[i]); + } + } + None + } + + fn set_prev_cpu_tsc(&mut self, pid: u32, val: u64) { for i in 0..self.prev_cpu_tsc_count { if self.prev_cpu_tsc_pids[i] == pid { - return self.prev_cpu_tsc_vals[i]; + self.prev_cpu_tsc_vals[i] = val; + return; } } - 0 + + if self.prev_cpu_tsc_count < MAX_PROCS { + let s = self.prev_cpu_tsc_count; + self.prev_cpu_tsc_pids[s] = pid; + self.prev_cpu_tsc_vals[s] = val; + self.prev_cpu_tsc_count += 1; + return; + } + + // Bounded replacement when PID map is full. + let victim = (pid as usize) % MAX_PROCS; + self.prev_cpu_tsc_pids[victim] = pid; + self.prev_cpu_tsc_vals[victim] = val; } /// Find or allocate an accumulator slot for `pid`. From f82e868df98b3e37e5a83fcf19dedbecf51e641e Mon Sep 17 00:00:00 2001 From: adrerl Date: Wed, 11 Mar 2026 09:57:26 +0100 Subject: [PATCH 02/12] reboot and shutdown --- hwinit/src/cpu/idt.rs | 14 +++ hwinit/src/cpu/mod.rs | 1 + hwinit/src/cpu/per_cpu.rs | 69 ++++++++++- hwinit/src/cpu/reset.rs | 123 ++++++++++++++++++++ hwinit/src/process/schedular/tick.rs | 13 +++ hwinit/src/syscall/handler/core.rs | 101 ++++++++++++++++ hwinit/src/syscall/mod.rs | 2 + libmorpheus/src/raw.rs | 8 ++ libmorpheus/src/sys.rs | 42 +++++++ persistent/src/pe/embedded_reloc_data.rs | 94 +++++++-------- shell/src/builtin/help.rs | 19 +++ shell/src/builtin/mod.rs | 6 + shell/src/builtin/proc_cmds.rs | 142 +++++++++++++++++++++++ 13 files changed, 586 insertions(+), 48 deletions(-) create mode 100644 hwinit/src/cpu/reset.rs diff --git a/hwinit/src/cpu/idt.rs b/hwinit/src/cpu/idt.rs index 6ca12c54..8af6b8bb 100644 --- a/hwinit/src/cpu/idt.rs +++ b/hwinit/src/cpu/idt.rs @@ -3,6 +3,7 @@ use crate::cpu::gdt::KERNEL_CS; use crate::serial::{newline, put_hex32, put_hex64, put_hex8, puts}; +use core::sync::atomic::{AtomicBool, Ordering}; // IDT ENTRY @@ -204,6 +205,7 @@ pub struct CrashInfo { pub type CrashHookFn = unsafe fn(&CrashInfo); static mut CRASH_HOOK: Option = None; +static RESET_ON_CRASH: AtomicBool = AtomicBool::new(false); /// Register the crash screen callback (typically called during boot init). /// @@ -213,6 +215,11 @@ pub unsafe fn set_crash_hook(hook: CrashHookFn) { CRASH_HOOK = Some(hook); } +/// If set, fatal exceptions reset the machine after the crash hook runs. +pub fn set_reset_on_crash(enable: bool) { + RESET_ON_CRASH.store(enable, Ordering::Relaxed); +} + /// Number of IDT entries const IDT_ENTRIES: usize = 256; @@ -681,6 +688,13 @@ pub extern "C" fn exception_handler( } } + if RESET_ON_CRASH.load(Ordering::Relaxed) { + unsafe { + crate::cpu::reset::wait_for_keypress_or_timeout_ms(10_000); + crate::cpu::reset::reset_machine_now(); + } + } + puts("!!! SYSTEM HALTED\n"); loop { unsafe { diff --git a/hwinit/src/cpu/mod.rs b/hwinit/src/cpu/mod.rs index 0cf58728..a90c2fd1 100644 --- a/hwinit/src/cpu/mod.rs +++ b/hwinit/src/cpu/mod.rs @@ -28,5 +28,6 @@ pub mod mmio; pub mod per_cpu; pub mod pic; pub mod pio; +pub mod reset; pub mod sse; pub mod tsc; diff --git a/hwinit/src/cpu/per_cpu.rs b/hwinit/src/cpu/per_cpu.rs index 43d097ee..832fc57f 100644 --- a/hwinit/src/cpu/per_cpu.rs +++ b/hwinit/src/cpu/per_cpu.rs @@ -11,7 +11,7 @@ //! the next timer tick will be your last. use crate::serial::{log_ok, put_hex32, puts}; -use core::sync::atomic::{AtomicU32, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; /// Maximum cores we support. 16 is generous for a desktop OS on QEMU. /// Increase if you're running on a Threadripper and hate yourself. @@ -111,6 +111,9 @@ static mut LAPIC_TO_IDX: [u8; 256] = [0xFF; 256]; /// Number of cores that have completed init. pub static AP_ONLINE_COUNT: AtomicU32 = AtomicU32::new(0); +static SHUTDOWN_QUIESCE_REQUESTED: AtomicBool = AtomicBool::new(false); +static SHUTDOWN_QUIESCE_ACK_MASK: AtomicU64 = AtomicU64::new(0); + /// Total number of detected CPUs (BSP + APs). Set by BSP during MADT parse /// or CPUID enumeration, before AP startup. static mut CPU_COUNT: u32 = 1; @@ -310,6 +313,70 @@ pub fn smp_active() -> bool { AP_ONLINE_COUNT.load(Ordering::Relaxed) > 1 } +#[inline(always)] +pub fn shutdown_quiesce_requested() -> bool { + SHUTDOWN_QUIESCE_REQUESTED.load(Ordering::Acquire) +} + +#[inline(always)] +pub fn shutdown_quiesce_ack(core_idx: u32) { + if core_idx < 64 { + SHUTDOWN_QUIESCE_ACK_MASK.fetch_or(1u64 << core_idx, Ordering::Release); + } +} + +pub fn request_shutdown_quiesce() { + SHUTDOWN_QUIESCE_ACK_MASK.store(1, Ordering::Release); + SHUTDOWN_QUIESCE_REQUESTED.store(true, Ordering::Release); +} + +pub fn wait_for_shutdown_quiesce(timeout_ms: u64) -> bool { + let online = AP_ONLINE_COUNT.load(Ordering::Acquire) as usize; + if online <= 1 { + return true; + } + + let expected = if online >= 64 { + u64::MAX + } else { + (1u64 << online) - 1 + }; + + let tsc_hz = crate::process::scheduler::tsc_frequency(); + let deadline = if tsc_hz > 0 { + let ticks_per_ms = (tsc_hz / 1000).max(1); + Some( + crate::cpu::tsc::read_tsc() + .saturating_add(timeout_ms.saturating_mul(ticks_per_ms)), + ) + } else { + None + }; + + // no calibrated TSC: still bound the wait so teardown cannot deadlock forever. + let mut fallback_spins = timeout_ms.saturating_mul(200_000).max(200_000); + + loop { + let acked = SHUTDOWN_QUIESCE_ACK_MASK.load(Ordering::Acquire); + if (acked & expected) == expected { + return true; + } + + if let Some(d) = deadline { + if crate::cpu::tsc::read_tsc() >= d { + return false; + } + } else { + if fallback_spins == 0 { + return false; + } + fallback_spins -= 1; + } + + core::hint::spin_loop(); + } +} + // ── Offset validation ──────────────────────────────────────────────────── /// Compile-time offset checks would be ideal, but we can't use diff --git a/hwinit/src/cpu/reset.rs b/hwinit/src/cpu/reset.rs new file mode 100644 index 00000000..62a3d7b5 --- /dev/null +++ b/hwinit/src/cpu/reset.rs @@ -0,0 +1,123 @@ +//! Reset helpers for machine restart paths. + +#[inline(always)] +fn shutdown_stage(msg: &str) { + crate::serial::fb_puts(msg); + crate::serial::fb_puts("\n"); + crate::serial::checkpoint(msg); +} + +#[inline(always)] +unsafe fn io_wait() { + crate::cpu::pio::outb(0x80, 0); +} + +#[inline(always)] +unsafe fn kbc_wait_input_empty() { + for _ in 0..100_000 { + if (crate::cpu::pio::inb(0x64) & 0x02) == 0 { + return; + } + core::hint::spin_loop(); + } +} + +#[inline(always)] +unsafe fn reset_via_cf9() { + crate::cpu::pio::outb(0xCF9, 0x02); + io_wait(); + crate::cpu::pio::outb(0xCF9, 0x06); +} + +#[inline(always)] +unsafe fn reset_via_port92() { + let mut v = crate::cpu::pio::inb(0x92); + v &= !0x01; + crate::cpu::pio::outb(0x92, v); + io_wait(); + crate::cpu::pio::outb(0x92, v | 0x01); +} + +#[inline(always)] +unsafe fn reset_via_8042() { + kbc_wait_input_empty(); + crate::cpu::pio::outb(0x64, 0xFE); +} + +#[inline(always)] +unsafe fn triple_fault_reset() -> ! { + #[repr(C, packed)] + struct Idtr { + limit: u16, + base: u64, + } + let idtr = Idtr { limit: 0, base: 0 }; + core::arch::asm!("lidt [{}]", in(reg) &idtr, options(readonly, nostack)); + core::arch::asm!("ud2", options(noreturn)); +} + +pub unsafe fn reset_machine_now() -> ! { + crate::serial::set_checkpoints_enabled(true); + shutdown_stage("shutdown: request ap quiesce"); + + // Ask APs to park before we start poking reset controls. + crate::cpu::per_cpu::request_shutdown_quiesce(); + let quiesced = crate::cpu::per_cpu::wait_for_shutdown_quiesce(500); + if quiesced { + shutdown_stage("shutdown: ap quiesce complete"); + } else { + shutdown_stage("shutdown: ap quiesce timeout; forcing reset"); + } + + crate::cpu::idt::disable_interrupts(); + + shutdown_stage("shutdown: reset via cf9"); + reset_via_cf9(); + for _ in 0..64 { + io_wait(); + } + + shutdown_stage("shutdown: reset via port92"); + reset_via_port92(); + for _ in 0..64 { + io_wait(); + } + + shutdown_stage("shutdown: reset via 8042"); + reset_via_8042(); + for _ in 0..64 { + io_wait(); + } + + shutdown_stage("shutdown: reset via triple-fault"); + triple_fault_reset() +} + +pub unsafe fn wait_for_keypress_or_timeout_ms(timeout_ms: u64) { + let tsc_hz = crate::process::scheduler::tsc_frequency(); + let deadline = if tsc_hz > 0 { + let ticks_per_ms = tsc_hz / 1000; + Some( + crate::cpu::tsc::read_tsc() + .saturating_add(timeout_ms.saturating_mul(ticks_per_ms.max(1))), + ) + } else { + None + }; + + loop { + let status = crate::cpu::pio::inb(0x64); + if (status & 0x01) != 0 { + let _ = crate::cpu::pio::inb(0x60); + break; + } + + if let Some(d) = deadline { + if crate::cpu::tsc::read_tsc() >= d { + break; + } + } + + core::hint::spin_loop(); + } +} diff --git a/hwinit/src/process/schedular/tick.rs b/hwinit/src/process/schedular/tick.rs index dd7887a3..33591560 100644 --- a/hwinit/src/process/schedular/tick.rs +++ b/hwinit/src/process/schedular/tick.rs @@ -31,6 +31,15 @@ unsafe fn ap_idle_hlt_loop() -> ! { } } +#[inline(never)] +unsafe fn ap_quiesce_hlt_loop(core_idx: u32) -> ! { + crate::cpu::per_cpu::shutdown_quiesce_ack(core_idx); + core::arch::asm!("cli", options(nostack, nomem)); + loop { + core::arch::asm!("hlt", options(nostack, nomem)); + } +} + unsafe fn ap_idle_context(core_idx: u32) -> &'static CpuContext { let ctx = &mut AP_IDLE_CTX[core_idx as usize]; let pcpu = crate::cpu::per_cpu::current(); @@ -64,6 +73,10 @@ pub unsafe extern "C" fn scheduler_tick(current_ctx: &CpuContext) -> &'static Cp let now_tsc = crate::cpu::tsc::read_tsc(); let core_idx = this_core_index(); + if core_idx != 0 && crate::cpu::per_cpu::shutdown_quiesce_requested() { + ap_quiesce_hlt_loop(core_idx); + } + if core_idx == 0 { crate::syscall::handler::fb_present_tick(); crate::ps2_mouse::poll(); diff --git a/hwinit/src/syscall/handler/core.rs b/hwinit/src/syscall/handler/core.rs index bb0a576d..d739ad70 100644 --- a/hwinit/src/syscall/handler/core.rs +++ b/hwinit/src/syscall/handler/core.rs @@ -6,6 +6,15 @@ use crate::process::signals::Signal; use crate::serial::puts; use morpheus_helix::types::open_flags::{O_PIPE_READ, O_PIPE_WRITE}; +const SYSCTL_REBOOT_GRACEFUL: u64 = 0; +const SYSCTL_REBOOT_FORCE: u64 = 1; +const SYSCTL_SHUTDOWN_GRACEFUL: u64 = 2; +const SYSCTL_SHUTDOWN_FORCE: u64 = 3; +const SYSCTL_SHUTDOWN_PANIC: u64 = 4; + +static SYSTEM_CONTROL_IN_PROGRESS: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + // SYS_EXIT — terminate the current process /// `SYS_EXIT(code: i32)` — terminate the calling process. @@ -261,3 +270,95 @@ pub unsafe fn sys_sleep(millis: u64) -> u64 { let deadline = crate::cpu::tsc::read_tsc().saturating_add(millis.saturating_mul(ticks_per_ms)); crate::process::scheduler::block_sleep(deadline) } + +pub unsafe fn sys_system_control(mode: u64) -> u64 { + // single transition owner. concurrent reboot/shutdown callers get EBUSY. + if SYSTEM_CONTROL_IN_PROGRESS + .compare_exchange( + false, + true, + core::sync::atomic::Ordering::AcqRel, + core::sync::atomic::Ordering::Acquire, + ) + .is_err() + { + return EBUSY; + } + + match mode { + SYSCTL_REBOOT_FORCE | SYSCTL_SHUTDOWN_FORCE => hard_reset_now(), + SYSCTL_SHUTDOWN_PANIC => { + // Show crash screen, then reset from exception handler path. + crate::cpu::idt::set_reset_on_crash(true); + core::arch::asm!("ud2", options(noreturn)); + } + SYSCTL_REBOOT_GRACEFUL | SYSCTL_SHUTDOWN_GRACEFUL => graceful_reset_now(), + _ => { + SYSTEM_CONTROL_IN_PROGRESS.store(false, core::sync::atomic::Ordering::Release); + EINVAL + } + } +} + +unsafe fn graceful_reset_now() -> ! { + const MAX_SNAPSHOT: usize = 64; + const DRAIN_ROUNDS: usize = 24; + const DRAIN_BACKOFF_SPINS: usize = 200_000; + + let caller = SCHEDULER.current_pid(); + + crate::serial::set_checkpoints_enabled(true); + crate::serial::fb_puts("[INFO] [SHUTDOWN] draining processes\n"); + crate::serial::checkpoint("shutdown-drain-begin"); + + // Best-effort graceful drain: TERM first, then KILL survivors. + for round in 0..DRAIN_ROUNDS { + let mut procs = [crate::process::scheduler::ProcessInfo::zeroed(); MAX_SNAPSHOT]; + let n = SCHEDULER.snapshot_processes(&mut procs); + + let mut alive_user = 0usize; + for p in &procs[..n] { + let pid = p.pid; + if pid == 0 + || pid == caller + || matches!( + p.state, + crate::process::ProcessState::Terminated | crate::process::ProcessState::Zombie + ) + { + continue; + } + alive_user += 1; + let sig = if round < (DRAIN_ROUNDS / 2) { + Signal::SIGTERM + } else { + Signal::SIGKILL + }; + let _ = SCHEDULER.send_signal(pid, sig); + } + + if alive_user == 0 { + crate::serial::checkpoint("shutdown-drain-empty"); + break; + } + + // No HLT here. if local timer IRQ is masked/misrouted this would hang forever. + // We still make forward progress to reset even if teardown is partial. + if round == (DRAIN_ROUNDS / 2) { + crate::serial::checkpoint("shutdown-drain-escalate-sigkill"); + } + for _ in 0..DRAIN_BACKOFF_SPINS { + core::hint::spin_loop(); + } + } + + crate::serial::fb_puts("[INFO] [SHUTDOWN] entering reset sequence\n"); + crate::serial::checkpoint("shutdown-reset-seq"); + + // Do not block here on VFS lock during teardown; reset path must always complete. + hard_reset_now() +} + +unsafe fn hard_reset_now() -> ! { + crate::cpu::reset::reset_machine_now() +} diff --git a/hwinit/src/syscall/mod.rs b/hwinit/src/syscall/mod.rs index dcb3edd6..de9dc880 100644 --- a/hwinit/src/syscall/mod.rs +++ b/hwinit/src/syscall/mod.rs @@ -230,6 +230,7 @@ pub const SYS_MOUSE_FORWARD: u64 = 94; pub const SYS_WIN_SURFACE_DIRTY_CLEAR: u64 = 95; pub const SYS_TRY_WAIT: u64 = 96; pub const SYS_FORWARD_INPUT: u64 = 97; +pub const SYS_SYSTEM_CONTROL: u64 = 98; // EXTERN ASM FUNCTIONS @@ -375,6 +376,7 @@ pub unsafe extern "C" fn syscall_dispatch( SYS_WIN_SURFACE_DIRTY_CLEAR => sys_win_surface_dirty_clear(a1), SYS_TRY_WAIT => sys_try_wait(a1), SYS_FORWARD_INPUT => sys_forward_input(a1, a2, a3), + SYS_SYSTEM_CONTROL => sys_system_control(a1), unknown => { crate::serial::log_warn("SYSCALL", 801, "unknown syscall number"); let _ = unknown; diff --git a/libmorpheus/src/raw.rs b/libmorpheus/src/raw.rs index 8f4323b1..adfc320b 100644 --- a/libmorpheus/src/raw.rs +++ b/libmorpheus/src/raw.rs @@ -139,6 +139,14 @@ pub const SYS_MOUSE_FORWARD: u64 = 94; pub const SYS_WIN_SURFACE_DIRTY_CLEAR: u64 = 95; pub const SYS_TRY_WAIT: u64 = 96; pub const SYS_FORWARD_INPUT: u64 = 97; +pub const SYS_SYSTEM_CONTROL: u64 = 98; + +// SYS_SYSTEM_CONTROL modes +pub const SYSCTL_REBOOT_GRACEFUL: u64 = 0; +pub const SYSCTL_REBOOT_FORCE: u64 = 1; +pub const SYSCTL_SHUTDOWN_GRACEFUL: u64 = 2; +pub const SYSCTL_SHUTDOWN_FORCE: u64 = 3; +pub const SYSCTL_SHUTDOWN_PANIC: u64 = 4; #[inline(always)] pub unsafe fn syscall0(nr: u64) -> u64 { diff --git a/libmorpheus/src/sys.rs b/libmorpheus/src/sys.rs index 1cc69b9c..0639d6f9 100644 --- a/libmorpheus/src/sys.rs +++ b/libmorpheus/src/sys.rs @@ -91,3 +91,45 @@ pub fn syslog(msg: &str) { syscall2(SYS_SYSLOG, msg.as_ptr() as u64, msg.len() as u64); } } + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u64)] +pub enum SystemControlMode { + RebootGraceful = SYSCTL_REBOOT_GRACEFUL, + RebootForce = SYSCTL_REBOOT_FORCE, + ShutdownGraceful = SYSCTL_SHUTDOWN_GRACEFUL, + ShutdownForce = SYSCTL_SHUTDOWN_FORCE, + ShutdownPanic = SYSCTL_SHUTDOWN_PANIC, +} + +/// System control request. On success this does not return. +pub fn system_control(mode: SystemControlMode) -> Result<(), u64> { + let ret = unsafe { syscall1(SYS_SYSTEM_CONTROL, mode as u64) }; + if is_error(ret) { + Err(ret) + } else { + Ok(()) + } +} + +pub fn reboot(force: bool) -> Result<(), u64> { + let mode = if force { + SystemControlMode::RebootForce + } else { + SystemControlMode::RebootGraceful + }; + system_control(mode) +} + +pub fn shutdown(force: bool) -> Result<(), u64> { + let mode = if force { + SystemControlMode::ShutdownForce + } else { + SystemControlMode::ShutdownGraceful + }; + system_control(mode) +} + +pub fn shutdown_panic() -> Result<(), u64> { + system_control(SystemControlMode::ShutdownPanic) +} diff --git a/persistent/src/pe/embedded_reloc_data.rs b/persistent/src/pe/embedded_reloc_data.rs index 0d51e504..7d2b9bbb 100644 --- a/persistent/src/pe/embedded_reloc_data.rs +++ b/persistent/src/pe/embedded_reloc_data.rs @@ -8,7 +8,7 @@ //! Run: ./tools/extract-reloc-data.sh after each build /// Original .reloc section RVA -pub const RELOC_RVA: u32 = 0x00611000; +pub const RELOC_RVA: u32 = 0x00613000; /// Original .reloc section size pub const RELOC_SIZE: u32 = 0x0000025c; @@ -17,59 +17,59 @@ pub const RELOC_SIZE: u32 = 0x0000025c; pub const ORIGINAL_IMAGE_BASE: u64 = 0x0000004001000000; /// Hardcoded .reloc section data (604 bytes) -/// Extracted from morpheus-bootloader.efi at file offset 0x001d3a00 +/// Extracted from morpheus-bootloader.efi at file offset 0x001d5400 #[allow(dead_code)] pub const RELOC_DATA: [u8; 604] = [ - 0x00, 0xf0, 0x04, 0x00, 0x34, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, + 0x00, 0x10, 0x05, 0x00, 0x34, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xf8, 0xa6, 0x10, 0xa7, 0x28, 0xa7, 0xb8, 0xa7, 0xc0, 0xa8, 0xc8, 0xaa, 0xe0, 0xaa, 0xe8, 0xab, 0xe8, 0xad, 0x00, 0xae, 0x18, 0xae, 0x30, 0xae, 0x48, 0xae, 0x60, 0xae, - 0x78, 0xae, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x38, 0x00, 0x00, 0x00, + 0x78, 0xae, 0x00, 0x00, 0x00, 0x20, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, 0x90, 0xa0, 0xa8, 0xa0, 0xc0, 0xa0, 0x68, 0xa3, 0x80, 0xa3, 0x98, 0xa3, 0xf8, 0xa4, 0x10, 0xa5, 0x28, 0xa5, 0x40, 0xa5, 0x58, 0xa5, 0x70, 0xa5, - 0xa8, 0xa5, 0xc0, 0xa5, 0x38, 0xa6, 0x70, 0xa6, 0x80, 0xa7, 0x98, 0xa7, - 0xe8, 0xa7, 0x00, 0xa8, 0x18, 0xa8, 0x50, 0xa8, 0x80, 0xa8, 0x00, 0x00, - 0x00, 0x10, 0x05, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0xaa, 0x50, 0xaa, - 0x68, 0xaa, 0xf0, 0xaa, 0x08, 0xab, 0xa0, 0xab, 0xb8, 0xab, 0xd0, 0xab, - 0xe8, 0xab, 0x00, 0xac, 0x10, 0xad, 0x70, 0xad, 0xe8, 0xad, 0x28, 0xaf, - 0x48, 0xaf, 0xc8, 0xaf, 0x00, 0x20, 0x05, 0x00, 0x38, 0x00, 0x00, 0x00, - 0x30, 0xa0, 0x48, 0xa0, 0x60, 0xa0, 0x68, 0xa2, 0x80, 0xa2, 0x98, 0xa2, - 0xb0, 0xa2, 0xc8, 0xa2, 0xe0, 0xa2, 0xf8, 0xa2, 0xb8, 0xa3, 0x20, 0xa4, - 0x40, 0xa4, 0x58, 0xa4, 0x70, 0xa4, 0x88, 0xa4, 0xa0, 0xa4, 0xf8, 0xa4, - 0x10, 0xa5, 0x28, 0xa5, 0x40, 0xa5, 0x58, 0xa5, 0x98, 0xa5, 0x28, 0xa6, - 0x00, 0x30, 0x05, 0x00, 0x64, 0x00, 0x00, 0x00, 0xb8, 0xa0, 0xa8, 0xa5, - 0xc0, 0xa5, 0xd8, 0xa5, 0xf0, 0xa5, 0x08, 0xa6, 0x20, 0xa6, 0x70, 0xa6, - 0x08, 0xa7, 0x78, 0xa9, 0xd0, 0xaa, 0xe0, 0xaa, 0xf0, 0xaa, 0x00, 0xab, - 0x10, 0xab, 0x20, 0xab, 0x30, 0xab, 0x40, 0xab, 0x50, 0xab, 0x60, 0xab, - 0x70, 0xab, 0x80, 0xab, 0x90, 0xab, 0xa0, 0xab, 0xb0, 0xab, 0xc0, 0xab, - 0xd0, 0xab, 0xe0, 0xab, 0xf0, 0xab, 0x00, 0xac, 0x10, 0xac, 0x20, 0xac, - 0x30, 0xac, 0x40, 0xac, 0x50, 0xac, 0x60, 0xac, 0x70, 0xac, 0x80, 0xac, - 0x90, 0xac, 0xa0, 0xac, 0xb0, 0xac, 0xc0, 0xac, 0xa0, 0xae, 0x38, 0xaf, - 0x90, 0xaf, 0x00, 0x00, 0x00, 0x40, 0x05, 0x00, 0x28, 0x00, 0x00, 0x00, - 0x00, 0xa1, 0x18, 0xa1, 0x68, 0xa1, 0x78, 0xa1, 0x88, 0xa1, 0x98, 0xa1, - 0xa8, 0xa1, 0xd8, 0xa1, 0x10, 0xa2, 0x58, 0xa2, 0xa8, 0xa2, 0xe0, 0xa2, - 0xf8, 0xa2, 0x40, 0xa3, 0xc8, 0xa3, 0x00, 0x00, 0x00, 0x50, 0x05, 0x00, - 0x54, 0x00, 0x00, 0x00, 0x80, 0xa0, 0x98, 0xa0, 0xd0, 0xa0, 0x00, 0xa1, - 0x18, 0xa1, 0x30, 0xa1, 0x48, 0xa1, 0x60, 0xa1, 0x78, 0xa1, 0x90, 0xa1, - 0xa8, 0xa1, 0xe8, 0xa1, 0x00, 0xa2, 0x18, 0xa2, 0x30, 0xa2, 0x48, 0xa2, - 0x80, 0xa2, 0xb0, 0xa2, 0xc8, 0xa2, 0xe0, 0xa2, 0xf8, 0xa2, 0x30, 0xa3, - 0x48, 0xa3, 0x60, 0xa3, 0x78, 0xa3, 0x90, 0xa3, 0xa8, 0xa3, 0x38, 0xa4, - 0x80, 0xaa, 0x90, 0xaa, 0xc0, 0xaa, 0xd8, 0xaa, 0x20, 0xab, 0x30, 0xab, - 0x40, 0xab, 0x68, 0xab, 0xa0, 0xab, 0x00, 0x00, 0x00, 0x60, 0x05, 0x00, - 0x68, 0x00, 0x00, 0x00, 0xe0, 0xa0, 0xf0, 0xa0, 0x00, 0xa1, 0x10, 0xa1, - 0x60, 0xa1, 0x70, 0xa1, 0x80, 0xa1, 0x90, 0xa1, 0xa0, 0xa1, 0xc8, 0xa1, - 0xd8, 0xa1, 0xe8, 0xa1, 0x50, 0xa2, 0x60, 0xa2, 0x70, 0xa2, 0xb8, 0xa2, - 0xc8, 0xa2, 0x00, 0xa3, 0x10, 0xa3, 0x38, 0xa3, 0x48, 0xa3, 0x80, 0xa3, - 0x98, 0xa3, 0xf0, 0xa3, 0x60, 0xaa, 0x78, 0xaa, 0xb0, 0xaa, 0x00, 0xab, - 0x48, 0xab, 0x58, 0xab, 0x58, 0xac, 0x70, 0xac, 0xa8, 0xac, 0xc0, 0xac, - 0xd8, 0xac, 0xf0, 0xac, 0x20, 0xae, 0x50, 0xae, 0x68, 0xae, 0xc0, 0xae, - 0xd8, 0xae, 0xf0, 0xae, 0x08, 0xaf, 0x20, 0xaf, 0x78, 0xaf, 0x90, 0xaf, - 0xa8, 0xaf, 0xd8, 0xaf, 0x00, 0x70, 0x05, 0x00, 0x18, 0x00, 0x00, 0x00, - 0x30, 0xa0, 0x48, 0xa0, 0x60, 0xa0, 0x78, 0xa0, 0x90, 0xa0, 0xa8, 0xa0, - 0xc0, 0xa0, 0x00, 0x00, 0x00, 0x80, 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x40, 0xa3, 0x48, 0xa3, 0x00, 0x20, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x80, 0xa3, 0xc0, 0xae, 0x00, 0x50, 0x1d, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0xf8, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x61, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0xa8, 0xa5, 0xc0, 0xa5, 0x58, 0xa6, 0x70, 0xa6, 0xc8, 0xa6, 0xe0, 0xa6, + 0x30, 0xa7, 0x48, 0xa7, 0x60, 0xa7, 0x98, 0xa7, 0xc8, 0xa7, 0xf8, 0xa7, + 0x10, 0xa8, 0xa8, 0xa8, 0xc0, 0xa8, 0xd8, 0xa8, 0xf0, 0xa8, 0x08, 0xa9, + 0x18, 0xaa, 0x78, 0xaa, 0xf0, 0xaa, 0x30, 0xac, 0x50, 0xac, 0x00, 0x00, + 0x00, 0x30, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0xae, 0x70, 0xae, + 0x88, 0xae, 0xa0, 0xae, 0x00, 0x40, 0x05, 0x00, 0x38, 0x00, 0x00, 0x00, + 0xa8, 0xa0, 0xc0, 0xa0, 0xd8, 0xa0, 0xf0, 0xa0, 0x08, 0xa1, 0x20, 0xa1, + 0x38, 0xa1, 0x38, 0xa2, 0x90, 0xa2, 0xb0, 0xa3, 0x18, 0xa4, 0x38, 0xa4, + 0x50, 0xa4, 0x68, 0xa4, 0x80, 0xa4, 0x98, 0xa4, 0x58, 0xa5, 0xb0, 0xa5, + 0xc8, 0xa5, 0xe0, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0x50, 0xa6, 0xe0, 0xa6, + 0x00, 0x50, 0x05, 0x00, 0x60, 0x00, 0x00, 0x00, 0x80, 0xa1, 0x98, 0xa1, + 0x88, 0xa6, 0xa0, 0xa6, 0xb8, 0xa6, 0xd0, 0xa6, 0xe8, 0xa6, 0x00, 0xa7, + 0x78, 0xa7, 0xb0, 0xa7, 0x90, 0xaa, 0xe8, 0xab, 0xf8, 0xab, 0x08, 0xac, + 0x18, 0xac, 0x28, 0xac, 0x38, 0xac, 0x48, 0xac, 0x58, 0xac, 0x68, 0xac, + 0x78, 0xac, 0x88, 0xac, 0x98, 0xac, 0xa8, 0xac, 0xb8, 0xac, 0xc8, 0xac, + 0xd8, 0xac, 0xe8, 0xac, 0xf8, 0xac, 0x08, 0xad, 0x18, 0xad, 0x28, 0xad, + 0x38, 0xad, 0x48, 0xad, 0x58, 0xad, 0x68, 0xad, 0x78, 0xad, 0x88, 0xad, + 0x98, 0xad, 0xa8, 0xad, 0xb8, 0xad, 0xc8, 0xad, 0xd8, 0xad, 0xb8, 0xaf, + 0x00, 0x60, 0x05, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x50, 0xa0, 0xa8, 0xa0, + 0x18, 0xa2, 0x30, 0xa2, 0x80, 0xa2, 0x90, 0xa2, 0xa0, 0xa2, 0xb0, 0xa2, + 0xc0, 0xa2, 0xf0, 0xa2, 0x28, 0xa3, 0x70, 0xa3, 0xc0, 0xa3, 0xf8, 0xa3, + 0x10, 0xa4, 0x58, 0xa4, 0xe0, 0xa4, 0x00, 0x00, 0x00, 0x70, 0x05, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x98, 0xa1, 0xb0, 0xa1, 0xe8, 0xa1, 0x18, 0xa2, + 0x30, 0xa2, 0x48, 0xa2, 0x60, 0xa2, 0x78, 0xa2, 0x90, 0xa2, 0xa8, 0xa2, + 0xc0, 0xa2, 0x00, 0xa3, 0x18, 0xa3, 0x30, 0xa3, 0x48, 0xa3, 0x60, 0xa3, + 0x98, 0xa3, 0xc8, 0xa3, 0xe0, 0xa3, 0xf8, 0xa3, 0x10, 0xa4, 0x48, 0xa4, + 0x60, 0xa4, 0x78, 0xa4, 0x90, 0xa4, 0xa8, 0xa4, 0xc0, 0xa4, 0x50, 0xa5, + 0x98, 0xab, 0xa8, 0xab, 0xd8, 0xab, 0xf0, 0xab, 0x38, 0xac, 0x48, 0xac, + 0x58, 0xac, 0x80, 0xac, 0xb8, 0xac, 0x00, 0x00, 0x00, 0x80, 0x05, 0x00, + 0x54, 0x00, 0x00, 0x00, 0xf8, 0xa1, 0x08, 0xa2, 0x18, 0xa2, 0x28, 0xa2, + 0x78, 0xa2, 0x88, 0xa2, 0x98, 0xa2, 0xa8, 0xa2, 0xb8, 0xa2, 0xe0, 0xa2, + 0xf0, 0xa2, 0x00, 0xa3, 0x68, 0xa3, 0x78, 0xa3, 0x88, 0xa3, 0xd0, 0xa3, + 0xe0, 0xa3, 0x18, 0xa4, 0x28, 0xa4, 0x50, 0xa4, 0x60, 0xa4, 0x98, 0xa4, + 0xb0, 0xa4, 0x08, 0xa5, 0x78, 0xab, 0x90, 0xab, 0xc8, 0xab, 0x18, 0xac, + 0x60, 0xac, 0x70, 0xac, 0x70, 0xad, 0x88, 0xad, 0xc0, 0xad, 0xd8, 0xad, + 0xf0, 0xad, 0x08, 0xae, 0x70, 0xae, 0xb8, 0xaf, 0x00, 0x90, 0x05, 0x00, + 0x2c, 0x00, 0x00, 0x00, 0x28, 0xa0, 0x80, 0xa0, 0x98, 0xa0, 0xb0, 0xa0, + 0xc8, 0xa0, 0xe0, 0xa0, 0x38, 0xa1, 0x50, 0xa1, 0x68, 0xa1, 0x98, 0xa1, + 0xf0, 0xa1, 0x08, 0xa2, 0x20, 0xa2, 0x38, 0xa2, 0x50, 0xa2, 0x68, 0xa2, + 0x80, 0xa2, 0x00, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x30, 0xa2, 0x38, 0xa2, 0x00, 0x40, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x80, 0xa2, 0x98, 0xa3, 0x00, 0x60, 0x1d, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0xd0, 0xa9, 0x00, 0x00, 0x00, 0x20, 0x61, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, ]; diff --git a/shell/src/builtin/help.rs b/shell/src/builtin/help.rs index a0c270bf..548bbee6 100644 --- a/shell/src/builtin/help.rs +++ b/shell/src/builtin/help.rs @@ -204,6 +204,25 @@ pub fn usage(cmd: &str) -> Option<&'static str> { "\n", "Suspends the shell for the given duration.\n", ), + "reboot" => concat!( + "reboot — restart the machine\n", + "\n", + "USAGE\n", + " reboot [-f|--force]\n", + "\n", + "Default is graceful reboot.\n", + "Use -f for immediate hard reset.\n", + ), + "shutdown" => concat!( + "shutdown — graceful shutdown then reset\n", + "\n", + "USAGE\n", + " shutdown [-f|--force] [-p]\n", + "\n", + "Default is graceful shutdown with reset.\n", + "-f: immediate hard reset\n", + "-p: intentional kernel panic (yes just because) then reset\n", + ), "true" => concat!( "true — return success\n", "\n", diff --git a/shell/src/builtin/mod.rs b/shell/src/builtin/mod.rs index 8673ec42..be48126e 100644 --- a/shell/src/builtin/mod.rs +++ b/shell/src/builtin/mod.rs @@ -78,6 +78,8 @@ pub fn dispatch(argv: &[String], cwd: &str) -> Option { "kill" => Some(proc_cmds::kill(args)), "sysinfo" => Some(proc_cmds::sysinfo()), "sleep" => Some(proc_cmds::sleep(args)), + "reboot" => Some(proc_cmds::reboot(args)), + "shutdown" => Some(proc_cmds::shutdown(args)), _ => None, } @@ -127,6 +129,8 @@ pub fn dispatch_fb(argv: &[String], cwd: &str, fb: &Framebuffer, con: &mut Conso "kill" => Some(proc_cmds::kill_fb(args, fb, con)), "sysinfo" => Some(proc_cmds::sysinfo_fb(fb, con)), "sleep" => Some(proc_cmds::sleep_fb(args, fb, con)), + "reboot" => Some(proc_cmds::reboot_fb(args, fb, con)), + "shutdown" => Some(proc_cmds::shutdown_fb(args, fb, con)), _ => None, } @@ -365,6 +369,8 @@ const HELP_TEXT: &str = concat!( " kill [-s] Send signal (default: SIGTERM)\n", " sysinfo System information\n", " sleep Sleep milliseconds\n", + " reboot [-f] Reboot (graceful default)\n", + " shutdown [-f|-p] Shutdown+reset (or panic+reset)\n", "\n", "Operators:\n", " cmd1 | cmd2 Pipeline\n", diff --git a/shell/src/builtin/proc_cmds.rs b/shell/src/builtin/proc_cmds.rs index 4b846460..e0c63e3a 100644 --- a/shell/src/builtin/proc_cmds.rs +++ b/shell/src/builtin/proc_cmds.rs @@ -241,3 +241,145 @@ pub fn sleep_fb(args: &[String], fb: &Framebuffer, con: &mut Console) -> i32 { process::sleep(ms); 0 } + +pub fn reboot(args: &[String]) -> i32 { + let mut force = false; + for arg in args { + match arg.as_str() { + "-f" | "--force" => force = true, + "-h" | "--help" => { + libmorpheus::io::print( + "reboot — restart the machine\n\nUSAGE\n reboot [-f|--force]\n\nDefault is graceful reboot.\n", + ); + return 0; + } + _ => { + libmorpheus::eprintln!("reboot: unknown option: {}", arg); + return 1; + } + } + } + + match libmorpheus::sys::reboot(force) { + Ok(()) => 0, + Err(e) => { + libmorpheus::eprintln!("reboot: error 0x{:x}", e); + 1 + } + } +} + +pub fn reboot_fb(args: &[String], fb: &Framebuffer, con: &mut Console) -> i32 { + let mut force = false; + for arg in args { + match arg.as_str() { + "-f" | "--force" => force = true, + "-h" | "--help" => { + con.write_str( + fb, + "reboot — restart the machine\n\nUSAGE\n reboot [-f|--force]\n\nDefault is graceful reboot.\n", + ); + return 0; + } + _ => { + con.write_colored(fb, &format!("reboot: unknown option: {}\n", arg), (170, 0, 0)); + return 1; + } + } + } + + match libmorpheus::sys::reboot(force) { + Ok(()) => 0, + Err(e) => { + con.write_colored(fb, &format!("reboot: error 0x{:x}\n", e), (170, 0, 0)); + 1 + } + } +} + +pub fn shutdown(args: &[String]) -> i32 { + let mut force = false; + let mut panic_mode = false; + + for arg in args { + match arg.as_str() { + "-f" | "--force" => force = true, + "-p" => panic_mode = true, + "-h" | "--help" => { + libmorpheus::io::print( + "shutdown — stop services and reset\n\nUSAGE\n shutdown [-f|--force] [-p]\n\nDefault is graceful reset.\n -f immediate hard reset\n -p intentional kernel panic (BSOD) then reset\n", + ); + return 0; + } + _ => { + libmorpheus::eprintln!("shutdown: unknown option: {}", arg); + return 1; + } + } + } + + if force && panic_mode { + libmorpheus::eprintln!("shutdown: -f and -p are mutually exclusive"); + return 1; + } + + let ret = if panic_mode { + libmorpheus::sys::shutdown_panic() + } else { + libmorpheus::sys::shutdown(force) + }; + + match ret { + Ok(()) => 0, + Err(e) => { + libmorpheus::eprintln!("shutdown: error 0x{:x}", e); + 1 + } + } +} + +pub fn shutdown_fb(args: &[String], fb: &Framebuffer, con: &mut Console) -> i32 { + let mut force = false; + let mut panic_mode = false; + + for arg in args { + match arg.as_str() { + "-f" | "--force" => force = true, + "-p" => panic_mode = true, + "-h" | "--help" => { + con.write_str( + fb, + "shutdown — stop services and reset\n\nUSAGE\n shutdown [-f|--force] [-p]\n\nDefault is graceful reset.\n -f immediate hard reset\n -p intentional kernel panic (BSOD) then reset\n", + ); + return 0; + } + _ => { + con.write_colored( + fb, + &format!("shutdown: unknown option: {}\n", arg), + (170, 0, 0), + ); + return 1; + } + } + } + + if force && panic_mode { + con.write_colored(fb, "shutdown: -f and -p are mutually exclusive\n", (170, 0, 0)); + return 1; + } + + let ret = if panic_mode { + libmorpheus::sys::shutdown_panic() + } else { + libmorpheus::sys::shutdown(force) + }; + + match ret { + Ok(()) => 0, + Err(e) => { + con.write_colored(fb, &format!("shutdown: error 0x{:x}\n", e), (170, 0, 0)); + 1 + } + } +} From e8ba205a6c9b3b059961e21f78c6572448580cbb Mon Sep 17 00:00:00 2001 From: adrerl Date: Wed, 11 Mar 2026 10:50:44 +0100 Subject: [PATCH 03/12] Implement shutdown and reboot handling with new ownership management - Added per-CPU reboot ownership tracking in per_cpu.rs - Enhanced reset_machine_now function to respect reboot ownership - Introduced shutdown handlers for PCI bus mastering and display ownership release - Created prepare handlers for shutdown processes - Updated syscall handlers to integrate new shutdown functionality - Registered built-in shutdown handlers in mod.rs --- hwinit/src/cpu/per_cpu.rs | 30 +++++- hwinit/src/cpu/reset.rs | 14 +++ hwinit/src/lib.rs | 1 + hwinit/src/process/schedular/tick.rs | 16 ++- hwinit/src/shutdown/handlers.rs | 99 ++++++++++++++++++ hwinit/src/shutdown/mod.rs | 23 +++++ hwinit/src/shutdown/prepare.rs | 125 +++++++++++++++++++++++ hwinit/src/syscall/handler/core.rs | 39 ++++++- hwinit/src/syscall/handler/fb.rs | 10 ++ persistent/src/pe/embedded_reloc_data.rs | 101 +++++++++--------- 10 files changed, 397 insertions(+), 61 deletions(-) create mode 100644 hwinit/src/shutdown/handlers.rs create mode 100644 hwinit/src/shutdown/mod.rs create mode 100644 hwinit/src/shutdown/prepare.rs diff --git a/hwinit/src/cpu/per_cpu.rs b/hwinit/src/cpu/per_cpu.rs index 832fc57f..5050ce8b 100644 --- a/hwinit/src/cpu/per_cpu.rs +++ b/hwinit/src/cpu/per_cpu.rs @@ -113,6 +113,7 @@ pub static AP_ONLINE_COUNT: AtomicU32 = AtomicU32::new(0); static SHUTDOWN_QUIESCE_REQUESTED: AtomicBool = AtomicBool::new(false); static SHUTDOWN_QUIESCE_ACK_MASK: AtomicU64 = AtomicU64::new(0); +static REBOOT_OWNER_CORE: AtomicU32 = AtomicU32::new(u32::MAX); /// Total number of detected CPUs (BSP + APs). Set by BSP during MADT parse /// or CPUID enumeration, before AP startup. @@ -326,7 +327,9 @@ pub fn shutdown_quiesce_ack(core_idx: u32) { } pub fn request_shutdown_quiesce() { - SHUTDOWN_QUIESCE_ACK_MASK.store(1, Ordering::Release); + let owner = reboot_owner().unwrap_or(0); + let owner_mask = if owner < 64 { 1u64 << owner } else { 1u64 }; + SHUTDOWN_QUIESCE_ACK_MASK.store(owner_mask, Ordering::Release); SHUTDOWN_QUIESCE_REQUESTED.store(true, Ordering::Release); } @@ -377,6 +380,31 @@ pub fn wait_for_shutdown_quiesce(timeout_ms: u64) -> bool { } } +#[inline(always)] +pub fn set_reboot_owner(core_idx: u32) { + REBOOT_OWNER_CORE.store(core_idx, Ordering::Release); +} + +#[inline(always)] +pub fn clear_reboot_owner() { + REBOOT_OWNER_CORE.store(u32::MAX, Ordering::Release); +} + +#[inline(always)] +pub fn reboot_owner() -> Option { + let owner = REBOOT_OWNER_CORE.load(Ordering::Acquire); + if owner == u32::MAX { + None + } else { + Some(owner) + } +} + +#[inline(always)] +pub fn is_reboot_owner(core_idx: u32) -> bool { + REBOOT_OWNER_CORE.load(Ordering::Acquire) == core_idx +} + // ── Offset validation ──────────────────────────────────────────────────── /// Compile-time offset checks would be ideal, but we can't use diff --git a/hwinit/src/cpu/reset.rs b/hwinit/src/cpu/reset.rs index 62a3d7b5..4a820ca2 100644 --- a/hwinit/src/cpu/reset.rs +++ b/hwinit/src/cpu/reset.rs @@ -57,6 +57,20 @@ unsafe fn triple_fault_reset() -> ! { } pub unsafe fn reset_machine_now() -> ! { + let core_idx = crate::cpu::per_cpu::current_core_index(); + if crate::cpu::per_cpu::reboot_owner().is_none() { + crate::cpu::per_cpu::set_reboot_owner(core_idx); + } + if let Some(owner) = crate::cpu::per_cpu::reboot_owner() { + if owner != core_idx { + shutdown_stage("shutdown: non-owner reset attempt blocked"); + crate::cpu::idt::disable_interrupts(); + loop { + core::arch::asm!("hlt", options(nostack, nomem)); + } + } + } + crate::serial::set_checkpoints_enabled(true); shutdown_stage("shutdown: request ap quiesce"); diff --git a/hwinit/src/lib.rs b/hwinit/src/lib.rs index d42e9b48..c1248dd9 100644 --- a/hwinit/src/lib.rs +++ b/hwinit/src/lib.rs @@ -98,6 +98,7 @@ pub mod platform; pub mod process; pub mod ps2_mouse; pub mod serial; +pub mod shutdown; pub mod stdin; pub mod stdout; pub mod sync; diff --git a/hwinit/src/process/schedular/tick.rs b/hwinit/src/process/schedular/tick.rs index 33591560..1f807496 100644 --- a/hwinit/src/process/schedular/tick.rs +++ b/hwinit/src/process/schedular/tick.rs @@ -68,15 +68,17 @@ unsafe fn ap_idle_context(core_idx: u32) -> &'static CpuContext { #[no_mangle] pub unsafe extern "C" fn scheduler_tick(current_ctx: &CpuContext) -> &'static CpuContext { - let tick = TICK_COUNT.fetch_add(1, Ordering::Relaxed); - - let now_tsc = crate::cpu::tsc::read_tsc(); let core_idx = this_core_index(); - if core_idx != 0 && crate::cpu::per_cpu::shutdown_quiesce_requested() { + if crate::cpu::per_cpu::shutdown_quiesce_requested() + && !crate::cpu::per_cpu::is_reboot_owner(core_idx) + { ap_quiesce_hlt_loop(core_idx); } + let tick = TICK_COUNT.fetch_add(1, Ordering::Relaxed); + let now_tsc = crate::cpu::tsc::read_tsc(); + if core_idx == 0 { crate::syscall::handler::fb_present_tick(); crate::ps2_mouse::poll(); @@ -344,6 +346,12 @@ pub(super) unsafe fn wake_expired_sleepers() { } unsafe fn pick_next(current: usize, skip_kernel: bool, core_idx: u32) -> usize { + if crate::cpu::per_cpu::shutdown_quiesce_requested() + && crate::cpu::per_cpu::is_reboot_owner(core_idx) + { + return 0; + } + let n = MAX_PROCESSES; let is_bsp = core_idx == 0; let system_state = scheduler_system_state(); diff --git a/hwinit/src/shutdown/handlers.rs b/hwinit/src/shutdown/handlers.rs new file mode 100644 index 00000000..8f23d21e --- /dev/null +++ b/hwinit/src/shutdown/handlers.rs @@ -0,0 +1,99 @@ + +use super::prepare::{ + register_poweroff_handler, register_prepare_handler, register_restart_handler, TransitionKind, +}; + +fn prepare_disable_pci_bus_mastering(_kind: TransitionKind) -> bool { + let mut touched = 0u32; + + for bus in 0..=255u8 { + for device in 0..32u8 { + let addr = crate::pci::PciAddr::new(bus, device, 0); + let vendor = crate::pci::pci_cfg_read16(addr, crate::pci::offset::VENDOR_ID); + if vendor == 0xFFFF || vendor == 0x0000 { + continue; + } + + touched += clear_bus_master_on_function(addr) as u32; + + let header_type = crate::pci::pci_cfg_read16(addr, crate::pci::offset::HEADER_TYPE) as u8; + if (header_type & 0x80) != 0 { + for function in 1..8u8 { + let faddr = crate::pci::PciAddr::new(bus, device, function); + let fv = crate::pci::pci_cfg_read16(faddr, crate::pci::offset::VENDOR_ID); + if fv == 0xFFFF || fv == 0x0000 { + continue; + } + touched += clear_bus_master_on_function(faddr) as u32; + } + } + } + } + + if touched > 0 { + crate::serial::checkpoint("shutdown-prepare-pci-bm-off"); + } else { + crate::serial::checkpoint("shutdown-prepare-pci-bm-already-off"); + } + true +} + +#[inline(always)] +fn clear_bus_master_on_function(addr: crate::pci::PciAddr) -> bool { + const CMD_BUS_MASTER: u16 = 1 << 2; + const CMD_MEM_SPACE: u16 = 1 << 1; + + let cmd = crate::pci::pci_cfg_read16(addr, crate::pci::offset::COMMAND); + let new_cmd = cmd & !(CMD_BUS_MASTER | CMD_MEM_SPACE); + if cmd != new_cmd { + crate::pci::pci_cfg_write16(addr, crate::pci::offset::COMMAND, new_cmd); + true + } else { + false + } +} + +fn prepare_display_release(_kind: TransitionKind) -> bool { + unsafe { + crate::syscall::handler::shutdown_release_display_ownership(); + } + crate::serial::checkpoint("shutdown-prepare-display"); + true +} + +fn prepare_storage_sync(_kind: TransitionKind) -> bool { + let rc = unsafe { crate::syscall::handler::sys_fs_sync() }; + if rc == 0 { + crate::serial::checkpoint("shutdown-prepare-sync-ok"); + true + } else { + crate::serial::checkpoint("shutdown-prepare-sync-fail"); + false + } +} + +fn restart_marker(kind: TransitionKind) { + match kind { + TransitionKind::RebootGraceful | TransitionKind::RebootForce => { + crate::serial::checkpoint("shutdown-restart-handlers-done"); + } + TransitionKind::ShutdownGraceful | TransitionKind::ShutdownForce => {} + } +} + +fn poweroff_marker(kind: TransitionKind) { + match kind { + TransitionKind::ShutdownGraceful | TransitionKind::ShutdownForce => { + crate::serial::checkpoint("shutdown-poweroff-handlers-done"); + } + TransitionKind::RebootGraceful | TransitionKind::RebootForce => {} + } +} + +pub fn register_builtin_handlers() { + register_prepare_handler(prepare_display_release); + register_prepare_handler(prepare_disable_pci_bus_mastering); + register_prepare_handler(prepare_storage_sync); + register_restart_handler(restart_marker); + register_poweroff_handler(poweroff_marker); +} diff --git a/hwinit/src/shutdown/mod.rs b/hwinit/src/shutdown/mod.rs new file mode 100644 index 00000000..481c1a65 --- /dev/null +++ b/hwinit/src/shutdown/mod.rs @@ -0,0 +1,23 @@ +pub mod handlers; +pub mod prepare; + +pub use prepare::{ + register_poweroff_handler, register_prepare_handler, register_restart_handler, + run_poweroff_handlers, run_prepare_handlers, run_restart_handlers, TransitionKind, +}; + +static SHUTDOWN_INIT: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); + +pub fn ensure_initialized() { + if SHUTDOWN_INIT + .compare_exchange( + false, + true, + core::sync::atomic::Ordering::AcqRel, + core::sync::atomic::Ordering::Acquire, + ) + .is_ok() + { + handlers::register_builtin_handlers(); + } +} diff --git a/hwinit/src/shutdown/prepare.rs b/hwinit/src/shutdown/prepare.rs new file mode 100644 index 00000000..28c2daf1 --- /dev/null +++ b/hwinit/src/shutdown/prepare.rs @@ -0,0 +1,125 @@ +use crate::sync::RawSpinLock; + +const MAX_PREPARE_HANDLERS: usize = 8; +const MAX_FINAL_HANDLERS: usize = 8; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum TransitionKind { + RebootGraceful, + RebootForce, + ShutdownGraceful, + ShutdownForce, +} + +pub type PrepareHandler = fn(kind: TransitionKind) -> bool; +pub type FinalHandler = fn(kind: TransitionKind); + +static SHUTDOWN_HANDLER_LOCK: RawSpinLock = RawSpinLock::new(); + +static mut PREPARE_HANDLERS: [Option; MAX_PREPARE_HANDLERS] = + [None; MAX_PREPARE_HANDLERS]; +static mut RESTART_HANDLERS: [Option; MAX_FINAL_HANDLERS] = [None; MAX_FINAL_HANDLERS]; +static mut POWEROFF_HANDLERS: [Option; MAX_FINAL_HANDLERS] = [None; MAX_FINAL_HANDLERS]; + +fn register_in_table(table: &mut [Option], handler: T) { + for slot in table.iter_mut() { + if let Some(existing) = slot { + if *existing == handler { + return; + } + } + } + for slot in table.iter_mut() { + if slot.is_none() { + *slot = Some(handler); + return; + } + } +} + +pub fn register_prepare_handler(handler: PrepareHandler) { + SHUTDOWN_HANDLER_LOCK.lock(); + unsafe { + register_in_table(&mut PREPARE_HANDLERS, handler); + } + SHUTDOWN_HANDLER_LOCK.unlock(); +} + +pub fn register_restart_handler(handler: FinalHandler) { + SHUTDOWN_HANDLER_LOCK.lock(); + unsafe { + register_in_table(&mut RESTART_HANDLERS, handler); + } + SHUTDOWN_HANDLER_LOCK.unlock(); +} + +pub fn register_poweroff_handler(handler: FinalHandler) { + SHUTDOWN_HANDLER_LOCK.lock(); + unsafe { + register_in_table(&mut POWEROFF_HANDLERS, handler); + } + SHUTDOWN_HANDLER_LOCK.unlock(); +} + +fn deadline_from_timeout_ms(timeout_ms: u64) -> Option { + let tsc_hz = crate::process::scheduler::tsc_frequency(); + if tsc_hz == 0 { + return None; + } + let ticks_per_ms = (tsc_hz / 1000).max(1); + Some( + crate::cpu::tsc::read_tsc().saturating_add(timeout_ms.saturating_mul(ticks_per_ms)), + ) +} + +pub fn run_prepare_handlers(kind: TransitionKind, timeout_ms: u64) -> bool { + let deadline = deadline_from_timeout_ms(timeout_ms); + + let mut all_ok = true; + + SHUTDOWN_HANDLER_LOCK.lock(); + unsafe { + for slot in PREPARE_HANDLERS.iter() { + if let Some(handler) = slot { + if let Some(d) = deadline { + if crate::cpu::tsc::read_tsc() >= d { + all_ok = false; + crate::serial::checkpoint("shutdown-prepare-timeout"); + break; + } + } + let ok = handler(kind); + if !ok { + all_ok = false; + } + } + } + } + SHUTDOWN_HANDLER_LOCK.unlock(); + + all_ok +} + +pub fn run_restart_handlers(kind: TransitionKind) { + SHUTDOWN_HANDLER_LOCK.lock(); + unsafe { + for slot in RESTART_HANDLERS.iter() { + if let Some(handler) = slot { + handler(kind); + } + } + } + SHUTDOWN_HANDLER_LOCK.unlock(); +} + +pub fn run_poweroff_handlers(kind: TransitionKind) { + SHUTDOWN_HANDLER_LOCK.lock(); + unsafe { + for slot in POWEROFF_HANDLERS.iter() { + if let Some(handler) = slot { + handler(kind); + } + } + } + SHUTDOWN_HANDLER_LOCK.unlock(); +} diff --git a/hwinit/src/syscall/handler/core.rs b/hwinit/src/syscall/handler/core.rs index d739ad70..10d7bccb 100644 --- a/hwinit/src/syscall/handler/core.rs +++ b/hwinit/src/syscall/handler/core.rs @@ -285,22 +285,33 @@ pub unsafe fn sys_system_control(mode: u64) -> u64 { return EBUSY; } + let owner_core = crate::cpu::per_cpu::current_core_index(); + crate::cpu::per_cpu::set_reboot_owner(owner_core); + crate::shutdown::ensure_initialized(); + match mode { - SYSCTL_REBOOT_FORCE | SYSCTL_SHUTDOWN_FORCE => hard_reset_now(), + SYSCTL_REBOOT_FORCE => hard_reset_now(crate::shutdown::TransitionKind::RebootForce), + SYSCTL_SHUTDOWN_FORCE => hard_reset_now(crate::shutdown::TransitionKind::ShutdownForce), SYSCTL_SHUTDOWN_PANIC => { // Show crash screen, then reset from exception handler path. crate::cpu::idt::set_reset_on_crash(true); core::arch::asm!("ud2", options(noreturn)); } - SYSCTL_REBOOT_GRACEFUL | SYSCTL_SHUTDOWN_GRACEFUL => graceful_reset_now(), + SYSCTL_REBOOT_GRACEFUL => { + graceful_reset_now(crate::shutdown::TransitionKind::RebootGraceful) + } + SYSCTL_SHUTDOWN_GRACEFUL => { + graceful_reset_now(crate::shutdown::TransitionKind::ShutdownGraceful) + } _ => { SYSTEM_CONTROL_IN_PROGRESS.store(false, core::sync::atomic::Ordering::Release); + crate::cpu::per_cpu::clear_reboot_owner(); EINVAL } } } -unsafe fn graceful_reset_now() -> ! { +unsafe fn graceful_reset_now(kind: crate::shutdown::TransitionKind) -> ! { const MAX_SNAPSHOT: usize = 64; const DRAIN_ROUNDS: usize = 24; const DRAIN_BACKOFF_SPINS: usize = 200_000; @@ -308,6 +319,14 @@ unsafe fn graceful_reset_now() -> ! { let caller = SCHEDULER.current_pid(); crate::serial::set_checkpoints_enabled(true); + crate::serial::checkpoint("shutdown-prepare-begin"); + let prepare_ok = crate::shutdown::run_prepare_handlers(kind, 300); + if prepare_ok { + crate::serial::checkpoint("shutdown-prepare-complete"); + } else { + crate::serial::checkpoint("shutdown-prepare-incomplete"); + } + crate::serial::fb_puts("[INFO] [SHUTDOWN] draining processes\n"); crate::serial::checkpoint("shutdown-drain-begin"); @@ -356,9 +375,19 @@ unsafe fn graceful_reset_now() -> ! { crate::serial::checkpoint("shutdown-reset-seq"); // Do not block here on VFS lock during teardown; reset path must always complete. - hard_reset_now() + hard_reset_now(kind) } -unsafe fn hard_reset_now() -> ! { +unsafe fn hard_reset_now(kind: crate::shutdown::TransitionKind) -> ! { + match kind { + crate::shutdown::TransitionKind::RebootGraceful + | crate::shutdown::TransitionKind::RebootForce => { + crate::shutdown::run_restart_handlers(kind); + } + crate::shutdown::TransitionKind::ShutdownGraceful + | crate::shutdown::TransitionKind::ShutdownForce => { + crate::shutdown::run_poweroff_handlers(kind); + } + } crate::cpu::reset::reset_machine_now() } diff --git a/hwinit/src/syscall/handler/fb.rs b/hwinit/src/syscall/handler/fb.rs index 2226c476..6f3180ed 100644 --- a/hwinit/src/syscall/handler/fb.rs +++ b/hwinit/src/syscall/handler/fb.rs @@ -112,6 +112,16 @@ pub fn fb_lock_holder() -> u32 { FB_LOCK_PID.load(Relaxed) } +/// Best-effort display ownership release for shutdown paths. +/// +/// Clears framebuffer lock and compositor owner so no stale holder state +/// survives into teardown and reset. +pub unsafe fn shutdown_release_display_ownership() { + use core::sync::atomic::Ordering::Relaxed; + FB_LOCK_PID.store(0, Relaxed); + COMPOSITOR_PID.store(0, Relaxed); +} + // SYS_FB_MAP (64) — map the back buffer into user virtual address space /// `SYS_FB_MAP() → virt_addr` diff --git a/persistent/src/pe/embedded_reloc_data.rs b/persistent/src/pe/embedded_reloc_data.rs index 7d2b9bbb..a20a93cc 100644 --- a/persistent/src/pe/embedded_reloc_data.rs +++ b/persistent/src/pe/embedded_reloc_data.rs @@ -8,68 +8,67 @@ //! Run: ./tools/extract-reloc-data.sh after each build /// Original .reloc section RVA -pub const RELOC_RVA: u32 = 0x00613000; +pub const RELOC_RVA: u32 = 0x00614000; /// Original .reloc section size -pub const RELOC_SIZE: u32 = 0x0000025c; +pub const RELOC_SIZE: u32 = 0x00000258; /// Original ImageBase from linker script pub const ORIGINAL_IMAGE_BASE: u64 = 0x0000004001000000; -/// Hardcoded .reloc section data (604 bytes) -/// Extracted from morpheus-bootloader.efi at file offset 0x001d5400 +/// Hardcoded .reloc section data (600 bytes) +/// Extracted from morpheus-bootloader.efi at file offset 0x001d6400 #[allow(dead_code)] -pub const RELOC_DATA: [u8; 604] = [ - 0x00, 0x10, 0x05, 0x00, 0x34, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, +pub const RELOC_DATA: [u8; 600] = [ + 0x00, 0x20, 0x05, 0x00, 0x34, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xf8, 0xa6, 0x10, 0xa7, 0x28, 0xa7, 0xb8, 0xa7, 0xc0, 0xa8, 0xc8, 0xaa, 0xe0, 0xaa, 0xe8, 0xab, 0xe8, 0xad, 0x00, 0xae, 0x18, 0xae, 0x30, 0xae, 0x48, 0xae, 0x60, 0xae, - 0x78, 0xae, 0x00, 0x00, 0x00, 0x20, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x78, 0xae, 0x00, 0x00, 0x00, 0x30, 0x05, 0x00, 0x58, 0x00, 0x00, 0x00, 0x90, 0xa0, 0xa8, 0xa0, 0xc0, 0xa0, 0x68, 0xa3, 0x80, 0xa3, 0x98, 0xa3, 0xf8, 0xa4, 0x10, 0xa5, 0x28, 0xa5, 0x40, 0xa5, 0x58, 0xa5, 0x70, 0xa5, - 0xa8, 0xa5, 0xc0, 0xa5, 0x58, 0xa6, 0x70, 0xa6, 0xc8, 0xa6, 0xe0, 0xa6, - 0x30, 0xa7, 0x48, 0xa7, 0x60, 0xa7, 0x98, 0xa7, 0xc8, 0xa7, 0xf8, 0xa7, - 0x10, 0xa8, 0xa8, 0xa8, 0xc0, 0xa8, 0xd8, 0xa8, 0xf0, 0xa8, 0x08, 0xa9, - 0x18, 0xaa, 0x78, 0xaa, 0xf0, 0xaa, 0x30, 0xac, 0x50, 0xac, 0x00, 0x00, - 0x00, 0x30, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, 0x18, 0xae, 0x70, 0xae, - 0x88, 0xae, 0xa0, 0xae, 0x00, 0x40, 0x05, 0x00, 0x38, 0x00, 0x00, 0x00, - 0xa8, 0xa0, 0xc0, 0xa0, 0xd8, 0xa0, 0xf0, 0xa0, 0x08, 0xa1, 0x20, 0xa1, - 0x38, 0xa1, 0x38, 0xa2, 0x90, 0xa2, 0xb0, 0xa3, 0x18, 0xa4, 0x38, 0xa4, - 0x50, 0xa4, 0x68, 0xa4, 0x80, 0xa4, 0x98, 0xa4, 0x58, 0xa5, 0xb0, 0xa5, - 0xc8, 0xa5, 0xe0, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0x50, 0xa6, 0xe0, 0xa6, - 0x00, 0x50, 0x05, 0x00, 0x60, 0x00, 0x00, 0x00, 0x80, 0xa1, 0x98, 0xa1, - 0x88, 0xa6, 0xa0, 0xa6, 0xb8, 0xa6, 0xd0, 0xa6, 0xe8, 0xa6, 0x00, 0xa7, - 0x78, 0xa7, 0xb0, 0xa7, 0x90, 0xaa, 0xe8, 0xab, 0xf8, 0xab, 0x08, 0xac, - 0x18, 0xac, 0x28, 0xac, 0x38, 0xac, 0x48, 0xac, 0x58, 0xac, 0x68, 0xac, - 0x78, 0xac, 0x88, 0xac, 0x98, 0xac, 0xa8, 0xac, 0xb8, 0xac, 0xc8, 0xac, - 0xd8, 0xac, 0xe8, 0xac, 0xf8, 0xac, 0x08, 0xad, 0x18, 0xad, 0x28, 0xad, - 0x38, 0xad, 0x48, 0xad, 0x58, 0xad, 0x68, 0xad, 0x78, 0xad, 0x88, 0xad, - 0x98, 0xad, 0xa8, 0xad, 0xb8, 0xad, 0xc8, 0xad, 0xd8, 0xad, 0xb8, 0xaf, - 0x00, 0x60, 0x05, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x50, 0xa0, 0xa8, 0xa0, - 0x18, 0xa2, 0x30, 0xa2, 0x80, 0xa2, 0x90, 0xa2, 0xa0, 0xa2, 0xb0, 0xa2, - 0xc0, 0xa2, 0xf0, 0xa2, 0x28, 0xa3, 0x70, 0xa3, 0xc0, 0xa3, 0xf8, 0xa3, - 0x10, 0xa4, 0x58, 0xa4, 0xe0, 0xa4, 0x00, 0x00, 0x00, 0x70, 0x05, 0x00, - 0x54, 0x00, 0x00, 0x00, 0x98, 0xa1, 0xb0, 0xa1, 0xe8, 0xa1, 0x18, 0xa2, - 0x30, 0xa2, 0x48, 0xa2, 0x60, 0xa2, 0x78, 0xa2, 0x90, 0xa2, 0xa8, 0xa2, - 0xc0, 0xa2, 0x00, 0xa3, 0x18, 0xa3, 0x30, 0xa3, 0x48, 0xa3, 0x60, 0xa3, - 0x98, 0xa3, 0xc8, 0xa3, 0xe0, 0xa3, 0xf8, 0xa3, 0x10, 0xa4, 0x48, 0xa4, - 0x60, 0xa4, 0x78, 0xa4, 0x90, 0xa4, 0xa8, 0xa4, 0xc0, 0xa4, 0x50, 0xa5, - 0x98, 0xab, 0xa8, 0xab, 0xd8, 0xab, 0xf0, 0xab, 0x38, 0xac, 0x48, 0xac, - 0x58, 0xac, 0x80, 0xac, 0xb8, 0xac, 0x00, 0x00, 0x00, 0x80, 0x05, 0x00, - 0x54, 0x00, 0x00, 0x00, 0xf8, 0xa1, 0x08, 0xa2, 0x18, 0xa2, 0x28, 0xa2, - 0x78, 0xa2, 0x88, 0xa2, 0x98, 0xa2, 0xa8, 0xa2, 0xb8, 0xa2, 0xe0, 0xa2, - 0xf0, 0xa2, 0x00, 0xa3, 0x68, 0xa3, 0x78, 0xa3, 0x88, 0xa3, 0xd0, 0xa3, - 0xe0, 0xa3, 0x18, 0xa4, 0x28, 0xa4, 0x50, 0xa4, 0x60, 0xa4, 0x98, 0xa4, - 0xb0, 0xa4, 0x08, 0xa5, 0x78, 0xab, 0x90, 0xab, 0xc8, 0xab, 0x18, 0xac, - 0x60, 0xac, 0x70, 0xac, 0x70, 0xad, 0x88, 0xad, 0xc0, 0xad, 0xd8, 0xad, - 0xf0, 0xad, 0x08, 0xae, 0x70, 0xae, 0xb8, 0xaf, 0x00, 0x90, 0x05, 0x00, - 0x2c, 0x00, 0x00, 0x00, 0x28, 0xa0, 0x80, 0xa0, 0x98, 0xa0, 0xb0, 0xa0, - 0xc8, 0xa0, 0xe0, 0xa0, 0x38, 0xa1, 0x50, 0xa1, 0x68, 0xa1, 0x98, 0xa1, - 0xf0, 0xa1, 0x08, 0xa2, 0x20, 0xa2, 0x38, 0xa2, 0x50, 0xa2, 0x68, 0xa2, - 0x80, 0xa2, 0x00, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x30, 0xa2, 0x38, 0xa2, 0x00, 0x40, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x80, 0xa2, 0x98, 0xa3, 0x00, 0x60, 0x1d, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0xd0, 0xa9, 0x00, 0x00, 0x00, 0x20, 0x61, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x20, 0xa0, 0x00, 0x00, + 0xa8, 0xa5, 0xc0, 0xa5, 0xf0, 0xa5, 0x08, 0xa6, 0x60, 0xa6, 0x78, 0xa6, + 0xc8, 0xa6, 0xe0, 0xa6, 0xf8, 0xa6, 0x30, 0xa7, 0x60, 0xa7, 0x58, 0xa9, + 0x70, 0xa9, 0x08, 0xaa, 0x20, 0xaa, 0x38, 0xaa, 0x50, 0xaa, 0x68, 0xaa, + 0x78, 0xab, 0xd8, 0xab, 0x50, 0xac, 0x48, 0xad, 0x60, 0xad, 0x78, 0xad, + 0x90, 0xad, 0xa8, 0xad, 0xd8, 0xad, 0xf8, 0xad, 0x00, 0x40, 0x05, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x40, 0xa3, 0x58, 0xa3, 0x70, 0xa3, 0x88, 0xa3, + 0xa0, 0xa3, 0xb8, 0xa3, 0xd8, 0xa4, 0xf0, 0xa4, 0x08, 0xa5, 0x80, 0xa5, + 0xb8, 0xa5, 0x28, 0xa8, 0x40, 0xa8, 0x58, 0xa8, 0x70, 0xa8, 0x88, 0xa8, + 0xa0, 0xa8, 0xb8, 0xa8, 0x08, 0xa9, 0x68, 0xa9, 0x78, 0xaa, 0xb8, 0xab, + 0x20, 0xac, 0x40, 0xac, 0x58, 0xac, 0x70, 0xac, 0x88, 0xac, 0xa0, 0xac, + 0xa8, 0xad, 0x18, 0xae, 0x30, 0xae, 0x48, 0xae, 0x60, 0xae, 0x78, 0xae, + 0x90, 0xae, 0xd0, 0xae, 0x60, 0xaf, 0x00, 0x00, 0x00, 0x50, 0x05, 0x00, + 0x4c, 0x00, 0x00, 0x00, 0xd0, 0xaa, 0x28, 0xac, 0x38, 0xac, 0x48, 0xac, + 0x58, 0xac, 0x68, 0xac, 0x78, 0xac, 0x88, 0xac, 0x98, 0xac, 0xa8, 0xac, + 0xb8, 0xac, 0xc8, 0xac, 0xd8, 0xac, 0xe8, 0xac, 0xf8, 0xac, 0x08, 0xad, + 0x18, 0xad, 0x28, 0xad, 0x38, 0xad, 0x48, 0xad, 0x58, 0xad, 0x68, 0xad, + 0x78, 0xad, 0x88, 0xad, 0x98, 0xad, 0xa8, 0xad, 0xb8, 0xad, 0xc8, 0xad, + 0xd8, 0xad, 0xe8, 0xad, 0xf8, 0xad, 0x08, 0xae, 0x18, 0xae, 0xf8, 0xaf, + 0x00, 0x70, 0x05, 0x00, 0x30, 0x00, 0x00, 0x00, 0xb8, 0xa1, 0xd0, 0xa1, + 0x20, 0xa2, 0x78, 0xa2, 0xe8, 0xa3, 0x00, 0xa4, 0x50, 0xa4, 0x60, 0xa4, + 0x70, 0xa4, 0x80, 0xa4, 0x90, 0xa4, 0xc0, 0xa4, 0xf8, 0xa4, 0x40, 0xa5, + 0x90, 0xa5, 0xc8, 0xa5, 0xe0, 0xa5, 0x28, 0xa6, 0xb0, 0xa6, 0x00, 0x00, + 0x00, 0x80, 0x05, 0x00, 0x54, 0x00, 0x00, 0x00, 0x68, 0xa3, 0x80, 0xa3, + 0xb8, 0xa3, 0xe8, 0xa3, 0x00, 0xa4, 0x18, 0xa4, 0x30, 0xa4, 0x48, 0xa4, + 0x60, 0xa4, 0x78, 0xa4, 0x90, 0xa4, 0xd0, 0xa4, 0xe8, 0xa4, 0x00, 0xa5, + 0x18, 0xa5, 0x30, 0xa5, 0x68, 0xa5, 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, + 0xe0, 0xa5, 0x18, 0xa6, 0x30, 0xa6, 0x48, 0xa6, 0x60, 0xa6, 0x78, 0xa6, + 0x90, 0xa6, 0x20, 0xa7, 0x68, 0xad, 0x78, 0xad, 0xa8, 0xad, 0xc0, 0xad, + 0x08, 0xae, 0x18, 0xae, 0x28, 0xae, 0x50, 0xae, 0x88, 0xae, 0x00, 0x00, + 0x00, 0x90, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, 0xc8, 0xa3, 0xd8, 0xa3, + 0xe8, 0xa3, 0xf8, 0xa3, 0x48, 0xa4, 0x58, 0xa4, 0x68, 0xa4, 0x78, 0xa4, + 0x88, 0xa4, 0xb0, 0xa4, 0xc0, 0xa4, 0xd0, 0xa4, 0x38, 0xa5, 0x48, 0xa5, + 0x58, 0xa5, 0xa0, 0xa5, 0xb0, 0xa5, 0xe8, 0xa5, 0xf8, 0xa5, 0x20, 0xa6, + 0x30, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xd8, 0xa6, 0x48, 0xad, 0x60, 0xad, + 0x98, 0xad, 0xe8, 0xad, 0x30, 0xae, 0x40, 0xae, 0x40, 0xaf, 0x58, 0xaf, + 0x90, 0xaf, 0xa8, 0xaf, 0xc0, 0xaf, 0xd8, 0xaf, 0x00, 0xa0, 0x05, 0x00, + 0x28, 0x00, 0x00, 0x00, 0x48, 0xa0, 0x20, 0xa1, 0x78, 0xa1, 0x90, 0xa1, + 0xa8, 0xa1, 0xc0, 0xa1, 0xd8, 0xa1, 0xf0, 0xa1, 0x08, 0xa2, 0x98, 0xa2, + 0x48, 0xa3, 0x60, 0xa3, 0x78, 0xa3, 0xe8, 0xa3, 0x18, 0xa4, 0x00, 0x00, + 0x00, 0xb0, 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x30, 0xa2, 0x38, 0xa2, + 0x00, 0x50, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x80, 0xa2, 0xc8, 0xae, + 0x00, 0x80, 0x1d, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, + 0x00, 0x30, 0x61, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, ]; From 4a2a817f26b460a448f904aad81e3c76766bb6f3 Mon Sep 17 00:00:00 2001 From: adrerl Date: Wed, 11 Mar 2026 12:01:42 +0100 Subject: [PATCH 04/12] settings app --- Cargo.lock | 8 + Cargo.toml | 1 + settings/Cargo.toml | 16 + settings/src/chambers/archive.rs | 255 ++++++++++++++ settings/src/chambers/gateway.rs | 191 ++++++++++ settings/src/chambers/hall.rs | 223 ++++++++++++ settings/src/chambers/mirror.rs | 203 +++++++++++ settings/src/chambers/mist.rs | 212 +++++++++++ settings/src/chambers/mod.rs | 7 + settings/src/chambers/net_obs.rs | 542 +++++++++++++++++++++++++++++ settings/src/chambers/sys_obs.rs | 276 +++++++++++++++ settings/src/font.rs | 3 + settings/src/font_data.inc | 97 ++++++ settings/src/layout.rs | 271 +++++++++++++++ settings/src/main.rs | 44 +++ settings/src/state.rs | 579 +++++++++++++++++++++++++++++++ settings/src/theme.rs | 124 +++++++ settings/src/widgets.rs | 231 ++++++++++++ setup-dev.sh | 1 + 19 files changed, 3284 insertions(+) create mode 100644 settings/Cargo.toml create mode 100644 settings/src/chambers/archive.rs create mode 100644 settings/src/chambers/gateway.rs create mode 100644 settings/src/chambers/hall.rs create mode 100644 settings/src/chambers/mirror.rs create mode 100644 settings/src/chambers/mist.rs create mode 100644 settings/src/chambers/mod.rs create mode 100644 settings/src/chambers/net_obs.rs create mode 100644 settings/src/chambers/sys_obs.rs create mode 100644 settings/src/font.rs create mode 100644 settings/src/font_data.inc create mode 100644 settings/src/layout.rs create mode 100644 settings/src/main.rs create mode 100644 settings/src/state.rs create mode 100644 settings/src/theme.rs create mode 100644 settings/src/widgets.rs diff --git a/Cargo.lock b/Cargo.lock index 1d962df3..599a9168 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,14 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "settings" +version = "0.1.0" +dependencies = [ + "libmorpheus", + "morpheus-ui", +] + [[package]] name = "shelld" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 5273a1f2..edb4ab1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "compd", "init", "shelld", + "settings", "tests/syscall-e2e", "tests/spinning-cube", "tests/system-visualizer", diff --git a/settings/Cargo.toml b/settings/Cargo.toml new file mode 100644 index 00000000..7347ebc4 --- /dev/null +++ b/settings/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "settings" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "settings" +path = "src/main.rs" + +[dependencies] +libmorpheus = { path = "../libmorpheus" } +morpheus-ui = { path = "../ui" } + +[profile.release] +opt-level = 3 +lto = true diff --git a/settings/src/chambers/archive.rs b/settings/src/chambers/archive.rs new file mode 100644 index 00000000..48d186e2 --- /dev/null +++ b/settings/src/chambers/archive.rs @@ -0,0 +1,255 @@ +// archive of echoes — changelog timeline viewer. +// every change ever applied in this session, searchable, with destructive markers. +// the system's memory. read-only — you cannot un-echo what has been spoken. + +use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; +use crate::state::{ChangeEntry, SettingsApp}; +use crate::widgets; + +const VISIBLE_ENTRIES: usize = 16; + +pub struct ArchiveChamber { + pub scroll_offset: usize, + pub selected: usize, + pub search_buf: [u8; 32], + pub search_len: usize, + pub searching: bool, +} + +impl ArchiveChamber { + pub fn new() -> Self { + Self { + scroll_offset: 0, + selected: 0, + search_buf: [0; 32], + search_len: 0, + searching: false, + } + } + + pub fn widget_count(&self) -> usize { + 3 + VISIBLE_ENTRIES + } + + pub fn activate(&mut self, idx: usize) { + match idx { + 0 => { + self.scroll_offset = self.scroll_offset.saturating_sub(1); + } + 1 => { + // scroll down — can't check max without changelog len, just increment + self.scroll_offset += 1; + } + 2 => { + self.searching = !self.searching; + if !self.searching { + self.search_len = 0; + } + } + _ => { + self.selected = idx - 3 + self.scroll_offset; + } + } + } + + pub fn handle_key(&mut self, scancode: u8) { + if self.searching { + match scancode { + 0x01 => { + self.searching = false; + self.search_len = 0; + } + 0x0E => { + if self.search_len > 0 { + self.search_len -= 1; + } + } + 0x1C => { + self.searching = false; + } + _ => { + if let Some(ch) = super::net_obs::scancode_to_char(scancode) { + if self.search_len < self.search_buf.len() { + self.search_buf[self.search_len] = ch; + self.search_len += 1; + } + } + } + } + } + } + + pub fn handle_click(&mut self, _px: i32, py: i32) { + let row_h = (widgets::FONT_H + 4) as i32; + let header = 60i32; + let idx = ((py - header) / row_h).max(0) as usize; + if idx < VISIBLE_ENTRIES { + self.selected = idx + self.scroll_offset; + } + } +} + +pub fn render(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + let arch = &app.archive; + let changelog_len = app.changelog.len(); + + let px = RAIL_WIDTH + PANE_PAD; + let mut cy = STRIP_HEIGHT + PANE_PAD; + + layout::draw_section(app, px, cy, "Archive of Echoes"); + cy += widgets::FONT_H + 4; + + // entry count + let mut buf = [0u8; 8]; + let n = widgets::u64_to_str(changelog_len as u64, &mut buf); + let count_str = core::str::from_utf8(&buf[..n]).unwrap_or("0"); + layout::draw_kv(app, px, cy, "Total entries:", count_str, t.telemetry); + cy += widgets::FONT_H + 4; + + // search bar + if arch.searching { + let search_str = core::str::from_utf8(&arch.search_buf[..arch.search_len]).unwrap_or(""); + widgets::fill_rect(s, st, px, cy, 400, widgets::FONT_H + 4, t.input_bg, w, h); + widgets::rect_outline(s, st, px, cy, 400, widgets::FONT_H + 4, t.signal, w, h); + widgets::draw_str(s, st, px + 4, cy + 2, "Search: ", t.glyph_dim, t.input_bg, w, h); + widgets::draw_str(s, st, px + 68, cy + 2, search_str, t.glyph, t.input_bg, w, h); + let cursor_x = px + 68 + arch.search_len as u32 * widgets::FONT_W; + widgets::fill_rect(s, st, cursor_x, cy + 2, 2, widgets::FONT_H, t.focus_ring, w, h); + } else { + widgets::draw_str(s, st, px, cy, "Press '/' or activate to search", t.glyph_dim, t.substrate, w, h); + } + cy += widgets::FONT_H + 8; + + // scroll indicators + let can_up = arch.scroll_offset > 0; + let can_down = changelog_len > arch.scroll_offset + VISIBLE_ENTRIES; + let up_color = if can_up { t.glyph } else { t.contour }; + let dn_color = if can_down { t.glyph } else { t.contour }; + widgets::draw_str(s, st, px, cy, "^^ Up", up_color, t.substrate, w, h); + widgets::draw_str(s, st, px + 80, cy, "vv Down", dn_color, t.substrate, w, h); + cy += widgets::FONT_H + 4; + + // column header + widgets::draw_str(s, st, px, cy, "# Route Field Value", t.glyph_dim, t.substrate, w, h); + cy += widgets::FONT_H + 2; + widgets::hline(s, st, px, cy, (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD), t.contour, w, h); + cy += 2; + + // entries + let search_term = &arch.search_buf[..arch.search_len]; + let mut displayed = 0; + let end = changelog_len.min(arch.scroll_offset + VISIBLE_ENTRIES); + for i in arch.scroll_offset..end { + let entry = &app.changelog[i]; + + // search filter + if arch.search_len > 0 { + let field_bytes = entry.field_name.as_bytes(); + let value_bytes = &entry.description[..entry.desc_len]; + if !contains_subsequence(field_bytes, search_term) + && !contains_subsequence(value_bytes, search_term) + { + continue; + } + } + + let is_selected = i == arch.selected; + let row_bg = if is_selected { t.surface } else { t.substrate }; + + let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + widgets::fill_rect(s, st, px, cy, row_w, widgets::FONT_H + 2, row_bg, w, h); + + // index + let mut nbuf = [0u8; 4]; + let nl = widgets::u64_to_str(i as u64, &mut nbuf); + let ns = core::str::from_utf8(&nbuf[..nl]).unwrap_or("?"); + widgets::draw_str(s, st, px + 2, cy, ns, t.glyph_dim, row_bg, w, h); + + // route + let route_str = entry.chamber.label(); + widgets::draw_str(s, st, px + 32, cy, route_str, t.glyph, row_bg, w, h); + + // field + widgets::draw_str(s, st, px + 168, cy, entry.field_name, t.telemetry, row_bg, w, h); + + // value + let val = core::str::from_utf8(&entry.description[..entry.desc_len]).unwrap_or("?"); + let val_color = if entry.destructive { t.destructive } else { t.success }; + widgets::draw_str_trunc(s, st, px + 260, cy, val, val_color, row_bg, w, h, row_w.saturating_sub(262) as usize); + + // destructive marker + if entry.destructive { + widgets::draw_str(s, st, px + row_w - 24, cy, "!!", t.destructive, row_bg, w, h); + } + + cy += widgets::FONT_H + 2; + displayed += 1; + } + + if displayed == 0 { + widgets::draw_str(s, st, px + 4, cy, "(no entries)", t.glyph_dim, t.substrate, w, h); + cy += widgets::FONT_H + 2; + } + + // detail panel for selected entry + if arch.selected < changelog_len { + cy += 8; + widgets::hline(s, st, px, cy, (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD), t.contour, w, h); + cy += 4; + + let entry = &app.changelog[arch.selected]; + layout::draw_section(app, px, cy, "Detail"); + cy += widgets::FONT_H + 4; + + layout::draw_kv(app, px, cy, "Chamber:", entry.chamber.label(), t.glyph); + cy += widgets::FONT_H + 2; + + layout::draw_kv(app, px, cy, "Field:", entry.field_name, t.telemetry); + cy += widgets::FONT_H + 2; + + let val = core::str::from_utf8(&entry.description[..entry.desc_len]).unwrap_or("?"); + let dc = if entry.destructive { t.destructive } else { t.success }; + layout::draw_kv(app, px, cy, "Value:", val, dc); + cy += widgets::FONT_H + 2; + + if entry.destructive { + layout::draw_risk_band(app, px, cy, "This was a destructive operation."); + } + } +} + +fn contains_subsequence(haystack: &[u8], needle: &[u8]) -> bool { + if needle.is_empty() { + return true; + } + if needle.len() > haystack.len() { + return false; + } + // case-insensitive substring search + let nlen = needle.len(); + for i in 0..=(haystack.len() - nlen) { + let mut matched = true; + for j in 0..nlen { + let h = to_lower(haystack[i + j]); + let n = to_lower(needle[j]); + if h != n { + matched = false; + break; + } + } + if matched { + return true; + } + } + false +} + +#[inline(always)] +fn to_lower(b: u8) -> u8 { + if b >= b'A' && b <= b'Z' { b + 32 } else { b } +} diff --git a/settings/src/chambers/gateway.rs b/settings/src/chambers/gateway.rs new file mode 100644 index 00000000..7beb32b7 --- /dev/null +++ b/settings/src/chambers/gateway.rs @@ -0,0 +1,191 @@ +// oneiric gateway — the entry chamber. +// safe/severe mode gate, quick-jump links, last-visited recall. + +use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; +use crate::state::{ArmState, Route, SafetyMode, SettingsApp}; +use crate::widgets; + +pub struct GatewayChamber { + pub recent: [Route; 3], + pub recent_count: usize, +} + +impl GatewayChamber { + pub fn new() -> Self { + Self { + recent: [Route::Gateway; 3], + recent_count: 0, + } + } + + pub fn record_visit(&mut self, route: Route) { + if route == Route::Gateway { + return; + } + let mut new = [Route::Gateway; 3]; + new[0] = route; + let mut n = 1; + for i in 0..self.recent_count { + if self.recent[i] != route && n < 3 { + new[n] = self.recent[i]; + n += 1; + } + } + self.recent = new; + self.recent_count = n; + } + + pub fn widget_count(&self) -> usize { + 1 + Route::ALL.len() + self.recent_count + } +} + +pub fn activate(app: &mut SettingsApp, idx: usize) { + if idx == 0 { + match app.safety { + SafetyMode::Safe => { + if app.severe_arm == ArmState::Disarmed { + app.severe_arm = ArmState::Armed; + app.set_status("Severe mode ARMED — press Enter to confirm, Esc to disarm", true); + } else if app.severe_arm == ArmState::Armed { + app.safety = SafetyMode::Severe; + app.severe_arm = ArmState::Confirmed; + app.set_status("Severe mode ACTIVE", true); + } + } + SafetyMode::Severe => { + app.safety = SafetyMode::Safe; + app.severe_arm = ArmState::Disarmed; + app.set_status("Safe mode restored", false); + } + } + } else if idx <= Route::ALL.len() { + let target = Route::from_index(idx - 1); + let cur = app.route; + app.gateway.record_visit(cur); + app.navigate(target); + } else { + let ri = idx - 1 - Route::ALL.len(); + if ri < app.gateway.recent_count { + let target = app.gateway.recent[ri]; + app.navigate(target); + } + } +} + +pub fn handle_key(app: &mut SettingsApp, scancode: u8) { + let num = match scancode { + 0x02 => Some(0), + 0x03 => Some(1), + 0x04 => Some(2), + 0x05 => Some(3), + 0x06 => Some(4), + 0x07 => Some(5), + 0x08 => Some(6), + _ => None, + }; + if let Some(i) = num { + if i < Route::ALL.len() { + let cur = app.route; + app.gateway.record_visit(cur); + app.navigate(Route::from_index(i)); + } + } +} + +pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { + let row_h = widgets::FONT_H + 8; + let idx = (py as u32 / row_h) as usize; + if idx >= 2 { + let adjusted = idx - 2; + app.pane_focus = adjusted; + activate(app, adjusted); + } +} + +pub fn render(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + let px = RAIL_WIDTH + PANE_PAD; + let mut cy = STRIP_HEIGHT + PANE_PAD; + + // welcome header + widgets::draw_str(s, st, px, cy, "Oneiric Gateway", t.signal, t.substrate, w, h); + cy += widgets::FONT_H + 4; + widgets::draw_str(s, st, px, cy, "System configuration interface", t.glyph_dim, t.substrate, w, h); + cy += widgets::FONT_H + 12; + + // mode gate + layout::draw_section(app, px, cy, "Mode"); + cy += widgets::FONT_H + 4; + + let (mode_label, mode_color) = match app.safety { + SafetyMode::Safe => ("[ ] Enter Severe Mode", t.glyph_dim), + SafetyMode::Severe => ("[X] Severe Mode ACTIVE", t.destructive), + }; + layout::draw_button_row(app, px, cy, mode_label, 0, mode_color); + cy += widgets::FONT_H + 8; + + // armed warning + if app.severe_arm == ArmState::Armed { + layout::draw_risk_band(app, px, cy, "WARNING: Severe mode unlocks destructive system controls. Press Enter to confirm."); + cy += widgets::FONT_H + 12; + } + + // chamber links + layout::draw_section(app, px, cy, "Chambers"); + cy += widgets::FONT_H + 4; + + for (i, route) in Route::ALL.iter().enumerate() { + let label = route.label(); + let sigil = route.sigil(); + let is_focused = !app.focus_in_rail && app.pane_focus == i + 1; + + let bg = if is_focused { t.surface } else { t.substrate }; + let fg = if is_focused { t.signal } else { t.glyph }; + + let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + widgets::fill_rect(s, st, px, cy, row_w, widgets::FONT_H + 4, bg, w, h); + if is_focused { + widgets::rect_outline(s, st, px, cy, row_w, widgets::FONT_H + 4, t.focus_ring, w, h); + } + + // number key hint + let mut num_buf = [0u8; 1]; + num_buf[0] = b'1' + i as u8; + let num_str = core::str::from_utf8(&num_buf).unwrap_or("?"); + widgets::draw_str(s, st, px + 4, cy + 2, num_str, t.glyph_dim, bg, w, h); + + // sigil + widgets::draw_str(s, st, px + 3 * widgets::FONT_W, cy + 2, sigil, t.signal, bg, w, h); + + // label + widgets::draw_str(s, st, px + 5 * widgets::FONT_W, cy + 2, label, fg, bg, w, h); + + // alias + let alias = route.technical_alias(); + let alias_x = px + 25 * widgets::FONT_W; + widgets::draw_str_trunc(s, st, alias_x, cy + 2, alias, t.glyph_dim, bg, w, h, 30); + + cy += widgets::FONT_H + 4; + } + + // recent chambers + if app.gateway.recent_count > 0 { + cy += 8; + layout::draw_section(app, px, cy, "Recent"); + cy += widgets::FONT_H + 4; + + for i in 0..app.gateway.recent_count { + let route = app.gateway.recent[i]; + let field_idx = 1 + Route::ALL.len() + i; + let label = route.label(); + layout::draw_button_row(app, px, cy, label, field_idx, t.glyph); + cy += widgets::FONT_H + 8; + } + } +} diff --git a/settings/src/chambers/hall.rs b/settings/src/chambers/hall.rs new file mode 100644 index 00000000..b01b679a --- /dev/null +++ b/settings/src/chambers/hall.rs @@ -0,0 +1,223 @@ +// hall of masks — preset bundles. +// named theme+config presets with delta preview before apply. +// "Default Dark", "Default Light", custom combos. snapshot → preview → apply. + +use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; +use crate::state::{Route, SettingsApp}; +use crate::theme::{self, OneiricTheme}; +use crate::widgets; + +const PRESET_COUNT: usize = 4; + +struct Preset { + name: &'static str, + desc: &'static str, + dark: bool, + accent_r: u8, + accent_g: u8, + accent_b: u8, +} + +const PRESETS: [Preset; PRESET_COUNT] = [ + Preset { + name: "Default Dark", + desc: "The canonical 3am terminal aesthetic. Green on black.", + dark: true, + accent_r: 0, + accent_g: 230, + accent_b: 118, + }, + Preset { + name: "Default Light", + desc: "Inverted substrate. For people who work with the lights on.", + dark: false, + accent_r: 0, + accent_g: 150, + accent_b: 80, + }, + Preset { + name: "Dream Protocol", + desc: "Blue-shifted. Calm. The sleep cycle palette.", + dark: true, + accent_r: 100, + accent_g: 181, + accent_b: 246, + }, + Preset { + name: "Oracle Fire", + desc: "Amber warnings. For those who like to see the future burn.", + dark: true, + accent_r: 255, + accent_g: 167, + accent_b: 38, + }, +]; + +const FIELD_APPLY: usize = PRESET_COUNT; +const FIELD_COUNT: usize = PRESET_COUNT + 1; + +pub struct HallChamber { + pub selected: usize, + pub preview_active: bool, +} + +impl HallChamber { + pub fn new() -> Self { + Self { + selected: 0, + preview_active: false, + } + } + + pub fn widget_count(&self) -> usize { + FIELD_COUNT + } + + pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { + if idx < PRESET_COUNT { + self.selected = idx; + self.preview_active = true; + // live preview — apply theme immediately + self.apply_theme_preview(app); + app.set_status("Previewing preset. Enter on Apply to commit.", false); + } else if idx == FIELD_APPLY { + self.apply(app); + } + } + + fn apply_theme_preview(&self, app: &mut SettingsApp) { + let p = &PRESETS[self.selected]; + let base = if p.dark { OneiricTheme::dark() } else { OneiricTheme::light() }; + let accent = theme::pack(p.accent_r, p.accent_g, p.accent_b); + + app.theme = base; + app.theme.signal = accent; + app.theme.focus_ring = accent; + app.theme.rail_active = accent; + + // sync mirror chamber state + app.mirror.dark_mode = p.dark; + // find closest accent idx + app.mirror.accent_idx = 0; + + app.frame_dirty = true; + } + + pub fn apply(&mut self, app: &mut SettingsApp) { + if !self.preview_active { + app.set_status("Select a preset first", false); + return; + } + self.apply_theme_preview(app); + app.mirror.apply(); + self.preview_active = false; + app.log_change(Route::HallOfMasks, "preset", PRESETS[self.selected].name, false); + app.set_status("Preset applied", false); + } + + pub fn revert(&mut self) { + self.preview_active = false; + } + + pub fn restore_defaults(&mut self) { + self.selected = 0; + self.preview_active = false; + } + + pub fn handle_key(&mut self, _scancode: u8, _app: &mut SettingsApp) {} + + pub fn handle_click(&mut self, _px: i32, py: i32, app: &mut SettingsApp) { + let row_h = (widgets::FONT_H + 12) as i32; + let header = 60i32; + let idx = ((py - header) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + self.activate(idx, app); + } + } +} + +pub fn render(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + let hall = &app.hall; + + let px = RAIL_WIDTH + PANE_PAD; + let mut cy = STRIP_HEIGHT + PANE_PAD; + + layout::draw_section(app, px, cy, "Hall of Masks"); + cy += widgets::FONT_H + 4; + + widgets::draw_str(s, st, px, cy, "Select a preset to preview, then Apply to commit.", t.glyph_dim, t.substrate, w, h); + cy += widgets::FONT_H + 8; + + // preset cards + for i in 0..PRESET_COUNT { + let p = &PRESETS[i]; + let is_selected = i == hall.selected; + let is_focused = !app.focus_in_rail && app.pane_focus == i; + + let card_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + let card_h = widgets::FONT_H * 2 + 12; + let card_bg = if is_selected { t.surface } else { t.substrate }; + + widgets::fill_rect(s, st, px, cy, card_w, card_h, card_bg, w, h); + + let border = if is_focused { + t.focus_ring + } else if is_selected { + t.signal + } else { + t.contour + }; + widgets::rect_outline(s, st, px, cy, card_w, card_h, border, w, h); + + // accent swatch + let accent = theme::pack(p.accent_r, p.accent_g, p.accent_b); + widgets::fill_rect(s, st, px + 4, cy + 4, 16, 16, accent, w, h); + + // name + let name_color = if is_selected { t.signal } else { t.glyph }; + widgets::draw_str(s, st, px + 24, cy + 4, p.name, name_color, card_bg, w, h); + + // description + widgets::draw_str(s, st, px + 24, cy + 4 + widgets::FONT_H + 2, p.desc, t.glyph_dim, card_bg, w, h); + + // selection marker + if is_selected { + widgets::draw_str(s, st, px + card_w - 28, cy + 4, ">>", t.signal, card_bg, w, h); + } + + cy += card_h + 4; + } + + cy += 8; + + // delta preview + if hall.preview_active { + layout::draw_section(app, px, cy, "Delta Preview"); + cy += widgets::FONT_H + 4; + + let p = &PRESETS[hall.selected]; + let mode_str = if p.dark { "Dark" } else { "Light" }; + layout::draw_kv(app, px, cy, "Mode:", mode_str, t.glyph); + cy += widgets::FONT_H + 2; + + // show accent color value + let accent = theme::pack(p.accent_r, p.accent_g, p.accent_b); + widgets::fill_rect(s, st, px + 80, cy, 48, 12, accent, w, h); + widgets::draw_str(s, st, px, cy, "Accent:", t.glyph, t.substrate, w, h); + cy += widgets::FONT_H + 2; + + layout::draw_kv(app, px, cy, "Name:", p.name, t.immutable); + cy += widgets::FONT_H + 8; + } + + // apply button + let apply_label = if hall.preview_active { "Apply This Mask" } else { "Select a Mask First" }; + let apply_color = if hall.preview_active { t.signal } else { t.glyph_dim }; + layout::draw_button_row(app, px, cy, apply_label, FIELD_APPLY, apply_color); +} diff --git a/settings/src/chambers/mirror.rs b/settings/src/chambers/mirror.rs new file mode 100644 index 00000000..3d2e6423 --- /dev/null +++ b/settings/src/chambers/mirror.rs @@ -0,0 +1,203 @@ +// mirror basin — appearance controls. +// dark/light toggle, accent color selection, intensity tweaking. +// purely local state — changes apply to the running app instance only. + +use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; +use crate::state::{Route, SettingsApp}; +use crate::theme::OneiricTheme; +use crate::widgets; + +const FIELD_THEME_TOGGLE: usize = 0; +const FIELD_ACCENT_PREV: usize = 1; +const FIELD_ACCENT_NEXT: usize = 2; +const FIELD_APPLY: usize = 3; +const FIELD_REVERT: usize = 4; +const FIELD_COUNT: usize = 5; + +// preset accent palettes +const ACCENT_COUNT: usize = 6; +const ACCENTS: [(u8, u8, u8, &str); ACCENT_COUNT] = [ + (0, 230, 118, "Morpheus Green"), + (100, 181, 246, "Dream Blue"), + (255, 167, 38, "Oracle Amber"), + (171, 71, 188, "Mist Violet"), + (244, 67, 54, "Wrath Red"), + (255, 255, 255, "Ghost White"), +]; + +pub struct MirrorChamber { + pub dark_mode: bool, + pub accent_idx: usize, + // snapshot for revert + pub saved_dark: bool, + pub saved_accent: usize, +} + +impl MirrorChamber { + pub fn new() -> Self { + Self { + dark_mode: true, + accent_idx: 0, + saved_dark: true, + saved_accent: 0, + } + } + + pub fn widget_count(&self) -> usize { + FIELD_COUNT + } + + pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { + match idx { + FIELD_THEME_TOGGLE => { + self.dark_mode = !self.dark_mode; + self.rebuild_theme(app); + app.mark_edited(Route::MirrorBasin, "theme_mode"); + } + FIELD_ACCENT_PREV => { + self.accent_idx = if self.accent_idx == 0 { ACCENT_COUNT - 1 } else { self.accent_idx - 1 }; + self.rebuild_theme(app); + app.mark_edited(Route::MirrorBasin, "accent"); + } + FIELD_ACCENT_NEXT => { + self.accent_idx = (self.accent_idx + 1) % ACCENT_COUNT; + self.rebuild_theme(app); + app.mark_edited(Route::MirrorBasin, "accent"); + } + FIELD_APPLY => { + self.apply(); + app.set_status("Appearance applied", false); + app.log_change(Route::MirrorBasin, "appearance", ACCENTS[self.accent_idx].3, false); + } + FIELD_REVERT => { + self.revert(); + self.rebuild_theme(app); + app.set_status("Appearance reverted", false); + } + _ => {} + } + } + + fn rebuild_theme(&self, app: &mut SettingsApp) { + let base = if self.dark_mode { OneiricTheme::dark() } else { OneiricTheme::light() }; + let (r, g, b, _) = ACCENTS[self.accent_idx]; + let accent = crate::theme::pack(r, g, b); + // override signal and focus ring with accent + app.theme.signal = accent; + app.theme.focus_ring = accent; + app.theme.rail_active = accent; + app.theme.substrate = base.substrate; + app.theme.contour = base.contour; + app.theme.glyph = base.glyph; + app.theme.glyph_dim = base.glyph_dim; + app.theme.surface = base.surface; + app.theme.input_bg = base.input_bg; + app.theme.rail_bg = base.rail_bg; + app.theme.bar_bg = base.bar_bg; + app.theme.strip_bg = base.strip_bg; + app.frame_dirty = true; + } + + pub fn apply(&mut self) { + self.saved_dark = self.dark_mode; + self.saved_accent = self.accent_idx; + } + + pub fn revert(&mut self) { + self.dark_mode = self.saved_dark; + self.accent_idx = self.saved_accent; + } + + pub fn restore_defaults(&mut self) { + self.dark_mode = true; + self.accent_idx = 0; + } + + pub fn handle_key(&mut self, _scancode: u8, _app: &mut SettingsApp) {} + + pub fn handle_click(&mut self, _px: i32, py: i32, app: &mut SettingsApp) { + let row_h = (widgets::FONT_H + 8) as i32; + let idx = ((py - 40) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + self.activate(idx, app); + } + } +} + +pub fn render(app: &SettingsApp) { + let t = &app.theme; + let mirror = &app.mirror; + + let px = RAIL_WIDTH + PANE_PAD; + let mut cy = STRIP_HEIGHT + PANE_PAD; + + // theme mode + layout::draw_section(app, px, cy, "Theme Mode"); + cy += widgets::FONT_H + 4; + + let mode_label = if mirror.dark_mode { "[X] Dark [ ] Light" } else { "[ ] Dark [X] Light" }; + layout::draw_button_row(app, px, cy, mode_label, FIELD_THEME_TOGGLE, t.glyph); + cy += widgets::FONT_H + 8; + + // accent color + layout::draw_section(app, px, cy, "Accent Color"); + cy += widgets::FONT_H + 4; + + let (ar, ag, ab, name) = ACCENTS[mirror.accent_idx]; + let accent = crate::theme::pack(ar, ag, ab); + + // show all accents in a row with selection marker + for i in 0..ACCENT_COUNT { + let (r, g, b, _) = ACCENTS[i]; + let c = crate::theme::pack(r, g, b); + let sx = px + i as u32 * 28; + let swatch_y = cy; + widgets::fill_rect(app.surface, app.fb_stride, sx, swatch_y, 24, 16, c, app.fb_w, app.fb_h); + if i == mirror.accent_idx { + widgets::rect_outline(app.surface, app.fb_stride, sx.saturating_sub(1), swatch_y.saturating_sub(1), 26, 18, t.focus_ring, app.fb_w, app.fb_h); + } + } + cy += 20; + + // selected name + widgets::draw_str(app.surface, app.fb_stride, px, cy, name, accent, t.substrate, app.fb_w, app.fb_h); + cy += widgets::FONT_H + 4; + + // nav buttons + layout::draw_button_row(app, px, cy, "<< Previous Accent", FIELD_ACCENT_PREV, t.glyph); + cy += widgets::FONT_H + 4; + layout::draw_button_row(app, px, cy, ">> Next Accent", FIELD_ACCENT_NEXT, t.glyph); + cy += widgets::FONT_H + 12; + + // preview section + layout::draw_section(app, px, cy, "Preview"); + cy += widgets::FONT_H + 4; + + // color swatch grid showing all theme tokens + let tokens: [(&str, u32); 10] = [ + ("substrate", t.substrate), + ("glyph", t.glyph), + ("signal", t.signal), + ("warning", t.warning), + ("destructive", t.destructive), + ("immutable", t.immutable), + ("telemetry", t.telemetry), + ("success", t.success), + ("focus_ring", t.focus_ring), + ("armed", t.armed), + ]; + + for (name, color) in &tokens { + widgets::fill_rect(app.surface, app.fb_stride, px, cy, 16, 12, *color, app.fb_w, app.fb_h); + widgets::draw_str(app.surface, app.fb_stride, px + 20, cy, name, t.glyph, t.substrate, app.fb_w, app.fb_h); + cy += widgets::FONT_H + 2; + } + + cy += 8; + + // action buttons + layout::draw_button_row(app, px, cy, "Apply Appearance", FIELD_APPLY, t.signal); + cy += widgets::FONT_H + 4; + layout::draw_button_row(app, px, cy, "Revert", FIELD_REVERT, t.glyph_dim); +} diff --git a/settings/src/chambers/mist.rs b/settings/src/chambers/mist.rs new file mode 100644 index 00000000..d448335b --- /dev/null +++ b/settings/src/chambers/mist.rs @@ -0,0 +1,212 @@ +// mist shore — display baseline controls. +// framebuffer introspection, pixel format confirmation, stride/resolution readout. +// read-only telemetry mostly — this is the calmest chamber. + +use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; +use crate::state::{Route, SettingsApp}; +use crate::widgets; + +use libmorpheus::hw; + +const FIELD_REFRESH: usize = 0; +const FIELD_COUNT: usize = 1; + +pub struct MistChamber { + pub fb_width: u32, + pub fb_height: u32, + pub fb_stride: u32, + pub fb_format: u32, + pub fb_size: u64, + pub fb_base: u64, +} + +impl MistChamber { + pub fn new() -> Self { + Self { + fb_width: 0, + fb_height: 0, + fb_stride: 0, + fb_format: 0, + fb_size: 0, + fb_base: 0, + } + } + + pub fn refresh(&mut self) { + if let Ok(info) = hw::fb_info() { + self.fb_width = info.width; + self.fb_height = info.height; + self.fb_stride = info.stride; + self.fb_format = info.format; + self.fb_size = info.size; + self.fb_base = info.base; + } + } + + pub fn widget_count(&self) -> usize { + FIELD_COUNT + } + + pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { + match idx { + FIELD_REFRESH => { + self.refresh(); + app.set_status("Display info refreshed", false); + } + _ => {} + } + } + + pub fn apply(&mut self) { + // mist shore is read-only telemetry. nothing to apply. + } + + pub fn revert(&mut self) {} + pub fn restore_defaults(&mut self) {} + + pub fn handle_key(&mut self, _scancode: u8, _app: &mut SettingsApp) {} + + pub fn handle_click(&mut self, _px: i32, py: i32, app: &mut SettingsApp) { + let row_h = (widgets::FONT_H + 8) as i32; + let idx = ((py - 40) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + self.activate(idx, app); + } + } +} + +pub fn render(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + let mist = &app.mist; + + let px = RAIL_WIDTH + PANE_PAD; + let mut cy = STRIP_HEIGHT + PANE_PAD; + + layout::draw_section(app, px, cy, "Framebuffer"); + cy += widgets::FONT_H + 4; + + // resolution + let mut buf = [0u8; 32]; + let mut res = [0u8; 24]; + let mut ri = 0; + let wn = widgets::u64_to_str(mist.fb_width as u64, &mut buf); + res[ri..ri + wn].copy_from_slice(&buf[..wn]); + ri += wn; + res[ri] = b'x'; + ri += 1; + let hn = widgets::u64_to_str(mist.fb_height as u64, &mut buf); + res[ri..ri + hn].copy_from_slice(&buf[..hn]); + ri += hn; + let res_str = core::str::from_utf8(&res[..ri]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Resolution:", res_str, t.telemetry); + cy += widgets::FONT_H + 2; + + // stride + let n = widgets::u64_to_str(mist.fb_stride as u64, &mut buf); + let stride_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Stride (bytes):", stride_str, t.telemetry); + cy += widgets::FONT_H + 2; + + // stride in pixels + let stride_px = mist.fb_stride / 4; + let n = widgets::u64_to_str(stride_px as u64, &mut buf); + let spx_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Stride (pixels):", spx_str, t.telemetry); + cy += widgets::FONT_H + 2; + + // pixel format + let fmt_str = match mist.fb_format { + 0 => "RGBX (0)", + 1 => "BGRX (1)", + 2 => "BitMask (2)", + 3 => "BltOnly (3)", + _ => "Unknown", + }; + layout::draw_kv(app, px, cy, "Pixel Format:", fmt_str, t.immutable); + cy += widgets::FONT_H + 2; + + // fb size + let n = widgets::format_bytes(mist.fb_size, &mut buf); + let size_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "FB Size:", size_str, t.telemetry); + cy += widgets::FONT_H + 2; + + // base address + let mut hex_buf = [0u8; 18]; + let hex_len = format_hex(mist.fb_base, &mut hex_buf); + let hex_str = core::str::from_utf8(&hex_buf[..hex_len]).unwrap_or("0x???"); + layout::draw_kv(app, px, cy, "Base Addr:", hex_str, t.immutable); + cy += widgets::FONT_H + 8; + + // pixel math section + layout::draw_section(app, px, cy, "Pixel Math"); + cy += widgets::FONT_H + 4; + + let bpp = 4u32; + let n = widgets::u64_to_str(bpp as u64, &mut buf); + let bpp_str = core::str::from_utf8(&buf[..n]).unwrap_or("4"); + layout::draw_kv(app, px, cy, "Bytes/Pixel:", bpp_str, t.telemetry); + cy += widgets::FONT_H + 2; + + // total pixels + let total_px = mist.fb_width as u64 * mist.fb_height as u64; + let n = widgets::u64_to_str(total_px, &mut buf); + let tpx_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Total Pixels:", tpx_str, t.telemetry); + cy += widgets::FONT_H + 2; + + // scanline padding + let pad = mist.fb_stride.saturating_sub(mist.fb_width * bpp); + let pad_label = if pad == 0 { "None" } else { "Present" }; + let pad_color = if pad == 0 { t.success } else { t.warning }; + layout::draw_kv(app, px, cy, "Scanline Pad:", pad_label, pad_color); + cy += widgets::FONT_H + 2; + + if pad > 0 { + let n = widgets::u64_to_str(pad as u64, &mut buf); + let pad_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, " Pad Bytes:", pad_str, t.warning); + cy += widgets::FONT_H + 2; + } + + cy += 8; + + // packing reminder + layout::draw_section(app, px, cy, "Packing Reference"); + cy += widgets::FONT_H + 4; + + let packing = match mist.fb_format { + 1 => "BGRX: b | (g<<8) | (r<<16) | (0xFF<<24)", + 0 => "RGBX: r | (g<<8) | (b<<16) | (0xFF<<24)", + _ => "(non-standard format)", + }; + widgets::draw_str(s, st, px, cy, packing, t.immutable, t.substrate, w, h); + cy += widgets::FONT_H + 2; + widgets::draw_str(s, st, px, cy, "addr = base + (y * stride) + (x * 4)", t.glyph_dim, t.substrate, w, h); + cy += widgets::FONT_H + 12; + + layout::draw_button_row(app, px, cy, "Refresh Display Info", FIELD_REFRESH, t.glyph); +} + +fn format_hex(val: u64, buf: &mut [u8; 18]) -> usize { + const HEX: &[u8; 16] = b"0123456789abcdef"; + buf[0] = b'0'; + buf[1] = b'x'; + let mut i = 2; + // skip leading zeros but keep at least one digit + let mut started = false; + for shift in (0..16).rev() { + let nib = ((val >> (shift * 4)) & 0xF) as usize; + if nib != 0 || started || shift == 0 { + buf[i] = HEX[nib]; + i += 1; + started = true; + } + } + i +} diff --git a/settings/src/chambers/mod.rs b/settings/src/chambers/mod.rs new file mode 100644 index 00000000..c562695d --- /dev/null +++ b/settings/src/chambers/mod.rs @@ -0,0 +1,7 @@ +pub mod archive; +pub mod gateway; +pub mod hall; +pub mod mirror; +pub mod mist; +pub mod net_obs; +pub mod sys_obs; diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs new file mode 100644 index 00000000..e8e3bf5f --- /dev/null +++ b/settings/src/chambers/net_obs.rs @@ -0,0 +1,542 @@ +// network observatory — operational connectivity controls. +// DHCP/static toggle, hostname, DNS, link status, MAC, stats. +// the most syscall-heavy chamber. exercises SYS_NET_CFG and SYS_NIC_INFO. + +use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; +use crate::state::{Route, SettingsApp}; +use crate::widgets; + +use libmorpheus::net; + +// editable field indices +const FIELD_DHCP_TOGGLE: usize = 0; +const FIELD_HOSTNAME: usize = 1; +const FIELD_IP: usize = 2; +const FIELD_PREFIX: usize = 3; +const FIELD_GATEWAY: usize = 4; +const FIELD_DNS1: usize = 5; +const FIELD_DNS2: usize = 6; +const FIELD_APPLY: usize = 7; +const FIELD_REFRESH: usize = 8; +const FIELD_COUNT: usize = 9; + +pub struct NetObsChamber { + // live state from kernel + pub state: u32, + pub flags: u32, + pub ip: u32, + pub prefix_len: u8, + pub gateway: u32, + pub dns1: u32, + pub dns2: u32, + pub mac: [u8; 6], + pub hostname: [u8; 64], + pub hostname_len: usize, + pub link_up: bool, + pub mtu: u32, + + // stats + pub tx_packets: u64, + pub rx_packets: u64, + pub tx_bytes: u64, + pub rx_bytes: u64, + + // edited fields (for pending change tracking) + pub edit_dhcp: bool, + pub edit_hostname: [u8; 64], + pub edit_hostname_len: usize, + pub edit_ip: [u8; 16], + pub edit_ip_len: usize, + pub edit_prefix: u8, + pub edit_gateway: [u8; 16], + pub edit_gw_len: usize, + pub edit_dns1: [u8; 16], + pub edit_dns1_len: usize, + pub edit_dns2: [u8; 16], + pub edit_dns2_len: usize, + + // which text field is being edited (cursor active) + pub editing_field: Option, +} + +impl NetObsChamber { + pub fn new() -> Self { + Self { + state: 0, + flags: 0, + ip: 0, + prefix_len: 0, + gateway: 0, + dns1: 0, + dns2: 0, + mac: [0; 6], + hostname: [0; 64], + hostname_len: 0, + link_up: false, + mtu: 0, + tx_packets: 0, + rx_packets: 0, + tx_bytes: 0, + rx_bytes: 0, + edit_dhcp: true, + edit_hostname: [0; 64], + edit_hostname_len: 0, + edit_ip: [0; 16], + edit_ip_len: 0, + edit_prefix: 24, + edit_gateway: [0; 16], + edit_gw_len: 0, + edit_dns1: [0; 16], + edit_dns1_len: 0, + edit_dns2: [0; 16], + edit_dns2_len: 0, + editing_field: None, + } + } + + pub fn refresh(&mut self) { + if let Ok(cfg) = net::net_config() { + self.state = cfg.state; + self.flags = cfg.flags; + self.ip = cfg.ipv4_addr; + self.prefix_len = cfg.prefix_len; + self.gateway = cfg.gateway; + self.dns1 = cfg.dns_primary; + self.dns2 = cfg.dns_secondary; + self.mac = [cfg.mac[0], cfg.mac[1], cfg.mac[2], cfg.mac[3], cfg.mac[4], cfg.mac[5]]; + self.mtu = cfg.mtu; + + // hostname + let hlen = cfg.hostname.iter().position(|&b| b == 0).unwrap_or(cfg.hostname.len()); + self.hostname[..hlen].copy_from_slice(&cfg.hostname[..hlen]); + self.hostname_len = hlen; + + self.edit_dhcp = (cfg.flags & net::NET_FLAG_DHCP) != 0; + + // populate edit fields from live state + self.sync_edit_from_live(); + } + + if let Ok(info) = net::nic_info() { + self.link_up = info.link_up != 0; + } + + if let Ok(stats) = net::net_stats() { + self.tx_packets = stats.tx_packets; + self.rx_packets = stats.rx_packets; + self.tx_bytes = stats.tx_bytes; + self.rx_bytes = stats.rx_bytes; + } + } + + fn sync_edit_from_live(&mut self) { + // hostname + self.edit_hostname[..self.hostname_len].copy_from_slice(&self.hostname[..self.hostname_len]); + self.edit_hostname_len = self.hostname_len; + + // ip + self.edit_ip_len = widgets::format_ip(self.ip, &mut self.edit_ip); + self.edit_prefix = self.prefix_len; + self.edit_gw_len = widgets::format_ip(self.gateway, &mut self.edit_gateway); + self.edit_dns1_len = widgets::format_ip(self.dns1, &mut self.edit_dns1); + self.edit_dns2_len = widgets::format_ip(self.dns2, &mut self.edit_dns2); + } + + pub fn widget_count(&self) -> usize { + FIELD_COUNT + } + + pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { + match idx { + FIELD_DHCP_TOGGLE => { + self.edit_dhcp = !self.edit_dhcp; + app.mark_edited(Route::NetObservatory, "dhcp"); + } + FIELD_HOSTNAME | FIELD_IP | FIELD_GATEWAY | FIELD_DNS1 | FIELD_DNS2 => { + self.editing_field = Some(idx); + } + FIELD_PREFIX => { + // cycle prefix: 8, 16, 24, 32 + self.edit_prefix = match self.edit_prefix { + 8 => 16, + 16 => 24, + 24 => 32, + _ => 8, + }; + app.mark_edited(Route::NetObservatory, "prefix"); + } + FIELD_APPLY => { + self.apply(app); + } + FIELD_REFRESH => { + self.refresh(); + app.set_status("Network refreshed", false); + } + _ => {} + } + } + + pub fn apply(&mut self, app: &mut SettingsApp) { + // hostname + if self.edit_hostname_len > 0 { + let hn = core::str::from_utf8(&self.edit_hostname[..self.edit_hostname_len]).unwrap_or(""); + if let Err(_) = net::net_set_hostname(hn) { + app.set_status("Hostname set failed", true); + return; + } + app.log_change(Route::NetObservatory, "hostname", hn, false); + } + + if self.edit_dhcp { + if let Err(_) = net::net_dhcp() { + app.set_status("DHCP request failed", true); + return; + } + app.log_change(Route::NetObservatory, "mode", "Switched to DHCP", false); + } else { + let ip = parse_ip(&self.edit_ip[..self.edit_ip_len]); + let gw = parse_ip(&self.edit_gateway[..self.edit_gw_len]); + if let Err(_) = net::net_static_ip(ip, self.edit_prefix, gw) { + app.set_status("Static IP set failed", true); + return; + } + app.log_change(Route::NetObservatory, "mode", "Switched to static", false); + + // dns + let d1 = parse_ip(&self.edit_dns1[..self.edit_dns1_len]); + let d2 = parse_ip(&self.edit_dns2[..self.edit_dns2_len]); + let servers = [d1, d2]; + let _ = net::dns_set_servers(&servers); + } + + self.refresh(); + app.set_status("Network config applied", false); + } + + pub fn revert(&mut self) { + self.editing_field = None; + self.sync_edit_from_live(); + } + + pub fn restore_defaults(&mut self) { + self.edit_dhcp = true; + self.edit_hostname_len = 0; + self.editing_field = None; + } + + pub fn handle_key(&mut self, scancode: u8, app: &mut SettingsApp) { + // if editing a text field, handle character input + if let Some(field) = self.editing_field { + match scancode { + 0x01 => { + // Escape — stop editing + self.editing_field = None; + } + 0x1C => { + // Enter — commit edit, stop editing + self.editing_field = None; + app.mark_edited(Route::NetObservatory, field_name(field)); + } + 0x0E => { + // Backspace + self.text_backspace(field); + app.mark_edited(Route::NetObservatory, field_name(field)); + } + _ => { + // try to map scancode to ascii char + if let Some(ch) = scancode_to_char(scancode) { + self.text_insert(field, ch); + app.mark_edited(Route::NetObservatory, field_name(field)); + } + } + } + app.frame_dirty = true; + return; + } + } + + fn text_insert(&mut self, field: usize, ch: u8) { + let (buf, len) = self.field_buf_mut(field); + if *len < buf.len() { + buf[*len] = ch; + *len += 1; + } + } + + fn text_backspace(&mut self, field: usize) { + let (_, len) = self.field_buf_mut(field); + if *len > 0 { + *len -= 1; + } + } + + fn field_buf_mut(&mut self, field: usize) -> (&mut [u8], &mut usize) { + match field { + FIELD_HOSTNAME => (&mut self.edit_hostname, &mut self.edit_hostname_len), + FIELD_IP => (&mut self.edit_ip, &mut self.edit_ip_len), + FIELD_GATEWAY => (&mut self.edit_gateway, &mut self.edit_gw_len), + FIELD_DNS1 => (&mut self.edit_dns1, &mut self.edit_dns1_len), + FIELD_DNS2 => (&mut self.edit_dns2, &mut self.edit_dns2_len), + _ => (&mut self.edit_hostname, &mut self.edit_hostname_len), + } + } + + pub fn handle_click(&mut self, px: i32, py: i32, app: &mut SettingsApp) { + let row_h = (widgets::FONT_H + 8) as i32; + // rough hit test — map y to field index + let idx = ((py - 40) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + self.activate(idx, app); + } + } +} + +pub fn render(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + let net = &app.net_obs; + + let px = RAIL_WIDTH + PANE_PAD; + let mut cy = STRIP_HEIGHT + PANE_PAD; + + // link status section + layout::draw_section(app, px, cy, "Link Status"); + cy += widgets::FONT_H + 4; + + let link_str = if net.link_up { "UP" } else { "DOWN" }; + let link_color = if net.link_up { t.success } else { t.destructive }; + layout::draw_kv(app, px, cy, "Link:", link_str, link_color); + cy += widgets::FONT_H + 2; + + let mut mac_buf = [0u8; 17]; + let mac_len = widgets::format_mac(&net.mac, &mut mac_buf); + let mac_str = core::str::from_utf8(&mac_buf[..mac_len]).unwrap_or("??"); + layout::draw_kv(app, px, cy, "MAC:", mac_str, t.immutable); + cy += widgets::FONT_H + 2; + + let mut mtu_buf = [0u8; 8]; + let mtu_len = widgets::u64_to_str(net.mtu as u64, &mut mtu_buf); + let mtu_str = core::str::from_utf8(&mtu_buf[..mtu_len]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "MTU:", mtu_str, t.telemetry); + cy += widgets::FONT_H + 8; + + // state + let state_str = match net.state { + 0 => "Unconfigured", + 1 => "DHCP Discovering", + 2 => "Ready", + 3 => "Error", + _ => "Unknown", + }; + let state_color = match net.state { + 2 => t.success, + 3 => t.destructive, + _ => t.warning, + }; + layout::draw_kv(app, px, cy, "State:", state_str, state_color); + cy += widgets::FONT_H + 8; + + // configuration section + layout::draw_section(app, px, cy, "Configuration"); + cy += widgets::FONT_H + 4; + + // DHCP toggle + let dhcp_label = if net.edit_dhcp { "[X] DHCP" } else { "[ ] Static" }; + layout::draw_button_row(app, px, cy, dhcp_label, FIELD_DHCP_TOGGLE, t.glyph); + cy += widgets::FONT_H + 8; + + // hostname + let hn = core::str::from_utf8(&net.edit_hostname[..net.edit_hostname_len]).unwrap_or(""); + let hn_display = if hn.is_empty() { "(none)" } else { hn }; + let hn_editing = net.editing_field == Some(FIELD_HOSTNAME); + draw_editable_field(app, px, cy, "Hostname:", hn_display, FIELD_HOSTNAME, hn_editing); + cy += widgets::FONT_H + 8; + + if !net.edit_dhcp { + // static ip fields + let ip_str = core::str::from_utf8(&net.edit_ip[..net.edit_ip_len]).unwrap_or("0.0.0.0"); + let ip_editing = net.editing_field == Some(FIELD_IP); + draw_editable_field(app, px, cy, "IP Address:", ip_str, FIELD_IP, ip_editing); + cy += widgets::FONT_H + 8; + + let mut pfx_buf = [0u8; 4]; + let pfx_len = widgets::u64_to_str(net.edit_prefix as u64, &mut pfx_buf); + let pfx_str = core::str::from_utf8(&pfx_buf[..pfx_len]).unwrap_or("24"); + layout::draw_field_row(app, px, cy, "Prefix Len:", pfx_str, false, FIELD_PREFIX); + cy += widgets::FONT_H + 8; + + let gw_str = core::str::from_utf8(&net.edit_gateway[..net.edit_gw_len]).unwrap_or("0.0.0.0"); + let gw_editing = net.editing_field == Some(FIELD_GATEWAY); + draw_editable_field(app, px, cy, "Gateway:", gw_str, FIELD_GATEWAY, gw_editing); + cy += widgets::FONT_H + 8; + + let d1_str = core::str::from_utf8(&net.edit_dns1[..net.edit_dns1_len]).unwrap_or("0.0.0.0"); + let d1_editing = net.editing_field == Some(FIELD_DNS1); + draw_editable_field(app, px, cy, "DNS Primary:", d1_str, FIELD_DNS1, d1_editing); + cy += widgets::FONT_H + 8; + + let d2_str = core::str::from_utf8(&net.edit_dns2[..net.edit_dns2_len]).unwrap_or("0.0.0.0"); + let d2_editing = net.editing_field == Some(FIELD_DNS2); + draw_editable_field(app, px, cy, "DNS Secondary:", d2_str, FIELD_DNS2, d2_editing); + cy += widgets::FONT_H + 8; + } else { + // show current DHCP-assigned values as read-only + let mut ip_buf = [0u8; 16]; + let ip_len = widgets::format_ip(net.ip, &mut ip_buf); + let ip_str = core::str::from_utf8(&ip_buf[..ip_len]).unwrap_or("0.0.0.0"); + layout::draw_kv(app, px, cy, "Assigned IP:", ip_str, t.immutable); + cy += widgets::FONT_H + 4; + + let mut gw_buf = [0u8; 16]; + let gw_len = widgets::format_ip(net.gateway, &mut gw_buf); + let gw_str = core::str::from_utf8(&gw_buf[..gw_len]).unwrap_or("0.0.0.0"); + layout::draw_kv(app, px, cy, "Gateway:", gw_str, t.immutable); + cy += widgets::FONT_H + 4; + + let mut d1_buf = [0u8; 16]; + let d1_len = widgets::format_ip(net.dns1, &mut d1_buf); + let d1_str = core::str::from_utf8(&d1_buf[..d1_len]).unwrap_or("0.0.0.0"); + layout::draw_kv(app, px, cy, "DNS:", d1_str, t.immutable); + cy += widgets::FONT_H + 8; + } + + // action buttons + layout::draw_button_row(app, px, cy, "Apply Network Config", FIELD_APPLY, t.signal); + cy += widgets::FONT_H + 8; + layout::draw_button_row(app, px, cy, "Refresh", FIELD_REFRESH, t.glyph); + cy += widgets::FONT_H + 12; + + // traffic stats section + layout::draw_section(app, px, cy, "Traffic"); + cy += widgets::FONT_H + 4; + + let mut buf = [0u8; 32]; + let n = widgets::u64_to_str(net.tx_packets, &mut buf); + let tx_p = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "TX Packets:", tx_p, t.telemetry); + cy += widgets::FONT_H + 2; + + let n = widgets::u64_to_str(net.rx_packets, &mut buf); + let rx_p = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "RX Packets:", rx_p, t.telemetry); + cy += widgets::FONT_H + 2; + + let n = widgets::format_bytes(net.tx_bytes, &mut buf); + let tx_b = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "TX Bytes:", tx_b, t.telemetry); + cy += widgets::FONT_H + 2; + + let n = widgets::format_bytes(net.rx_bytes, &mut buf); + let rx_b = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "RX Bytes:", rx_b, t.telemetry); +} + +fn draw_editable_field(app: &SettingsApp, x: u32, y: u32, label: &str, value: &str, field_idx: usize, editing: bool) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + let is_focused = !app.focus_in_rail && app.pane_focus == field_idx; + let bg = if editing { t.input_bg } else if is_focused { t.surface } else { t.substrate }; + let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + widgets::fill_rect(s, st, x, y, row_w, widgets::FONT_H + 4, bg, w, h); + + let border_color = if editing { + t.signal + } else if is_focused { + t.focus_ring + } else { + t.contour + }; + widgets::rect_outline(s, st, x, y, row_w, widgets::FONT_H + 4, border_color, w, h); + + widgets::draw_str(s, st, x + 4, y + 2, label, t.glyph_dim, bg, w, h); + let vx = x + 20 * widgets::FONT_W; + widgets::draw_str(s, st, vx, y + 2, value, t.glyph, bg, w, h); + + // cursor + if editing { + let cursor_x = vx + value.len() as u32 * widgets::FONT_W; + widgets::fill_rect(s, st, cursor_x, y + 2, 2, widgets::FONT_H, t.focus_ring, w, h); + } +} + +fn field_name(idx: usize) -> &'static str { + match idx { + FIELD_DHCP_TOGGLE => "dhcp", + FIELD_HOSTNAME => "hostname", + FIELD_IP => "ip", + FIELD_PREFIX => "prefix", + FIELD_GATEWAY => "gateway", + FIELD_DNS1 => "dns1", + FIELD_DNS2 => "dns2", + _ => "unknown", + } +} + +fn parse_ip(buf: &[u8]) -> u32 { + let s = core::str::from_utf8(buf).unwrap_or("0.0.0.0"); + let mut octets = [0u8; 4]; + let mut oi = 0; + let mut acc: u32 = 0; + for &b in s.as_bytes() { + if b == b'.' { + if oi < 4 { + octets[oi] = acc.min(255) as u8; + oi += 1; + acc = 0; + } + } else if b >= b'0' && b <= b'9' { + acc = acc * 10 + (b - b'0') as u32; + } + } + if oi < 4 { + octets[oi] = acc.min(255) as u8; + } + u32::from_be_bytes(octets) +} + +pub fn scancode_to_char(sc: u8) -> Option { + match sc { + 0x02..=0x0A => Some(b'1' + (sc - 0x02)), + 0x0B => Some(b'0'), + 0x10 => Some(b'q'), + 0x11 => Some(b'w'), + 0x12 => Some(b'e'), + 0x13 => Some(b'r'), + 0x14 => Some(b't'), + 0x15 => Some(b'y'), + 0x16 => Some(b'u'), + 0x17 => Some(b'i'), + 0x18 => Some(b'o'), + 0x19 => Some(b'p'), + 0x1E => Some(b'a'), + 0x1F => Some(b's'), + 0x20 => Some(b'd'), + 0x21 => Some(b'f'), + 0x22 => Some(b'g'), + 0x23 => Some(b'h'), + 0x24 => Some(b'j'), + 0x25 => Some(b'k'), + 0x26 => Some(b'l'), + 0x2C => Some(b'z'), + 0x2D => Some(b'x'), + 0x2E => Some(b'c'), + 0x2F => Some(b'v'), + 0x30 => Some(b'b'), + 0x31 => Some(b'n'), + 0x32 => Some(b'm'), + 0x33 => Some(b','), + 0x34 => Some(b'.'), + 0x27 => Some(b';'), + 0x28 => Some(b'\''), + 0x0C => Some(b'-'), + _ => None, + } +} diff --git a/settings/src/chambers/sys_obs.rs b/settings/src/chambers/sys_obs.rs new file mode 100644 index 00000000..79d36f39 --- /dev/null +++ b/settings/src/chambers/sys_obs.rs @@ -0,0 +1,276 @@ +// system observatory — telemetry dashboard and power controls. +// memory, cpu uptime, heap stats, reboot, shutdown. +// the power buttons have arm→consequence→confirm because bricking on accident is not a feature. + +use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; +use crate::state::{ArmState, Route, SettingsApp}; +use crate::widgets; + +use libmorpheus::sys; + +const FIELD_REFRESH: usize = 0; +const FIELD_REBOOT: usize = 1; +const FIELD_SHUTDOWN: usize = 2; +const FIELD_FORCE_REBOOT: usize = 3; +const FIELD_FORCE_SHUTDOWN: usize = 4; +const FIELD_COUNT: usize = 5; + +pub struct SysObsChamber { + pub total_mem: u64, + pub used_mem: u64, + pub free_mem: u64, + pub uptime_secs: u64, + pub heap_used: u64, + pub heap_total: u64, + pub cpu_count: u32, + pub idle_pct: u32, + + // power action arming + pub reboot_arm: ArmState, + pub shutdown_arm: ArmState, +} + +impl SysObsChamber { + pub fn new() -> Self { + Self { + total_mem: 0, + used_mem: 0, + free_mem: 0, + uptime_secs: 0, + heap_used: 0, + heap_total: 0, + cpu_count: 1, + idle_pct: 0, + reboot_arm: ArmState::Disarmed, + shutdown_arm: ArmState::Disarmed, + } + } + + pub fn refresh(&mut self) { + let mut info = sys::SysInfo::zeroed(); + let _ = sys::sysinfo(&mut info); + self.total_mem = info.total_mem; + self.used_mem = info.total_mem.saturating_sub(info.free_mem); + self.free_mem = info.free_mem; + self.uptime_secs = info.uptime_ms() / 1000; + self.heap_used = info.heap_used; + self.heap_total = info.heap_total; + self.cpu_count = info.cpu_count; + // idle fraction: idle_tsc / uptime_ticks. both are TSC-derived. + self.idle_pct = if info.idle_tsc > 0 && info.uptime_ticks > 0 { + ((info.idle_tsc * 100) / info.uptime_ticks) as u32 + } else { + 0 + }; + } + + pub fn widget_count(&self) -> usize { + FIELD_COUNT + } + + pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { + match idx { + FIELD_REFRESH => { + self.refresh(); + app.set_status("Telemetry refreshed", false); + } + FIELD_REBOOT => { + match self.reboot_arm { + ArmState::Disarmed => { + self.reboot_arm = ArmState::Armed; + app.set_status("Reboot ARMED. Press again to confirm.", false); + } + ArmState::Armed => { + self.reboot_arm = ArmState::Confirmed; + app.log_change(Route::SysObservatory, "power", "Graceful reboot", true); + let _ = sys::reboot(false); + } + ArmState::Confirmed => {} + } + } + FIELD_SHUTDOWN => { + match self.shutdown_arm { + ArmState::Disarmed => { + self.shutdown_arm = ArmState::Armed; + app.set_status("Shutdown ARMED. Press again to confirm.", false); + } + ArmState::Armed => { + self.shutdown_arm = ArmState::Confirmed; + app.log_change(Route::SysObservatory, "power", "Graceful shutdown", true); + let _ = sys::shutdown(false); + } + ArmState::Confirmed => {} + } + } + FIELD_FORCE_REBOOT => { + match self.reboot_arm { + ArmState::Armed => { + app.log_change(Route::SysObservatory, "power", "Force reboot", true); + let _ = sys::reboot(true); + } + _ => { + app.set_status("Arm reboot first (Enter on Reboot)", false); + } + } + } + FIELD_FORCE_SHUTDOWN => { + match self.shutdown_arm { + ArmState::Armed => { + app.log_change(Route::SysObservatory, "power", "Force shutdown", true); + let _ = sys::shutdown(true); + } + _ => { + app.set_status("Arm shutdown first (Enter on Shutdown)", false); + } + } + } + _ => {} + } + } + + pub fn handle_key(&mut self, scancode: u8, app: &mut SettingsApp) { + if scancode == 0x01 { + // Escape — disarm all + self.reboot_arm = ArmState::Disarmed; + self.shutdown_arm = ArmState::Disarmed; + app.set_status("Power actions disarmed", false); + } + } + + pub fn handle_click(&mut self, _px: i32, py: i32, app: &mut SettingsApp) { + let row_h = (widgets::FONT_H + 8) as i32; + let idx = ((py - 40) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + self.activate(idx, app); + } + } +} + +pub fn render(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + let sys = &app.sys_obs; + + let px = RAIL_WIDTH + PANE_PAD; + let mut cy = STRIP_HEIGHT + PANE_PAD; + + // memory section + layout::draw_section(app, px, cy, "Memory"); + cy += widgets::FONT_H + 4; + + let mut buf = [0u8; 32]; + let n = widgets::format_bytes(sys.total_mem, &mut buf); + let total_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Total:", total_str, t.telemetry); + cy += widgets::FONT_H + 2; + + let n = widgets::format_bytes(sys.used_mem, &mut buf); + let used_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Used:", used_str, t.telemetry); + cy += widgets::FONT_H + 2; + + let n = widgets::format_bytes(sys.free_mem, &mut buf); + let free_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Free:", free_str, t.success); + cy += widgets::FONT_H + 4; + + // memory usage bar + let bar_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + let pct = if sys.total_mem > 0 { + ((sys.used_mem * 100) / sys.total_mem) as u32 + } else { + 0 + }; + let bar_color = if pct > 90 { t.destructive } else if pct > 70 { t.warning } else { t.signal }; + widgets::draw_bar(s, st, px, cy, bar_w, 10, pct, 100, bar_color, t.substrate, t.contour, w, h); + cy += 14; + + // heap section + layout::draw_section(app, px, cy, "Heap"); + cy += widgets::FONT_H + 4; + + let n = widgets::format_bytes(sys.heap_used, &mut buf); + let hu_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Used:", hu_str, t.telemetry); + cy += widgets::FONT_H + 2; + + let n = widgets::format_bytes(sys.heap_total, &mut buf); + let ht_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Total:", ht_str, t.telemetry); + cy += widgets::FONT_H + 8; + + // cpu section + layout::draw_section(app, px, cy, "CPU"); + cy += widgets::FONT_H + 4; + + let n = widgets::u64_to_str(sys.cpu_count as u64, &mut buf); + let cpu_str = core::str::from_utf8(&buf[..n]).unwrap_or("1"); + layout::draw_kv(app, px, cy, "Cores:", cpu_str, t.telemetry); + cy += widgets::FONT_H + 2; + + let n = widgets::u64_to_str(sys.idle_pct as u64, &mut buf); + let idle_str = core::str::from_utf8(&buf[..n]).unwrap_or("0"); + layout::draw_kv(app, px, cy, "Idle %:", idle_str, t.telemetry); + cy += widgets::FONT_H + 8; + + // uptime + layout::draw_section(app, px, cy, "Uptime"); + cy += widgets::FONT_H + 4; + + let n = widgets::format_uptime(sys.uptime_secs, &mut buf); + let up_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); + layout::draw_kv(app, px, cy, "Since boot:", up_str, t.immutable); + cy += widgets::FONT_H + 12; + + // power controls + layout::draw_section(app, px, cy, "Power Controls"); + cy += widgets::FONT_H + 4; + + layout::draw_button_row(app, px, cy, "Refresh Telemetry", FIELD_REFRESH, t.glyph); + cy += widgets::FONT_H + 8; + + // reboot + let rb_label = match sys.reboot_arm { + ArmState::Disarmed => "Reboot (graceful)", + ArmState::Armed => "!! CONFIRM REBOOT !!", + ArmState::Confirmed => "(rebooting...)", + }; + let rb_color = match sys.reboot_arm { + ArmState::Disarmed => t.warning, + ArmState::Armed => t.armed, + ArmState::Confirmed => t.destructive, + }; + layout::draw_button_row(app, px, cy, rb_label, FIELD_REBOOT, rb_color); + cy += widgets::FONT_H + 4; + + if matches!(sys.reboot_arm, ArmState::Armed) { + layout::draw_risk_band(app, px, cy, "System will restart. Unsaved work is lost."); + cy += widgets::FONT_H + 4; + layout::draw_button_row(app, px, cy, "Force Reboot (skip cleanup)", FIELD_FORCE_REBOOT, t.destructive); + cy += widgets::FONT_H + 4; + } + + // shutdown + let sd_label = match sys.shutdown_arm { + ArmState::Disarmed => "Shutdown (graceful)", + ArmState::Armed => "!! CONFIRM SHUTDOWN !!", + ArmState::Confirmed => "(shutting down...)", + }; + let sd_color = match sys.shutdown_arm { + ArmState::Disarmed => t.destructive, + ArmState::Armed => t.armed, + ArmState::Confirmed => t.destructive, + }; + layout::draw_button_row(app, px, cy, sd_label, FIELD_SHUTDOWN, sd_color); + cy += widgets::FONT_H + 4; + + if matches!(sys.shutdown_arm, ArmState::Armed) { + layout::draw_risk_band(app, px, cy, "System will power off. No recovery without physical restart."); + cy += widgets::FONT_H + 4; + layout::draw_button_row(app, px, cy, "Force Shutdown (immediate)", FIELD_FORCE_SHUTDOWN, t.destructive); + } +} diff --git a/settings/src/font.rs b/settings/src/font.rs new file mode 100644 index 00000000..6289bb4b --- /dev/null +++ b/settings/src/font.rs @@ -0,0 +1,3 @@ +// vga 8x16 font — same as compd/shelld. 95 printable ascii glyphs. +pub const FONT_H: u32 = 16; +pub const FONT_W: u32 = 8; diff --git a/settings/src/font_data.inc b/settings/src/font_data.inc new file mode 100644 index 00000000..b36b4a6a --- /dev/null +++ b/settings/src/font_data.inc @@ -0,0 +1,97 @@ +[ + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x66, 0x66, 0x66, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x6C, 0x6C, 0xFE, 0x6C, 0x6C, 0x6C, 0xFE, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00], + [0x18, 0x18, 0x7C, 0xC6, 0xC2, 0xC0, 0x7C, 0x06, 0x06, 0x86, 0xC6, 0x7C, 0x18, 0x18, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0xC2, 0xC6, 0x0C, 0x18, 0x30, 0x60, 0xC6, 0x86, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x30, 0x30, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xD6, 0xD6, 0xC6, 0xC6, 0x6C, 0x38, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x7C, 0xC6, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x7C, 0xC6, 0x06, 0x06, 0x3C, 0x06, 0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xFE, 0x0C, 0x0C, 0x0C, 0x1E, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xFE, 0xC0, 0xC0, 0xC0, 0xFC, 0x06, 0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x38, 0x60, 0xC0, 0xC0, 0xFC, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xFE, 0xC6, 0x06, 0x06, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x06, 0x06, 0x0C, 0x78, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x0C, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xDE, 0xDE, 0xDE, 0xDC, 0xC0, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x66, 0x66, 0xFC, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xC0, 0xC0, 0xC2, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xF8, 0x6C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x6C, 0xF8, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, 0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xDE, 0xC6, 0xC6, 0x66, 0x3A, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0xCC, 0x78, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xE6, 0x66, 0x66, 0x6C, 0x78, 0x78, 0x6C, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xF0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xC6, 0xEE, 0xFE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE, 0xCE, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xDE, 0x7C, 0x0C, 0x0E, 0x00, 0x00], + [0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x60, 0x38, 0x0C, 0x06, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xFF, 0xDB, 0x99, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xD6, 0xD6, 0xFE, 0xEE, 0x6C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xC6, 0xC6, 0x6C, 0x7C, 0x38, 0x38, 0x7C, 0x6C, 0xC6, 0xC6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xFE, 0xC6, 0x86, 0x0C, 0x18, 0x30, 0x60, 0xC2, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00, 0x00, 0x00], + [0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00], + [0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0xE0, 0x60, 0x60, 0x78, 0x6C, 0x66, 0x66, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC0, 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x1C, 0x0C, 0x0C, 0x3C, 0x6C, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x38, 0x6C, 0x64, 0x60, 0xF0, 0x60, 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0xCC, 0x78, 0x00], + [0x00, 0x00, 0xE0, 0x60, 0x60, 0x6C, 0x76, 0x66, 0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x06, 0x06, 0x00, 0x0E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x66, 0x66, 0x3C, 0x00], + [0x00, 0x00, 0xE0, 0x60, 0x60, 0x66, 0x6C, 0x78, 0x78, 0x6C, 0x66, 0xE6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xE6, 0xFF, 0xDB, 0xDB, 0xDB, 0xDB, 0xDB, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0x0C, 0x1E, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x76, 0x66, 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0x60, 0x38, 0x0C, 0xC6, 0x7C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x10, 0x30, 0x30, 0xFC, 0x30, 0x30, 0x30, 0x30, 0x36, 0x1C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xC3, 0xC3, 0xC3, 0xC3, 0x66, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xD6, 0xD6, 0xFE, 0x6C, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x6C, 0x38, 0x38, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0xF8, 0x00], + [0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xCC, 0x18, 0x30, 0x60, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x0E, 0x18, 0x18, 0x18, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0E, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x70, 0x18, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00, 0x00], + [0x00, 0x00, 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], +] diff --git a/settings/src/layout.rs b/settings/src/layout.rs new file mode 100644 index 00000000..d3f3fcb9 --- /dev/null +++ b/settings/src/layout.rs @@ -0,0 +1,271 @@ +// global frame layout — the three-column oneiric structure. +// top strip, left rail, right console pane, bottom command bar. +// every pixel accounted for, every margin deliberate. + +use crate::state::{Route, SafetyMode, SettingsApp}; +use crate::theme::OneiricTheme; +use crate::widgets; + +pub const STRIP_HEIGHT: u32 = 20; +pub const BAR_HEIGHT: u32 = 20; +pub const RAIL_WIDTH: u32 = 160; +pub const RAIL_ITEM_HEIGHT: u32 = 20; +pub const SECTION_PAD: u32 = 8; +pub const PANE_PAD: u32 = 12; + +pub fn render_frame(app: &mut SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + // clear entire surface + widgets::fill_rect(s, st, 0, 0, w, h, t.substrate, w, h); + + // top command strip + render_strip(app); + + // left rail + render_rail(app); + + // right console pane + render_pane(app); + + // bottom command bar + render_bar(app); +} + +fn render_strip(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + widgets::fill_rect(s, st, 0, 0, w, STRIP_HEIGHT, t.strip_bg, w, h); + widgets::hline(s, st, 0, STRIP_HEIGHT - 1, w, t.contour, w, h); + + // chamber title + let label = app.route.label(); + widgets::draw_str(s, st, 4, 2, label, t.glyph, t.strip_bg, w, h); + + // technical alias + let alias = app.route.technical_alias(); + let alias_x = 4 + (label.len() as u32 + 2) * widgets::FONT_W; + widgets::draw_str(s, st, alias_x, 2, alias, t.glyph_dim, t.strip_bg, w, h); + + // mode badge + let (badge, badge_color) = match app.safety { + SafetyMode::Safe => ("SAFE", t.signal), + SafetyMode::Severe => ("SEVERE", t.destructive), + }; + let badge_x = w.saturating_sub((badge.len() as u32 + 2) * widgets::FONT_W); + widgets::draw_str(s, st, badge_x, 2, badge, badge_color, t.strip_bg, w, h); + + // pending indicator + if app.has_any_pending() { + let dot_x = badge_x.saturating_sub(3 * widgets::FONT_W); + widgets::draw_str(s, st, dot_x, 2, "[*]", t.warning, t.strip_bg, w, h); + } +} + +fn render_rail(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + let rail_h = h - STRIP_HEIGHT - BAR_HEIGHT; + widgets::fill_rect(s, st, 0, STRIP_HEIGHT, RAIL_WIDTH, rail_h, t.rail_bg, w, h); + widgets::vline(s, st, RAIL_WIDTH - 1, STRIP_HEIGHT, rail_h, t.contour, w, h); + + for (i, route) in Route::ALL.iter().enumerate() { + let y = STRIP_HEIGHT + i as u32 * RAIL_ITEM_HEIGHT; + let is_current = *route == app.route; + let is_focused = app.focus_in_rail && app.rail_focus == i; + + let bg = if is_current { + t.rail_active + } else if is_focused { + t.surface + } else { + t.rail_bg + }; + + widgets::fill_rect(s, st, 0, y, RAIL_WIDTH - 1, RAIL_ITEM_HEIGHT, bg, w, h); + + // focus ring + if is_focused { + widgets::rect_outline(s, st, 0, y, RAIL_WIDTH - 1, RAIL_ITEM_HEIGHT, t.focus_ring, w, h); + } + + // sigil + label + let sigil = route.sigil(); + let label = route.label(); + let fg = if is_current { t.glyph } else { t.glyph_dim }; + widgets::draw_str(s, st, 4, y + 2, sigil, t.signal, bg, w, h); + widgets::draw_str_trunc(s, st, 4 + 2 * widgets::FONT_W, y + 2, label, fg, bg, w, h, 16); + + // keyboard hint (1-7) + let mut hint = [0u8; 1]; + hint[0] = b'1' + i as u8; + let hint_str = core::str::from_utf8(&hint).unwrap_or("?"); + let hint_x = RAIL_WIDTH - 3 * widgets::FONT_W; + widgets::draw_str(s, st, hint_x, y + 2, hint_str, t.glyph_dim, bg, w, h); + + // pending dot for this chamber + if app.has_pending_for(*route) { + let dot_x = RAIL_WIDTH - 5 * widgets::FONT_W; + widgets::draw_str(s, st, dot_x, y + 2, "*", t.warning, bg, w, h); + } + } +} + +fn render_pane(app: &mut SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + let pane_x = RAIL_WIDTH; + let pane_y = STRIP_HEIGHT; + let pane_w = w - RAIL_WIDTH; + let pane_h = h - STRIP_HEIGHT - BAR_HEIGHT; + + widgets::fill_rect(s, st, pane_x, pane_y, pane_w, pane_h, t.substrate, w, h); + + // dispatch to the active chamber renderer + match app.route { + Route::Gateway => crate::chambers::gateway::render(app), + Route::MistShore => crate::chambers::mist::render(app), + Route::MirrorBasin => crate::chambers::mirror::render(app), + Route::NetObservatory => crate::chambers::net_obs::render(app), + Route::SysObservatory => crate::chambers::sys_obs::render(app), + Route::Archive => crate::chambers::archive::render(app), + Route::HallOfMasks => crate::chambers::hall::render(app), + } +} + +fn render_bar(app: &SettingsApp) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + let bar_y = h - BAR_HEIGHT; + widgets::fill_rect(s, st, 0, bar_y, w, BAR_HEIGHT, t.bar_bg, w, h); + widgets::hline(s, st, 0, bar_y, w, t.contour, w, h); + + let pane_w = w - RAIL_WIDTH; + let btn_w = pane_w / 4; + let btn_y = bar_y + 2; + + // command buttons + let buttons = ["[A]pply", "[R]evert", "[D]efaults", ""]; + for (i, label) in buttons.iter().enumerate() { + if label.is_empty() { + continue; + } + let bx = RAIL_WIDTH + i as u32 * btn_w; + let fg = if i == 0 && app.has_any_pending() { + t.signal + } else { + t.glyph_dim + }; + widgets::draw_str(s, st, bx + 4, btn_y, label, fg, t.bar_bg, w, h); + } + + // status message (right-aligned in bar) + if app.status_len > 0 { + let msg = core::str::from_utf8(&app.status_msg[..app.status_len]).unwrap_or(""); + let fg = if app.status_is_error { t.destructive } else { t.success }; + let sx = w.saturating_sub((msg.len() as u32 + 1) * widgets::FONT_W); + widgets::draw_str(s, st, sx, btn_y, msg, fg, t.bar_bg, w, h); + } +} + +// section header — a labeled divider within the pane +pub fn draw_section(app: &SettingsApp, x: u32, y: u32, title: &str) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + widgets::draw_str(s, st, x, y, title, t.signal, t.substrate, w, h); + let line_x = x + (title.len() as u32 + 1) * widgets::FONT_W; + let line_w = (w - RAIL_WIDTH).saturating_sub(line_x - RAIL_WIDTH + PANE_PAD); + widgets::hline(s, st, line_x, y + widgets::FONT_H / 2, line_w, t.contour, w, h); +} + +// labeled value row — "Label: value" with alignment +pub fn draw_kv(app: &SettingsApp, x: u32, y: u32, key: &str, val: &str, val_color: u32) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + widgets::draw_str(s, st, x, y, key, t.glyph_dim, t.substrate, w, h); + let vx = x + (key.len() as u32 + 1) * widgets::FONT_W; + widgets::draw_str(s, st, vx, y, val, val_color, t.substrate, w, h); +} + +// focusable field row — highlighted when focused +pub fn draw_field_row(app: &SettingsApp, x: u32, y: u32, label: &str, value: &str, focused: bool, field_idx: usize) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + let is_focused = !app.focus_in_rail && app.pane_focus == field_idx; + + let bg = if is_focused { t.surface } else { t.substrate }; + let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + widgets::fill_rect(s, st, x, y, row_w, widgets::FONT_H + 4, bg, w, h); + + if is_focused { + widgets::rect_outline(s, st, x, y, row_w, widgets::FONT_H + 4, t.focus_ring, w, h); + } + + widgets::draw_str(s, st, x + 4, y + 2, label, t.glyph_dim, bg, w, h); + let vx = x + 20 * widgets::FONT_W; + widgets::draw_str(s, st, vx, y + 2, value, t.glyph, bg, w, h); +} + +// button row — rendered as a text button +pub fn draw_button_row(app: &SettingsApp, x: u32, y: u32, label: &str, field_idx: usize, color: u32) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + let is_focused = !app.focus_in_rail && app.pane_focus == field_idx; + + let bg = if is_focused { t.surface } else { t.substrate }; + let btn_w = (label.len() as u32 + 4) * widgets::FONT_W; + let btn_h = widgets::FONT_H + 4; + + widgets::fill_rect(s, st, x, y, btn_w, btn_h, bg, w, h); + widgets::rect_outline(s, st, x, y, btn_w, btn_h, if is_focused { t.focus_ring } else { t.contour }, w, h); + widgets::draw_str(s, st, x + 2 * widgets::FONT_W, y + 2, label, color, bg, w, h); +} + +// risk band — a warning banner for destructive context +pub fn draw_risk_band(app: &SettingsApp, x: u32, y: u32, msg: &str) { + let s = app.surface; + let st = app.fb_stride; + let w = app.fb_w; + let h = app.fb_h; + let t = &app.theme; + + let band_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + widgets::fill_rect(s, st, x, y, band_w, widgets::FONT_H + 8, t.destructive, w, h); + widgets::draw_str(s, st, x + 4, y + 4, msg, t.substrate, t.destructive, w, h); +} diff --git a/settings/src/main.rs b/settings/src/main.rs new file mode 100644 index 00000000..f4c93239 --- /dev/null +++ b/settings/src/main.rs @@ -0,0 +1,44 @@ +#![no_std] +#![no_main] +#![allow(dead_code)] + +extern crate alloc; + +mod chambers; +mod font; +mod layout; +mod state; +mod theme; +mod widgets; + +use libmorpheus::{entry, hw, io, process}; + +entry!(main); + +fn main() -> i32 { + io::println("settings: starting"); + + let fb_info = hw::fb_info().expect("settings: fb_info failed"); + let surface_vaddr = hw::fb_map().expect("settings: fb_map failed"); + let surface_ptr = surface_vaddr as *mut u32; + + // stride is bytes not pixels. yes again. + let fb_stride_px = fb_info.stride / 4; + let is_bgrx = fb_info.format == 1; + + let mut app = state::SettingsApp::new( + surface_ptr, + fb_info.width, + fb_info.height, + fb_stride_px, + is_bgrx, + ); + + app.init(); + + loop { + app.tick(); + hw::fb_mark_dirty(); + process::yield_cpu(); + } +} diff --git a/settings/src/state.rs b/settings/src/state.rs new file mode 100644 index 00000000..430b17bd --- /dev/null +++ b/settings/src/state.rs @@ -0,0 +1,579 @@ +use alloc::vec::Vec; +use core::fmt; + +use crate::chambers::{ + archive::ArchiveChamber, gateway::GatewayChamber, hall::HallChamber, + mirror::MirrorChamber, mist::MistChamber, net_obs::NetObsChamber, + sys_obs::SysObsChamber, +}; +use crate::layout; +use crate::theme::OneiricTheme; + +// route ids — flat enum, zero-cost dispatch +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Route { + Gateway = 0, + MistShore = 1, + MirrorBasin = 2, + NetObservatory = 3, + SysObservatory = 4, + Archive = 5, + HallOfMasks = 6, +} + +impl Route { + pub const ALL: [Route; 7] = [ + Route::Gateway, + Route::MistShore, + Route::MirrorBasin, + Route::NetObservatory, + Route::SysObservatory, + Route::Archive, + Route::HallOfMasks, + ]; + + pub fn label(self) -> &'static str { + match self { + Route::Gateway => "Gateway", + Route::MistShore => "Mist Shore", + Route::MirrorBasin => "Mirror Basin", + Route::NetObservatory => "Observatory:Net", + Route::SysObservatory => "Observatory:Sys", + Route::Archive => "Archive", + Route::HallOfMasks => "Hall of Masks", + } + } + + pub fn sigil(self) -> &'static str { + match self { + Route::Gateway => ">", + Route::MistShore => "~", + Route::MirrorBasin => "o", + Route::NetObservatory => "*", + Route::SysObservatory => "#", + Route::Archive => "=", + Route::HallOfMasks => "%", + } + } + + pub fn technical_alias(self) -> &'static str { + match self { + Route::Gateway => "/gateway", + Route::MistShore => "/visual/mist-shore", + Route::MirrorBasin => "/visual/mirror-basin", + Route::NetObservatory => "/network/observatory", + Route::SysObservatory => "/system/observatory", + Route::Archive => "/storage/archive", + Route::HallOfMasks => "/presets/hall-of-masks", + } + } + + pub fn from_index(i: usize) -> Route { + Route::ALL[i.min(Route::ALL.len() - 1)] + } +} + +// setting lifecycle states +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum FieldState { + Pristine = 0, + Edited = 1, + Staged = 2, + Applied = 3, + Failed = 4, +} + +// safety mode +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SafetyMode { + Safe = 0, + Severe = 1, +} + +// armed confirmation for destructive actions +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ArmState { + Disarmed = 0, + Armed = 1, + Confirmed = 2, +} + +// pending change tracker +pub struct PendingChange { + pub chamber: Route, + pub field_name: &'static str, + pub state: FieldState, +} + +// changelog entry for Archive of Echoes +pub struct ChangeEntry { + pub timestamp_ms: u64, + pub chamber: Route, + pub field_name: &'static str, + pub description: [u8; 128], + pub desc_len: usize, + pub destructive: bool, +} + +// the master state machine +pub struct SettingsApp { + // display surface + pub surface: *mut u32, + pub fb_w: u32, + pub fb_h: u32, + pub fb_stride: u32, + pub is_bgrx: bool, + + // navigation + pub route: Route, + pub prev_route: Route, + pub rail_focus: usize, + pub pane_focus: usize, + pub focus_in_rail: bool, + + // mode + pub safety: SafetyMode, + pub severe_arm: ArmState, + + // theme + pub theme: OneiricTheme, + + // dirty tracking + pub pending: Vec, + pub changelog: Vec, + pub frame_dirty: bool, + pub status_msg: [u8; 128], + pub status_len: usize, + pub status_is_error: bool, + + // mouse state + pub mouse_x: i32, + pub mouse_y: i32, + pub last_buttons: u8, + + // chamber states + pub gateway: GatewayChamber, + pub mist: MistChamber, + pub mirror: MirrorChamber, + pub net_obs: NetObsChamber, + pub sys_obs: SysObsChamber, + pub archive: ArchiveChamber, + pub hall: HallChamber, + + // tick counter for animations + pub tick_count: u64, +} + +impl SettingsApp { + pub fn new( + surface: *mut u32, + fb_w: u32, + fb_h: u32, + fb_stride: u32, + is_bgrx: bool, + ) -> Self { + Self { + surface, + fb_w, + fb_h, + fb_stride, + is_bgrx, + + route: Route::Gateway, + prev_route: Route::Gateway, + rail_focus: 0, + pane_focus: 0, + focus_in_rail: true, + + safety: SafetyMode::Safe, + severe_arm: ArmState::Disarmed, + + theme: OneiricTheme::dark(), + + pending: Vec::new(), + changelog: Vec::new(), + frame_dirty: true, + status_msg: [0; 128], + status_len: 0, + status_is_error: false, + + mouse_x: 0, + mouse_y: 0, + last_buttons: 0, + + gateway: GatewayChamber::new(), + mist: MistChamber::new(), + mirror: MirrorChamber::new(), + net_obs: NetObsChamber::new(), + sys_obs: SysObsChamber::new(), + archive: ArchiveChamber::new(), + hall: HallChamber::new(), + + tick_count: 0, + } + } + + pub fn init(&mut self) { + self.frame_dirty = true; + self.net_obs.refresh(); + self.sys_obs.refresh(); + self.mist.refresh(); + } + + pub fn tick(&mut self) { + self.tick_count += 1; + + // poll input from compositor + self.poll_input(); + + // periodic refresh for observatory chambers (every ~60 ticks ≈ 1 second) + if self.tick_count % 60 == 0 { + match self.route { + Route::SysObservatory => self.sys_obs.refresh(), + Route::NetObservatory => self.net_obs.refresh(), + _ => {} + } + self.frame_dirty = true; + } + + if self.frame_dirty { + self.render(); + self.frame_dirty = false; + } + } + + fn poll_input(&mut self) { + // keyboard + let mut buf = [0u8; 1]; + let n = libmorpheus::io::read_stdin(&mut buf); + if n > 0 { + self.handle_key(buf[0]); + } + + // mouse + let ms = libmorpheus::hw::mouse_read(); + if ms.dx != 0 || ms.dy != 0 || ms.buttons != 0 { + self.mouse_x = (self.mouse_x + ms.dx as i32).clamp(0, self.fb_w as i32 - 1); + self.mouse_y = (self.mouse_y + ms.dy as i32).clamp(0, self.fb_h as i32 - 1); + + let left = (ms.buttons & 1) != 0; + let left_was = (self.last_buttons & 1) != 0; + if left && !left_was { + self.handle_click(self.mouse_x, self.mouse_y); + } + self.last_buttons = ms.buttons; + self.frame_dirty = true; + } + } + + fn handle_key(&mut self, scancode: u8) { + self.frame_dirty = true; + + match scancode { + // Tab = toggle rail/pane focus + 0x0F => { + self.focus_in_rail = !self.focus_in_rail; + } + // Up arrow + 0x48 => { + if self.focus_in_rail { + if self.rail_focus > 0 { + self.rail_focus -= 1; + } + } else { + self.pane_focus_up(); + } + } + // Down arrow + 0x50 => { + if self.focus_in_rail { + if self.rail_focus < Route::ALL.len() - 1 { + self.rail_focus += 1; + } + } else { + self.pane_focus_down(); + } + } + // Enter = navigate to rail selection or activate pane widget + 0x1C => { + if self.focus_in_rail { + self.navigate(Route::from_index(self.rail_focus)); + } else { + self.pane_activate(); + } + } + // Escape = back to gateway or disarm + 0x01 => { + if self.severe_arm == ArmState::Armed { + self.severe_arm = ArmState::Disarmed; + self.set_status("Disarmed", false); + } else if self.route != Route::Gateway { + if self.has_pending_for(self.route) { + self.set_status("Unsaved changes — apply or revert first", true); + } else { + self.navigate(Route::Gateway); + } + } + } + // Left arrow = focus rail + 0x4B => { + self.focus_in_rail = true; + } + // Right arrow = focus pane + 0x4D => { + self.focus_in_rail = false; + } + // 'a' key (apply staged) — scancode 0x1E + 0x1E => { + if !self.focus_in_rail { + self.apply_pending(); + } + } + // 'r' key (revert) — scancode 0x13 + 0x13 => { + if !self.focus_in_rail { + self.revert_pending(); + } + } + // forward to active chamber for chamber-specific keys + _ => { + self.chamber_key(scancode); + } + } + } + + fn handle_click(&mut self, x: i32, y: i32) { + self.frame_dirty = true; + + let rail_w = layout::RAIL_WIDTH; + let strip_h = layout::STRIP_HEIGHT; + let bar_h = layout::BAR_HEIGHT; + + // click in rail? + if x < rail_w as i32 && y >= strip_h as i32 && y < (self.fb_h - bar_h) as i32 { + let rel_y = (y - strip_h as i32) as u32; + let item_h = layout::RAIL_ITEM_HEIGHT; + let idx = (rel_y / item_h) as usize; + if idx < Route::ALL.len() { + self.rail_focus = idx; + self.focus_in_rail = true; + self.navigate(Route::from_index(idx)); + } + return; + } + + // click in command bar? + if y >= (self.fb_h - bar_h) as i32 { + self.handle_bar_click(x); + return; + } + + // click in pane — delegate to chamber + if x >= rail_w as i32 && y >= strip_h as i32 { + self.focus_in_rail = false; + let pane_x = x - rail_w as i32; + let pane_y = y - strip_h as i32; + self.chamber_click(pane_x, pane_y); + } + } + + fn handle_bar_click(&mut self, x: i32) { + let pane_w = self.fb_w - layout::RAIL_WIDTH; + let btn_w = pane_w / 4; + let base_x = layout::RAIL_WIDTH as i32; + + if x >= base_x && x < base_x + btn_w as i32 { + self.apply_pending(); + } else if x >= base_x + btn_w as i32 && x < base_x + 2 * btn_w as i32 { + self.revert_pending(); + } else if x >= base_x + 2 * btn_w as i32 && x < base_x + 3 * btn_w as i32 { + self.restore_defaults(); + } + } + + pub fn navigate(&mut self, target: Route) { + if target == self.route { + return; + } + if self.has_pending_for(self.route) { + self.set_status("Unsaved changes — apply or revert first", true); + return; + } + self.prev_route = self.route; + self.route = target; + self.pane_focus = 0; + self.focus_in_rail = false; + + // refresh data for the target chamber + match target { + Route::NetObservatory => self.net_obs.refresh(), + Route::SysObservatory => self.sys_obs.refresh(), + Route::MistShore => self.mist.refresh(), + _ => {} + } + } + + pub fn has_pending_for(&self, route: Route) -> bool { + self.pending.iter().any(|p| p.chamber == route && p.state == FieldState::Edited) + } + + pub fn has_any_pending(&self) -> bool { + self.pending.iter().any(|p| p.state == FieldState::Edited) + } + + fn apply_pending(&mut self) { + let route = self.route; + match route { + Route::NetObservatory => self.net_obs.apply(self), + Route::MistShore => self.mist.apply(), + Route::MirrorBasin => self.mirror.apply(), + Route::HallOfMasks => self.hall.apply(self), + _ => {} + } + self.pending.retain(|p| p.chamber != route); + self.set_status("Applied", false); + } + + fn revert_pending(&mut self) { + let route = self.route; + match route { + Route::NetObservatory => self.net_obs.revert(), + Route::MistShore => self.mist.revert(), + Route::MirrorBasin => self.mirror.revert(), + _ => {} + } + self.pending.retain(|p| p.chamber != route); + self.set_status("Reverted", false); + } + + fn restore_defaults(&mut self) { + let route = self.route; + match route { + Route::NetObservatory => self.net_obs.restore_defaults(), + Route::MistShore => self.mist.restore_defaults(), + Route::MirrorBasin => self.mirror.restore_defaults(), + _ => {} + } + self.pending.retain(|p| p.chamber != route); + self.set_status("Defaults restored", false); + } + + fn pane_focus_up(&mut self) { + if self.pane_focus > 0 { + self.pane_focus -= 1; + } + } + + fn pane_focus_down(&mut self) { + let max = self.pane_widget_count(); + if self.pane_focus + 1 < max { + self.pane_focus += 1; + } + } + + fn pane_activate(&mut self) { + match self.route { + Route::Gateway => { + self.gateway.activate(self.pane_focus, self); + } + Route::NetObservatory => { + self.net_obs.activate(self.pane_focus, self); + } + Route::SysObservatory => { + self.sys_obs.activate(self.pane_focus, self); + } + Route::MistShore => { + self.mist.activate(self.pane_focus, self); + } + Route::MirrorBasin => { + self.mirror.activate(self.pane_focus, self); + } + Route::Archive => { + self.archive.activate(self.pane_focus); + } + Route::HallOfMasks => { + self.hall.activate(self.pane_focus, self); + } + } + } + + fn chamber_key(&mut self, scancode: u8) { + match self.route { + Route::Gateway => self.gateway.handle_key(scancode, self), + Route::NetObservatory => self.net_obs.handle_key(scancode, self), + Route::SysObservatory => self.sys_obs.handle_key(scancode, self), + Route::MistShore => self.mist.handle_key(scancode, self), + Route::MirrorBasin => self.mirror.handle_key(scancode, self), + Route::Archive => self.archive.handle_key(scancode), + Route::HallOfMasks => self.hall.handle_key(scancode, self), + } + } + + fn chamber_click(&mut self, pane_x: i32, pane_y: i32) { + match self.route { + Route::Gateway => self.gateway.handle_click(pane_x, pane_y, self), + Route::NetObservatory => self.net_obs.handle_click(pane_x, pane_y, self), + Route::SysObservatory => self.sys_obs.handle_click(pane_x, pane_y, self), + Route::MistShore => self.mist.handle_click(pane_x, pane_y, self), + Route::MirrorBasin => self.mirror.handle_click(pane_x, pane_y, self), + Route::Archive => self.archive.handle_click(pane_x, pane_y), + Route::HallOfMasks => self.hall.handle_click(pane_x, pane_y, self), + } + } + + fn pane_widget_count(&self) -> usize { + match self.route { + Route::Gateway => self.gateway.widget_count(), + Route::MistShore => self.mist.widget_count(), + Route::MirrorBasin => self.mirror.widget_count(), + Route::NetObservatory => self.net_obs.widget_count(), + Route::SysObservatory => self.sys_obs.widget_count(), + Route::Archive => self.archive.widget_count(), + Route::HallOfMasks => self.hall.widget_count(), + } + } + + pub fn set_status(&mut self, msg: &str, is_error: bool) { + let n = msg.len().min(self.status_msg.len()); + self.status_msg[..n].copy_from_slice(&msg.as_bytes()[..n]); + self.status_len = n; + self.status_is_error = is_error; + self.frame_dirty = true; + } + + pub fn log_change(&mut self, chamber: Route, field: &'static str, desc: &str, destructive: bool) { + let ts = libmorpheus::time::uptime_ms(); + let mut buf = [0u8; 128]; + let n = desc.len().min(128); + buf[..n].copy_from_slice(&desc.as_bytes()[..n]); + self.changelog.push(ChangeEntry { + timestamp_ms: ts, + chamber, + field_name: field, + description: buf, + desc_len: n, + destructive, + }); + } + + pub fn mark_edited(&mut self, chamber: Route, field: &'static str) { + // upsert + if let Some(p) = self.pending.iter_mut().find(|p| p.chamber == chamber && p.field_name == field) { + p.state = FieldState::Edited; + } else { + self.pending.push(PendingChange { + chamber, + field_name: field, + state: FieldState::Edited, + }); + } + } + + fn render(&mut self) { + layout::render_frame(self); + } +} diff --git a/settings/src/theme.rs b/settings/src/theme.rs new file mode 100644 index 00000000..dc6438ab --- /dev/null +++ b/settings/src/theme.rs @@ -0,0 +1,124 @@ +// semantic color tokens for the oneiric chamber system. +// not your typical rgba palette — these map to meaning, not pixels. + +#[derive(Clone, Copy)] +pub struct OneiricTheme { + // substrate — the deepest background layer + pub substrate: u32, + // contour — borders, dividers, structural edges + pub contour: u32, + // glyph — primary text + pub glyph: u32, + // glyph_dim — secondary/muted text + pub glyph_dim: u32, + // signal — accent, active selection, highlighted elements + pub signal: u32, + // signal_dim — muted accent for inactive highlights + pub signal_dim: u32, + // warning — caution states + pub warning: u32, + // destructive — dangerous action highlights + pub destructive: u32, + // immutable — read-only info display + pub immutable: u32, + // telemetry — live data readouts + pub telemetry: u32, + // archive — historical/log entries + pub archive: u32, + // surface — elevated panel backgrounds + pub surface: u32, + // input_bg — text input field backgrounds + pub input_bg: u32, + // focus_ring — keyboard focus indicator + pub focus_ring: u32, + // armed — the moment before the trigger pull + pub armed: u32, + // success — positive outcome confirmation + pub success: u32, + // rail_bg — navigation rail background + pub rail_bg: u32, + // rail_active — active rail item + pub rail_active: u32, + // bar_bg — command bar background + pub bar_bg: u32, + // strip_bg — top command strip background + pub strip_bg: u32, +} + +impl OneiricTheme { + // dark mode — the canonical morpheus aesthetic. + // deep blacks, desaturated greens, everything feels like 3am serial console. + pub const fn dark() -> Self { + Self { + substrate: pack(10, 10, 14), + contour: pack(40, 45, 40), + glyph: pack(180, 200, 180), + glyph_dim: pack(90, 100, 90), + signal: pack(0, 170, 0), + signal_dim: pack(0, 85, 0), + warning: pack(200, 170, 0), + destructive: pack(200, 40, 40), + immutable: pack(100, 120, 140), + telemetry: pack(0, 200, 180), + archive: pack(120, 140, 120), + surface: pack(18, 20, 22), + input_bg: pack(14, 16, 18), + focus_ring: pack(85, 255, 85), + armed: pack(255, 100, 0), + success: pack(0, 200, 80), + rail_bg: pack(14, 14, 18), + rail_active: pack(0, 85, 0), + bar_bg: pack(12, 12, 16), + strip_bg: pack(16, 16, 20), + } + } + + // light mode — still oneiric, just inverted substrate. + // high contrast, same signal semantics. + pub const fn light() -> Self { + Self { + substrate: pack(220, 225, 220), + contour: pack(160, 165, 160), + glyph: pack(20, 25, 20), + glyph_dim: pack(100, 110, 100), + signal: pack(0, 130, 0), + signal_dim: pack(0, 70, 0), + warning: pack(180, 140, 0), + destructive: pack(180, 30, 30), + immutable: pack(60, 80, 100), + telemetry: pack(0, 150, 130), + archive: pack(80, 100, 80), + surface: pack(210, 215, 210), + input_bg: pack(235, 240, 235), + focus_ring: pack(0, 170, 0), + armed: pack(220, 80, 0), + success: pack(0, 160, 60), + rail_bg: pack(200, 205, 200), + rail_active: pack(0, 130, 0), + bar_bg: pack(190, 195, 190), + strip_bg: pack(200, 205, 200), + } + } +} + +// pack rgb into bgrx u32. uefi said b comes first and uefi answers to no one. +#[inline(always)] +pub const fn pack(r: u8, g: u8, b: u8) -> u32 { + (b as u32) | ((g as u32) << 8) | ((r as u32) << 16) | (0xFF << 24) +} + +// pack for rgbx format (the other one) +#[inline(always)] +pub const fn pack_rgbx(r: u8, g: u8, b: u8) -> u32 { + (r as u32) | ((g as u32) << 8) | ((b as u32) << 16) | (0xFF << 24) +} + +// runtime format-aware packing +#[inline(always)] +pub fn pack_pixel(r: u8, g: u8, b: u8, is_bgrx: bool) -> u32 { + if is_bgrx { + pack(r, g, b) + } else { + pack_rgbx(r, g, b) + } +} diff --git a/settings/src/widgets.rs b/settings/src/widgets.rs new file mode 100644 index 00000000..11aca8a9 --- /dev/null +++ b/settings/src/widgets.rs @@ -0,0 +1,231 @@ +// direct framebuffer text and shape primitives. +// no canvas abstraction. no trait dispatch. just pointer math and pixel writes. +// this is a standalone process with its own mapped surface. + +use crate::theme::OneiricTheme; + +// vga 8x16 font constants +pub const FONT_W: u32 = 8; +pub const FONT_H: u32 = 16; + +// embedded vga font — 95 printable ascii glyphs (0x20..=0x7E) +pub static FONT_DATA: [[u8; 16]; 95] = include!("font_data.inc"); + +#[inline(always)] +pub fn put_pixel(surface: *mut u32, stride: u32, x: u32, y: u32, color: u32) { + unsafe { + let offset = (y * stride + x) as isize; + surface.offset(offset).write_volatile(color); + } +} + +pub fn fill_rect(surface: *mut u32, stride: u32, x: u32, y: u32, w: u32, h: u32, color: u32, fb_w: u32, fb_h: u32) { + // clamp to bounds + let x1 = x.min(fb_w); + let y1 = y.min(fb_h); + let x2 = (x + w).min(fb_w); + let y2 = (y + h).min(fb_h); + if x1 >= x2 || y1 >= y2 { + return; + } + let row_len = (x2 - x1) as usize; + for row in y1..y2 { + unsafe { + let start = surface.offset((row * stride + x1) as isize); + for col in 0..row_len { + start.add(col).write_volatile(color); + } + } + } +} + +pub fn draw_char(surface: *mut u32, stride: u32, x: u32, y: u32, ch: u8, fg: u32, bg: u32, fb_w: u32, fb_h: u32) { + let idx = if ch >= 0x20 && ch <= 0x7E { + (ch - 0x20) as usize + } else { + 0 // space fallback + }; + let glyph = &FONT_DATA[idx]; + + for row in 0..16u32 { + let py = y + row; + if py >= fb_h { + break; + } + let bits = glyph[row as usize]; + for col in 0..8u32 { + let px = x + col; + if px >= fb_w { + break; + } + let color = if (bits >> (7 - col)) & 1 == 1 { fg } else { bg }; + unsafe { + surface.offset((py * stride + px) as isize).write_volatile(color); + } + } + } +} + +pub fn draw_str(surface: *mut u32, stride: u32, x: u32, y: u32, s: &str, fg: u32, bg: u32, fb_w: u32, fb_h: u32) { + let mut cx = x; + for &b in s.as_bytes() { + if cx + FONT_W > fb_w { + break; + } + draw_char(surface, stride, cx, y, b, fg, bg, fb_w, fb_h); + cx += FONT_W; + } +} + +// draw string with max width (truncates with ".." if too long) +pub fn draw_str_trunc(surface: *mut u32, stride: u32, x: u32, y: u32, s: &str, fg: u32, bg: u32, fb_w: u32, fb_h: u32, max_chars: usize) { + if s.len() <= max_chars { + draw_str(surface, stride, x, y, s, fg, bg, fb_w, fb_h); + } else { + let trunc = &s[..max_chars.saturating_sub(2)]; + draw_str(surface, stride, x, y, trunc, fg, bg, fb_w, fb_h); + let cx = x + (max_chars as u32 - 2) * FONT_W; + draw_str(surface, stride, cx, y, "..", fg, bg, fb_w, fb_h); + } +} + +// horizontal line +pub fn hline(surface: *mut u32, stride: u32, x: u32, y: u32, w: u32, color: u32, fb_w: u32, fb_h: u32) { + fill_rect(surface, stride, x, y, w, 1, color, fb_w, fb_h); +} + +// vertical line +pub fn vline(surface: *mut u32, stride: u32, x: u32, y: u32, h: u32, color: u32, fb_w: u32, fb_h: u32) { + fill_rect(surface, stride, x, y, 1, h, color, fb_w, fb_h); +} + +// outlined rectangle +pub fn rect_outline(surface: *mut u32, stride: u32, x: u32, y: u32, w: u32, h: u32, color: u32, fb_w: u32, fb_h: u32) { + hline(surface, stride, x, y, w, color, fb_w, fb_h); + hline(surface, stride, x, y + h.saturating_sub(1), w, color, fb_w, fb_h); + vline(surface, stride, x, y, h, color, fb_w, fb_h); + vline(surface, stride, x + w.saturating_sub(1), y, h, color, fb_w, fb_h); +} + +// integer to decimal string — no alloc, no format!, no core::fmt. +// returns the number of bytes written to `buf`. +pub fn u64_to_str(val: u64, buf: &mut [u8]) -> usize { + if val == 0 { + if !buf.is_empty() { + buf[0] = b'0'; + } + return 1; + } + let mut tmp = [0u8; 20]; + let mut n = 0; + let mut v = val; + while v > 0 { + tmp[n] = b'0' + (v % 10) as u8; + v /= 10; + n += 1; + } + let len = n.min(buf.len()); + for i in 0..len { + buf[i] = tmp[n - 1 - i]; + } + len +} + +// format bytes as human readable (KB/MB/GB) +pub fn format_bytes(bytes: u64, buf: &mut [u8]) -> usize { + if bytes < 1024 { + let n = u64_to_str(bytes, buf); + let suffix = b" B"; + let end = (n + suffix.len()).min(buf.len()); + buf[n..end].copy_from_slice(&suffix[..end - n]); + return end; + } + let (val, suffix) = if bytes < 1024 * 1024 { + (bytes / 1024, &b" KB"[..]) + } else if bytes < 1024 * 1024 * 1024 { + (bytes / (1024 * 1024), &b" MB"[..]) + } else { + (bytes / (1024 * 1024 * 1024), &b" GB"[..]) + }; + let n = u64_to_str(val, buf); + let end = (n + suffix.len()).min(buf.len()); + buf[n..end].copy_from_slice(&suffix[..end - n]); + end +} + +// format ip address (network byte order u32 -> dotted decimal) +pub fn format_ip(ip: u32, buf: &mut [u8]) -> usize { + let octets = ip.to_be_bytes(); + let mut pos = 0; + for (i, &o) in octets.iter().enumerate() { + let n = u64_to_str(o as u64, &mut buf[pos..]); + pos += n; + if i < 3 && pos < buf.len() { + buf[pos] = b'.'; + pos += 1; + } + } + pos +} + +// format mac address +pub fn format_mac(mac: &[u8; 6], buf: &mut [u8]) -> usize { + let hex = b"0123456789ABCDEF"; + let mut pos = 0; + for (i, &b) in mac.iter().enumerate() { + if pos + 2 > buf.len() { break; } + buf[pos] = hex[(b >> 4) as usize]; + buf[pos + 1] = hex[(b & 0x0F) as usize]; + pos += 2; + if i < 5 && pos < buf.len() { + buf[pos] = b':'; + pos += 1; + } + } + pos +} + +// format uptime from ticks +pub fn format_uptime(ms: u64, buf: &mut [u8]) -> usize { + let secs = ms / 1000; + let mins = secs / 60; + let hours = mins / 60; + let days = hours / 24; + + let mut pos = 0; + if days > 0 { + let n = u64_to_str(days, &mut buf[pos..]); + pos += n; + let s = b"d "; + let end = (pos + s.len()).min(buf.len()); + buf[pos..end].copy_from_slice(&s[..end - pos]); + pos = end; + } + let n = u64_to_str(hours % 24, &mut buf[pos..]); + pos += n; + if pos < buf.len() { buf[pos] = b'h'; pos += 1; } + if pos < buf.len() { buf[pos] = b' '; pos += 1; } + let n = u64_to_str(mins % 60, &mut buf[pos..]); + pos += n; + if pos < buf.len() { buf[pos] = b'm'; pos += 1; } + if pos < buf.len() { buf[pos] = b' '; pos += 1; } + let n = u64_to_str(secs % 60, &mut buf[pos..]); + pos += n; + if pos < buf.len() { buf[pos] = b's'; pos += 1; } + pos +} + +// percentage bar — a horizontal bar filled proportionally +pub fn draw_bar(surface: *mut u32, stride: u32, x: u32, y: u32, w: u32, h: u32, + fraction: u32, max: u32, fg: u32, bg: u32, border: u32, fb_w: u32, fb_h: u32) { + rect_outline(surface, stride, x, y, w, h, border, fb_w, fb_h); + let inner_w = w.saturating_sub(2); + let inner_h = h.saturating_sub(2); + fill_rect(surface, stride, x + 1, y + 1, inner_w, inner_h, bg, fb_w, fb_h); + if max > 0 { + let fill = (inner_w as u64 * fraction as u64 / max as u64) as u32; + if fill > 0 { + fill_rect(surface, stride, x + 1, y + 1, fill, inner_h, fg, fb_w, fb_h); + } + } +} diff --git a/setup-dev.sh b/setup-dev.sh index 4b3f9dfc..924102dc 100755 --- a/setup-dev.sh +++ b/setup-dev.sh @@ -272,6 +272,7 @@ USER_APPS=( "init,/bin/init" "compd,/bin/compd" "shelld,/bin/shelld" + "settings,/bin/settings" "syscall-e2e,/bin/syscall-e2e" "msh,/bin/msh" "spinning-cube,/bin/spinning-cube" From f9886adf34085929ae7afc46323b830132083a8f Mon Sep 17 00:00:00 2001 From: adrerl Date: Wed, 11 Mar 2026 12:25:05 +0100 Subject: [PATCH 05/12] lol --- settings/src/chambers/archive.rs | 2 +- settings/src/chambers/hall.rs | 100 +++++++-------- settings/src/chambers/mirror.rs | 120 ++++++++--------- settings/src/chambers/mist.rs | 43 +++---- settings/src/chambers/net_obs.rs | 214 +++++++++++++++---------------- settings/src/chambers/sys_obs.rs | 125 +++++++++--------- settings/src/layout.rs | 4 +- settings/src/main.rs | 5 +- settings/src/state.rs | 59 ++++----- settings/src/widgets.rs | 2 +- 10 files changed, 326 insertions(+), 348 deletions(-) diff --git a/settings/src/chambers/archive.rs b/settings/src/chambers/archive.rs index 48d186e2..758226fc 100644 --- a/settings/src/chambers/archive.rs +++ b/settings/src/chambers/archive.rs @@ -3,7 +3,7 @@ // the system's memory. read-only — you cannot un-echo what has been spoken. use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; -use crate::state::{ChangeEntry, SettingsApp}; +use crate::state::SettingsApp; use crate::widgets; const VISIBLE_ENTRIES: usize = 16; diff --git a/settings/src/chambers/hall.rs b/settings/src/chambers/hall.rs index b01b679a..aa128902 100644 --- a/settings/src/chambers/hall.rs +++ b/settings/src/chambers/hall.rs @@ -73,48 +73,6 @@ impl HallChamber { FIELD_COUNT } - pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { - if idx < PRESET_COUNT { - self.selected = idx; - self.preview_active = true; - // live preview — apply theme immediately - self.apply_theme_preview(app); - app.set_status("Previewing preset. Enter on Apply to commit.", false); - } else if idx == FIELD_APPLY { - self.apply(app); - } - } - - fn apply_theme_preview(&self, app: &mut SettingsApp) { - let p = &PRESETS[self.selected]; - let base = if p.dark { OneiricTheme::dark() } else { OneiricTheme::light() }; - let accent = theme::pack(p.accent_r, p.accent_g, p.accent_b); - - app.theme = base; - app.theme.signal = accent; - app.theme.focus_ring = accent; - app.theme.rail_active = accent; - - // sync mirror chamber state - app.mirror.dark_mode = p.dark; - // find closest accent idx - app.mirror.accent_idx = 0; - - app.frame_dirty = true; - } - - pub fn apply(&mut self, app: &mut SettingsApp) { - if !self.preview_active { - app.set_status("Select a preset first", false); - return; - } - self.apply_theme_preview(app); - app.mirror.apply(); - self.preview_active = false; - app.log_change(Route::HallOfMasks, "preset", PRESETS[self.selected].name, false); - app.set_status("Preset applied", false); - } - pub fn revert(&mut self) { self.preview_active = false; } @@ -123,17 +81,57 @@ impl HallChamber { self.selected = 0; self.preview_active = false; } +} + +pub fn activate(app: &mut SettingsApp, idx: usize) { + if idx < PRESET_COUNT { + app.hall.selected = idx; + app.hall.preview_active = true; + apply_theme_preview(app); + app.set_status("Previewing preset. Enter on Apply to commit.", false); + } else if idx == FIELD_APPLY { + apply(app); + } +} - pub fn handle_key(&mut self, _scancode: u8, _app: &mut SettingsApp) {} +fn apply_theme_preview(app: &mut SettingsApp) { + let sel = app.hall.selected; + let p = &PRESETS[sel]; + let base = if p.dark { OneiricTheme::dark() } else { OneiricTheme::light() }; + let accent = theme::pack(p.accent_r, p.accent_g, p.accent_b); - pub fn handle_click(&mut self, _px: i32, py: i32, app: &mut SettingsApp) { - let row_h = (widgets::FONT_H + 12) as i32; - let header = 60i32; - let idx = ((py - header) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - self.activate(idx, app); - } + app.theme = base; + app.theme.signal = accent; + app.theme.focus_ring = accent; + app.theme.rail_active = accent; + + app.mirror.dark_mode = p.dark; + app.mirror.accent_idx = 0; + app.frame_dirty = true; +} + +pub fn apply(app: &mut SettingsApp) { + if !app.hall.preview_active { + app.set_status("Select a preset first", false); + return; + } + apply_theme_preview(app); + app.mirror.apply(); + app.hall.preview_active = false; + let name = PRESETS[app.hall.selected].name; + app.log_change(Route::HallOfMasks, "preset", name, false); + app.set_status("Preset applied", false); +} + +pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} + +pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { + let row_h = (widgets::FONT_H + 12) as i32; + let header = 60i32; + let idx = ((py - header) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + activate(app, idx); } } diff --git a/settings/src/chambers/mirror.rs b/settings/src/chambers/mirror.rs index 3d2e6423..6c329400 100644 --- a/settings/src/chambers/mirror.rs +++ b/settings/src/chambers/mirror.rs @@ -47,57 +47,6 @@ impl MirrorChamber { FIELD_COUNT } - pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { - match idx { - FIELD_THEME_TOGGLE => { - self.dark_mode = !self.dark_mode; - self.rebuild_theme(app); - app.mark_edited(Route::MirrorBasin, "theme_mode"); - } - FIELD_ACCENT_PREV => { - self.accent_idx = if self.accent_idx == 0 { ACCENT_COUNT - 1 } else { self.accent_idx - 1 }; - self.rebuild_theme(app); - app.mark_edited(Route::MirrorBasin, "accent"); - } - FIELD_ACCENT_NEXT => { - self.accent_idx = (self.accent_idx + 1) % ACCENT_COUNT; - self.rebuild_theme(app); - app.mark_edited(Route::MirrorBasin, "accent"); - } - FIELD_APPLY => { - self.apply(); - app.set_status("Appearance applied", false); - app.log_change(Route::MirrorBasin, "appearance", ACCENTS[self.accent_idx].3, false); - } - FIELD_REVERT => { - self.revert(); - self.rebuild_theme(app); - app.set_status("Appearance reverted", false); - } - _ => {} - } - } - - fn rebuild_theme(&self, app: &mut SettingsApp) { - let base = if self.dark_mode { OneiricTheme::dark() } else { OneiricTheme::light() }; - let (r, g, b, _) = ACCENTS[self.accent_idx]; - let accent = crate::theme::pack(r, g, b); - // override signal and focus ring with accent - app.theme.signal = accent; - app.theme.focus_ring = accent; - app.theme.rail_active = accent; - app.theme.substrate = base.substrate; - app.theme.contour = base.contour; - app.theme.glyph = base.glyph; - app.theme.glyph_dim = base.glyph_dim; - app.theme.surface = base.surface; - app.theme.input_bg = base.input_bg; - app.theme.rail_bg = base.rail_bg; - app.theme.bar_bg = base.bar_bg; - app.theme.strip_bg = base.strip_bg; - app.frame_dirty = true; - } - pub fn apply(&mut self) { self.saved_dark = self.dark_mode; self.saved_accent = self.accent_idx; @@ -112,16 +61,69 @@ impl MirrorChamber { self.dark_mode = true; self.accent_idx = 0; } +} - pub fn handle_key(&mut self, _scancode: u8, _app: &mut SettingsApp) {} - - pub fn handle_click(&mut self, _px: i32, py: i32, app: &mut SettingsApp) { - let row_h = (widgets::FONT_H + 8) as i32; - let idx = ((py - 40) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - self.activate(idx, app); +pub fn activate(app: &mut SettingsApp, idx: usize) { + match idx { + FIELD_THEME_TOGGLE => { + app.mirror.dark_mode = !app.mirror.dark_mode; + rebuild_theme(app); + app.mark_edited(Route::MirrorBasin, "theme_mode"); + } + FIELD_ACCENT_PREV => { + app.mirror.accent_idx = if app.mirror.accent_idx == 0 { ACCENT_COUNT - 1 } else { app.mirror.accent_idx - 1 }; + rebuild_theme(app); + app.mark_edited(Route::MirrorBasin, "accent"); + } + FIELD_ACCENT_NEXT => { + app.mirror.accent_idx = (app.mirror.accent_idx + 1) % ACCENT_COUNT; + rebuild_theme(app); + app.mark_edited(Route::MirrorBasin, "accent"); } + FIELD_APPLY => { + let accent_name = ACCENTS[app.mirror.accent_idx].3; + app.mirror.apply(); + app.set_status("Appearance applied", false); + app.log_change(Route::MirrorBasin, "appearance", accent_name, false); + } + FIELD_REVERT => { + app.mirror.revert(); + rebuild_theme(app); + app.set_status("Appearance reverted", false); + } + _ => {} + } +} + +fn rebuild_theme(app: &mut SettingsApp) { + let dark = app.mirror.dark_mode; + let ai = app.mirror.accent_idx; + let base = if dark { OneiricTheme::dark() } else { OneiricTheme::light() }; + let (r, g, b, _) = ACCENTS[ai]; + let accent = crate::theme::pack(r, g, b); + app.theme.signal = accent; + app.theme.focus_ring = accent; + app.theme.rail_active = accent; + app.theme.substrate = base.substrate; + app.theme.contour = base.contour; + app.theme.glyph = base.glyph; + app.theme.glyph_dim = base.glyph_dim; + app.theme.surface = base.surface; + app.theme.input_bg = base.input_bg; + app.theme.rail_bg = base.rail_bg; + app.theme.bar_bg = base.bar_bg; + app.theme.strip_bg = base.strip_bg; + app.frame_dirty = true; +} + +pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} + +pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { + let row_h = (widgets::FONT_H + 8) as i32; + let idx = ((py - 40) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + activate(app, idx); } } diff --git a/settings/src/chambers/mist.rs b/settings/src/chambers/mist.rs index d448335b..8a6f4da5 100644 --- a/settings/src/chambers/mist.rs +++ b/settings/src/chambers/mist.rs @@ -3,7 +3,7 @@ // read-only telemetry mostly — this is the calmest chamber. use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; -use crate::state::{Route, SettingsApp}; +use crate::state::SettingsApp; use crate::widgets; use libmorpheus::hw; @@ -47,32 +47,29 @@ impl MistChamber { FIELD_COUNT } - pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { - match idx { - FIELD_REFRESH => { - self.refresh(); - app.set_status("Display info refreshed", false); - } - _ => {} - } - } - - pub fn apply(&mut self) { - // mist shore is read-only telemetry. nothing to apply. - } - + pub fn apply(&mut self) {} pub fn revert(&mut self) {} pub fn restore_defaults(&mut self) {} +} - pub fn handle_key(&mut self, _scancode: u8, _app: &mut SettingsApp) {} - - pub fn handle_click(&mut self, _px: i32, py: i32, app: &mut SettingsApp) { - let row_h = (widgets::FONT_H + 8) as i32; - let idx = ((py - 40) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - self.activate(idx, app); +pub fn activate(app: &mut SettingsApp, idx: usize) { + match idx { + FIELD_REFRESH => { + app.mist.refresh(); + app.set_status("Display info refreshed", false); } + _ => {} + } +} + +pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} + +pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { + let row_h = (widgets::FONT_H + 8) as i32; + let idx = ((py - 40) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + activate(app, idx); } } diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs index e8e3bf5f..e6abce36 100644 --- a/settings/src/chambers/net_obs.rs +++ b/settings/src/chambers/net_obs.rs @@ -146,73 +146,6 @@ impl NetObsChamber { FIELD_COUNT } - pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { - match idx { - FIELD_DHCP_TOGGLE => { - self.edit_dhcp = !self.edit_dhcp; - app.mark_edited(Route::NetObservatory, "dhcp"); - } - FIELD_HOSTNAME | FIELD_IP | FIELD_GATEWAY | FIELD_DNS1 | FIELD_DNS2 => { - self.editing_field = Some(idx); - } - FIELD_PREFIX => { - // cycle prefix: 8, 16, 24, 32 - self.edit_prefix = match self.edit_prefix { - 8 => 16, - 16 => 24, - 24 => 32, - _ => 8, - }; - app.mark_edited(Route::NetObservatory, "prefix"); - } - FIELD_APPLY => { - self.apply(app); - } - FIELD_REFRESH => { - self.refresh(); - app.set_status("Network refreshed", false); - } - _ => {} - } - } - - pub fn apply(&mut self, app: &mut SettingsApp) { - // hostname - if self.edit_hostname_len > 0 { - let hn = core::str::from_utf8(&self.edit_hostname[..self.edit_hostname_len]).unwrap_or(""); - if let Err(_) = net::net_set_hostname(hn) { - app.set_status("Hostname set failed", true); - return; - } - app.log_change(Route::NetObservatory, "hostname", hn, false); - } - - if self.edit_dhcp { - if let Err(_) = net::net_dhcp() { - app.set_status("DHCP request failed", true); - return; - } - app.log_change(Route::NetObservatory, "mode", "Switched to DHCP", false); - } else { - let ip = parse_ip(&self.edit_ip[..self.edit_ip_len]); - let gw = parse_ip(&self.edit_gateway[..self.edit_gw_len]); - if let Err(_) = net::net_static_ip(ip, self.edit_prefix, gw) { - app.set_status("Static IP set failed", true); - return; - } - app.log_change(Route::NetObservatory, "mode", "Switched to static", false); - - // dns - let d1 = parse_ip(&self.edit_dns1[..self.edit_dns1_len]); - let d2 = parse_ip(&self.edit_dns2[..self.edit_dns2_len]); - let servers = [d1, d2]; - let _ = net::dns_set_servers(&servers); - } - - self.refresh(); - app.set_status("Network config applied", false); - } - pub fn revert(&mut self) { self.editing_field = None; self.sync_edit_from_live(); @@ -224,37 +157,6 @@ impl NetObsChamber { self.editing_field = None; } - pub fn handle_key(&mut self, scancode: u8, app: &mut SettingsApp) { - // if editing a text field, handle character input - if let Some(field) = self.editing_field { - match scancode { - 0x01 => { - // Escape — stop editing - self.editing_field = None; - } - 0x1C => { - // Enter — commit edit, stop editing - self.editing_field = None; - app.mark_edited(Route::NetObservatory, field_name(field)); - } - 0x0E => { - // Backspace - self.text_backspace(field); - app.mark_edited(Route::NetObservatory, field_name(field)); - } - _ => { - // try to map scancode to ascii char - if let Some(ch) = scancode_to_char(scancode) { - self.text_insert(field, ch); - app.mark_edited(Route::NetObservatory, field_name(field)); - } - } - } - app.frame_dirty = true; - return; - } - } - fn text_insert(&mut self, field: usize, ch: u8) { let (buf, len) = self.field_buf_mut(field); if *len < buf.len() { @@ -280,23 +182,117 @@ impl NetObsChamber { _ => (&mut self.edit_hostname, &mut self.edit_hostname_len), } } +} + +pub fn activate(app: &mut SettingsApp, idx: usize) { + match idx { + FIELD_DHCP_TOGGLE => { + app.net_obs.edit_dhcp = !app.net_obs.edit_dhcp; + app.mark_edited(Route::NetObservatory, "dhcp"); + } + FIELD_HOSTNAME | FIELD_IP | FIELD_GATEWAY | FIELD_DNS1 | FIELD_DNS2 => { + app.net_obs.editing_field = Some(idx); + } + FIELD_PREFIX => { + app.net_obs.edit_prefix = match app.net_obs.edit_prefix { + 8 => 16, + 16 => 24, + 24 => 32, + _ => 8, + }; + app.mark_edited(Route::NetObservatory, "prefix"); + } + FIELD_APPLY => { + apply(app); + } + FIELD_REFRESH => { + app.net_obs.refresh(); + app.set_status("Network refreshed", false); + } + _ => {} + } +} - pub fn handle_click(&mut self, px: i32, py: i32, app: &mut SettingsApp) { - let row_h = (widgets::FONT_H + 8) as i32; - // rough hit test — map y to field index - let idx = ((py - 40) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - self.activate(idx, app); +pub fn apply(app: &mut SettingsApp) { + // hostname — copy to local buf so we don't hold a borrow on app across log_change + let hl = app.net_obs.edit_hostname_len; + if hl > 0 { + let mut hn_buf = [0u8; 64]; + hn_buf[..hl].copy_from_slice(&app.net_obs.edit_hostname[..hl]); + let hn = core::str::from_utf8(&hn_buf[..hl]).unwrap_or(""); + if let Err(_) = net::net_set_hostname(hn) { + app.set_status("Hostname set failed", true); + return; } + app.log_change(Route::NetObservatory, "hostname", hn, false); + } + + let dhcp = app.net_obs.edit_dhcp; + if dhcp { + if let Err(_) = net::net_dhcp() { + app.set_status("DHCP request failed", true); + return; + } + app.log_change(Route::NetObservatory, "mode", "Switched to DHCP", false); + } else { + let ip_len = app.net_obs.edit_ip_len; + let gw_len = app.net_obs.edit_gw_len; + let prefix = app.net_obs.edit_prefix; + let ip = parse_ip(&app.net_obs.edit_ip[..ip_len]); + let gw = parse_ip(&app.net_obs.edit_gateway[..gw_len]); + if let Err(_) = net::net_static_ip(ip, prefix, gw) { + app.set_status("Static IP set failed", true); + return; + } + app.log_change(Route::NetObservatory, "mode", "Switched to static", false); + + let d1_len = app.net_obs.edit_dns1_len; + let d2_len = app.net_obs.edit_dns2_len; + let d1 = parse_ip(&app.net_obs.edit_dns1[..d1_len]); + let d2 = parse_ip(&app.net_obs.edit_dns2[..d2_len]); + let servers = [d1, d2]; + let _ = net::dns_set_servers(&servers); + } + + app.net_obs.refresh(); + app.set_status("Network config applied", false); +} + +pub fn handle_key(app: &mut SettingsApp, scancode: u8) { + if let Some(field) = app.net_obs.editing_field { + match scancode { + 0x01 => { + app.net_obs.editing_field = None; + } + 0x1C => { + app.net_obs.editing_field = None; + app.mark_edited(Route::NetObservatory, field_name(field)); + } + 0x0E => { + app.net_obs.text_backspace(field); + app.mark_edited(Route::NetObservatory, field_name(field)); + } + _ => { + if let Some(ch) = scancode_to_char(scancode) { + app.net_obs.text_insert(field, ch); + app.mark_edited(Route::NetObservatory, field_name(field)); + } + } + } + app.frame_dirty = true; + } +} + +pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { + let row_h = (widgets::FONT_H + 8) as i32; + let idx = ((py - 40) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + activate(app, idx); } } pub fn render(app: &SettingsApp) { - let s = app.surface; - let st = app.fb_stride; - let w = app.fb_w; - let h = app.fb_h; let t = &app.theme; let net = &app.net_obs; diff --git a/settings/src/chambers/sys_obs.rs b/settings/src/chambers/sys_obs.rs index 79d36f39..bd91af4c 100644 --- a/settings/src/chambers/sys_obs.rs +++ b/settings/src/chambers/sys_obs.rs @@ -67,83 +67,82 @@ impl SysObsChamber { pub fn widget_count(&self) -> usize { FIELD_COUNT } +} - pub fn activate(&mut self, idx: usize, app: &mut SettingsApp) { - match idx { - FIELD_REFRESH => { - self.refresh(); - app.set_status("Telemetry refreshed", false); - } - FIELD_REBOOT => { - match self.reboot_arm { - ArmState::Disarmed => { - self.reboot_arm = ArmState::Armed; - app.set_status("Reboot ARMED. Press again to confirm.", false); - } - ArmState::Armed => { - self.reboot_arm = ArmState::Confirmed; - app.log_change(Route::SysObservatory, "power", "Graceful reboot", true); - let _ = sys::reboot(false); - } - ArmState::Confirmed => {} +pub fn activate(app: &mut SettingsApp, idx: usize) { + match idx { + FIELD_REFRESH => { + app.sys_obs.refresh(); + app.set_status("Telemetry refreshed", false); + } + FIELD_REBOOT => { + match app.sys_obs.reboot_arm { + ArmState::Disarmed => { + app.sys_obs.reboot_arm = ArmState::Armed; + app.set_status("Reboot ARMED. Press again to confirm.", false); + } + ArmState::Armed => { + app.sys_obs.reboot_arm = ArmState::Confirmed; + app.log_change(Route::SysObservatory, "power", "Graceful reboot", true); + let _ = sys::reboot(false); } + ArmState::Confirmed => {} } - FIELD_SHUTDOWN => { - match self.shutdown_arm { - ArmState::Disarmed => { - self.shutdown_arm = ArmState::Armed; - app.set_status("Shutdown ARMED. Press again to confirm.", false); - } - ArmState::Armed => { - self.shutdown_arm = ArmState::Confirmed; - app.log_change(Route::SysObservatory, "power", "Graceful shutdown", true); - let _ = sys::shutdown(false); - } - ArmState::Confirmed => {} + } + FIELD_SHUTDOWN => { + match app.sys_obs.shutdown_arm { + ArmState::Disarmed => { + app.sys_obs.shutdown_arm = ArmState::Armed; + app.set_status("Shutdown ARMED. Press again to confirm.", false); + } + ArmState::Armed => { + app.sys_obs.shutdown_arm = ArmState::Confirmed; + app.log_change(Route::SysObservatory, "power", "Graceful shutdown", true); + let _ = sys::shutdown(false); } + ArmState::Confirmed => {} } - FIELD_FORCE_REBOOT => { - match self.reboot_arm { - ArmState::Armed => { - app.log_change(Route::SysObservatory, "power", "Force reboot", true); - let _ = sys::reboot(true); - } - _ => { - app.set_status("Arm reboot first (Enter on Reboot)", false); - } + } + FIELD_FORCE_REBOOT => { + match app.sys_obs.reboot_arm { + ArmState::Armed => { + app.log_change(Route::SysObservatory, "power", "Force reboot", true); + let _ = sys::reboot(true); + } + _ => { + app.set_status("Arm reboot first (Enter on Reboot)", false); } } - FIELD_FORCE_SHUTDOWN => { - match self.shutdown_arm { - ArmState::Armed => { - app.log_change(Route::SysObservatory, "power", "Force shutdown", true); - let _ = sys::shutdown(true); - } - _ => { - app.set_status("Arm shutdown first (Enter on Shutdown)", false); - } + } + FIELD_FORCE_SHUTDOWN => { + match app.sys_obs.shutdown_arm { + ArmState::Armed => { + app.log_change(Route::SysObservatory, "power", "Force shutdown", true); + let _ = sys::shutdown(true); + } + _ => { + app.set_status("Arm shutdown first (Enter on Shutdown)", false); } } - _ => {} } + _ => {} } +} - pub fn handle_key(&mut self, scancode: u8, app: &mut SettingsApp) { - if scancode == 0x01 { - // Escape — disarm all - self.reboot_arm = ArmState::Disarmed; - self.shutdown_arm = ArmState::Disarmed; - app.set_status("Power actions disarmed", false); - } +pub fn handle_key(app: &mut SettingsApp, scancode: u8) { + if scancode == 0x01 { + app.sys_obs.reboot_arm = ArmState::Disarmed; + app.sys_obs.shutdown_arm = ArmState::Disarmed; + app.set_status("Power actions disarmed", false); } +} - pub fn handle_click(&mut self, _px: i32, py: i32, app: &mut SettingsApp) { - let row_h = (widgets::FONT_H + 8) as i32; - let idx = ((py - 40) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - self.activate(idx, app); - } +pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { + let row_h = (widgets::FONT_H + 8) as i32; + let idx = ((py - 40) / row_h).max(0) as usize; + if idx < FIELD_COUNT { + app.pane_focus = idx; + activate(app, idx); } } diff --git a/settings/src/layout.rs b/settings/src/layout.rs index d3f3fcb9..6bece9ea 100644 --- a/settings/src/layout.rs +++ b/settings/src/layout.rs @@ -3,7 +3,7 @@ // every pixel accounted for, every margin deliberate. use crate::state::{Route, SafetyMode, SettingsApp}; -use crate::theme::OneiricTheme; + use crate::widgets; pub const STRIP_HEIGHT: u32 = 20; @@ -216,7 +216,7 @@ pub fn draw_kv(app: &SettingsApp, x: u32, y: u32, key: &str, val: &str, val_colo } // focusable field row — highlighted when focused -pub fn draw_field_row(app: &SettingsApp, x: u32, y: u32, label: &str, value: &str, focused: bool, field_idx: usize) { +pub fn draw_field_row(app: &SettingsApp, x: u32, y: u32, label: &str, value: &str, _focused: bool, field_idx: usize) { let s = app.surface; let st = app.fb_stride; let w = app.fb_w; diff --git a/settings/src/main.rs b/settings/src/main.rs index f4c93239..077aedbf 100644 --- a/settings/src/main.rs +++ b/settings/src/main.rs @@ -11,7 +11,7 @@ mod state; mod theme; mod widgets; -use libmorpheus::{entry, hw, io, process}; +use libmorpheus::{entry, hw, io}; entry!(main); @@ -38,7 +38,6 @@ fn main() -> i32 { loop { app.tick(); - hw::fb_mark_dirty(); - process::yield_cpu(); + let _ = hw::fb_present(); } } diff --git a/settings/src/state.rs b/settings/src/state.rs index 430b17bd..3a3d131f 100644 --- a/settings/src/state.rs +++ b/settings/src/state.rs @@ -1,5 +1,5 @@ use alloc::vec::Vec; -use core::fmt; + use crate::chambers::{ archive::ArchiveChamber, gateway::GatewayChamber, hall::HallChamber, @@ -428,10 +428,10 @@ impl SettingsApp { fn apply_pending(&mut self) { let route = self.route; match route { - Route::NetObservatory => self.net_obs.apply(self), + Route::NetObservatory => crate::chambers::net_obs::apply(self), Route::MistShore => self.mist.apply(), Route::MirrorBasin => self.mirror.apply(), - Route::HallOfMasks => self.hall.apply(self), + Route::HallOfMasks => crate::chambers::hall::apply(self), _ => {} } self.pending.retain(|p| p.chamber != route); @@ -476,52 +476,39 @@ impl SettingsApp { } fn pane_activate(&mut self) { + let idx = self.pane_focus; match self.route { - Route::Gateway => { - self.gateway.activate(self.pane_focus, self); - } - Route::NetObservatory => { - self.net_obs.activate(self.pane_focus, self); - } - Route::SysObservatory => { - self.sys_obs.activate(self.pane_focus, self); - } - Route::MistShore => { - self.mist.activate(self.pane_focus, self); - } - Route::MirrorBasin => { - self.mirror.activate(self.pane_focus, self); - } - Route::Archive => { - self.archive.activate(self.pane_focus); - } - Route::HallOfMasks => { - self.hall.activate(self.pane_focus, self); - } + Route::Gateway => crate::chambers::gateway::activate(self, idx), + Route::NetObservatory => crate::chambers::net_obs::activate(self, idx), + Route::SysObservatory => crate::chambers::sys_obs::activate(self, idx), + Route::MistShore => crate::chambers::mist::activate(self, idx), + Route::MirrorBasin => crate::chambers::mirror::activate(self, idx), + Route::Archive => self.archive.activate(idx), + Route::HallOfMasks => crate::chambers::hall::activate(self, idx), } } fn chamber_key(&mut self, scancode: u8) { match self.route { - Route::Gateway => self.gateway.handle_key(scancode, self), - Route::NetObservatory => self.net_obs.handle_key(scancode, self), - Route::SysObservatory => self.sys_obs.handle_key(scancode, self), - Route::MistShore => self.mist.handle_key(scancode, self), - Route::MirrorBasin => self.mirror.handle_key(scancode, self), + Route::Gateway => crate::chambers::gateway::handle_key(self, scancode), + Route::NetObservatory => crate::chambers::net_obs::handle_key(self, scancode), + Route::SysObservatory => crate::chambers::sys_obs::handle_key(self, scancode), + Route::MistShore => crate::chambers::mist::handle_key(self, scancode), + Route::MirrorBasin => crate::chambers::mirror::handle_key(self, scancode), Route::Archive => self.archive.handle_key(scancode), - Route::HallOfMasks => self.hall.handle_key(scancode, self), + Route::HallOfMasks => crate::chambers::hall::handle_key(self, scancode), } } fn chamber_click(&mut self, pane_x: i32, pane_y: i32) { match self.route { - Route::Gateway => self.gateway.handle_click(pane_x, pane_y, self), - Route::NetObservatory => self.net_obs.handle_click(pane_x, pane_y, self), - Route::SysObservatory => self.sys_obs.handle_click(pane_x, pane_y, self), - Route::MistShore => self.mist.handle_click(pane_x, pane_y, self), - Route::MirrorBasin => self.mirror.handle_click(pane_x, pane_y, self), + Route::Gateway => crate::chambers::gateway::handle_click(self, pane_x, pane_y), + Route::NetObservatory => crate::chambers::net_obs::handle_click(self, pane_x, pane_y), + Route::SysObservatory => crate::chambers::sys_obs::handle_click(self, pane_x, pane_y), + Route::MistShore => crate::chambers::mist::handle_click(self, pane_x, pane_y), + Route::MirrorBasin => crate::chambers::mirror::handle_click(self, pane_x, pane_y), Route::Archive => self.archive.handle_click(pane_x, pane_y), - Route::HallOfMasks => self.hall.handle_click(pane_x, pane_y, self), + Route::HallOfMasks => crate::chambers::hall::handle_click(self, pane_x, pane_y), } } diff --git a/settings/src/widgets.rs b/settings/src/widgets.rs index 11aca8a9..1a197423 100644 --- a/settings/src/widgets.rs +++ b/settings/src/widgets.rs @@ -2,7 +2,7 @@ // no canvas abstraction. no trait dispatch. just pointer math and pixel writes. // this is a standalone process with its own mapped surface. -use crate::theme::OneiricTheme; + // vga 8x16 font constants pub const FONT_W: u32 = 8; From ca51dcac7bf6ef433452d6a496dec30838463f39 Mon Sep 17 00:00:00 2001 From: adrerl Date: Wed, 11 Mar 2026 12:53:35 +0100 Subject: [PATCH 06/12] ui work --- settings/src/chambers/archive.rs | 47 +++++++++++-------- settings/src/chambers/gateway.rs | 44 +++++++++--------- settings/src/chambers/hall.rs | 17 ++++--- settings/src/chambers/mirror.rs | 24 +++++----- settings/src/chambers/mist.rs | 36 ++++++++------- settings/src/chambers/net_obs.rs | 67 +++++++++++++++------------ settings/src/chambers/sys_obs.rs | 46 ++++++++++--------- settings/src/layout.rs | 77 ++++++++++++++++++++++---------- settings/src/main.rs | 16 ++++++- settings/src/state.rs | 39 ++++++++-------- 10 files changed, 246 insertions(+), 167 deletions(-) diff --git a/settings/src/chambers/archive.rs b/settings/src/chambers/archive.rs index 758226fc..3c14288d 100644 --- a/settings/src/chambers/archive.rs +++ b/settings/src/chambers/archive.rs @@ -100,22 +100,26 @@ pub fn render(app: &SettingsApp) { let px = RAIL_WIDTH + PANE_PAD; let mut cy = STRIP_HEIGHT + PANE_PAD; + let r2 = layout::row_step(app, 2); + let r4 = layout::row_step(app, 4); + let r8 = layout::row_step(app, 8); - layout::draw_section(app, px, cy, "Archive of Echoes"); - cy += widgets::FONT_H + 4; + layout::draw_section(app, px, cy, "Activity Log"); + cy += r4; // entry count let mut buf = [0u8; 8]; let n = widgets::u64_to_str(changelog_len as u64, &mut buf); let count_str = core::str::from_utf8(&buf[..n]).unwrap_or("0"); layout::draw_kv(app, px, cy, "Total entries:", count_str, t.telemetry); - cy += widgets::FONT_H + 4; + cy += r4; // search bar if arch.searching { let search_str = core::str::from_utf8(&arch.search_buf[..arch.search_len]).unwrap_or(""); - widgets::fill_rect(s, st, px, cy, 400, widgets::FONT_H + 4, t.input_bg, w, h); - widgets::rect_outline(s, st, px, cy, 400, widgets::FONT_H + 4, t.signal, w, h); + let search_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + widgets::fill_rect(s, st, px, cy, search_w, widgets::FONT_H + 4, t.input_bg, w, h); + widgets::rect_outline(s, st, px, cy, search_w, widgets::FONT_H + 4, t.signal, w, h); widgets::draw_str(s, st, px + 4, cy + 2, "Search: ", t.glyph_dim, t.input_bg, w, h); widgets::draw_str(s, st, px + 68, cy + 2, search_str, t.glyph, t.input_bg, w, h); let cursor_x = px + 68 + arch.search_len as u32 * widgets::FONT_W; @@ -123,7 +127,7 @@ pub fn render(app: &SettingsApp) { } else { widgets::draw_str(s, st, px, cy, "Press '/' or activate to search", t.glyph_dim, t.substrate, w, h); } - cy += widgets::FONT_H + 8; + cy += r8; // scroll indicators let can_up = arch.scroll_offset > 0; @@ -132,11 +136,11 @@ pub fn render(app: &SettingsApp) { let dn_color = if can_down { t.glyph } else { t.contour }; widgets::draw_str(s, st, px, cy, "^^ Up", up_color, t.substrate, w, h); widgets::draw_str(s, st, px + 80, cy, "vv Down", dn_color, t.substrate, w, h); - cy += widgets::FONT_H + 4; + cy += r4; // column header - widgets::draw_str(s, st, px, cy, "# Route Field Value", t.glyph_dim, t.substrate, w, h); - cy += widgets::FONT_H + 2; + widgets::draw_str(s, st, px, cy, "# Section Field Value", t.glyph_dim, t.substrate, w, h); + cy += r2; widgets::hline(s, st, px, cy, (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD), t.contour, w, h); cy += 2; @@ -170,30 +174,37 @@ pub fn render(app: &SettingsApp) { let ns = core::str::from_utf8(&nbuf[..nl]).unwrap_or("?"); widgets::draw_str(s, st, px + 2, cy, ns, t.glyph_dim, row_bg, w, h); + let col_route = px + 4 * widgets::FONT_W; + let col_field = px + row_w / 3; + let col_value = px + (row_w * 2) / 3; + // route let route_str = entry.chamber.label(); - widgets::draw_str(s, st, px + 32, cy, route_str, t.glyph, row_bg, w, h); + let route_chars = (col_field.saturating_sub(col_route + 2)) / widgets::FONT_W; + widgets::draw_str_trunc(s, st, col_route, cy, route_str, t.glyph, row_bg, w, h, route_chars as usize); // field - widgets::draw_str(s, st, px + 168, cy, entry.field_name, t.telemetry, row_bg, w, h); + let field_chars = (col_value.saturating_sub(col_field + 2)) / widgets::FONT_W; + widgets::draw_str_trunc(s, st, col_field, cy, entry.field_name, t.telemetry, row_bg, w, h, field_chars as usize); // value let val = core::str::from_utf8(&entry.description[..entry.desc_len]).unwrap_or("?"); let val_color = if entry.destructive { t.destructive } else { t.success }; - widgets::draw_str_trunc(s, st, px + 260, cy, val, val_color, row_bg, w, h, row_w.saturating_sub(262) as usize); + let value_chars = (row_w.saturating_sub(col_value - px + 3)) / widgets::FONT_W; + widgets::draw_str_trunc(s, st, col_value, cy, val, val_color, row_bg, w, h, value_chars as usize); // destructive marker if entry.destructive { widgets::draw_str(s, st, px + row_w - 24, cy, "!!", t.destructive, row_bg, w, h); } - cy += widgets::FONT_H + 2; + cy += r2; displayed += 1; } if displayed == 0 { widgets::draw_str(s, st, px + 4, cy, "(no entries)", t.glyph_dim, t.substrate, w, h); - cy += widgets::FONT_H + 2; + cy += r2; } // detail panel for selected entry @@ -204,18 +215,18 @@ pub fn render(app: &SettingsApp) { let entry = &app.changelog[arch.selected]; layout::draw_section(app, px, cy, "Detail"); - cy += widgets::FONT_H + 4; + cy += r4; layout::draw_kv(app, px, cy, "Chamber:", entry.chamber.label(), t.glyph); - cy += widgets::FONT_H + 2; + cy += r2; layout::draw_kv(app, px, cy, "Field:", entry.field_name, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; let val = core::str::from_utf8(&entry.description[..entry.desc_len]).unwrap_or("?"); let dc = if entry.destructive { t.destructive } else { t.success }; layout::draw_kv(app, px, cy, "Value:", val, dc); - cy += widgets::FONT_H + 2; + cy += r2; if entry.destructive { layout::draw_risk_band(app, px, cy, "This was a destructive operation."); diff --git a/settings/src/chambers/gateway.rs b/settings/src/chambers/gateway.rs index 7beb32b7..7d2922ce 100644 --- a/settings/src/chambers/gateway.rs +++ b/settings/src/chambers/gateway.rs @@ -94,7 +94,7 @@ pub fn handle_key(app: &mut SettingsApp, scancode: u8) { } pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = widgets::FONT_H + 8; + let row_h = layout::row_step(app, 8); let idx = (py as u32 / row_h) as usize; if idx >= 2 { let adjusted = idx - 2; @@ -112,33 +112,36 @@ pub fn render(app: &SettingsApp) { let px = RAIL_WIDTH + PANE_PAD; let mut cy = STRIP_HEIGHT + PANE_PAD; + let r4 = layout::row_step(app, 4); + let r8 = layout::row_step(app, 8); + let r12 = layout::row_step(app, 12); // welcome header - widgets::draw_str(s, st, px, cy, "Oneiric Gateway", t.signal, t.substrate, w, h); - cy += widgets::FONT_H + 4; + widgets::draw_str(s, st, px, cy, "General Settings", t.signal, t.substrate, w, h); + cy += r4; widgets::draw_str(s, st, px, cy, "System configuration interface", t.glyph_dim, t.substrate, w, h); - cy += widgets::FONT_H + 12; + cy += r12; // mode gate layout::draw_section(app, px, cy, "Mode"); - cy += widgets::FONT_H + 4; + cy += r4; let (mode_label, mode_color) = match app.safety { SafetyMode::Safe => ("[ ] Enter Severe Mode", t.glyph_dim), SafetyMode::Severe => ("[X] Severe Mode ACTIVE", t.destructive), }; layout::draw_button_row(app, px, cy, mode_label, 0, mode_color); - cy += widgets::FONT_H + 8; + cy += r8; // armed warning if app.severe_arm == ArmState::Armed { layout::draw_risk_band(app, px, cy, "WARNING: Severe mode unlocks destructive system controls. Press Enter to confirm."); - cy += widgets::FONT_H + 12; + cy += r12; } // chamber links - layout::draw_section(app, px, cy, "Chambers"); - cy += widgets::FONT_H + 4; + layout::draw_section(app, px, cy, "Sections"); + cy += r4; for (i, route) in Route::ALL.iter().enumerate() { let label = route.label(); @@ -149,43 +152,40 @@ pub fn render(app: &SettingsApp) { let fg = if is_focused { t.signal } else { t.glyph }; let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); - widgets::fill_rect(s, st, px, cy, row_w, widgets::FONT_H + 4, bg, w, h); + widgets::fill_rect(s, st, px, cy, row_w, r4, bg, w, h); if is_focused { - widgets::rect_outline(s, st, px, cy, row_w, widgets::FONT_H + 4, t.focus_ring, w, h); + widgets::rect_outline(s, st, px, cy, row_w, r4, t.focus_ring, w, h); } + let ty = cy + r4.saturating_sub(widgets::FONT_H) / 2; + // number key hint let mut num_buf = [0u8; 1]; num_buf[0] = b'1' + i as u8; let num_str = core::str::from_utf8(&num_buf).unwrap_or("?"); - widgets::draw_str(s, st, px + 4, cy + 2, num_str, t.glyph_dim, bg, w, h); + widgets::draw_str(s, st, px + 4, ty, num_str, t.glyph_dim, bg, w, h); // sigil - widgets::draw_str(s, st, px + 3 * widgets::FONT_W, cy + 2, sigil, t.signal, bg, w, h); + widgets::draw_str(s, st, px + 3 * widgets::FONT_W, ty, sigil, t.signal, bg, w, h); // label - widgets::draw_str(s, st, px + 5 * widgets::FONT_W, cy + 2, label, fg, bg, w, h); - - // alias - let alias = route.technical_alias(); - let alias_x = px + 25 * widgets::FONT_W; - widgets::draw_str_trunc(s, st, alias_x, cy + 2, alias, t.glyph_dim, bg, w, h, 30); + widgets::draw_str(s, st, px + 5 * widgets::FONT_W, ty, label, fg, bg, w, h); - cy += widgets::FONT_H + 4; + cy += r4; } // recent chambers if app.gateway.recent_count > 0 { cy += 8; layout::draw_section(app, px, cy, "Recent"); - cy += widgets::FONT_H + 4; + cy += r4; for i in 0..app.gateway.recent_count { let route = app.gateway.recent[i]; let field_idx = 1 + Route::ALL.len() + i; let label = route.label(); layout::draw_button_row(app, px, cy, label, field_idx, t.glyph); - cy += widgets::FONT_H + 8; + cy += r8; } } } diff --git a/settings/src/chambers/hall.rs b/settings/src/chambers/hall.rs index aa128902..14ee9c51 100644 --- a/settings/src/chambers/hall.rs +++ b/settings/src/chambers/hall.rs @@ -145,12 +145,15 @@ pub fn render(app: &SettingsApp) { let px = RAIL_WIDTH + PANE_PAD; let mut cy = STRIP_HEIGHT + PANE_PAD; + let r2 = layout::row_step(app, 2); + let r4 = layout::row_step(app, 4); + let r8 = layout::row_step(app, 8); - layout::draw_section(app, px, cy, "Hall of Masks"); - cy += widgets::FONT_H + 4; + layout::draw_section(app, px, cy, "Profiles"); + cy += r4; widgets::draw_str(s, st, px, cy, "Select a preset to preview, then Apply to commit.", t.glyph_dim, t.substrate, w, h); - cy += widgets::FONT_H + 8; + cy += r8; // preset cards for i in 0..PRESET_COUNT { @@ -197,21 +200,21 @@ pub fn render(app: &SettingsApp) { // delta preview if hall.preview_active { layout::draw_section(app, px, cy, "Delta Preview"); - cy += widgets::FONT_H + 4; + cy += r4; let p = &PRESETS[hall.selected]; let mode_str = if p.dark { "Dark" } else { "Light" }; layout::draw_kv(app, px, cy, "Mode:", mode_str, t.glyph); - cy += widgets::FONT_H + 2; + cy += r2; // show accent color value let accent = theme::pack(p.accent_r, p.accent_g, p.accent_b); widgets::fill_rect(s, st, px + 80, cy, 48, 12, accent, w, h); widgets::draw_str(s, st, px, cy, "Accent:", t.glyph, t.substrate, w, h); - cy += widgets::FONT_H + 2; + cy += r2; layout::draw_kv(app, px, cy, "Name:", p.name, t.immutable); - cy += widgets::FONT_H + 8; + cy += r8; } // apply button diff --git a/settings/src/chambers/mirror.rs b/settings/src/chambers/mirror.rs index 6c329400..7a9a53ba 100644 --- a/settings/src/chambers/mirror.rs +++ b/settings/src/chambers/mirror.rs @@ -119,7 +119,7 @@ fn rebuild_theme(app: &mut SettingsApp) { pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = (widgets::FONT_H + 8) as i32; + let row_h = layout::row_step(app, 8) as i32; let idx = ((py - 40) / row_h).max(0) as usize; if idx < FIELD_COUNT { app.pane_focus = idx; @@ -133,18 +133,22 @@ pub fn render(app: &SettingsApp) { let px = RAIL_WIDTH + PANE_PAD; let mut cy = STRIP_HEIGHT + PANE_PAD; + let r2 = layout::row_step(app, 2); + let r4 = layout::row_step(app, 4); + let r8 = layout::row_step(app, 8); + let r12 = layout::row_step(app, 12); // theme mode layout::draw_section(app, px, cy, "Theme Mode"); - cy += widgets::FONT_H + 4; + cy += r4; let mode_label = if mirror.dark_mode { "[X] Dark [ ] Light" } else { "[ ] Dark [X] Light" }; layout::draw_button_row(app, px, cy, mode_label, FIELD_THEME_TOGGLE, t.glyph); - cy += widgets::FONT_H + 8; + cy += r8; // accent color layout::draw_section(app, px, cy, "Accent Color"); - cy += widgets::FONT_H + 4; + cy += r4; let (ar, ag, ab, name) = ACCENTS[mirror.accent_idx]; let accent = crate::theme::pack(ar, ag, ab); @@ -164,17 +168,17 @@ pub fn render(app: &SettingsApp) { // selected name widgets::draw_str(app.surface, app.fb_stride, px, cy, name, accent, t.substrate, app.fb_w, app.fb_h); - cy += widgets::FONT_H + 4; + cy += r4; // nav buttons layout::draw_button_row(app, px, cy, "<< Previous Accent", FIELD_ACCENT_PREV, t.glyph); - cy += widgets::FONT_H + 4; + cy += r4; layout::draw_button_row(app, px, cy, ">> Next Accent", FIELD_ACCENT_NEXT, t.glyph); - cy += widgets::FONT_H + 12; + cy += r12; // preview section layout::draw_section(app, px, cy, "Preview"); - cy += widgets::FONT_H + 4; + cy += r4; // color swatch grid showing all theme tokens let tokens: [(&str, u32); 10] = [ @@ -193,13 +197,13 @@ pub fn render(app: &SettingsApp) { for (name, color) in &tokens { widgets::fill_rect(app.surface, app.fb_stride, px, cy, 16, 12, *color, app.fb_w, app.fb_h); widgets::draw_str(app.surface, app.fb_stride, px + 20, cy, name, t.glyph, t.substrate, app.fb_w, app.fb_h); - cy += widgets::FONT_H + 2; + cy += r2; } cy += 8; // action buttons layout::draw_button_row(app, px, cy, "Apply Appearance", FIELD_APPLY, t.signal); - cy += widgets::FONT_H + 4; + cy += r4; layout::draw_button_row(app, px, cy, "Revert", FIELD_REVERT, t.glyph_dim); } diff --git a/settings/src/chambers/mist.rs b/settings/src/chambers/mist.rs index 8a6f4da5..93629ca4 100644 --- a/settings/src/chambers/mist.rs +++ b/settings/src/chambers/mist.rs @@ -65,7 +65,7 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = (widgets::FONT_H + 8) as i32; + let row_h = layout::row_step(app, 8) as i32; let idx = ((py - 40) / row_h).max(0) as usize; if idx < FIELD_COUNT { app.pane_focus = idx; @@ -83,9 +83,13 @@ pub fn render(app: &SettingsApp) { let px = RAIL_WIDTH + PANE_PAD; let mut cy = STRIP_HEIGHT + PANE_PAD; + let r2 = layout::row_step(app, 2); + let r4 = layout::row_step(app, 4); + let r8 = layout::row_step(app, 8); + let r12 = layout::row_step(app, 12); layout::draw_section(app, px, cy, "Framebuffer"); - cy += widgets::FONT_H + 4; + cy += r4; // resolution let mut buf = [0u8; 32]; @@ -101,20 +105,20 @@ pub fn render(app: &SettingsApp) { ri += hn; let res_str = core::str::from_utf8(&res[..ri]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Resolution:", res_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; // stride let n = widgets::u64_to_str(mist.fb_stride as u64, &mut buf); let stride_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Stride (bytes):", stride_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; // stride in pixels let stride_px = mist.fb_stride / 4; let n = widgets::u64_to_str(stride_px as u64, &mut buf); let spx_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Stride (pixels):", spx_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; // pixel format let fmt_str = match mist.fb_format { @@ -125,57 +129,57 @@ pub fn render(app: &SettingsApp) { _ => "Unknown", }; layout::draw_kv(app, px, cy, "Pixel Format:", fmt_str, t.immutable); - cy += widgets::FONT_H + 2; + cy += r2; // fb size let n = widgets::format_bytes(mist.fb_size, &mut buf); let size_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "FB Size:", size_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; // base address let mut hex_buf = [0u8; 18]; let hex_len = format_hex(mist.fb_base, &mut hex_buf); let hex_str = core::str::from_utf8(&hex_buf[..hex_len]).unwrap_or("0x???"); layout::draw_kv(app, px, cy, "Base Addr:", hex_str, t.immutable); - cy += widgets::FONT_H + 8; + cy += r8; // pixel math section layout::draw_section(app, px, cy, "Pixel Math"); - cy += widgets::FONT_H + 4; + cy += r4; let bpp = 4u32; let n = widgets::u64_to_str(bpp as u64, &mut buf); let bpp_str = core::str::from_utf8(&buf[..n]).unwrap_or("4"); layout::draw_kv(app, px, cy, "Bytes/Pixel:", bpp_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; // total pixels let total_px = mist.fb_width as u64 * mist.fb_height as u64; let n = widgets::u64_to_str(total_px, &mut buf); let tpx_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Total Pixels:", tpx_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; // scanline padding let pad = mist.fb_stride.saturating_sub(mist.fb_width * bpp); let pad_label = if pad == 0 { "None" } else { "Present" }; let pad_color = if pad == 0 { t.success } else { t.warning }; layout::draw_kv(app, px, cy, "Scanline Pad:", pad_label, pad_color); - cy += widgets::FONT_H + 2; + cy += r2; if pad > 0 { let n = widgets::u64_to_str(pad as u64, &mut buf); let pad_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, " Pad Bytes:", pad_str, t.warning); - cy += widgets::FONT_H + 2; + cy += r2; } cy += 8; // packing reminder layout::draw_section(app, px, cy, "Packing Reference"); - cy += widgets::FONT_H + 4; + cy += r4; let packing = match mist.fb_format { 1 => "BGRX: b | (g<<8) | (r<<16) | (0xFF<<24)", @@ -183,9 +187,9 @@ pub fn render(app: &SettingsApp) { _ => "(non-standard format)", }; widgets::draw_str(s, st, px, cy, packing, t.immutable, t.substrate, w, h); - cy += widgets::FONT_H + 2; + cy += r2; widgets::draw_str(s, st, px, cy, "addr = base + (y * stride) + (x * 4)", t.glyph_dim, t.substrate, w, h); - cy += widgets::FONT_H + 12; + cy += r12; layout::draw_button_row(app, px, cy, "Refresh Display Info", FIELD_REFRESH, t.glyph); } diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs index e6abce36..f052c4c7 100644 --- a/settings/src/chambers/net_obs.rs +++ b/settings/src/chambers/net_obs.rs @@ -284,7 +284,7 @@ pub fn handle_key(app: &mut SettingsApp, scancode: u8) { } pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = (widgets::FONT_H + 8) as i32; + let row_h = layout::row_step(app, 8) as i32; let idx = ((py - 40) / row_h).max(0) as usize; if idx < FIELD_COUNT { app.pane_focus = idx; @@ -298,27 +298,31 @@ pub fn render(app: &SettingsApp) { let px = RAIL_WIDTH + PANE_PAD; let mut cy = STRIP_HEIGHT + PANE_PAD; + let r2 = layout::row_step(app, 2); + let r4 = layout::row_step(app, 4); + let r8 = layout::row_step(app, 8); + let r12 = layout::row_step(app, 12); // link status section layout::draw_section(app, px, cy, "Link Status"); - cy += widgets::FONT_H + 4; + cy += r4; let link_str = if net.link_up { "UP" } else { "DOWN" }; let link_color = if net.link_up { t.success } else { t.destructive }; layout::draw_kv(app, px, cy, "Link:", link_str, link_color); - cy += widgets::FONT_H + 2; + cy += r2; let mut mac_buf = [0u8; 17]; let mac_len = widgets::format_mac(&net.mac, &mut mac_buf); let mac_str = core::str::from_utf8(&mac_buf[..mac_len]).unwrap_or("??"); layout::draw_kv(app, px, cy, "MAC:", mac_str, t.immutable); - cy += widgets::FONT_H + 2; + cy += r2; let mut mtu_buf = [0u8; 8]; let mtu_len = widgets::u64_to_str(net.mtu as u64, &mut mtu_buf); let mtu_str = core::str::from_utf8(&mtu_buf[..mtu_len]).unwrap_or("?"); layout::draw_kv(app, px, cy, "MTU:", mtu_str, t.telemetry); - cy += widgets::FONT_H + 8; + cy += r8; // state let state_str = match net.state { @@ -334,97 +338,97 @@ pub fn render(app: &SettingsApp) { _ => t.warning, }; layout::draw_kv(app, px, cy, "State:", state_str, state_color); - cy += widgets::FONT_H + 8; + cy += r8; // configuration section layout::draw_section(app, px, cy, "Configuration"); - cy += widgets::FONT_H + 4; + cy += r4; // DHCP toggle let dhcp_label = if net.edit_dhcp { "[X] DHCP" } else { "[ ] Static" }; layout::draw_button_row(app, px, cy, dhcp_label, FIELD_DHCP_TOGGLE, t.glyph); - cy += widgets::FONT_H + 8; + cy += r8; // hostname let hn = core::str::from_utf8(&net.edit_hostname[..net.edit_hostname_len]).unwrap_or(""); let hn_display = if hn.is_empty() { "(none)" } else { hn }; let hn_editing = net.editing_field == Some(FIELD_HOSTNAME); draw_editable_field(app, px, cy, "Hostname:", hn_display, FIELD_HOSTNAME, hn_editing); - cy += widgets::FONT_H + 8; + cy += r8; if !net.edit_dhcp { // static ip fields let ip_str = core::str::from_utf8(&net.edit_ip[..net.edit_ip_len]).unwrap_or("0.0.0.0"); let ip_editing = net.editing_field == Some(FIELD_IP); draw_editable_field(app, px, cy, "IP Address:", ip_str, FIELD_IP, ip_editing); - cy += widgets::FONT_H + 8; + cy += r8; let mut pfx_buf = [0u8; 4]; let pfx_len = widgets::u64_to_str(net.edit_prefix as u64, &mut pfx_buf); let pfx_str = core::str::from_utf8(&pfx_buf[..pfx_len]).unwrap_or("24"); layout::draw_field_row(app, px, cy, "Prefix Len:", pfx_str, false, FIELD_PREFIX); - cy += widgets::FONT_H + 8; + cy += r8; let gw_str = core::str::from_utf8(&net.edit_gateway[..net.edit_gw_len]).unwrap_or("0.0.0.0"); let gw_editing = net.editing_field == Some(FIELD_GATEWAY); draw_editable_field(app, px, cy, "Gateway:", gw_str, FIELD_GATEWAY, gw_editing); - cy += widgets::FONT_H + 8; + cy += r8; let d1_str = core::str::from_utf8(&net.edit_dns1[..net.edit_dns1_len]).unwrap_or("0.0.0.0"); let d1_editing = net.editing_field == Some(FIELD_DNS1); draw_editable_field(app, px, cy, "DNS Primary:", d1_str, FIELD_DNS1, d1_editing); - cy += widgets::FONT_H + 8; + cy += r8; let d2_str = core::str::from_utf8(&net.edit_dns2[..net.edit_dns2_len]).unwrap_or("0.0.0.0"); let d2_editing = net.editing_field == Some(FIELD_DNS2); draw_editable_field(app, px, cy, "DNS Secondary:", d2_str, FIELD_DNS2, d2_editing); - cy += widgets::FONT_H + 8; + cy += r8; } else { // show current DHCP-assigned values as read-only let mut ip_buf = [0u8; 16]; let ip_len = widgets::format_ip(net.ip, &mut ip_buf); let ip_str = core::str::from_utf8(&ip_buf[..ip_len]).unwrap_or("0.0.0.0"); layout::draw_kv(app, px, cy, "Assigned IP:", ip_str, t.immutable); - cy += widgets::FONT_H + 4; + cy += r4; let mut gw_buf = [0u8; 16]; let gw_len = widgets::format_ip(net.gateway, &mut gw_buf); let gw_str = core::str::from_utf8(&gw_buf[..gw_len]).unwrap_or("0.0.0.0"); layout::draw_kv(app, px, cy, "Gateway:", gw_str, t.immutable); - cy += widgets::FONT_H + 4; + cy += r4; let mut d1_buf = [0u8; 16]; let d1_len = widgets::format_ip(net.dns1, &mut d1_buf); let d1_str = core::str::from_utf8(&d1_buf[..d1_len]).unwrap_or("0.0.0.0"); layout::draw_kv(app, px, cy, "DNS:", d1_str, t.immutable); - cy += widgets::FONT_H + 8; + cy += r8; } // action buttons layout::draw_button_row(app, px, cy, "Apply Network Config", FIELD_APPLY, t.signal); - cy += widgets::FONT_H + 8; + cy += r8; layout::draw_button_row(app, px, cy, "Refresh", FIELD_REFRESH, t.glyph); - cy += widgets::FONT_H + 12; + cy += r12; // traffic stats section layout::draw_section(app, px, cy, "Traffic"); - cy += widgets::FONT_H + 4; + cy += r4; let mut buf = [0u8; 32]; let n = widgets::u64_to_str(net.tx_packets, &mut buf); let tx_p = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "TX Packets:", tx_p, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; let n = widgets::u64_to_str(net.rx_packets, &mut buf); let rx_p = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "RX Packets:", rx_p, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; let n = widgets::format_bytes(net.tx_bytes, &mut buf); let tx_b = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "TX Bytes:", tx_b, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; let n = widgets::format_bytes(net.rx_bytes, &mut buf); let rx_b = core::str::from_utf8(&buf[..n]).unwrap_or("?"); @@ -441,7 +445,8 @@ fn draw_editable_field(app: &SettingsApp, x: u32, y: u32, label: &str, value: &s let is_focused = !app.focus_in_rail && app.pane_focus == field_idx; let bg = if editing { t.input_bg } else if is_focused { t.surface } else { t.substrate }; let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); - widgets::fill_rect(s, st, x, y, row_w, widgets::FONT_H + 4, bg, w, h); + let row_h = layout::row_step(app, 4); + widgets::fill_rect(s, st, x, y, row_w, row_h, bg, w, h); let border_color = if editing { t.signal @@ -450,16 +455,20 @@ fn draw_editable_field(app: &SettingsApp, x: u32, y: u32, label: &str, value: &s } else { t.contour }; - widgets::rect_outline(s, st, x, y, row_w, widgets::FONT_H + 4, border_color, w, h); + widgets::rect_outline(s, st, x, y, row_w, row_h, border_color, w, h); - widgets::draw_str(s, st, x + 4, y + 2, label, t.glyph_dim, bg, w, h); - let vx = x + 20 * widgets::FONT_W; - widgets::draw_str(s, st, vx, y + 2, value, t.glyph, bg, w, h); + let ty = y + row_h.saturating_sub(widgets::FONT_H) / 2; + let label_w = (row_w / 3).clamp(12 * widgets::FONT_W, 22 * widgets::FONT_W); + let label_chars = label_w.saturating_sub(8) / widgets::FONT_W; + widgets::draw_str_trunc(s, st, x + 4, ty, label, t.glyph_dim, bg, w, h, label_chars as usize); + let vx = x + label_w; + let value_chars = row_w.saturating_sub(label_w + 6) / widgets::FONT_W; + widgets::draw_str_trunc(s, st, vx, ty, value, t.glyph, bg, w, h, value_chars as usize); // cursor if editing { let cursor_x = vx + value.len() as u32 * widgets::FONT_W; - widgets::fill_rect(s, st, cursor_x, y + 2, 2, widgets::FONT_H, t.focus_ring, w, h); + widgets::fill_rect(s, st, cursor_x, ty, 2, widgets::FONT_H, t.focus_ring, w, h); } } diff --git a/settings/src/chambers/sys_obs.rs b/settings/src/chambers/sys_obs.rs index bd91af4c..e11209c4 100644 --- a/settings/src/chambers/sys_obs.rs +++ b/settings/src/chambers/sys_obs.rs @@ -138,7 +138,7 @@ pub fn handle_key(app: &mut SettingsApp, scancode: u8) { } pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = (widgets::FONT_H + 8) as i32; + let row_h = layout::row_step(app, 8) as i32; let idx = ((py - 40) / row_h).max(0) as usize; if idx < FIELD_COUNT { app.pane_focus = idx; @@ -156,26 +156,30 @@ pub fn render(app: &SettingsApp) { let px = RAIL_WIDTH + PANE_PAD; let mut cy = STRIP_HEIGHT + PANE_PAD; + let r2 = layout::row_step(app, 2); + let r4 = layout::row_step(app, 4); + let r8 = layout::row_step(app, 8); + let r12 = layout::row_step(app, 12); // memory section layout::draw_section(app, px, cy, "Memory"); - cy += widgets::FONT_H + 4; + cy += r4; let mut buf = [0u8; 32]; let n = widgets::format_bytes(sys.total_mem, &mut buf); let total_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Total:", total_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; let n = widgets::format_bytes(sys.used_mem, &mut buf); let used_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Used:", used_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; let n = widgets::format_bytes(sys.free_mem, &mut buf); let free_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Free:", free_str, t.success); - cy += widgets::FONT_H + 4; + cy += r4; // memory usage bar let bar_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); @@ -186,51 +190,51 @@ pub fn render(app: &SettingsApp) { }; let bar_color = if pct > 90 { t.destructive } else if pct > 70 { t.warning } else { t.signal }; widgets::draw_bar(s, st, px, cy, bar_w, 10, pct, 100, bar_color, t.substrate, t.contour, w, h); - cy += 14; + cy += (r4 / 2).max(10); // heap section layout::draw_section(app, px, cy, "Heap"); - cy += widgets::FONT_H + 4; + cy += r4; let n = widgets::format_bytes(sys.heap_used, &mut buf); let hu_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Used:", hu_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; let n = widgets::format_bytes(sys.heap_total, &mut buf); let ht_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Total:", ht_str, t.telemetry); - cy += widgets::FONT_H + 8; + cy += r8; // cpu section layout::draw_section(app, px, cy, "CPU"); - cy += widgets::FONT_H + 4; + cy += r4; let n = widgets::u64_to_str(sys.cpu_count as u64, &mut buf); let cpu_str = core::str::from_utf8(&buf[..n]).unwrap_or("1"); layout::draw_kv(app, px, cy, "Cores:", cpu_str, t.telemetry); - cy += widgets::FONT_H + 2; + cy += r2; let n = widgets::u64_to_str(sys.idle_pct as u64, &mut buf); let idle_str = core::str::from_utf8(&buf[..n]).unwrap_or("0"); layout::draw_kv(app, px, cy, "Idle %:", idle_str, t.telemetry); - cy += widgets::FONT_H + 8; + cy += r8; // uptime layout::draw_section(app, px, cy, "Uptime"); - cy += widgets::FONT_H + 4; + cy += r4; let n = widgets::format_uptime(sys.uptime_secs, &mut buf); let up_str = core::str::from_utf8(&buf[..n]).unwrap_or("?"); layout::draw_kv(app, px, cy, "Since boot:", up_str, t.immutable); - cy += widgets::FONT_H + 12; + cy += r12; // power controls layout::draw_section(app, px, cy, "Power Controls"); - cy += widgets::FONT_H + 4; + cy += r4; layout::draw_button_row(app, px, cy, "Refresh Telemetry", FIELD_REFRESH, t.glyph); - cy += widgets::FONT_H + 8; + cy += r8; // reboot let rb_label = match sys.reboot_arm { @@ -244,13 +248,13 @@ pub fn render(app: &SettingsApp) { ArmState::Confirmed => t.destructive, }; layout::draw_button_row(app, px, cy, rb_label, FIELD_REBOOT, rb_color); - cy += widgets::FONT_H + 4; + cy += r4; if matches!(sys.reboot_arm, ArmState::Armed) { layout::draw_risk_band(app, px, cy, "System will restart. Unsaved work is lost."); - cy += widgets::FONT_H + 4; + cy += r4; layout::draw_button_row(app, px, cy, "Force Reboot (skip cleanup)", FIELD_FORCE_REBOOT, t.destructive); - cy += widgets::FONT_H + 4; + cy += r4; } // shutdown @@ -265,11 +269,11 @@ pub fn render(app: &SettingsApp) { ArmState::Confirmed => t.destructive, }; layout::draw_button_row(app, px, cy, sd_label, FIELD_SHUTDOWN, sd_color); - cy += widgets::FONT_H + 4; + cy += r4; if matches!(sys.shutdown_arm, ArmState::Armed) { layout::draw_risk_band(app, px, cy, "System will power off. No recovery without physical restart."); - cy += widgets::FONT_H + 4; + cy += r4; layout::draw_button_row(app, px, cy, "Force Shutdown (immediate)", FIELD_FORCE_SHUTDOWN, t.destructive); } } diff --git a/settings/src/layout.rs b/settings/src/layout.rs index 6bece9ea..135b44ff 100644 --- a/settings/src/layout.rs +++ b/settings/src/layout.rs @@ -6,13 +6,23 @@ use crate::state::{Route, SafetyMode, SettingsApp}; use crate::widgets; -pub const STRIP_HEIGHT: u32 = 20; -pub const BAR_HEIGHT: u32 = 20; +pub const STRIP_HEIGHT: u32 = 24; +pub const BAR_HEIGHT: u32 = 24; pub const RAIL_WIDTH: u32 = 160; -pub const RAIL_ITEM_HEIGHT: u32 = 20; +pub const RAIL_ITEM_HEIGHT: u32 = 24; pub const SECTION_PAD: u32 = 8; pub const PANE_PAD: u32 = 12; +#[inline(always)] +pub fn row_step(app: &SettingsApp, extra: u32) -> u32 { + let pane_h = app + .fb_h + .saturating_sub(STRIP_HEIGHT + BAR_HEIGHT + 2 * PANE_PAD); + // grow spacing on taller panes so content doesn't collapse into the top third + let bonus = (pane_h / 120).saturating_sub(4).min(8); + widgets::FONT_H + extra + bonus +} + pub fn render_frame(app: &mut SettingsApp) { let s = app.surface; let st = app.fb_stride; @@ -46,14 +56,10 @@ fn render_strip(app: &SettingsApp) { widgets::fill_rect(s, st, 0, 0, w, STRIP_HEIGHT, t.strip_bg, w, h); widgets::hline(s, st, 0, STRIP_HEIGHT - 1, w, t.contour, w, h); - // chamber title + widgets::draw_str(s, st, 4, 4, "Settings", t.glyph_dim, t.strip_bg, w, h); let label = app.route.label(); - widgets::draw_str(s, st, 4, 2, label, t.glyph, t.strip_bg, w, h); - - // technical alias - let alias = app.route.technical_alias(); - let alias_x = 4 + (label.len() as u32 + 2) * widgets::FONT_W; - widgets::draw_str(s, st, alias_x, 2, alias, t.glyph_dim, t.strip_bg, w, h); + let title_x = 4 + 10 * widgets::FONT_W; + widgets::draw_str(s, st, title_x, 4, label, t.glyph, t.strip_bg, w, h); // mode badge let (badge, badge_color) = match app.safety { @@ -106,19 +112,19 @@ fn render_rail(app: &SettingsApp) { let label = route.label(); let fg = if is_current { t.glyph } else { t.glyph_dim }; widgets::draw_str(s, st, 4, y + 2, sigil, t.signal, bg, w, h); - widgets::draw_str_trunc(s, st, 4 + 2 * widgets::FONT_W, y + 2, label, fg, bg, w, h, 16); + widgets::draw_str_trunc(s, st, 4 + 2 * widgets::FONT_W, y + 4, label, fg, bg, w, h, 14); // keyboard hint (1-7) let mut hint = [0u8; 1]; hint[0] = b'1' + i as u8; let hint_str = core::str::from_utf8(&hint).unwrap_or("?"); let hint_x = RAIL_WIDTH - 3 * widgets::FONT_W; - widgets::draw_str(s, st, hint_x, y + 2, hint_str, t.glyph_dim, bg, w, h); + widgets::draw_str(s, st, hint_x, y + 4, hint_str, t.glyph_dim, bg, w, h); // pending dot for this chamber if app.has_pending_for(*route) { let dot_x = RAIL_WIDTH - 5 * widgets::FONT_W; - widgets::draw_str(s, st, dot_x, y + 2, "*", t.warning, bg, w, h); + widgets::draw_str(s, st, dot_x, y + 4, "*", t.warning, bg, w, h); } } } @@ -212,7 +218,9 @@ pub fn draw_kv(app: &SettingsApp, x: u32, y: u32, key: &str, val: &str, val_colo widgets::draw_str(s, st, x, y, key, t.glyph_dim, t.substrate, w, h); let vx = x + (key.len() as u32 + 1) * widgets::FONT_W; - widgets::draw_str(s, st, vx, y, val, val_color, t.substrate, w, h); + let pane_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + let max_chars = pane_w.saturating_sub(vx.saturating_sub(x)) / widgets::FONT_W; + widgets::draw_str_trunc(s, st, vx, y, val, val_color, t.substrate, w, h, max_chars as usize); } // focusable field row — highlighted when focused @@ -227,15 +235,21 @@ pub fn draw_field_row(app: &SettingsApp, x: u32, y: u32, label: &str, value: &st let bg = if is_focused { t.surface } else { t.substrate }; let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); - widgets::fill_rect(s, st, x, y, row_w, widgets::FONT_H + 4, bg, w, h); + let row_h = row_step(app, 4); + widgets::fill_rect(s, st, x, y, row_w, row_h, bg, w, h); if is_focused { - widgets::rect_outline(s, st, x, y, row_w, widgets::FONT_H + 4, t.focus_ring, w, h); + widgets::rect_outline(s, st, x, y, row_w, row_h, t.focus_ring, w, h); } - widgets::draw_str(s, st, x + 4, y + 2, label, t.glyph_dim, bg, w, h); - let vx = x + 20 * widgets::FONT_W; - widgets::draw_str(s, st, vx, y + 2, value, t.glyph, bg, w, h); + let ty = y + row_h.saturating_sub(widgets::FONT_H) / 2; + + let label_w = (row_w / 3).clamp(12 * widgets::FONT_W, 22 * widgets::FONT_W); + let label_chars = label_w.saturating_sub(8) / widgets::FONT_W; + widgets::draw_str_trunc(s, st, x + 4, ty, label, t.glyph_dim, bg, w, h, label_chars as usize); + let vx = x + label_w; + let value_chars = row_w.saturating_sub(label_w + 6) / widgets::FONT_W; + widgets::draw_str_trunc(s, st, vx, ty, value, t.glyph, bg, w, h, value_chars as usize); } // button row — rendered as a text button @@ -249,12 +263,14 @@ pub fn draw_button_row(app: &SettingsApp, x: u32, y: u32, label: &str, field_idx let is_focused = !app.focus_in_rail && app.pane_focus == field_idx; let bg = if is_focused { t.surface } else { t.substrate }; - let btn_w = (label.len() as u32 + 4) * widgets::FONT_W; - let btn_h = widgets::FONT_H + 4; + let btn_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + let btn_h = row_step(app, 4); widgets::fill_rect(s, st, x, y, btn_w, btn_h, bg, w, h); widgets::rect_outline(s, st, x, y, btn_w, btn_h, if is_focused { t.focus_ring } else { t.contour }, w, h); - widgets::draw_str(s, st, x + 2 * widgets::FONT_W, y + 2, label, color, bg, w, h); + let label_chars = btn_w.saturating_sub(4 * widgets::FONT_W) / widgets::FONT_W; + let ty = y + btn_h.saturating_sub(widgets::FONT_H) / 2; + widgets::draw_str_trunc(s, st, x + 2 * widgets::FONT_W, ty, label, color, bg, w, h, label_chars as usize); } // risk band — a warning banner for destructive context @@ -266,6 +282,19 @@ pub fn draw_risk_band(app: &SettingsApp, x: u32, y: u32, msg: &str) { let t = &app.theme; let band_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); - widgets::fill_rect(s, st, x, y, band_w, widgets::FONT_H + 8, t.destructive, w, h); - widgets::draw_str(s, st, x + 4, y + 4, msg, t.substrate, t.destructive, w, h); + let band_h = row_step(app, 8); + widgets::fill_rect(s, st, x, y, band_w, band_h, t.destructive, w, h); + let ty = y + band_h.saturating_sub(widgets::FONT_H) / 2; + widgets::draw_str_trunc( + s, + st, + x + 4, + ty, + msg, + t.substrate, + t.destructive, + w, + h, + band_w.saturating_sub(8) as usize / widgets::FONT_W as usize, + ); } diff --git a/settings/src/main.rs b/settings/src/main.rs index 077aedbf..2b1247c8 100644 --- a/settings/src/main.rs +++ b/settings/src/main.rs @@ -4,6 +4,8 @@ extern crate alloc; +use alloc::vec; + mod chambers; mod font; mod layout; @@ -20,14 +22,17 @@ fn main() -> i32 { let fb_info = hw::fb_info().expect("settings: fb_info failed"); let surface_vaddr = hw::fb_map().expect("settings: fb_map failed"); - let surface_ptr = surface_vaddr as *mut u32; + let mapped_surface_ptr = surface_vaddr as *mut u32; // stride is bytes not pixels. yes again. let fb_stride_px = fb_info.stride / 4; let is_bgrx = fb_info.format == 1; + // render to private software buffer, then publish one memcpy per frame. + let mut backbuf = vec![0u32; (fb_stride_px as usize).saturating_mul(fb_info.height as usize)]; + let mut app = state::SettingsApp::new( - surface_ptr, + backbuf.as_mut_ptr(), fb_info.width, fb_info.height, fb_stride_px, @@ -38,6 +43,13 @@ fn main() -> i32 { loop { app.tick(); + unsafe { + core::ptr::copy_nonoverlapping( + backbuf.as_ptr(), + mapped_surface_ptr, + (fb_stride_px as usize).saturating_mul(fb_info.height as usize), + ); + } let _ = hw::fb_present(); } } diff --git a/settings/src/state.rs b/settings/src/state.rs index 3a3d131f..bc2f19b3 100644 --- a/settings/src/state.rs +++ b/settings/src/state.rs @@ -35,25 +35,25 @@ impl Route { pub fn label(self) -> &'static str { match self { - Route::Gateway => "Gateway", - Route::MistShore => "Mist Shore", - Route::MirrorBasin => "Mirror Basin", - Route::NetObservatory => "Observatory:Net", - Route::SysObservatory => "Observatory:Sys", - Route::Archive => "Archive", - Route::HallOfMasks => "Hall of Masks", + Route::Gateway => "General", + Route::MistShore => "Display", + Route::MirrorBasin => "Appearance", + Route::NetObservatory => "Network", + Route::SysObservatory => "System", + Route::Archive => "Activity", + Route::HallOfMasks => "Profiles", } } pub fn sigil(self) -> &'static str { match self { - Route::Gateway => ">", - Route::MistShore => "~", - Route::MirrorBasin => "o", - Route::NetObservatory => "*", - Route::SysObservatory => "#", - Route::Archive => "=", - Route::HallOfMasks => "%", + Route::Gateway => "G", + Route::MistShore => "D", + Route::MirrorBasin => "A", + Route::NetObservatory => "N", + Route::SysObservatory => "S", + Route::Archive => "L", + Route::HallOfMasks => "P", } } @@ -248,10 +248,13 @@ impl SettingsApp { fn poll_input(&mut self) { // keyboard - let mut buf = [0u8; 1]; - let n = libmorpheus::io::read_stdin(&mut buf); - if n > 0 { - self.handle_key(buf[0]); + let avail = libmorpheus::io::stdin_available(); + if avail > 0 { + let mut buf = [0u8; 1]; + let n = libmorpheus::io::read_stdin(&mut buf); + if n > 0 { + self.handle_key(buf[0]); + } } // mouse From 14134d9a054787fa6c21a44cdc79461ab54f229f Mon Sep 17 00:00:00 2001 From: adrerl Date: Thu, 12 Mar 2026 08:41:56 +0100 Subject: [PATCH 07/12] Enhance input handling and hitbox management in SettingsApp; improve string drawing with truncation support --- settings/src/chambers/net_obs.rs | 3 + settings/src/state.rs | 194 +++++++++++++++++++++++++++---- settings/src/widgets.rs | 30 ++++- 3 files changed, 200 insertions(+), 27 deletions(-) diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs index f052c4c7..03a6d519 100644 --- a/settings/src/chambers/net_obs.rs +++ b/settings/src/chambers/net_obs.rs @@ -508,6 +508,9 @@ fn parse_ip(buf: &[u8]) -> u32 { } pub fn scancode_to_char(sc: u8) -> Option { + if sc.is_ascii_graphic() || sc == b' ' { + return Some(sc.to_ascii_lowercase()); + } match sc { 0x02..=0x0A => Some(b'1' + (sc - 0x02)), 0x0B => Some(b'0'), diff --git a/settings/src/state.rs b/settings/src/state.rs index bc2f19b3..efa1cc27 100644 --- a/settings/src/state.rs +++ b/settings/src/state.rs @@ -8,6 +8,7 @@ use crate::chambers::{ }; use crate::layout; use crate::theme::OneiricTheme; +use crate::widgets; // route ids — flat enum, zero-cost dispatch #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -119,6 +120,15 @@ pub struct ChangeEntry { pub destructive: bool, } +#[derive(Clone, Copy)] +pub struct Hitbox { + pub x: i32, + pub y: i32, + pub w: i32, + pub h: i32, + pub widget_idx: usize, +} + // the master state machine pub struct SettingsApp { // display surface @@ -166,6 +176,9 @@ pub struct SettingsApp { // tick counter for animations pub tick_count: u64, + + pub hitboxes: [Hitbox; 128], + pub hitbox_count: usize, } impl SettingsApp { @@ -214,6 +227,15 @@ impl SettingsApp { hall: HallChamber::new(), tick_count: 0, + + hitboxes: [Hitbox { + x: 0, + y: 0, + w: 0, + h: 0, + widget_idx: 0, + }; 128], + hitbox_count: 0, } } @@ -222,6 +244,7 @@ impl SettingsApp { self.net_obs.refresh(); self.sys_obs.refresh(); self.mist.refresh(); + self.set_status("Tab switch focus, W/S move, Enter activate, 1-7 jump sections", false); } pub fn tick(&mut self) { @@ -273,16 +296,23 @@ impl SettingsApp { } } - fn handle_key(&mut self, scancode: u8) { + fn handle_key(&mut self, key: u8) { self.frame_dirty = true; - match scancode { + if matches!(key, b'1'..=b'7') { + let idx = (key - b'1') as usize; + self.rail_focus = idx; + self.navigate(Route::from_index(idx)); + return; + } + + match key { // Tab = toggle rail/pane focus - 0x0F => { + 0x0F | b'\t' => { self.focus_in_rail = !self.focus_in_rail; } - // Up arrow - 0x48 => { + // up navigation + b'k' | b'K' | b'w' | b'W' => { if self.focus_in_rail { if self.rail_focus > 0 { self.rail_focus -= 1; @@ -291,8 +321,8 @@ impl SettingsApp { self.pane_focus_up(); } } - // Down arrow - 0x50 => { + // down navigation + b'j' | b'J' | b's' | b'S' => { if self.focus_in_rail { if self.rail_focus < Route::ALL.len() - 1 { self.rail_focus += 1; @@ -302,7 +332,7 @@ impl SettingsApp { } } // Enter = navigate to rail selection or activate pane widget - 0x1C => { + 0x1C | b'\r' | b'\n' => { if self.focus_in_rail { self.navigate(Route::from_index(self.rail_focus)); } else { @@ -310,7 +340,7 @@ impl SettingsApp { } } // Escape = back to gateway or disarm - 0x01 => { + 0x01 | 0x1B => { if self.severe_arm == ArmState::Armed { self.severe_arm = ArmState::Disarmed; self.set_status("Disarmed", false); @@ -322,29 +352,35 @@ impl SettingsApp { } } } - // Left arrow = focus rail - 0x4B => { + // focus rail + b'h' | b'H' => { self.focus_in_rail = true; } - // Right arrow = focus pane - 0x4D => { + // focus pane + b'l' | b'L' => { self.focus_in_rail = false; } // 'a' key (apply staged) — scancode 0x1E - 0x1E => { + 0x1E | b'a' | b'A' => { if !self.focus_in_rail { self.apply_pending(); } } // 'r' key (revert) — scancode 0x13 - 0x13 => { + 0x13 | b'r' | b'R' => { if !self.focus_in_rail { self.revert_pending(); } } + // 'd' key (defaults) + 0x20 | b'd' | b'D' => { + if !self.focus_in_rail { + self.restore_defaults(); + } + } // forward to active chamber for chamber-specific keys _ => { - self.chamber_key(scancode); + self.chamber_key(key); } } } @@ -466,12 +502,14 @@ impl SettingsApp { } fn pane_focus_up(&mut self) { + self.clamp_pane_focus(); if self.pane_focus > 0 { self.pane_focus -= 1; } } fn pane_focus_down(&mut self) { + self.clamp_pane_focus(); let max = self.pane_widget_count(); if self.pane_focus + 1 < max { self.pane_focus += 1; @@ -479,6 +517,7 @@ impl SettingsApp { } fn pane_activate(&mut self) { + self.clamp_pane_focus(); let idx = self.pane_focus; match self.route { Route::Gateway => crate::chambers::gateway::activate(self, idx), @@ -504,14 +543,34 @@ impl SettingsApp { } fn chamber_click(&mut self, pane_x: i32, pane_y: i32) { - match self.route { - Route::Gateway => crate::chambers::gateway::handle_click(self, pane_x, pane_y), - Route::NetObservatory => crate::chambers::net_obs::handle_click(self, pane_x, pane_y), - Route::SysObservatory => crate::chambers::sys_obs::handle_click(self, pane_x, pane_y), - Route::MistShore => crate::chambers::mist::handle_click(self, pane_x, pane_y), - Route::MirrorBasin => crate::chambers::mirror::handle_click(self, pane_x, pane_y), - Route::Archive => self.archive.handle_click(pane_x, pane_y), - Route::HallOfMasks => crate::chambers::hall::handle_click(self, pane_x, pane_y), + let abs_x = pane_x + layout::RAIL_WIDTH as i32; + let abs_y = pane_y + layout::STRIP_HEIGHT as i32; + if let Some(idx) = self.hitbox_at(abs_x, abs_y) { + self.pane_focus = idx; + self.pane_activate(); + return; + } + + // fallback for clicks outside registered controls: pick nearest slot. + let count = self.pane_widget_count(); + if count > 0 { + let pane_h = self + .fb_h + .saturating_sub(layout::STRIP_HEIGHT + layout::BAR_HEIGHT + 2 * layout::PANE_PAD) + .max(1); + let y = pane_y.max(0) as u32; + let idx = ((y as u64 * count as u64) / pane_h as u64) as usize; + self.pane_focus = idx.min(count - 1); + self.pane_activate(); + } + } + + fn clamp_pane_focus(&mut self) { + let count = self.pane_widget_count(); + if count == 0 { + self.pane_focus = 0; + } else if self.pane_focus >= count { + self.pane_focus = count - 1; } } @@ -565,5 +624,92 @@ impl SettingsApp { fn render(&mut self) { layout::render_frame(self); + self.rebuild_hitboxes(); + } + + fn clear_hitboxes(&mut self) { + self.hitbox_count = 0; + } + + fn push_hitbox(&mut self, x: i32, y: i32, w: i32, h: i32, widget_idx: usize) { + if self.hitbox_count >= self.hitboxes.len() || w <= 0 || h <= 0 { + return; + } + self.hitboxes[self.hitbox_count] = Hitbox { + x, + y, + w, + h, + widget_idx, + }; + self.hitbox_count += 1; + } + + fn rebuild_hitboxes(&mut self) { + self.clear_hitboxes(); + + let count = self.pane_widget_count(); + if count == 0 { + return; + } + + let px = layout::RAIL_WIDTH as i32 + layout::PANE_PAD as i32; + let pw = (self.fb_w.saturating_sub(layout::RAIL_WIDTH + 2 * layout::PANE_PAD)) as i32; + if pw <= 0 { + return; + } + + match self.route { + Route::Gateway => { + let row_h = layout::row_step(self, 8) as i32; + let base_y = layout::STRIP_HEIGHT as i32 + 2 * row_h; + for i in 0..count { + let y = base_y + i as i32 * row_h; + self.push_hitbox(px, y, pw, row_h.max(14), i); + } + } + Route::NetObservatory | Route::SysObservatory | Route::MistShore | Route::MirrorBasin => { + let row_h = layout::row_step(self, 8) as i32; + let base_y = layout::STRIP_HEIGHT as i32 + 40; + for i in 0..count { + let y = base_y + i as i32 * row_h; + self.push_hitbox(px, y, pw, row_h.max(14), i); + } + } + Route::HallOfMasks => { + let row_h = (widgets::FONT_H + 12) as i32; + let base_y = layout::STRIP_HEIGHT as i32 + 60; + for i in 0..count { + let y = base_y + i as i32 * row_h; + self.push_hitbox(px, y, pw, row_h.max(14), i); + } + } + Route::Archive => { + // row picks in the log table (index 3 + visible row). + let row_h = (widgets::FONT_H + 4) as i32; + let table_y = layout::STRIP_HEIGHT as i32 + 60; + let half = (pw / 2).max(20); + self.push_hitbox(px, table_y - row_h, half, row_h, 0); + self.push_hitbox(px + half, table_y - row_h, pw - half, row_h, 1); + self.push_hitbox(px, table_y - 2 * row_h, pw, row_h, 2); + + let rows = count.saturating_sub(3); + for i in 0..rows { + let y = table_y + i as i32 * row_h; + self.push_hitbox(px, y, pw, row_h.max(14), i + 3); + } + } + } + } + + fn hitbox_at(&self, x: i32, y: i32) -> Option { + // last one wins in case of overlap + for i in (0..self.hitbox_count).rev() { + let hb = self.hitboxes[i]; + if x >= hb.x && y >= hb.y && x < hb.x + hb.w && y < hb.y + hb.h { + return Some(hb.widget_idx); + } + } + None } } diff --git a/settings/src/widgets.rs b/settings/src/widgets.rs index 1a197423..47bb1ea5 100644 --- a/settings/src/widgets.rs +++ b/settings/src/widgets.rs @@ -79,12 +79,36 @@ pub fn draw_str(surface: *mut u32, stride: u32, x: u32, y: u32, s: &str, fg: u32 // draw string with max width (truncates with ".." if too long) pub fn draw_str_trunc(surface: *mut u32, stride: u32, x: u32, y: u32, s: &str, fg: u32, bg: u32, fb_w: u32, fb_h: u32, max_chars: usize) { - if s.len() <= max_chars { + if max_chars == 0 { + return; + } + let total_chars = s.chars().count(); + if total_chars <= max_chars { draw_str(surface, stride, x, y, s, fg, bg, fb_w, fb_h); } else { - let trunc = &s[..max_chars.saturating_sub(2)]; + if max_chars <= 2 { + draw_str(surface, stride, x, y, "..", fg, bg, fb_w, fb_h); + return; + } + + let keep = max_chars - 2; + let mut end = 0usize; + for (i, (bi, _)) in s.char_indices().enumerate() { + if i == keep { + break; + } + end = bi; + } + // include last kept character boundary + if keep > 0 { + if let Some((bi, ch)) = s.char_indices().nth(keep - 1) { + end = bi + ch.len_utf8(); + } + } + + let trunc = &s[..end.min(s.len())]; draw_str(surface, stride, x, y, trunc, fg, bg, fb_w, fb_h); - let cx = x + (max_chars as u32 - 2) * FONT_W; + let cx = x + (keep as u32) * FONT_W; draw_str(surface, stride, cx, y, "..", fg, bg, fb_w, fb_h); } } From 9d83d7d2be8ad0b07b4948e732fc3965b17ae996 Mon Sep 17 00:00:00 2001 From: "T. Andrew Davis" Date: Thu, 12 Mar 2026 09:27:44 +0100 Subject: [PATCH 08/12] Refactor hitbox management in SettingsApp; remove unused click handlers and enhance hitbox registration --- settings/src/chambers/archive.rs | 14 ++-- settings/src/chambers/gateway.rs | 11 +--- settings/src/chambers/hall.rs | 11 +--- settings/src/chambers/mirror.rs | 9 --- settings/src/chambers/mist.rs | 9 --- settings/src/chambers/net_obs.rs | 10 +-- settings/src/chambers/sys_obs.rs | 9 --- settings/src/layout.rs | 2 + settings/src/state.rs | 109 +++++++------------------------ 9 files changed, 33 insertions(+), 151 deletions(-) diff --git a/settings/src/chambers/archive.rs b/settings/src/chambers/archive.rs index 3c14288d..8e9018c4 100644 --- a/settings/src/chambers/archive.rs +++ b/settings/src/chambers/archive.rs @@ -78,15 +78,6 @@ impl ArchiveChamber { } } } - - pub fn handle_click(&mut self, _px: i32, py: i32) { - let row_h = (widgets::FONT_H + 4) as i32; - let header = 60i32; - let idx = ((py - header) / row_h).max(0) as usize; - if idx < VISIBLE_ENTRIES { - self.selected = idx + self.scroll_offset; - } - } } pub fn render(app: &SettingsApp) { @@ -118,6 +109,7 @@ pub fn render(app: &SettingsApp) { if arch.searching { let search_str = core::str::from_utf8(&arch.search_buf[..arch.search_len]).unwrap_or(""); let search_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + app.register_widget_hitbox(px, cy, search_w, widgets::FONT_H + 4, 2); widgets::fill_rect(s, st, px, cy, search_w, widgets::FONT_H + 4, t.input_bg, w, h); widgets::rect_outline(s, st, px, cy, search_w, widgets::FONT_H + 4, t.signal, w, h); widgets::draw_str(s, st, px + 4, cy + 2, "Search: ", t.glyph_dim, t.input_bg, w, h); @@ -125,6 +117,7 @@ pub fn render(app: &SettingsApp) { let cursor_x = px + 68 + arch.search_len as u32 * widgets::FONT_W; widgets::fill_rect(s, st, cursor_x, cy + 2, 2, widgets::FONT_H, t.focus_ring, w, h); } else { + app.register_widget_hitbox(px, cy, (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD), widgets::FONT_H + 4, 2); widgets::draw_str(s, st, px, cy, "Press '/' or activate to search", t.glyph_dim, t.substrate, w, h); } cy += r8; @@ -134,6 +127,8 @@ pub fn render(app: &SettingsApp) { let can_down = changelog_len > arch.scroll_offset + VISIBLE_ENTRIES; let up_color = if can_up { t.glyph } else { t.contour }; let dn_color = if can_down { t.glyph } else { t.contour }; + app.register_widget_hitbox(px, cy, 72, widgets::FONT_H, 0); + app.register_widget_hitbox(px + 80, cy, 80, widgets::FONT_H, 1); widgets::draw_str(s, st, px, cy, "^^ Up", up_color, t.substrate, w, h); widgets::draw_str(s, st, px + 80, cy, "vv Down", dn_color, t.substrate, w, h); cy += r4; @@ -166,6 +161,7 @@ pub fn render(app: &SettingsApp) { let row_bg = if is_selected { t.surface } else { t.substrate }; let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + app.register_widget_hitbox(px, cy, row_w, widgets::FONT_H + 2, 3 + displayed); widgets::fill_rect(s, st, px, cy, row_w, widgets::FONT_H + 2, row_bg, w, h); // index diff --git a/settings/src/chambers/gateway.rs b/settings/src/chambers/gateway.rs index 7d2922ce..e07e4b15 100644 --- a/settings/src/chambers/gateway.rs +++ b/settings/src/chambers/gateway.rs @@ -93,16 +93,6 @@ pub fn handle_key(app: &mut SettingsApp, scancode: u8) { } } -pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = layout::row_step(app, 8); - let idx = (py as u32 / row_h) as usize; - if idx >= 2 { - let adjusted = idx - 2; - app.pane_focus = adjusted; - activate(app, adjusted); - } -} - pub fn render(app: &SettingsApp) { let s = app.surface; let st = app.fb_stride; @@ -152,6 +142,7 @@ pub fn render(app: &SettingsApp) { let fg = if is_focused { t.signal } else { t.glyph }; let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); + app.register_widget_hitbox(px, cy, row_w, r4, i + 1); widgets::fill_rect(s, st, px, cy, row_w, r4, bg, w, h); if is_focused { widgets::rect_outline(s, st, px, cy, row_w, r4, t.focus_ring, w, h); diff --git a/settings/src/chambers/hall.rs b/settings/src/chambers/hall.rs index 14ee9c51..b0de8407 100644 --- a/settings/src/chambers/hall.rs +++ b/settings/src/chambers/hall.rs @@ -125,16 +125,6 @@ pub fn apply(app: &mut SettingsApp) { pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} -pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = (widgets::FONT_H + 12) as i32; - let header = 60i32; - let idx = ((py - header) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - activate(app, idx); - } -} - pub fn render(app: &SettingsApp) { let s = app.surface; let st = app.fb_stride; @@ -163,6 +153,7 @@ pub fn render(app: &SettingsApp) { let card_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); let card_h = widgets::FONT_H * 2 + 12; + app.register_widget_hitbox(px, cy, card_w, card_h, i); let card_bg = if is_selected { t.surface } else { t.substrate }; widgets::fill_rect(s, st, px, cy, card_w, card_h, card_bg, w, h); diff --git a/settings/src/chambers/mirror.rs b/settings/src/chambers/mirror.rs index 7a9a53ba..b0838d4b 100644 --- a/settings/src/chambers/mirror.rs +++ b/settings/src/chambers/mirror.rs @@ -118,15 +118,6 @@ fn rebuild_theme(app: &mut SettingsApp) { pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} -pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = layout::row_step(app, 8) as i32; - let idx = ((py - 40) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - activate(app, idx); - } -} - pub fn render(app: &SettingsApp) { let t = &app.theme; let mirror = &app.mirror; diff --git a/settings/src/chambers/mist.rs b/settings/src/chambers/mist.rs index 93629ca4..77998a8d 100644 --- a/settings/src/chambers/mist.rs +++ b/settings/src/chambers/mist.rs @@ -64,15 +64,6 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} -pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = layout::row_step(app, 8) as i32; - let idx = ((py - 40) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - activate(app, idx); - } -} - pub fn render(app: &SettingsApp) { let s = app.surface; let st = app.fb_stride; diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs index 03a6d519..a0f4ced3 100644 --- a/settings/src/chambers/net_obs.rs +++ b/settings/src/chambers/net_obs.rs @@ -283,15 +283,6 @@ pub fn handle_key(app: &mut SettingsApp, scancode: u8) { } } -pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = layout::row_step(app, 8) as i32; - let idx = ((py - 40) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - activate(app, idx); - } -} - pub fn render(app: &SettingsApp) { let t = &app.theme; let net = &app.net_obs; @@ -446,6 +437,7 @@ fn draw_editable_field(app: &SettingsApp, x: u32, y: u32, label: &str, value: &s let bg = if editing { t.input_bg } else if is_focused { t.surface } else { t.substrate }; let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); let row_h = layout::row_step(app, 4); + app.register_widget_hitbox(x, y, row_w, row_h, field_idx); widgets::fill_rect(s, st, x, y, row_w, row_h, bg, w, h); let border_color = if editing { diff --git a/settings/src/chambers/sys_obs.rs b/settings/src/chambers/sys_obs.rs index e11209c4..abfeda46 100644 --- a/settings/src/chambers/sys_obs.rs +++ b/settings/src/chambers/sys_obs.rs @@ -137,15 +137,6 @@ pub fn handle_key(app: &mut SettingsApp, scancode: u8) { } } -pub fn handle_click(app: &mut SettingsApp, _px: i32, py: i32) { - let row_h = layout::row_step(app, 8) as i32; - let idx = ((py - 40) / row_h).max(0) as usize; - if idx < FIELD_COUNT { - app.pane_focus = idx; - activate(app, idx); - } -} - pub fn render(app: &SettingsApp) { let s = app.surface; let st = app.fb_stride; diff --git a/settings/src/layout.rs b/settings/src/layout.rs index 135b44ff..c52c5479 100644 --- a/settings/src/layout.rs +++ b/settings/src/layout.rs @@ -236,6 +236,7 @@ pub fn draw_field_row(app: &SettingsApp, x: u32, y: u32, label: &str, value: &st let bg = if is_focused { t.surface } else { t.substrate }; let row_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); let row_h = row_step(app, 4); + app.register_widget_hitbox(x, y, row_w, row_h, field_idx); widgets::fill_rect(s, st, x, y, row_w, row_h, bg, w, h); if is_focused { @@ -265,6 +266,7 @@ pub fn draw_button_row(app: &SettingsApp, x: u32, y: u32, label: &str, field_idx let bg = if is_focused { t.surface } else { t.substrate }; let btn_w = (w - RAIL_WIDTH).saturating_sub(2 * PANE_PAD); let btn_h = row_step(app, 4); + app.register_widget_hitbox(x, y, btn_w, btn_h, field_idx); widgets::fill_rect(s, st, x, y, btn_w, btn_h, bg, w, h); widgets::rect_outline(s, st, x, y, btn_w, btn_h, if is_focused { t.focus_ring } else { t.contour }, w, h); diff --git a/settings/src/state.rs b/settings/src/state.rs index efa1cc27..64b92adc 100644 --- a/settings/src/state.rs +++ b/settings/src/state.rs @@ -1,4 +1,5 @@ use alloc::vec::Vec; +use core::cell::{Cell, RefCell}; use crate::chambers::{ @@ -8,7 +9,6 @@ use crate::chambers::{ }; use crate::layout; use crate::theme::OneiricTheme; -use crate::widgets; // route ids — flat enum, zero-cost dispatch #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -177,8 +177,8 @@ pub struct SettingsApp { // tick counter for animations pub tick_count: u64, - pub hitboxes: [Hitbox; 128], - pub hitbox_count: usize, + pub hitboxes: RefCell<[Hitbox; 128]>, + pub hitbox_count: Cell, } impl SettingsApp { @@ -228,14 +228,14 @@ impl SettingsApp { tick_count: 0, - hitboxes: [Hitbox { + hitboxes: RefCell::new([Hitbox { x: 0, y: 0, w: 0, h: 0, widget_idx: 0, - }; 128], - hitbox_count: 0, + }; 128]), + hitbox_count: Cell::new(0), } } @@ -548,20 +548,6 @@ impl SettingsApp { if let Some(idx) = self.hitbox_at(abs_x, abs_y) { self.pane_focus = idx; self.pane_activate(); - return; - } - - // fallback for clicks outside registered controls: pick nearest slot. - let count = self.pane_widget_count(); - if count > 0 { - let pane_h = self - .fb_h - .saturating_sub(layout::STRIP_HEIGHT + layout::BAR_HEIGHT + 2 * layout::PANE_PAD) - .max(1); - let y = pane_y.max(0) as u32; - let idx = ((y as u64 * count as u64) / pane_h as u64) as usize; - self.pane_focus = idx.min(count - 1); - self.pane_activate(); } } @@ -623,89 +609,40 @@ impl SettingsApp { } fn render(&mut self) { + self.clear_hitboxes(); layout::render_frame(self); - self.rebuild_hitboxes(); } - fn clear_hitboxes(&mut self) { - self.hitbox_count = 0; + fn clear_hitboxes(&self) { + self.hitbox_count.set(0); + } + + pub fn register_widget_hitbox(&self, x: u32, y: u32, w: u32, h: u32, widget_idx: usize) { + self.push_hitbox(x as i32, y as i32, w as i32, h as i32, widget_idx); } - fn push_hitbox(&mut self, x: i32, y: i32, w: i32, h: i32, widget_idx: usize) { - if self.hitbox_count >= self.hitboxes.len() || w <= 0 || h <= 0 { + fn push_hitbox(&self, x: i32, y: i32, w: i32, h: i32, widget_idx: usize) { + let count = self.hitbox_count.get(); + if count >= 128 || w <= 0 || h <= 0 { return; } - self.hitboxes[self.hitbox_count] = Hitbox { + let mut hitboxes = self.hitboxes.borrow_mut(); + hitboxes[count] = Hitbox { x, y, w, h, widget_idx, }; - self.hitbox_count += 1; - } - - fn rebuild_hitboxes(&mut self) { - self.clear_hitboxes(); - - let count = self.pane_widget_count(); - if count == 0 { - return; - } - - let px = layout::RAIL_WIDTH as i32 + layout::PANE_PAD as i32; - let pw = (self.fb_w.saturating_sub(layout::RAIL_WIDTH + 2 * layout::PANE_PAD)) as i32; - if pw <= 0 { - return; - } - - match self.route { - Route::Gateway => { - let row_h = layout::row_step(self, 8) as i32; - let base_y = layout::STRIP_HEIGHT as i32 + 2 * row_h; - for i in 0..count { - let y = base_y + i as i32 * row_h; - self.push_hitbox(px, y, pw, row_h.max(14), i); - } - } - Route::NetObservatory | Route::SysObservatory | Route::MistShore | Route::MirrorBasin => { - let row_h = layout::row_step(self, 8) as i32; - let base_y = layout::STRIP_HEIGHT as i32 + 40; - for i in 0..count { - let y = base_y + i as i32 * row_h; - self.push_hitbox(px, y, pw, row_h.max(14), i); - } - } - Route::HallOfMasks => { - let row_h = (widgets::FONT_H + 12) as i32; - let base_y = layout::STRIP_HEIGHT as i32 + 60; - for i in 0..count { - let y = base_y + i as i32 * row_h; - self.push_hitbox(px, y, pw, row_h.max(14), i); - } - } - Route::Archive => { - // row picks in the log table (index 3 + visible row). - let row_h = (widgets::FONT_H + 4) as i32; - let table_y = layout::STRIP_HEIGHT as i32 + 60; - let half = (pw / 2).max(20); - self.push_hitbox(px, table_y - row_h, half, row_h, 0); - self.push_hitbox(px + half, table_y - row_h, pw - half, row_h, 1); - self.push_hitbox(px, table_y - 2 * row_h, pw, row_h, 2); - - let rows = count.saturating_sub(3); - for i in 0..rows { - let y = table_y + i as i32 * row_h; - self.push_hitbox(px, y, pw, row_h.max(14), i + 3); - } - } - } + self.hitbox_count.set(count + 1); } fn hitbox_at(&self, x: i32, y: i32) -> Option { // last one wins in case of overlap - for i in (0..self.hitbox_count).rev() { - let hb = self.hitboxes[i]; + let count = self.hitbox_count.get(); + let hitboxes = self.hitboxes.borrow(); + for i in (0..count).rev() { + let hb = hitboxes[i]; if x >= hb.x && y >= hb.y && x < hb.x + hb.w && y < hb.y + hb.h { return Some(hb.widget_idx); } From 73e750d9600d4ebcd8f5e6b60f16ff9c80fb20df Mon Sep 17 00:00:00 2001 From: "T. Andrew Davis" Date: Thu, 12 Mar 2026 11:57:43 +0100 Subject: [PATCH 09/12] feat(networking): implement userspace network activation - Added support for activating networking from userspace via the `netup` command. - Introduced `MouseSpatialMsg` and `MouseZRouteMsg` for improved mouse input handling. - Enhanced `CompState` to manage mouse state and routing more effectively. - Updated `hwinit` to register a network activation callback, allowing the kernel to remain offline by default. - Modified various chambers in the settings to support new networking features and improve user experience. - Improved error handling and user feedback for network activation and configuration. --- bootloader/src/baremetal.rs | 102 +++++++++++++++++ compd/src/islands/input.rs | 175 ++++++++++++++++++++++++------ compd/src/islands/mod.rs | 7 ++ compd/src/islands/surface_mgr.rs | 45 ++++++++ compd/src/messages.rs | 19 ++++ hwinit/src/lib.rs | 1 + hwinit/src/syscall/handler/net.rs | 29 +++++ libmorpheus/src/net.rs | 16 +++ settings/src/chambers/archive.rs | 7 ++ settings/src/chambers/mirror.rs | 2 + settings/src/chambers/net_obs.rs | 123 ++++++++++++++++----- settings/src/layout.rs | 7 ++ settings/src/state.rs | 95 +++++++++++++--- shell/src/builtin/help.rs | 9 ++ shell/src/builtin/mod.rs | 3 + shell/src/builtin/proc_cmds.rs | 66 +++++++++++ 16 files changed, 633 insertions(+), 73 deletions(-) diff --git a/bootloader/src/baremetal.rs b/bootloader/src/baremetal.rs index 6b08f940..4c5b76bd 100644 --- a/bootloader/src/baremetal.rs +++ b/bootloader/src/baremetal.rs @@ -5,6 +5,8 @@ use core::sync::atomic::{AtomicBool, Ordering}; use morpheus_display::console::TextConsole; +use morpheus_network::device::UnifiedNetDevice; +use morpheus_network::driver::traits::NetworkDriver; // TYPES THAT CROSS THE BORDER @@ -49,6 +51,101 @@ static BAREMETAL_MODE: AtomicBool = AtomicBool::new(false); static mut LIVE_CONSOLE: Option = None; +// userspace-activated networking state. offline by default until requested. +static mut USER_NET_DRIVER: Option = None; +static mut USER_NET_DMA: Option = None; +static mut USER_NET_TSC_FREQ: u64 = 0; + +#[inline] +unsafe fn user_net_driver_mut() -> Option<&'static mut UnifiedNetDevice> { + USER_NET_DRIVER.as_mut() +} + +unsafe fn user_net_tx(frame: *const u8, len: usize) -> i64 { + let Some(driver) = user_net_driver_mut() else { + return -1; + }; + let frame = core::slice::from_raw_parts(frame, len); + if driver.transmit(frame).is_ok() { + 0 + } else { + -1 + } +} + +unsafe fn user_net_rx(buf: *mut u8, buf_len: usize) -> i64 { + let Some(driver) = user_net_driver_mut() else { + return -1; + }; + let buf = core::slice::from_raw_parts_mut(buf, buf_len); + match driver.receive(buf) { + Ok(Some(n)) => n as i64, + Ok(None) => 0, + Err(_) => -1, + } +} + +unsafe fn user_net_link_up() -> i64 { + let Some(driver) = user_net_driver_mut() else { + return 0; + }; + driver.link_up() as i64 +} + +unsafe fn user_net_mac(out: *mut u8) -> i64 { + let Some(driver) = user_net_driver_mut() else { + return -1; + }; + let mac = driver.mac_address(); + core::ptr::copy_nonoverlapping(mac.as_ptr(), out, 6); + 0 +} + +unsafe fn user_net_refill() -> i64 { + let Some(driver) = user_net_driver_mut() else { + return -1; + }; + driver.refill_rx_queue(); + driver.collect_tx_completions(); + 0 +} + +unsafe fn user_net_ctrl(_cmd: u32, _arg: u64) -> i64 { + // full NIC control routing can be added later per-driver. + -1 +} + +unsafe fn activate_network_from_userspace() -> i64 { + if USER_NET_DRIVER.is_some() { + return 1; + } + + let Some(dma) = USER_NET_DMA else { + return -1; + }; + let tsc_freq = USER_NET_TSC_FREQ; + + let driver = match UnifiedNetDevice::probe(&dma, tsc_freq) { + Ok(d) => d, + Err(_) => return -1, + }; + + USER_NET_DRIVER = Some(driver); + + morpheus_hwinit::register_nic(morpheus_hwinit::NicOps { + tx: Some(user_net_tx), + rx: Some(user_net_rx), + link_up: Some(user_net_link_up), + mac: Some(user_net_mac), + refill: Some(user_net_refill), + ctrl: Some(user_net_ctrl), + }); + + // prime descriptor maintenance immediately. + let _ = user_net_refill(); + 0 +} + /// Called by the hwinit serial hook for every byte emitted via `puts()`. /// Writes directly to the TextConsole backed by the GOP framebuffer. pub unsafe fn live_console_putc(b: u8) { @@ -287,6 +384,11 @@ pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { } }; + // userspace network activation hook. we stay offline by default. + USER_NET_DMA = Some(*platform.dma()); + USER_NET_TSC_FREQ = platform.tsc_freq(); + morpheus_hwinit::register_net_activation(activate_network_from_userspace); + // persistent storage — try to mount a real block device crate::storage::init_persistent_storage(platform.dma(), platform.tsc_freq()); diff --git a/compd/src/islands/input.rs b/compd/src/islands/input.rs index a513de4e..668d69f1 100644 --- a/compd/src/islands/input.rs +++ b/compd/src/islands/input.rs @@ -1,7 +1,7 @@ extern crate alloc; use crate::islands::{CompState, HitRegion, MouseCapture, BORDER, MAX_WINDOWS, TITLE_H}; -use crate::messages::InputMsg; +use crate::messages::{InputMsg, MouseSpatialMsg, MouseZRouteMsg}; use libmorpheus::{compositor as compsys, hw, io, process}; const CTRL_BRACKET: u8 = 0x1D; // Ctrl+] — focus cycle scancode @@ -68,22 +68,49 @@ fn poll_mouse(state: &mut CompState) { state.mouse_x = (state.mouse_x + ms.dx as i32).clamp(0, fb_w - 1); state.mouse_y = (state.mouse_y + ms.dy as i32).clamp(0, fb_h - 1); - // always forward movement to shelld so its cursor position tracks ours. - // buttons are only forwarded when the click is actually for shelld. - forward_to_desktop(state, ms.dx, ms.dy, 0); - let left = (ms.buttons & 1) != 0; let left_was = (state.last_buttons & 1) != 0; - let left_pressed = left && !left_was; - let left_released = !left && left_was; - let mut route_to_child = true; + let sample = MouseSpatialMsg { + mx: state.mouse_x, + my: state.mouse_y, + buttons: ms.buttons, + left_pressed: left && !left_was, + left_released: !left && left_was, + in_panel: state.mouse_y >= (state.fb_h as i32 - crate::islands::PANEL_H as i32), + }; + + if let Err(msg) = state.ch_mouse_spatial.send(sample) { + route_mouse_spatial(state, msg); + } + + while let Some(msg) = state.ch_mouse_spatial.recv() { + route_mouse_spatial(state, msg); + } + + while let Some(msg) = state.ch_mouse_route.recv() { + dispatch_mouse_route(state, msg); + } +} + +fn route_mouse_spatial(state: &mut CompState, msg: MouseSpatialMsg) { + // always keep desktop cursor in sync with absolute position. + enqueue_mouse_route(state, MouseZRouteMsg::Desktop { buttons: 0 }); - if left_released { + if msg.left_released { state.capture = None; } - if left_pressed { - if let Some((idx, region)) = hit_test(state, state.mouse_x, state.mouse_y) { + // panel is visually over windows (z3 overlay), so input there belongs to shelld. + if msg.in_panel { + enqueue_mouse_route(state, MouseZRouteMsg::Desktop { buttons: msg.buttons }); + state.last_buttons = msg.buttons; + return; + } + + let mut route_to_child = true; + + if msg.left_pressed { + if let Some((idx, region)) = hit_test(state, msg.mx, msg.my) { state.focused = Some(idx); match region { HitRegion::Close => { @@ -97,8 +124,8 @@ fn poll_mouse(state: &mut CompState) { if let Some(ref win) = state.windows[idx] { state.capture = Some(MouseCapture::Move { idx, - off_x: state.mouse_x - win.x, - off_y: state.mouse_y - win.y, + off_x: msg.mx - win.x, + off_y: msg.my - win.y, }); } route_to_child = false; @@ -107,8 +134,8 @@ fn poll_mouse(state: &mut CompState) { if let Some(ref win) = state.windows[idx] { state.capture = Some(MouseCapture::Resize { idx, - start_mx: state.mouse_x, - start_my: state.mouse_y, + start_mx: msg.mx, + start_my: msg.my, start_w: win.w, start_h: win.h, }); @@ -118,24 +145,24 @@ fn poll_mouse(state: &mut CompState) { HitRegion::Content => {} } } else { - // no window hit: panel area goes to shelld, else desktop click also goes to shelld. - forward_to_desktop(state, 0, 0, ms.buttons); - state.last_buttons = ms.buttons; + enqueue_mouse_route(state, MouseZRouteMsg::Desktop { buttons: msg.buttons }); + state.last_buttons = msg.buttons; return; } } - if left { + if (msg.buttons & 1) != 0 { if let Some(capture) = state.capture { match capture { MouseCapture::Move { idx, off_x, off_y } => { if let Some(ref mut win) = state.windows[idx] { - let nx = state.mouse_x - off_x; - let ny = state.mouse_y - off_y; + let nx = msg.mx - off_x; + let ny = msg.my - off_y; let max_x = (state.fb_w as i32 - win.w as i32).max(0); let max_y = (state.fb_h as i32 - win.h as i32).max(TITLE_H as i32); win.x = nx.clamp(0, max_x); win.y = ny.clamp(TITLE_H as i32, max_y); + win.mouse_local_valid = false; } route_to_child = false; } @@ -147,14 +174,15 @@ fn poll_mouse(state: &mut CompState) { start_h, } => { if let Some(ref mut win) = state.windows[idx] { - let dx = state.mouse_x - start_mx; - let dy = state.mouse_y - start_my; + let dx = msg.mx - start_mx; + let dy = msg.my - start_my; let max_w = state.fb_w.saturating_sub(win.x.max(0) as u32).max(160); let max_h = state.fb_h.saturating_sub(win.y.max(0) as u32).max(120); let nw = (start_w as i32 + dx).clamp(160, max_w as i32); let nh = (start_h as i32 + dy).clamp(120, max_h as i32); win.w = nw as u32; win.h = nh as u32; + win.mouse_local_valid = false; } route_to_child = false; } @@ -162,27 +190,112 @@ fn poll_mouse(state: &mut CompState) { } } - state.last_buttons = ms.buttons; - - // forward unhandled mouse to focused child if route_to_child { if let Some(idx) = state.focused { - if let Some(ref win) = state.windows[idx] { - let _ = compsys::mouse_forward(win.pid, ms.dx, ms.dy, ms.buttons); + enqueue_mouse_route( + state, + MouseZRouteMsg::Child { + idx: idx as u8, + buttons: msg.buttons, + }, + ); + } else { + enqueue_mouse_route(state, MouseZRouteMsg::None); + } + } + + state.last_buttons = msg.buttons; +} + +#[inline(always)] +fn enqueue_mouse_route(state: &mut CompState, msg: MouseZRouteMsg) { + if let Err(msg) = state.ch_mouse_route.send(msg) { + // channel full. handle inline so no route decision gets dropped. + dispatch_mouse_route(state, msg); + } +} + +fn dispatch_mouse_route(state: &mut CompState, msg: MouseZRouteMsg) { + match msg { + MouseZRouteMsg::Desktop { buttons } => { + forward_to_desktop(state, buttons); + } + MouseZRouteMsg::Child { idx, buttons } => { + let idx = idx as usize; + if let Some(ref mut win) = state.windows[idx] { + let (local_x, local_y) = map_global_to_local(state.mouse_x, state.mouse_y, win); + let (dx, dy) = if win.mouse_local_valid { + (local_x - win.mouse_local_x, local_y - win.mouse_local_y) + } else { + win.mouse_local_valid = true; + (0, 0) + }; + win.mouse_local_x = local_x; + win.mouse_local_y = local_y; + let dx = dx.clamp(i16::MIN as i32, i16::MAX as i32) as i16; + let dy = dy.clamp(i16::MIN as i32, i16::MAX as i32) as i16; + let _ = compsys::mouse_forward(win.pid, dx, dy, buttons); } } + MouseZRouteMsg::None => {} } } -/// forward mouse to shelld (desktop surface). always forward so shelld's position tracks ours. -fn forward_to_desktop(state: &CompState, dx: i16, dy: i16, buttons: u8) { +/// forward mouse to shelld (desktop surface), derived from absolute global cursor. +fn forward_to_desktop(state: &mut CompState, buttons: u8) { if let Some(di) = state.desktop_idx { - if let Some(ref dw) = state.windows[di] { + if let Some(ref mut dw) = state.windows[di] { + let (local_x, local_y) = map_global_to_local(state.mouse_x, state.mouse_y, dw); + let (dx, dy) = if dw.mouse_local_valid { + (local_x - dw.mouse_local_x, local_y - dw.mouse_local_y) + } else { + dw.mouse_local_valid = true; + (0, 0) + }; + dw.mouse_local_x = local_x; + dw.mouse_local_y = local_y; + let dx = dx.clamp(i16::MIN as i32, i16::MAX as i32) as i16; + let dy = dy.clamp(i16::MIN as i32, i16::MAX as i32) as i16; let _ = compsys::mouse_forward(dw.pid, dx, dy, buttons); } } } +#[inline(always)] +fn map_global_to_local(mx: i32, my: i32, win: &crate::islands::ChildWindow) -> (i32, i32) { + let sw = win.src_w.max(1) as i32; + let sh = win.src_h.max(1) as i32; + + // z0 is desktop-space; z1 is window content-space. + let (rel_x, rel_y, ww, wh) = if win.z_layer == 0 { + let ww = sw.max(1); + let wh = sh.max(1); + (mx.clamp(0, ww - 1), my.clamp(0, wh - 1), ww, wh) + } else { + let ww = win.w.max(1) as i32; + let wh = win.h.max(1) as i32; + ( + (mx - win.x).clamp(0, ww - 1), + (my - win.y).clamp(0, wh - 1), + ww, + wh, + ) + }; + + let local_x = if ww <= 1 || sw <= 1 { + 0 + } else { + (rel_x as i64 * (sw - 1) as i64 / (ww - 1) as i64) as i32 + }; + let local_y = if wh <= 1 || sh <= 1 { + 0 + } else { + (rel_y as i64 * (sh - 1) as i64 / (wh - 1) as i64) as i32 + }; + + (local_x, local_y) +} + fn hit_test(state: &CompState, mx: i32, my: i32) -> Option<(usize, HitRegion)> { let mut candidates: [Option; MAX_WINDOWS] = [None; MAX_WINDOWS]; let mut cn = 0usize; diff --git a/compd/src/islands/mod.rs b/compd/src/islands/mod.rs index aaaf9dce..9fc07a2e 100644 --- a/compd/src/islands/mod.rs +++ b/compd/src/islands/mod.rs @@ -38,6 +38,9 @@ pub struct ChildWindow { pub src_w: u32, pub src_h: u32, pub src_stride: u32, // in pixels. not bytes. the framebuffer stride IS bytes. yes again. + pub mouse_local_x: i32, + pub mouse_local_y: i32, + pub mouse_local_valid: bool, pub title: [u8; 64], pub title_len: usize, pub z_layer: u8, // 0=desktop background, 1=normal window, 3=overlay @@ -92,6 +95,8 @@ pub struct CompState { // --- channels (SPSC) --- pub ch_input_to_focus: Channel, + pub ch_mouse_spatial: Channel, + pub ch_mouse_route: Channel, } impl CompState { @@ -113,6 +118,8 @@ impl CompState { last_buttons: 0, capture: None, ch_input_to_focus: Channel::new(), + ch_mouse_spatial: Channel::new(), + ch_mouse_route: Channel::new(), } } diff --git a/compd/src/islands/surface_mgr.rs b/compd/src/islands/surface_mgr.rs index b0295734..870094d0 100644 --- a/compd/src/islands/surface_mgr.rs +++ b/compd/src/islands/surface_mgr.rs @@ -109,6 +109,9 @@ pub fn update(state: &mut CompState) { src_w: entry.width, src_h: entry.height, src_stride: (entry.stride / 4).max(entry.width.max(1)), + mouse_local_x: 0, + mouse_local_y: 0, + mouse_local_valid: false, title: [0u8; 64], title_len: 0, z_layer: 0, @@ -146,13 +149,55 @@ pub fn update(state: &mut CompState) { src_w: entry.width, src_h: entry.height, src_stride: (entry.stride / 4).max(entry.width.max(1)), + mouse_local_x: 0, + mouse_local_y: 0, + mouse_local_valid: false, title: [0u8; 64], title_len: 0, z_layer: 1, }); + // seed app-local cursor at spawn so first click lands where the cursor already is. + if let Some(ref mut win) = state.windows[idx] { + let (local_x, local_y) = map_global_to_local_spawn(state.mouse_x, state.mouse_y, win); + let dx = local_x.clamp(i16::MIN as i32, i16::MAX as i32) as i16; + let dy = local_y.clamp(i16::MIN as i32, i16::MAX as i32) as i16; + let _ = compsys::mouse_forward(win.pid, dx, dy, 0); + win.mouse_local_x = local_x; + win.mouse_local_y = local_y; + win.mouse_local_valid = true; + } + state.focused = Some(idx); } } } } + +#[inline(always)] +fn map_global_to_local_spawn( + mx: i32, + my: i32, + win: &ChildWindow, +) -> (i32, i32) { + let sw = win.src_w.max(1) as i32; + let sh = win.src_h.max(1) as i32; + let ww = win.w.max(1) as i32; + let wh = win.h.max(1) as i32; + + let rel_x = (mx - win.x).clamp(0, ww - 1); + let rel_y = (my - win.y).clamp(0, wh - 1); + + let local_x = if ww <= 1 || sw <= 1 { + 0 + } else { + (rel_x as i64 * (sw - 1) as i64 / (ww - 1) as i64) as i32 + }; + let local_y = if wh <= 1 || sh <= 1 { + 0 + } else { + (rel_y as i64 * (sh - 1) as i64 / (wh - 1) as i64) as i32 + }; + + (local_x, local_y) +} diff --git a/compd/src/messages.rs b/compd/src/messages.rs index 7e9ba75a..ba97adb8 100644 --- a/compd/src/messages.rs +++ b/compd/src/messages.rs @@ -51,3 +51,22 @@ pub enum InputMsg { pub enum FocusMsg { FocusChanged { old: Option, new: Option }, } + +/// raw spatial sample from input polling: absolute desktop position + edge transitions. +#[derive(Clone, Copy)] +pub struct MouseSpatialMsg { + pub mx: i32, + pub my: i32, + pub buttons: u8, + pub left_pressed: bool, + pub left_released: bool, + pub in_panel: bool, +} + +/// z-layer routing decision derived from spatial sample. +#[derive(Clone, Copy)] +pub enum MouseZRouteMsg { + Desktop { buttons: u8 }, + Child { idx: u8, buttons: u8 }, + None, +} diff --git a/hwinit/src/lib.rs b/hwinit/src/lib.rs index c1248dd9..6fd3361d 100644 --- a/hwinit/src/lib.rs +++ b/hwinit/src/lib.rs @@ -307,6 +307,7 @@ pub use syscall::{ // hardware backends that hwinit cannot depend on directly. pub use syscall::handler::{ register_framebuffer, + register_net_activation, register_net_stack, register_nic, FbInfo, diff --git a/hwinit/src/syscall/handler/net.rs b/hwinit/src/syscall/handler/net.rs index 1d7b835d..63d721be 100644 --- a/hwinit/src/syscall/handler/net.rs +++ b/hwinit/src/syscall/handler/net.rs @@ -42,6 +42,7 @@ pub const NET_CFG_GET: u64 = 0; pub const NET_CFG_DHCP: u64 = 1; pub const NET_CFG_STATIC: u64 = 2; pub const NET_CFG_HOSTNAME: u64 = 3; +pub const NET_CFG_ACTIVATE: u64 = 4; // sub-commands for sys_net_poll (41) pub const NET_POLL_DRIVE: u64 = 0; @@ -195,6 +196,10 @@ static mut NET_STACK_OPS: NetStackOps = NetStackOps { poll_stats: None, }; +// userspace-triggered network bring-up callback. +// bootloader registers this so the kernel can stay offline by default. +static mut NET_ACTIVATE_FN: Option i64> = None; + /// Register network stack function pointers. /// /// Called by the bootloader after it creates a `NetInterface` and @@ -203,6 +208,17 @@ pub unsafe fn register_net_stack(ops: NetStackOps) { NET_STACK_OPS = ops; } +/// Register userspace-triggered network activation callback. +/// +/// This callback is invoked by `SYS_NET_CFG(NET_CFG_ACTIVATE)`. +/// Return values: +/// - `0` => activated now +/// - `>0` => already active / no-op success +/// - `<0` => failure +pub unsafe fn register_net_activation(callback: unsafe fn() -> i64) { + NET_ACTIVATE_FN = Some(callback); +} + /// Check if a network stack is registered. fn net_stack_present() -> bool { unsafe { NET_STACK_OPS.tcp_socket.is_some() } @@ -595,6 +611,19 @@ pub unsafe fn sys_net_cfg(subcmd: u64, a2: u64, a3: u64, _a4: u64) -> u64 { } } } + // CFG_ACTIVATE() — explicit userspace network bring-up. + // offline by default; userspace opts in when it wants networking. + NET_CFG_ACTIVATE => match NET_ACTIVATE_FN { + Some(f) => { + let rc = f(); + if rc < 0 { + EIO + } else { + rc as u64 + } + } + None => ENODEV, + }, // All remaining subcmds require the stack. _ if !net_stack_present() => ENODEV, diff --git a/libmorpheus/src/net.rs b/libmorpheus/src/net.rs index cdceb095..57b403a5 100644 --- a/libmorpheus/src/net.rs +++ b/libmorpheus/src/net.rs @@ -515,6 +515,7 @@ const CFG_GET: u64 = 0; const CFG_DHCP: u64 = 1; const CFG_STATIC: u64 = 2; const CFG_HOSTNAME: u64 = 3; +const CFG_ACTIVATE: u64 = 4; /// Network configuration snapshot. #[repr(C)] @@ -599,6 +600,21 @@ pub fn net_set_hostname(hostname: &str) -> Result<(), u64> { } } +/// Explicitly activate networking from userspace. +/// +/// This requests NIC driver bring-up while keeping boot default offline. +/// Returns: +/// - `0` when activated now +/// - `>0` when already active/no-op +pub fn net_activate() -> Result { + let ret = unsafe { syscall1(SYS_NET_CFG, CFG_ACTIVATE) }; + if crate::is_error(ret) { + Err(ret) + } else { + Ok(ret) + } +} + // stack polling const POLL_DRIVE: u64 = 0; diff --git a/settings/src/chambers/archive.rs b/settings/src/chambers/archive.rs index 8e9018c4..62dfbe1f 100644 --- a/settings/src/chambers/archive.rs +++ b/settings/src/chambers/archive.rs @@ -53,6 +53,13 @@ impl ArchiveChamber { } pub fn handle_key(&mut self, scancode: u8) { + if !self.searching { + if scancode == 0x35 || scancode == b'/' { + self.searching = true; + return; + } + } + if self.searching { match scancode { 0x01 => { diff --git a/settings/src/chambers/mirror.rs b/settings/src/chambers/mirror.rs index b0838d4b..b0720b4e 100644 --- a/settings/src/chambers/mirror.rs +++ b/settings/src/chambers/mirror.rs @@ -83,12 +83,14 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { FIELD_APPLY => { let accent_name = ACCENTS[app.mirror.accent_idx].3; app.mirror.apply(); + app.clear_pending_for(Route::MirrorBasin); app.set_status("Appearance applied", false); app.log_change(Route::MirrorBasin, "appearance", accent_name, false); } FIELD_REVERT => { app.mirror.revert(); rebuild_theme(app); + app.clear_pending_for(Route::MirrorBasin); app.set_status("Appearance reverted", false); } _ => {} diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs index a0f4ced3..b116d166 100644 --- a/settings/src/chambers/net_obs.rs +++ b/settings/src/chambers/net_obs.rs @@ -17,8 +17,9 @@ const FIELD_GATEWAY: usize = 4; const FIELD_DNS1: usize = 5; const FIELD_DNS2: usize = 6; const FIELD_APPLY: usize = 7; -const FIELD_REFRESH: usize = 8; -const FIELD_COUNT: usize = 9; +const FIELD_ACTIVATE: usize = 8; +const FIELD_REFRESH: usize = 9; +const FIELD_COUNT: usize = 10; pub struct NetObsChamber { // live state from kernel @@ -203,8 +204,24 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { app.mark_edited(Route::NetObservatory, "prefix"); } FIELD_APPLY => { - apply(app); + if apply(app) { + app.clear_pending_for(Route::NetObservatory); + app.net_obs.editing_field = None; + } } + FIELD_ACTIVATE => match net::net_activate() { + Ok(rc) => { + app.net_obs.refresh(); + if rc == 0 { + app.set_status("Networking activated", false); + } else { + app.set_status("Networking already active", false); + } + } + Err(_) => { + app.set_status("Networking activation failed", true); + } + }, FIELD_REFRESH => { app.net_obs.refresh(); app.set_status("Network refreshed", false); @@ -213,7 +230,7 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { } } -pub fn apply(app: &mut SettingsApp) { +pub fn apply(app: &mut SettingsApp) -> bool { // hostname — copy to local buf so we don't hold a borrow on app across log_change let hl = app.net_obs.edit_hostname_len; if hl > 0 { @@ -222,7 +239,7 @@ pub fn apply(app: &mut SettingsApp) { let hn = core::str::from_utf8(&hn_buf[..hl]).unwrap_or(""); if let Err(_) = net::net_set_hostname(hn) { app.set_status("Hostname set failed", true); - return; + return false; } app.log_change(Route::NetObservatory, "hostname", hn, false); } @@ -231,31 +248,64 @@ pub fn apply(app: &mut SettingsApp) { if dhcp { if let Err(_) = net::net_dhcp() { app.set_status("DHCP request failed", true); - return; + return false; } app.log_change(Route::NetObservatory, "mode", "Switched to DHCP", false); } else { let ip_len = app.net_obs.edit_ip_len; let gw_len = app.net_obs.edit_gw_len; let prefix = app.net_obs.edit_prefix; - let ip = parse_ip(&app.net_obs.edit_ip[..ip_len]); - let gw = parse_ip(&app.net_obs.edit_gateway[..gw_len]); + if prefix == 0 || prefix > 32 { + app.set_status("Invalid prefix length", true); + return false; + } + + let ip = match parse_ipv4_strict(&app.net_obs.edit_ip[..ip_len]) { + Ok(v) => v, + Err(_) => { + app.set_status("Invalid static IP", true); + return false; + } + }; + let gw = match parse_ipv4_strict(&app.net_obs.edit_gateway[..gw_len]) { + Ok(v) => v, + Err(_) => { + app.set_status("Invalid gateway IP", true); + return false; + } + }; if let Err(_) = net::net_static_ip(ip, prefix, gw) { app.set_status("Static IP set failed", true); - return; + return false; } app.log_change(Route::NetObservatory, "mode", "Switched to static", false); let d1_len = app.net_obs.edit_dns1_len; let d2_len = app.net_obs.edit_dns2_len; - let d1 = parse_ip(&app.net_obs.edit_dns1[..d1_len]); - let d2 = parse_ip(&app.net_obs.edit_dns2[..d2_len]); + let d1 = match parse_ipv4_or_empty(&app.net_obs.edit_dns1[..d1_len]) { + Ok(v) => v, + Err(_) => { + app.set_status("Invalid primary DNS", true); + return false; + } + }; + let d2 = match parse_ipv4_or_empty(&app.net_obs.edit_dns2[..d2_len]) { + Ok(v) => v, + Err(_) => { + app.set_status("Invalid secondary DNS", true); + return false; + } + }; let servers = [d1, d2]; - let _ = net::dns_set_servers(&servers); + if let Err(_) = net::dns_set_servers(&servers) { + app.set_status("DNS set failed", true); + return false; + } } app.net_obs.refresh(); app.set_status("Network config applied", false); + true } pub fn handle_key(app: &mut SettingsApp, scancode: u8) { @@ -398,6 +448,8 @@ pub fn render(app: &SettingsApp) { // action buttons layout::draw_button_row(app, px, cy, "Apply Network Config", FIELD_APPLY, t.signal); cy += r8; + layout::draw_button_row(app, px, cy, "Activate Networking", FIELD_ACTIVATE, t.warning); + cy += r8; layout::draw_button_row(app, px, cy, "Refresh", FIELD_REFRESH, t.glyph); cy += r12; @@ -477,26 +529,47 @@ fn field_name(idx: usize) -> &'static str { } } -fn parse_ip(buf: &[u8]) -> u32 { - let s = core::str::from_utf8(buf).unwrap_or("0.0.0.0"); +fn parse_ipv4_or_empty(buf: &[u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + parse_ipv4_strict(buf) +} + +fn parse_ipv4_strict(buf: &[u8]) -> Result { let mut octets = [0u8; 4]; - let mut oi = 0; + let mut oi = 0usize; let mut acc: u32 = 0; - for &b in s.as_bytes() { + let mut saw_digit = false; + + for &b in buf { if b == b'.' { - if oi < 4 { - octets[oi] = acc.min(255) as u8; - oi += 1; - acc = 0; + if !saw_digit || oi >= 3 || acc > 255 { + return Err(()); } - } else if b >= b'0' && b <= b'9' { - acc = acc * 10 + (b - b'0') as u32; + octets[oi] = acc as u8; + oi += 1; + acc = 0; + saw_digit = false; + continue; + } + + if !b.is_ascii_digit() { + return Err(()); + } + + saw_digit = true; + acc = acc.saturating_mul(10).saturating_add((b - b'0') as u32); + if acc > 255 { + return Err(()); } } - if oi < 4 { - octets[oi] = acc.min(255) as u8; + + if !saw_digit || oi != 3 || acc > 255 { + return Err(()); } - u32::from_be_bytes(octets) + octets[3] = acc as u8; + Ok(u32::from_be_bytes(octets)) } pub fn scancode_to_char(sc: u8) -> Option { diff --git a/settings/src/layout.rs b/settings/src/layout.rs index c52c5479..6f2d0e1c 100644 --- a/settings/src/layout.rs +++ b/settings/src/layout.rs @@ -89,6 +89,13 @@ fn render_rail(app: &SettingsApp) { for (i, route) in Route::ALL.iter().enumerate() { let y = STRIP_HEIGHT + i as u32 * RAIL_ITEM_HEIGHT; + app.register_widget_hitbox( + 0, + y, + RAIL_WIDTH.saturating_sub(1), + RAIL_ITEM_HEIGHT, + SettingsApp::rail_hitbox_widget_idx(i), + ); let is_current = *route == app.route; let is_focused = app.focus_in_rail && app.rail_focus == i; diff --git a/settings/src/state.rs b/settings/src/state.rs index 64b92adc..3c0923b3 100644 --- a/settings/src/state.rs +++ b/settings/src/state.rs @@ -182,6 +182,21 @@ pub struct SettingsApp { } impl SettingsApp { + const RAIL_HITBOX_BASE: usize = 10_000; + + pub fn rail_hitbox_widget_idx(route_index: usize) -> usize { + Self::RAIL_HITBOX_BASE + route_index + } + + fn rail_index_from_widget_idx(widget_idx: usize) -> Option { + let idx = widget_idx.checked_sub(Self::RAIL_HITBOX_BASE)?; + if idx < Route::ALL.len() { + Some(idx) + } else { + None + } + } + pub fn new( surface: *mut u32, fb_w: u32, @@ -271,12 +286,21 @@ impl SettingsApp { fn poll_input(&mut self) { // keyboard - let avail = libmorpheus::io::stdin_available(); - if avail > 0 { - let mut buf = [0u8; 1]; - let n = libmorpheus::io::read_stdin(&mut buf); - if n > 0 { - self.handle_key(buf[0]); + let mut kbuf = [0u8; 32]; + loop { + let avail = libmorpheus::io::stdin_available(); + if avail == 0 { + break; + } + let n = libmorpheus::io::read_stdin(&mut kbuf); + if n == 0 { + break; + } + for b in kbuf.iter().take(n) { + self.handle_key(*b); + } + if n < kbuf.len() { + break; } } @@ -299,6 +323,13 @@ impl SettingsApp { fn handle_key(&mut self, key: u8) { self.frame_dirty = true; + // when a chamber owns text input, global hotkeys are disabled. + // otherwise typing an IP turns into navigation chaos. + if self.is_text_input_active() { + self.chamber_key(key); + return; + } + if matches!(key, b'1'..=b'7') { let idx = (key - b'1') as usize; self.rail_focus = idx; @@ -394,10 +425,10 @@ impl SettingsApp { // click in rail? if x < rail_w as i32 && y >= strip_h as i32 && y < (self.fb_h - bar_h) as i32 { - let rel_y = (y - strip_h as i32) as u32; - let item_h = layout::RAIL_ITEM_HEIGHT; - let idx = (rel_y / item_h) as usize; - if idx < Route::ALL.len() { + if let Some(idx) = self + .hitbox_at(x, y) + .and_then(Self::rail_index_from_widget_idx) + { self.rail_focus = idx; self.focus_in_rail = true; self.navigate(Route::from_index(idx)); @@ -466,15 +497,41 @@ impl SettingsApp { fn apply_pending(&mut self) { let route = self.route; - match route { + let applied = match route { Route::NetObservatory => crate::chambers::net_obs::apply(self), - Route::MistShore => self.mist.apply(), - Route::MirrorBasin => self.mirror.apply(), - Route::HallOfMasks => crate::chambers::hall::apply(self), - _ => {} + Route::MistShore => { + self.mist.apply(); + true + } + Route::MirrorBasin => { + self.mirror.apply(); + true + } + Route::HallOfMasks => { + crate::chambers::hall::apply(self); + true + } + _ => true, + }; + + if !applied { + return; + } + + // clear route-local pending only after successful apply path. + self.clear_pending_for(route); + + if route != Route::NetObservatory { + self.set_status("Applied", false); + } + } + + fn is_text_input_active(&self) -> bool { + match self.route { + Route::NetObservatory => self.net_obs.editing_field.is_some(), + Route::Archive => self.archive.searching, + _ => false, } - self.pending.retain(|p| p.chamber != route); - self.set_status("Applied", false); } fn revert_pending(&mut self) { @@ -501,6 +558,10 @@ impl SettingsApp { self.set_status("Defaults restored", false); } + pub fn clear_pending_for(&mut self, route: Route) { + self.pending.retain(|p| p.chamber != route); + } + fn pane_focus_up(&mut self) { self.clamp_pane_focus(); if self.pane_focus > 0 { diff --git a/shell/src/builtin/help.rs b/shell/src/builtin/help.rs index 548bbee6..2660d9d8 100644 --- a/shell/src/builtin/help.rs +++ b/shell/src/builtin/help.rs @@ -223,6 +223,15 @@ pub fn usage(cmd: &str) -> Option<&'static str> { "-f: immediate hard reset\n", "-p: intentional kernel panic (yes just because) then reset\n", ), + "netup" | "nicup" => concat!( + "netup — activate networking from userspace\n", + "\n", + "USAGE\n", + " netup\n", + "\n", + "Brings up NIC/driver on demand.\n", + "Boot remains offline by default until this is called.\n", + ), "true" => concat!( "true — return success\n", "\n", diff --git a/shell/src/builtin/mod.rs b/shell/src/builtin/mod.rs index be48126e..588ac56c 100644 --- a/shell/src/builtin/mod.rs +++ b/shell/src/builtin/mod.rs @@ -80,6 +80,7 @@ pub fn dispatch(argv: &[String], cwd: &str) -> Option { "sleep" => Some(proc_cmds::sleep(args)), "reboot" => Some(proc_cmds::reboot(args)), "shutdown" => Some(proc_cmds::shutdown(args)), + "netup" | "nicup" => Some(proc_cmds::netup(args)), _ => None, } @@ -131,6 +132,7 @@ pub fn dispatch_fb(argv: &[String], cwd: &str, fb: &Framebuffer, con: &mut Conso "sleep" => Some(proc_cmds::sleep_fb(args, fb, con)), "reboot" => Some(proc_cmds::reboot_fb(args, fb, con)), "shutdown" => Some(proc_cmds::shutdown_fb(args, fb, con)), + "netup" | "nicup" => Some(proc_cmds::netup_fb(args, fb, con)), _ => None, } @@ -371,6 +373,7 @@ const HELP_TEXT: &str = concat!( " sleep Sleep milliseconds\n", " reboot [-f] Reboot (graceful default)\n", " shutdown [-f|-p] Shutdown+reset (or panic+reset)\n", + " netup Activate networking now (alias: nicup)\n", "\n", "Operators:\n", " cmd1 | cmd2 Pipeline\n", diff --git a/shell/src/builtin/proc_cmds.rs b/shell/src/builtin/proc_cmds.rs index e0c63e3a..3d49ccdb 100644 --- a/shell/src/builtin/proc_cmds.rs +++ b/shell/src/builtin/proc_cmds.rs @@ -3,6 +3,7 @@ extern crate alloc; use alloc::format; use alloc::string::String; +use libmorpheus::net; use libmorpheus::process::{self, PsEntry}; use libmorpheus::sys::{self, SysInfo}; @@ -383,3 +384,68 @@ pub fn shutdown_fb(args: &[String], fb: &Framebuffer, con: &mut Console) -> i32 } } } + +pub fn netup(args: &[String]) -> i32 { + for arg in args { + match arg.as_str() { + "-h" | "--help" => { + libmorpheus::io::print( + "netup — activate networking from userspace\n\nUSAGE\n netup\n\nBrings up NIC/driver on demand. Boot stays offline by default.\n", + ); + return 0; + } + _ => { + libmorpheus::eprintln!("netup: unknown option: {}", arg); + return 1; + } + } + } + + match net::net_activate() { + Ok(rc) => { + if rc == 0 { + libmorpheus::println!("netup: networking activated"); + } else { + libmorpheus::println!("netup: networking already active"); + } + 0 + } + Err(e) => { + libmorpheus::eprintln!("netup: activation failed: 0x{:x}", e); + 1 + } + } +} + +pub fn netup_fb(args: &[String], fb: &Framebuffer, con: &mut Console) -> i32 { + for arg in args { + match arg.as_str() { + "-h" | "--help" => { + con.write_str( + fb, + "netup — activate networking from userspace\n\nUSAGE\n netup\n\nBrings up NIC/driver on demand. Boot stays offline by default.\n", + ); + return 0; + } + _ => { + con.write_colored(fb, &format!("netup: unknown option: {}\n", arg), (170, 0, 0)); + return 1; + } + } + } + + match net::net_activate() { + Ok(rc) => { + if rc == 0 { + con.write_str(fb, "netup: networking activated\n"); + } else { + con.write_str(fb, "netup: networking already active\n"); + } + 0 + } + Err(e) => { + con.write_colored(fb, &format!("netup: activation failed: 0x{:x}\n", e), (170, 0, 0)); + 1 + } + } +} From 9c4061933794a3dd7f905082c4b17af189019fd8 Mon Sep 17 00:00:00 2001 From: "T. Andrew Davis" Date: Thu, 12 Mar 2026 12:51:40 +0100 Subject: [PATCH 10/12] Refactor networking configuration and enhance DHCP handling - Updated the sys_net_cfg function to provide more specific error codes for network activation failures. - Enhanced the VirtioNetDriver initialization to prefer PCI modern transport, falling back to legacy MMIO if necessary. - Modified embedded relocation data to reflect updated values and sizes. - Refactored net_obs.rs to introduce a mode selector for DHCP and static IP configurations, improving user interaction. - Added detailed logging for DHCP requests and network activation status in the shell commands. - Improved error handling and user feedback in the networking settings and commands. --- bootloader/src/baremetal.rs | 118 +++++++++++++++++++++-- hwinit/src/syscall/handler/net.rs | 11 ++- network/src/boot/probe.rs | 20 +++- persistent/src/pe/embedded_reloc_data.rs | 117 +++++++++++----------- settings/src/chambers/net_obs.rs | 102 ++++++++++++++++---- shell/src/builtin/proc_cmds.rs | 90 +++++++++++++++++ 6 files changed, 369 insertions(+), 89 deletions(-) diff --git a/bootloader/src/baremetal.rs b/bootloader/src/baremetal.rs index 4c5b76bd..5cc19282 100644 --- a/bootloader/src/baremetal.rs +++ b/bootloader/src/baremetal.rs @@ -5,8 +5,8 @@ use core::sync::atomic::{AtomicBool, Ordering}; use morpheus_display::console::TextConsole; -use morpheus_network::device::UnifiedNetDevice; -use morpheus_network::driver::traits::NetworkDriver; +use morpheus_network::device::{NetworkDevice, UnifiedNetDevice}; +use morpheus_network::boot::{probe_and_create_driver, ProbeError, ProbeResult}; // TYPES THAT CROSS THE BORDER @@ -53,9 +53,64 @@ static mut LIVE_CONSOLE: Option = None; // userspace-activated networking state. offline by default until requested. static mut USER_NET_DRIVER: Option = None; -static mut USER_NET_DMA: Option = None; +static mut USER_NET_DMA: Option = None; static mut USER_NET_TSC_FREQ: u64 = 0; +unsafe fn log_pci_network_candidates() { + let mut found = 0u32; + for bus in 0..=255u8 { + for dev in 0..32u8 { + for func in 0..8u8 { + let addr = morpheus_hwinit::PciAddr::new(bus, dev, func); + let vendor = morpheus_hwinit::pci_cfg_read16(addr, 0x00); + if vendor == 0xFFFF { + if func == 0 { + break; + } + continue; + } + + let class = morpheus_hwinit::pci_cfg_read8(addr, 0x0B); + let subclass = morpheus_hwinit::pci_cfg_read8(addr, 0x0A); + if class != 0x02 { + if func == 0 { + let header = morpheus_hwinit::pci_cfg_read8(addr, 0x0E); + if (header & 0x80) == 0 { + break; + } + } + continue; + } + + let device = morpheus_hwinit::pci_cfg_read16(addr, 0x02); + found = found.wrapping_add(1); + morpheus_hwinit::serial::puts("[INFO] [NET] pci net candidate bdf="); + morpheus_hwinit::serial::put_hex64(((bus as u64) << 16) | ((dev as u64) << 8) | func as u64); + morpheus_hwinit::serial::puts(" ven="); + morpheus_hwinit::serial::put_hex64(vendor as u64); + morpheus_hwinit::serial::puts(" dev="); + morpheus_hwinit::serial::put_hex64(device as u64); + morpheus_hwinit::serial::puts(" sub="); + morpheus_hwinit::serial::put_hex64(subclass as u64); + morpheus_hwinit::serial::puts("\n"); + + if func == 0 { + let header = morpheus_hwinit::pci_cfg_read8(addr, 0x0E); + if (header & 0x80) == 0 { + break; + } + } + } + } + } + + if found == 0 { + morpheus_hwinit::serial::log_warn("NET", 953, "pci scan found zero class-0x02 devices"); + } else { + morpheus_hwinit::serial::log_info("NET", 954, "pci net candidates logged"); + } +} + #[inline] unsafe fn user_net_driver_mut() -> Option<&'static mut UnifiedNetDevice> { USER_NET_DRIVER.as_mut() @@ -116,22 +171,57 @@ unsafe fn user_net_ctrl(_cmd: u32, _arg: u64) -> i64 { } unsafe fn activate_network_from_userspace() -> i64 { + morpheus_hwinit::serial::log_info("NET", 940, "userspace activation requested"); + if USER_NET_DRIVER.is_some() { + morpheus_hwinit::serial::log_info("NET", 941, "already active"); return 1; } - let Some(dma) = USER_NET_DMA else { + let Some(dma) = USER_NET_DMA.as_ref() else { + morpheus_hwinit::serial::log_error("NET", 942, "activation failed: dma unavailable"); return -1; }; let tsc_freq = USER_NET_TSC_FREQ; - let driver = match UnifiedNetDevice::probe(&dma, tsc_freq) { - Ok(d) => d, - Err(_) => return -1, + morpheus_hwinit::serial::log_info("NET", 943, "probing NIC via network::probe_and_create_driver"); + let driver = match probe_and_create_driver(dma, tsc_freq) { + Ok(ProbeResult::VirtIO(v)) => { + morpheus_hwinit::serial::log_info("NET", 949, "probe selected virtio NIC"); + UnifiedNetDevice::VirtIO(v) + } + Ok(ProbeResult::Intel(i)) => { + morpheus_hwinit::serial::log_info("NET", 951, "probe selected intel NIC"); + UnifiedNetDevice::Intel(i) + } + Err(ProbeError::NoDevice) => { + morpheus_hwinit::serial::log_error("NET", 944, "probe failed: no supported NIC detected"); + log_pci_network_candidates(); + return -2; + } + Err(ProbeError::VirtioInitFailed) => { + morpheus_hwinit::serial::log_error("NET", 950, "virtio init failed"); + return -3; + } + Err(ProbeError::IntelInitFailed) => { + morpheus_hwinit::serial::log_error("NET", 952, "intel init failed"); + return -3; + } + Err(ProbeError::DeviceNotResponding) => { + morpheus_hwinit::serial::log_error("NET", 955, "nic mmio not responding"); + return -4; + } + Err(ProbeError::BarMappingFailed) => { + morpheus_hwinit::serial::log_error("NET", 956, "nic bar mapping failure"); + return -5; + } }; + morpheus_hwinit::serial::log_ok("NET", 945, "driver initialized"); + USER_NET_DRIVER = Some(driver); + morpheus_hwinit::serial::log_info("NET", 946, "registering NIC ops"); morpheus_hwinit::register_nic(morpheus_hwinit::NicOps { tx: Some(user_net_tx), rx: Some(user_net_rx), @@ -143,6 +233,14 @@ unsafe fn activate_network_from_userspace() -> i64 { // prime descriptor maintenance immediately. let _ = user_net_refill(); + + let link_now = user_net_link_up(); + if link_now != 0 { + morpheus_hwinit::serial::log_ok("NET", 947, "activation complete: link up"); + } else { + morpheus_hwinit::serial::log_info("NET", 948, "activation complete: link down"); + } + 0 } @@ -385,7 +483,11 @@ pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { }; // userspace network activation hook. we stay offline by default. - USER_NET_DMA = Some(*platform.dma()); + USER_NET_DMA = Some(morpheus_network::dma::DmaRegion::new( + platform.dma().cpu_base(), + platform.dma().bus_base(), + platform.dma().size(), + )); USER_NET_TSC_FREQ = platform.tsc_freq(); morpheus_hwinit::register_net_activation(activate_network_from_userspace); diff --git a/hwinit/src/syscall/handler/net.rs b/hwinit/src/syscall/handler/net.rs index 63d721be..4a01807a 100644 --- a/hwinit/src/syscall/handler/net.rs +++ b/hwinit/src/syscall/handler/net.rs @@ -616,10 +616,13 @@ pub unsafe fn sys_net_cfg(subcmd: u64, a2: u64, a3: u64, _a4: u64) -> u64 { NET_CFG_ACTIVATE => match NET_ACTIVATE_FN { Some(f) => { let rc = f(); - if rc < 0 { - EIO - } else { - rc as u64 + match rc { + 0.. => rc as u64, + -1 => EIO, + -2 => ENODEV, + -4 => EIO, + -5 => EFAULT, + _ => EIO, } } None => ENODEV, diff --git a/network/src/boot/probe.rs b/network/src/boot/probe.rs index 4968e934..ec5b8569 100644 --- a/network/src/boot/probe.rs +++ b/network/src/boot/probe.rs @@ -26,6 +26,8 @@ use crate::driver::intel::{ IntelNicInfo, }; use crate::driver::virtio::{VirtioConfig, VirtioInitError, VirtioNetDriver}; +use crate::driver::virtio::transport::{PciModernConfig, VirtioTransport}; +use crate::pci::capability::probe_virtio_caps; use crate::pci::config::{offset, pci_cfg_read16, pci_cfg_read32, PciAddr}; // ═══════════════════════════════════════════════════════════════════════════ @@ -235,8 +237,22 @@ pub unsafe fn probe_and_create_driver( buffer_size: 2048, }; - // Create driver - let driver = VirtioNetDriver::new(mmio_base, config)?; + // Prefer PCI modern transport; fallback to legacy MMIO for old QEMU. + let caps = probe_virtio_caps(pci_addr); + let driver = if caps.has_required() { + let pci_cfg = PciModernConfig { + common_cfg: caps.common_cfg_addr().unwrap_or(0), + notify_cfg: caps.notify_addr().unwrap_or(0), + notify_off_multiplier: caps.notify_multiplier(), + isr_cfg: caps.isr_addr().unwrap_or(0), + device_cfg: caps.device_cfg_addr().unwrap_or(0), + pci_cfg: 0, + }; + let transport = VirtioTransport::pci_modern(pci_cfg); + VirtioNetDriver::new_with_transport(transport, config, tsc_freq)? + } else { + VirtioNetDriver::new(mmio_base, config)? + }; Ok(ProbeResult::VirtIO(driver)) } } diff --git a/persistent/src/pe/embedded_reloc_data.rs b/persistent/src/pe/embedded_reloc_data.rs index a20a93cc..fa7ea13f 100644 --- a/persistent/src/pe/embedded_reloc_data.rs +++ b/persistent/src/pe/embedded_reloc_data.rs @@ -8,67 +8,74 @@ //! Run: ./tools/extract-reloc-data.sh after each build /// Original .reloc section RVA -pub const RELOC_RVA: u32 = 0x00614000; +pub const RELOC_RVA: u32 = 0x0061b000; /// Original .reloc section size -pub const RELOC_SIZE: u32 = 0x00000258; +pub const RELOC_SIZE: u32 = 0x000002a8; /// Original ImageBase from linker script pub const ORIGINAL_IMAGE_BASE: u64 = 0x0000004001000000; -/// Hardcoded .reloc section data (600 bytes) -/// Extracted from morpheus-bootloader.efi at file offset 0x001d6400 +/// Hardcoded .reloc section data (680 bytes) +/// Extracted from morpheus-bootloader.efi at file offset 0x001de000 #[allow(dead_code)] -pub const RELOC_DATA: [u8; 600] = [ - 0x00, 0x20, 0x05, 0x00, 0x34, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, - 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xf8, 0xa6, 0x10, 0xa7, - 0x28, 0xa7, 0xb8, 0xa7, 0xc0, 0xa8, 0xc8, 0xaa, 0xe0, 0xaa, 0xe8, 0xab, - 0xe8, 0xad, 0x00, 0xae, 0x18, 0xae, 0x30, 0xae, 0x48, 0xae, 0x60, 0xae, - 0x78, 0xae, 0x00, 0x00, 0x00, 0x30, 0x05, 0x00, 0x58, 0x00, 0x00, 0x00, - 0x90, 0xa0, 0xa8, 0xa0, 0xc0, 0xa0, 0x68, 0xa3, 0x80, 0xa3, 0x98, 0xa3, - 0xf8, 0xa4, 0x10, 0xa5, 0x28, 0xa5, 0x40, 0xa5, 0x58, 0xa5, 0x70, 0xa5, - 0xa8, 0xa5, 0xc0, 0xa5, 0xf0, 0xa5, 0x08, 0xa6, 0x60, 0xa6, 0x78, 0xa6, - 0xc8, 0xa6, 0xe0, 0xa6, 0xf8, 0xa6, 0x30, 0xa7, 0x60, 0xa7, 0x58, 0xa9, - 0x70, 0xa9, 0x08, 0xaa, 0x20, 0xaa, 0x38, 0xaa, 0x50, 0xaa, 0x68, 0xaa, - 0x78, 0xab, 0xd8, 0xab, 0x50, 0xac, 0x48, 0xad, 0x60, 0xad, 0x78, 0xad, - 0x90, 0xad, 0xa8, 0xad, 0xd8, 0xad, 0xf8, 0xad, 0x00, 0x40, 0x05, 0x00, - 0x54, 0x00, 0x00, 0x00, 0x40, 0xa3, 0x58, 0xa3, 0x70, 0xa3, 0x88, 0xa3, - 0xa0, 0xa3, 0xb8, 0xa3, 0xd8, 0xa4, 0xf0, 0xa4, 0x08, 0xa5, 0x80, 0xa5, - 0xb8, 0xa5, 0x28, 0xa8, 0x40, 0xa8, 0x58, 0xa8, 0x70, 0xa8, 0x88, 0xa8, - 0xa0, 0xa8, 0xb8, 0xa8, 0x08, 0xa9, 0x68, 0xa9, 0x78, 0xaa, 0xb8, 0xab, - 0x20, 0xac, 0x40, 0xac, 0x58, 0xac, 0x70, 0xac, 0x88, 0xac, 0xa0, 0xac, - 0xa8, 0xad, 0x18, 0xae, 0x30, 0xae, 0x48, 0xae, 0x60, 0xae, 0x78, 0xae, - 0x90, 0xae, 0xd0, 0xae, 0x60, 0xaf, 0x00, 0x00, 0x00, 0x50, 0x05, 0x00, - 0x4c, 0x00, 0x00, 0x00, 0xd0, 0xaa, 0x28, 0xac, 0x38, 0xac, 0x48, 0xac, - 0x58, 0xac, 0x68, 0xac, 0x78, 0xac, 0x88, 0xac, 0x98, 0xac, 0xa8, 0xac, - 0xb8, 0xac, 0xc8, 0xac, 0xd8, 0xac, 0xe8, 0xac, 0xf8, 0xac, 0x08, 0xad, - 0x18, 0xad, 0x28, 0xad, 0x38, 0xad, 0x48, 0xad, 0x58, 0xad, 0x68, 0xad, - 0x78, 0xad, 0x88, 0xad, 0x98, 0xad, 0xa8, 0xad, 0xb8, 0xad, 0xc8, 0xad, - 0xd8, 0xad, 0xe8, 0xad, 0xf8, 0xad, 0x08, 0xae, 0x18, 0xae, 0xf8, 0xaf, - 0x00, 0x70, 0x05, 0x00, 0x30, 0x00, 0x00, 0x00, 0xb8, 0xa1, 0xd0, 0xa1, - 0x20, 0xa2, 0x78, 0xa2, 0xe8, 0xa3, 0x00, 0xa4, 0x50, 0xa4, 0x60, 0xa4, - 0x70, 0xa4, 0x80, 0xa4, 0x90, 0xa4, 0xc0, 0xa4, 0xf8, 0xa4, 0x40, 0xa5, - 0x90, 0xa5, 0xc8, 0xa5, 0xe0, 0xa5, 0x28, 0xa6, 0xb0, 0xa6, 0x00, 0x00, - 0x00, 0x80, 0x05, 0x00, 0x54, 0x00, 0x00, 0x00, 0x68, 0xa3, 0x80, 0xa3, - 0xb8, 0xa3, 0xe8, 0xa3, 0x00, 0xa4, 0x18, 0xa4, 0x30, 0xa4, 0x48, 0xa4, - 0x60, 0xa4, 0x78, 0xa4, 0x90, 0xa4, 0xd0, 0xa4, 0xe8, 0xa4, 0x00, 0xa5, - 0x18, 0xa5, 0x30, 0xa5, 0x68, 0xa5, 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, - 0xe0, 0xa5, 0x18, 0xa6, 0x30, 0xa6, 0x48, 0xa6, 0x60, 0xa6, 0x78, 0xa6, - 0x90, 0xa6, 0x20, 0xa7, 0x68, 0xad, 0x78, 0xad, 0xa8, 0xad, 0xc0, 0xad, - 0x08, 0xae, 0x18, 0xae, 0x28, 0xae, 0x50, 0xae, 0x88, 0xae, 0x00, 0x00, - 0x00, 0x90, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, 0xc8, 0xa3, 0xd8, 0xa3, - 0xe8, 0xa3, 0xf8, 0xa3, 0x48, 0xa4, 0x58, 0xa4, 0x68, 0xa4, 0x78, 0xa4, - 0x88, 0xa4, 0xb0, 0xa4, 0xc0, 0xa4, 0xd0, 0xa4, 0x38, 0xa5, 0x48, 0xa5, - 0x58, 0xa5, 0xa0, 0xa5, 0xb0, 0xa5, 0xe8, 0xa5, 0xf8, 0xa5, 0x20, 0xa6, - 0x30, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xd8, 0xa6, 0x48, 0xad, 0x60, 0xad, - 0x98, 0xad, 0xe8, 0xad, 0x30, 0xae, 0x40, 0xae, 0x40, 0xaf, 0x58, 0xaf, - 0x90, 0xaf, 0xa8, 0xaf, 0xc0, 0xaf, 0xd8, 0xaf, 0x00, 0xa0, 0x05, 0x00, - 0x28, 0x00, 0x00, 0x00, 0x48, 0xa0, 0x20, 0xa1, 0x78, 0xa1, 0x90, 0xa1, - 0xa8, 0xa1, 0xc0, 0xa1, 0xd8, 0xa1, 0xf0, 0xa1, 0x08, 0xa2, 0x98, 0xa2, - 0x48, 0xa3, 0x60, 0xa3, 0x78, 0xa3, 0xe8, 0xa3, 0x18, 0xa4, 0x00, 0x00, - 0x00, 0xb0, 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x30, 0xa2, 0x38, 0xa2, - 0x00, 0x50, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x80, 0xa2, 0xc8, 0xae, - 0x00, 0x80, 0x1d, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0xa5, 0x00, 0x00, - 0x00, 0x30, 0x61, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, +pub const RELOC_DATA: [u8; 680] = [ + 0x00, 0x80, 0x05, 0x00, 0x38, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, + 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xd8, 0xa6, 0xf0, 0xa6, + 0x08, 0xa7, 0x60, 0xa7, 0x78, 0xa7, 0x90, 0xa7, 0x20, 0xa8, 0x28, 0xa9, + 0x30, 0xab, 0x48, 0xab, 0x50, 0xac, 0x28, 0xad, 0x40, 0xad, 0x58, 0xad, + 0x70, 0xad, 0x88, 0xad, 0xa0, 0xad, 0xb8, 0xad, 0x00, 0x90, 0x05, 0x00, + 0x44, 0x00, 0x00, 0x00, 0x00, 0xa3, 0x18, 0xa3, 0x30, 0xa3, 0xf8, 0xa6, + 0x10, 0xa7, 0x68, 0xa7, 0x80, 0xa7, 0xd0, 0xa7, 0xe8, 0xa7, 0x00, 0xa8, + 0x38, 0xa8, 0x68, 0xa8, 0x60, 0xaa, 0x78, 0xaa, 0x10, 0xab, 0x28, 0xab, + 0x40, 0xab, 0x58, 0xab, 0x70, 0xab, 0x80, 0xac, 0xe0, 0xac, 0x58, 0xad, + 0x50, 0xae, 0x68, 0xae, 0x80, 0xae, 0x98, 0xae, 0xb0, 0xae, 0xe0, 0xae, + 0x00, 0xaf, 0x00, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, + 0x48, 0xa4, 0x60, 0xa4, 0x78, 0xa4, 0x90, 0xa4, 0xa8, 0xa4, 0xc0, 0xa4, + 0xe0, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0x88, 0xa6, 0xc0, 0xa6, 0x30, 0xa9, + 0x48, 0xa9, 0x60, 0xa9, 0x78, 0xa9, 0x90, 0xa9, 0xa8, 0xa9, 0xc0, 0xa9, + 0x10, 0xaa, 0x70, 0xaa, 0x80, 0xab, 0xc0, 0xac, 0x28, 0xad, 0x48, 0xad, + 0x60, 0xad, 0x78, 0xad, 0x90, 0xad, 0xa8, 0xad, 0xb0, 0xae, 0x20, 0xaf, + 0x38, 0xaf, 0x50, 0xaf, 0x68, 0xaf, 0x80, 0xaf, 0x98, 0xaf, 0xd8, 0xaf, + 0x00, 0xb0, 0x05, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x68, 0xa0, 0xd8, 0xab, + 0x30, 0xad, 0x40, 0xad, 0x50, 0xad, 0x60, 0xad, 0x70, 0xad, 0x80, 0xad, + 0x90, 0xad, 0xa0, 0xad, 0xb0, 0xad, 0xc0, 0xad, 0xd0, 0xad, 0xe0, 0xad, + 0xf0, 0xad, 0x00, 0xae, 0x10, 0xae, 0x20, 0xae, 0x30, 0xae, 0x40, 0xae, + 0x50, 0xae, 0x60, 0xae, 0x70, 0xae, 0x80, 0xae, 0x90, 0xae, 0xa0, 0xae, + 0xb0, 0xae, 0xc0, 0xae, 0xd0, 0xae, 0xe0, 0xae, 0xf0, 0xae, 0x00, 0xaf, + 0x10, 0xaf, 0x20, 0xaf, 0x00, 0xc0, 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x00, 0xa1, 0x00, 0x00, 0x00, 0xd0, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, + 0xc0, 0xa2, 0xd8, 0xa2, 0x28, 0xa3, 0x80, 0xa3, 0x00, 0xe0, 0x05, 0x00, + 0x74, 0x00, 0x00, 0x00, 0xe8, 0xa0, 0x00, 0xa1, 0x38, 0xa1, 0x68, 0xa1, + 0x80, 0xa1, 0x98, 0xa1, 0xb0, 0xa1, 0xc8, 0xa1, 0xe0, 0xa1, 0xf8, 0xa1, + 0x10, 0xa2, 0x50, 0xa2, 0x68, 0xa2, 0x80, 0xa2, 0x98, 0xa2, 0xb0, 0xa2, + 0xe8, 0xa2, 0x18, 0xa3, 0x30, 0xa3, 0x48, 0xa3, 0x60, 0xa3, 0x98, 0xa3, + 0xb0, 0xa3, 0xc8, 0xa3, 0xe0, 0xa3, 0xf8, 0xa3, 0x10, 0xa4, 0xa0, 0xa4, + 0xe0, 0xa4, 0xf8, 0xa4, 0x58, 0xa5, 0x70, 0xa5, 0x88, 0xa5, 0xa0, 0xa5, + 0xb8, 0xa5, 0xd0, 0xa5, 0x08, 0xa6, 0xb8, 0xa6, 0xd0, 0xa6, 0x20, 0xa7, + 0x30, 0xa7, 0x40, 0xa7, 0x50, 0xa7, 0x60, 0xa7, 0x90, 0xa7, 0xc8, 0xa7, + 0x10, 0xa8, 0x60, 0xa8, 0x98, 0xa8, 0xb0, 0xa8, 0xf8, 0xa8, 0x80, 0xa9, + 0xd0, 0xaf, 0xe0, 0xaf, 0x00, 0xf0, 0x05, 0x00, 0x4c, 0x00, 0x00, 0x00, + 0x10, 0xa0, 0x28, 0xa0, 0x70, 0xa0, 0x80, 0xa0, 0x90, 0xa0, 0xb8, 0xa0, + 0xf0, 0xa0, 0x30, 0xa6, 0x40, 0xa6, 0x50, 0xa6, 0x60, 0xa6, 0xb0, 0xa6, + 0xc0, 0xa6, 0xd0, 0xa6, 0xe0, 0xa6, 0xf0, 0xa6, 0x18, 0xa7, 0x28, 0xa7, + 0x38, 0xa7, 0xa0, 0xa7, 0xb0, 0xa7, 0xc0, 0xa7, 0x08, 0xa8, 0x18, 0xa8, + 0x50, 0xa8, 0x60, 0xa8, 0x88, 0xa8, 0x98, 0xa8, 0xd0, 0xa8, 0xe8, 0xa8, + 0x40, 0xa9, 0xb0, 0xaf, 0xc8, 0xaf, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, + 0x74, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x50, 0xa0, 0x98, 0xa0, 0xa8, 0xa0, + 0xc8, 0xa1, 0xa0, 0xa2, 0xf8, 0xa2, 0x10, 0xa3, 0x28, 0xa3, 0x40, 0xa3, + 0x58, 0xa3, 0x70, 0xa3, 0x88, 0xa3, 0x18, 0xa4, 0xc8, 0xa4, 0xe0, 0xa4, + 0xf8, 0xa4, 0x68, 0xa5, 0x98, 0xa5, 0x60, 0xa6, 0x98, 0xa6, 0xb0, 0xa6, + 0x78, 0xa7, 0x90, 0xa7, 0x40, 0xa8, 0x58, 0xa8, 0xa8, 0xa8, 0xb8, 0xa8, + 0xe8, 0xa8, 0x20, 0xa9, 0x30, 0xa9, 0x80, 0xa9, 0x90, 0xa9, 0xd8, 0xa9, + 0xe8, 0xa9, 0x78, 0xaa, 0x88, 0xaa, 0xc0, 0xaa, 0xd0, 0xaa, 0x00, 0xab, + 0x10, 0xab, 0x28, 0xab, 0x68, 0xab, 0x78, 0xab, 0x90, 0xab, 0xe8, 0xab, + 0xf8, 0xab, 0x10, 0xac, 0x28, 0xac, 0x40, 0xac, 0x78, 0xac, 0x90, 0xac, + 0xa8, 0xac, 0xc0, 0xac, 0x00, 0x20, 0x06, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0xd8, 0xab, 0xe0, 0xab, 0x00, 0xc0, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x00, + 0x40, 0xac, 0x00, 0x00, 0x00, 0xd0, 0x1c, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x88, 0xa8, 0x90, 0xa8, 0x98, 0xa8, 0x00, 0x00, 0x00, 0xf0, 0x1d, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0xd0, 0xae, 0x00, 0x00, 0x00, 0xa0, 0x61, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, ]; diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs index b116d166..0f9b6724 100644 --- a/settings/src/chambers/net_obs.rs +++ b/settings/src/chambers/net_obs.rs @@ -9,17 +9,19 @@ use crate::widgets; use libmorpheus::net; // editable field indices -const FIELD_DHCP_TOGGLE: usize = 0; -const FIELD_HOSTNAME: usize = 1; -const FIELD_IP: usize = 2; -const FIELD_PREFIX: usize = 3; -const FIELD_GATEWAY: usize = 4; -const FIELD_DNS1: usize = 5; -const FIELD_DNS2: usize = 6; -const FIELD_APPLY: usize = 7; -const FIELD_ACTIVATE: usize = 8; -const FIELD_REFRESH: usize = 9; -const FIELD_COUNT: usize = 10; +const FIELD_MODE_DHCP: usize = 0; +const FIELD_MODE_STATIC: usize = 1; +const FIELD_DHCP_REQUEST: usize = 2; +const FIELD_HOSTNAME: usize = 3; +const FIELD_IP: usize = 4; +const FIELD_PREFIX: usize = 5; +const FIELD_GATEWAY: usize = 6; +const FIELD_DNS1: usize = 7; +const FIELD_DNS2: usize = 8; +const FIELD_APPLY: usize = 9; +const FIELD_ACTIVATE: usize = 10; +const FIELD_REFRESH: usize = 11; +const FIELD_COUNT: usize = 12; pub struct NetObsChamber { // live state from kernel @@ -187,10 +189,48 @@ impl NetObsChamber { pub fn activate(app: &mut SettingsApp, idx: usize) { match idx { - FIELD_DHCP_TOGGLE => { - app.net_obs.edit_dhcp = !app.net_obs.edit_dhcp; + FIELD_MODE_DHCP => { + app.net_obs.edit_dhcp = true; app.mark_edited(Route::NetObservatory, "dhcp"); } + FIELD_MODE_STATIC => { + app.net_obs.edit_dhcp = false; + app.mark_edited(Route::NetObservatory, "dhcp"); + } + FIELD_DHCP_REQUEST => { + // DHCP can take a few polls to settle; drive it inline so UI reflects results now. + app.net_obs.edit_dhcp = true; + libmorpheus::io::print("[settings/net] dhcp request requested\n"); + match net::net_dhcp() { + Ok(()) => { + for _ in 0..128 { + let _ = net::nic_refill(); + let _ = net::net_poll_drive(0); + } + app.net_obs.refresh(); + + let mut ip_buf = [0u8; 16]; + let ip_len = widgets::format_ip(app.net_obs.ip, &mut ip_buf); + let ip = core::str::from_utf8(&ip_buf[..ip_len]).unwrap_or("0.0.0.0"); + if app.net_obs.ip != 0 { + app.set_status("DHCP requested (lease info refreshed)", false); + libmorpheus::println!( + "[settings/net] dhcp lease ip={} gw=0x{:08x} dns1=0x{:08x}", + ip, + app.net_obs.gateway, + app.net_obs.dns1 + ); + } else { + app.set_status("DHCP requested (no lease yet)", true); + libmorpheus::println!("[settings/net] dhcp no lease yet"); + } + } + Err(e) => { + libmorpheus::println!("[settings/net] dhcp request failed err=0x{:x}", e); + app.set_status("DHCP request failed", true); + } + } + } FIELD_HOSTNAME | FIELD_IP | FIELD_GATEWAY | FIELD_DNS1 | FIELD_DNS2 => { app.net_obs.editing_field = Some(idx); } @@ -204,13 +244,18 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { app.mark_edited(Route::NetObservatory, "prefix"); } FIELD_APPLY => { + libmorpheus::io::print("[settings/net] apply requested\n"); if apply(app) { app.clear_pending_for(Route::NetObservatory); app.net_obs.editing_field = None; + libmorpheus::io::print("[settings/net] apply succeeded\n"); + } else { + libmorpheus::io::print("[settings/net] apply failed\n"); } } FIELD_ACTIVATE => match net::net_activate() { Ok(rc) => { + libmorpheus::println!("[settings/net] activation rc={}", rc); app.net_obs.refresh(); if rc == 0 { app.set_status("Networking activated", false); @@ -218,11 +263,13 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { app.set_status("Networking already active", false); } } - Err(_) => { + Err(e) => { + libmorpheus::println!("[settings/net] activation failed err=0x{:x}", e); app.set_status("Networking activation failed", true); } }, FIELD_REFRESH => { + libmorpheus::io::print("[settings/net] refresh requested\n"); app.net_obs.refresh(); app.set_status("Network refreshed", false); } @@ -381,15 +428,27 @@ pub fn render(app: &SettingsApp) { layout::draw_kv(app, px, cy, "State:", state_str, state_color); cy += r8; + // explicit activation control — always visible near top. + layout::draw_button_row(app, px, cy, "Activate Networking", FIELD_ACTIVATE, t.warning); + cy += r8; + // configuration section layout::draw_section(app, px, cy, "Configuration"); cy += r4; - // DHCP toggle - let dhcp_label = if net.edit_dhcp { "[X] DHCP" } else { "[ ] Static" }; - layout::draw_button_row(app, px, cy, dhcp_label, FIELD_DHCP_TOGGLE, t.glyph); + // mode selector + explicit DHCP action + let dhcp_label = if net.edit_dhcp { "[X] Mode: DHCP" } else { "[ ] Mode: DHCP" }; + let static_label = if net.edit_dhcp { "[ ] Mode: Static" } else { "[X] Mode: Static" }; + layout::draw_button_row(app, px, cy, dhcp_label, FIELD_MODE_DHCP, t.glyph); + cy += r8; + layout::draw_button_row(app, px, cy, static_label, FIELD_MODE_STATIC, t.glyph); cy += r8; + if net.edit_dhcp { + layout::draw_button_row(app, px, cy, "Request IP (DHCP)", FIELD_DHCP_REQUEST, t.warning); + cy += r8; + } + // hostname let hn = core::str::from_utf8(&net.edit_hostname[..net.edit_hostname_len]).unwrap_or(""); let hn_display = if hn.is_empty() { "(none)" } else { hn }; @@ -442,14 +501,16 @@ pub fn render(app: &SettingsApp) { let d1_len = widgets::format_ip(net.dns1, &mut d1_buf); let d1_str = core::str::from_utf8(&d1_buf[..d1_len]).unwrap_or("0.0.0.0"); layout::draw_kv(app, px, cy, "DNS:", d1_str, t.immutable); + cy += r4; + + let dhcp_flag = if (net.flags & net::NET_FLAG_DHCP) != 0 { "yes" } else { "no" }; + layout::draw_kv(app, px, cy, "DHCP Active:", dhcp_flag, t.telemetry); cy += r8; } // action buttons layout::draw_button_row(app, px, cy, "Apply Network Config", FIELD_APPLY, t.signal); cy += r8; - layout::draw_button_row(app, px, cy, "Activate Networking", FIELD_ACTIVATE, t.warning); - cy += r8; layout::draw_button_row(app, px, cy, "Refresh", FIELD_REFRESH, t.glyph); cy += r12; @@ -518,7 +579,8 @@ fn draw_editable_field(app: &SettingsApp, x: u32, y: u32, label: &str, value: &s fn field_name(idx: usize) -> &'static str { match idx { - FIELD_DHCP_TOGGLE => "dhcp", + FIELD_MODE_DHCP | FIELD_MODE_STATIC => "dhcp", + FIELD_DHCP_REQUEST => "dhcp_request", FIELD_HOSTNAME => "hostname", FIELD_IP => "ip", FIELD_PREFIX => "prefix", diff --git a/shell/src/builtin/proc_cmds.rs b/shell/src/builtin/proc_cmds.rs index 3d49ccdb..5a7e3e41 100644 --- a/shell/src/builtin/proc_cmds.rs +++ b/shell/src/builtin/proc_cmds.rs @@ -401,6 +401,25 @@ pub fn netup(args: &[String]) -> i32 { } } + match net::nic_info() { + Ok(info) => { + libmorpheus::println!( + "netup: pre-state present={} link={} mac={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + info.present, + info.link_up, + info.mac[0], + info.mac[1], + info.mac[2], + info.mac[3], + info.mac[4], + info.mac[5] + ); + } + Err(e) => { + libmorpheus::eprintln!("netup: nic_info pre-check failed: 0x{:x}", e); + } + } + match net::net_activate() { Ok(rc) => { if rc == 0 { @@ -408,6 +427,25 @@ pub fn netup(args: &[String]) -> i32 { } else { libmorpheus::println!("netup: networking already active"); } + + match net::nic_info() { + Ok(info) => { + libmorpheus::println!( + "netup: post-state present={} link={} mac={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + info.present, + info.link_up, + info.mac[0], + info.mac[1], + info.mac[2], + info.mac[3], + info.mac[4], + info.mac[5] + ); + } + Err(e) => { + libmorpheus::eprintln!("netup: nic_info post-check failed: 0x{:x}", e); + } + } 0 } Err(e) => { @@ -434,6 +472,32 @@ pub fn netup_fb(args: &[String], fb: &Framebuffer, con: &mut Console) -> i32 { } } + match net::nic_info() { + Ok(info) => { + con.write_str( + fb, + &format!( + "netup: pre-state present={} link={} mac={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}\n", + info.present, + info.link_up, + info.mac[0], + info.mac[1], + info.mac[2], + info.mac[3], + info.mac[4], + info.mac[5] + ), + ); + } + Err(e) => { + con.write_colored( + fb, + &format!("netup: nic_info pre-check failed: 0x{:x}\n", e), + (170, 0, 0), + ); + } + } + match net::net_activate() { Ok(rc) => { if rc == 0 { @@ -441,6 +505,32 @@ pub fn netup_fb(args: &[String], fb: &Framebuffer, con: &mut Console) -> i32 { } else { con.write_str(fb, "netup: networking already active\n"); } + + match net::nic_info() { + Ok(info) => { + con.write_str( + fb, + &format!( + "netup: post-state present={} link={} mac={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}\n", + info.present, + info.link_up, + info.mac[0], + info.mac[1], + info.mac[2], + info.mac[3], + info.mac[4], + info.mac[5] + ), + ); + } + Err(e) => { + con.write_colored( + fb, + &format!("netup: nic_info post-check failed: 0x{:x}\n", e), + (170, 0, 0), + ); + } + } 0 } Err(e) => { From 0ca5bbe585ecf8db90037ee0ace8b2f04b0f0306 Mon Sep 17 00:00:00 2001 From: "T. Andrew Davis" Date: Thu, 12 Mar 2026 14:29:31 +0100 Subject: [PATCH 11/12] feat(network): enhance DHCP handling and network stack integration - Added `stack_available` field to `NetObsChamber` to track network stack status. - Improved DHCP request handling in `run_dhcp_request` function, including better error handling for NIC registration. - Refactored DHCP request logic in `activate` and `apply` functions to use the new `run_dhcp_request` function. - Introduced new network activation module in the bootloader, including functions for network configuration, TCP, and UDP operations. - Implemented user-space network activation with probing for NIC drivers and registering network stack operations. - Added functions for managing TCP and UDP sockets, including sending, receiving, and DNS query handling. - Enhanced state management for network operations, including dynamic allocation of socket handles and DNS queries. - Updated mouse input handling in `state.rs` to detect button changes more accurately. --- bootloader/src/baremetal.rs | 203 +----------------- bootloader/src/baremetal_ops/mod.rs | 1 + .../src/baremetal_ops/network/activate.rs | 115 ++++++++++ .../src/baremetal_ops/network/config.rs | 86 ++++++++ bootloader/src/baremetal_ops/network/mod.rs | 13 ++ bootloader/src/baremetal_ops/network/nic.rs | 113 ++++++++++ bootloader/src/baremetal_ops/network/state.rs | 203 ++++++++++++++++++ bootloader/src/baremetal_ops/network/tcp.rs | 182 ++++++++++++++++ .../src/baremetal_ops/network/udp_dns.rs | 150 +++++++++++++ bootloader/src/main.rs | 1 + bootloader/src/tui/mouse.rs | 24 +++ compd/src/islands/input.rs | 2 +- hwinit/src/mouse.rs | 20 +- network/src/stack/interface.rs | 189 +++++++++++++--- network/src/stack/mod.rs | 2 + persistent/src/pe/embedded_reloc_data.rs | 161 ++++++++------ settings/src/chambers/net_obs.rs | 161 ++++++++++---- settings/src/state.rs | 3 +- 18 files changed, 1289 insertions(+), 340 deletions(-) create mode 100644 bootloader/src/baremetal_ops/mod.rs create mode 100644 bootloader/src/baremetal_ops/network/activate.rs create mode 100644 bootloader/src/baremetal_ops/network/config.rs create mode 100644 bootloader/src/baremetal_ops/network/mod.rs create mode 100644 bootloader/src/baremetal_ops/network/nic.rs create mode 100644 bootloader/src/baremetal_ops/network/state.rs create mode 100644 bootloader/src/baremetal_ops/network/tcp.rs create mode 100644 bootloader/src/baremetal_ops/network/udp_dns.rs diff --git a/bootloader/src/baremetal.rs b/bootloader/src/baremetal.rs index 5cc19282..5bc20eb8 100644 --- a/bootloader/src/baremetal.rs +++ b/bootloader/src/baremetal.rs @@ -5,8 +5,8 @@ use core::sync::atomic::{AtomicBool, Ordering}; use morpheus_display::console::TextConsole; -use morpheus_network::device::{NetworkDevice, UnifiedNetDevice}; -use morpheus_network::boot::{probe_and_create_driver, ProbeError, ProbeResult}; + +use crate::baremetal_ops::network; // TYPES THAT CROSS THE BORDER @@ -51,199 +51,6 @@ static BAREMETAL_MODE: AtomicBool = AtomicBool::new(false); static mut LIVE_CONSOLE: Option = None; -// userspace-activated networking state. offline by default until requested. -static mut USER_NET_DRIVER: Option = None; -static mut USER_NET_DMA: Option = None; -static mut USER_NET_TSC_FREQ: u64 = 0; - -unsafe fn log_pci_network_candidates() { - let mut found = 0u32; - for bus in 0..=255u8 { - for dev in 0..32u8 { - for func in 0..8u8 { - let addr = morpheus_hwinit::PciAddr::new(bus, dev, func); - let vendor = morpheus_hwinit::pci_cfg_read16(addr, 0x00); - if vendor == 0xFFFF { - if func == 0 { - break; - } - continue; - } - - let class = morpheus_hwinit::pci_cfg_read8(addr, 0x0B); - let subclass = morpheus_hwinit::pci_cfg_read8(addr, 0x0A); - if class != 0x02 { - if func == 0 { - let header = morpheus_hwinit::pci_cfg_read8(addr, 0x0E); - if (header & 0x80) == 0 { - break; - } - } - continue; - } - - let device = morpheus_hwinit::pci_cfg_read16(addr, 0x02); - found = found.wrapping_add(1); - morpheus_hwinit::serial::puts("[INFO] [NET] pci net candidate bdf="); - morpheus_hwinit::serial::put_hex64(((bus as u64) << 16) | ((dev as u64) << 8) | func as u64); - morpheus_hwinit::serial::puts(" ven="); - morpheus_hwinit::serial::put_hex64(vendor as u64); - morpheus_hwinit::serial::puts(" dev="); - morpheus_hwinit::serial::put_hex64(device as u64); - morpheus_hwinit::serial::puts(" sub="); - morpheus_hwinit::serial::put_hex64(subclass as u64); - morpheus_hwinit::serial::puts("\n"); - - if func == 0 { - let header = morpheus_hwinit::pci_cfg_read8(addr, 0x0E); - if (header & 0x80) == 0 { - break; - } - } - } - } - } - - if found == 0 { - morpheus_hwinit::serial::log_warn("NET", 953, "pci scan found zero class-0x02 devices"); - } else { - morpheus_hwinit::serial::log_info("NET", 954, "pci net candidates logged"); - } -} - -#[inline] -unsafe fn user_net_driver_mut() -> Option<&'static mut UnifiedNetDevice> { - USER_NET_DRIVER.as_mut() -} - -unsafe fn user_net_tx(frame: *const u8, len: usize) -> i64 { - let Some(driver) = user_net_driver_mut() else { - return -1; - }; - let frame = core::slice::from_raw_parts(frame, len); - if driver.transmit(frame).is_ok() { - 0 - } else { - -1 - } -} - -unsafe fn user_net_rx(buf: *mut u8, buf_len: usize) -> i64 { - let Some(driver) = user_net_driver_mut() else { - return -1; - }; - let buf = core::slice::from_raw_parts_mut(buf, buf_len); - match driver.receive(buf) { - Ok(Some(n)) => n as i64, - Ok(None) => 0, - Err(_) => -1, - } -} - -unsafe fn user_net_link_up() -> i64 { - let Some(driver) = user_net_driver_mut() else { - return 0; - }; - driver.link_up() as i64 -} - -unsafe fn user_net_mac(out: *mut u8) -> i64 { - let Some(driver) = user_net_driver_mut() else { - return -1; - }; - let mac = driver.mac_address(); - core::ptr::copy_nonoverlapping(mac.as_ptr(), out, 6); - 0 -} - -unsafe fn user_net_refill() -> i64 { - let Some(driver) = user_net_driver_mut() else { - return -1; - }; - driver.refill_rx_queue(); - driver.collect_tx_completions(); - 0 -} - -unsafe fn user_net_ctrl(_cmd: u32, _arg: u64) -> i64 { - // full NIC control routing can be added later per-driver. - -1 -} - -unsafe fn activate_network_from_userspace() -> i64 { - morpheus_hwinit::serial::log_info("NET", 940, "userspace activation requested"); - - if USER_NET_DRIVER.is_some() { - morpheus_hwinit::serial::log_info("NET", 941, "already active"); - return 1; - } - - let Some(dma) = USER_NET_DMA.as_ref() else { - morpheus_hwinit::serial::log_error("NET", 942, "activation failed: dma unavailable"); - return -1; - }; - let tsc_freq = USER_NET_TSC_FREQ; - - morpheus_hwinit::serial::log_info("NET", 943, "probing NIC via network::probe_and_create_driver"); - let driver = match probe_and_create_driver(dma, tsc_freq) { - Ok(ProbeResult::VirtIO(v)) => { - morpheus_hwinit::serial::log_info("NET", 949, "probe selected virtio NIC"); - UnifiedNetDevice::VirtIO(v) - } - Ok(ProbeResult::Intel(i)) => { - morpheus_hwinit::serial::log_info("NET", 951, "probe selected intel NIC"); - UnifiedNetDevice::Intel(i) - } - Err(ProbeError::NoDevice) => { - morpheus_hwinit::serial::log_error("NET", 944, "probe failed: no supported NIC detected"); - log_pci_network_candidates(); - return -2; - } - Err(ProbeError::VirtioInitFailed) => { - morpheus_hwinit::serial::log_error("NET", 950, "virtio init failed"); - return -3; - } - Err(ProbeError::IntelInitFailed) => { - morpheus_hwinit::serial::log_error("NET", 952, "intel init failed"); - return -3; - } - Err(ProbeError::DeviceNotResponding) => { - morpheus_hwinit::serial::log_error("NET", 955, "nic mmio not responding"); - return -4; - } - Err(ProbeError::BarMappingFailed) => { - morpheus_hwinit::serial::log_error("NET", 956, "nic bar mapping failure"); - return -5; - } - }; - - morpheus_hwinit::serial::log_ok("NET", 945, "driver initialized"); - - USER_NET_DRIVER = Some(driver); - - morpheus_hwinit::serial::log_info("NET", 946, "registering NIC ops"); - morpheus_hwinit::register_nic(morpheus_hwinit::NicOps { - tx: Some(user_net_tx), - rx: Some(user_net_rx), - link_up: Some(user_net_link_up), - mac: Some(user_net_mac), - refill: Some(user_net_refill), - ctrl: Some(user_net_ctrl), - }); - - // prime descriptor maintenance immediately. - let _ = user_net_refill(); - - let link_now = user_net_link_up(); - if link_now != 0 { - morpheus_hwinit::serial::log_ok("NET", 947, "activation complete: link up"); - } else { - morpheus_hwinit::serial::log_info("NET", 948, "activation complete: link down"); - } - - 0 -} - /// Called by the hwinit serial hook for every byte emitted via `puts()`. /// Writes directly to the TextConsole backed by the GOP framebuffer. pub unsafe fn live_console_putc(b: u8) { @@ -483,13 +290,11 @@ pub unsafe fn enter_baremetal(config: BaremetalEntryConfig) -> ! { }; // userspace network activation hook. we stay offline by default. - USER_NET_DMA = Some(morpheus_network::dma::DmaRegion::new( + network::init_userspace_network_activation(morpheus_network::dma::DmaRegion::new( platform.dma().cpu_base(), platform.dma().bus_base(), platform.dma().size(), - )); - USER_NET_TSC_FREQ = platform.tsc_freq(); - morpheus_hwinit::register_net_activation(activate_network_from_userspace); + ), platform.tsc_freq()); // persistent storage — try to mount a real block device crate::storage::init_persistent_storage(platform.dma(), platform.tsc_freq()); diff --git a/bootloader/src/baremetal_ops/mod.rs b/bootloader/src/baremetal_ops/mod.rs new file mode 100644 index 00000000..a61610bd --- /dev/null +++ b/bootloader/src/baremetal_ops/mod.rs @@ -0,0 +1 @@ +pub mod network; diff --git a/bootloader/src/baremetal_ops/network/activate.rs b/bootloader/src/baremetal_ops/network/activate.rs new file mode 100644 index 00000000..5935e12b --- /dev/null +++ b/bootloader/src/baremetal_ops/network/activate.rs @@ -0,0 +1,115 @@ +use morpheus_network::boot::{probe_and_create_driver, ProbeError, ProbeResult}; +use morpheus_network::device::UnifiedNetDevice; +use morpheus_network::stack::{NetConfig, NetInterface}; + +use super::{config, nic, state, tcp, udp_dns}; + +unsafe fn activate_network_from_userspace() -> i64 { + morpheus_hwinit::serial::log_info("NET", 940, "userspace activation requested"); + + if state::has_driver() { + morpheus_hwinit::serial::log_info("NET", 941, "already active"); + return 1; + } + + let Some((dma, tsc_freq)) = state::activation_context() else { + morpheus_hwinit::serial::log_error("NET", 942, "activation failed: dma unavailable"); + return -1; + }; + + morpheus_hwinit::serial::log_info("NET", 943, "probing NIC via network::probe_and_create_driver"); + let driver = match probe_and_create_driver(dma, tsc_freq) { + Ok(ProbeResult::VirtIO(v)) => { + morpheus_hwinit::serial::log_info("NET", 949, "probe selected virtio NIC"); + UnifiedNetDevice::VirtIO(v) + } + Ok(ProbeResult::Intel(i)) => { + morpheus_hwinit::serial::log_info("NET", 951, "probe selected intel NIC"); + UnifiedNetDevice::Intel(i) + } + Err(ProbeError::NoDevice) => { + morpheus_hwinit::serial::log_error("NET", 944, "probe failed: no supported NIC detected"); + nic::log_pci_network_candidates(); + return -2; + } + Err(ProbeError::VirtioInitFailed) => { + morpheus_hwinit::serial::log_error("NET", 950, "virtio init failed"); + return -3; + } + Err(ProbeError::IntelInitFailed) => { + morpheus_hwinit::serial::log_error("NET", 952, "intel init failed"); + return -3; + } + Err(ProbeError::DeviceNotResponding) => { + morpheus_hwinit::serial::log_error("NET", 955, "nic mmio not responding"); + return -4; + } + Err(ProbeError::BarMappingFailed) => { + morpheus_hwinit::serial::log_error("NET", 956, "nic bar mapping failure"); + return -5; + } + }; + + morpheus_hwinit::serial::log_ok("NET", 945, "driver initialized"); + + let stack = NetInterface::new(driver, NetConfig::dhcp()); + state::clear_net_handle_tables(); + state::set_stack(stack); + + morpheus_hwinit::serial::log_info("NET", 946, "registering NIC ops"); + morpheus_hwinit::register_nic(morpheus_hwinit::NicOps { + tx: Some(nic::user_net_tx), + rx: Some(nic::user_net_rx), + link_up: Some(nic::user_net_link_up), + mac: Some(nic::user_net_mac), + refill: Some(nic::user_net_refill), + ctrl: Some(nic::user_net_ctrl), + }); + + morpheus_hwinit::serial::log_info("NET", 957, "registering net stack ops"); + morpheus_hwinit::register_net_stack(morpheus_hwinit::NetStackOps { + tcp_socket: Some(tcp::net_tcp_socket_impl), + tcp_connect: Some(tcp::net_tcp_connect_impl), + tcp_send: Some(tcp::net_tcp_send_impl), + tcp_recv: Some(tcp::net_tcp_recv_impl), + tcp_close: Some(tcp::net_tcp_close_impl), + tcp_state: Some(tcp::net_tcp_state_impl), + tcp_listen: Some(tcp::net_tcp_listen_impl), + tcp_accept: Some(tcp::net_tcp_accept_impl), + tcp_shutdown: Some(tcp::net_tcp_shutdown_impl), + tcp_nodelay: Some(tcp::net_tcp_nodelay_impl), + tcp_keepalive: Some(tcp::net_tcp_keepalive_impl), + udp_socket: Some(udp_dns::net_udp_socket_impl), + udp_send_to: Some(udp_dns::net_udp_send_to_impl), + udp_recv_from: Some(udp_dns::net_udp_recv_from_impl), + udp_close: Some(udp_dns::net_udp_close_impl), + dns_start: Some(udp_dns::net_dns_start_impl), + dns_result: Some(udp_dns::net_dns_result_impl), + dns_set_servers: Some(udp_dns::net_dns_set_servers_impl), + cfg_get: Some(config::net_cfg_get), + cfg_dhcp: Some(config::net_cfg_dhcp), + cfg_static_ip: Some(config::net_cfg_static_ip), + cfg_hostname: Some(config::net_cfg_hostname), + poll_drive: Some(config::net_poll_drive), + poll_stats: Some(config::net_poll_stats), + }); + + let _ = nic::user_net_refill(); + + let link_now = nic::user_net_link_up(); + if link_now != 0 { + morpheus_hwinit::serial::log_ok("NET", 947, "activation complete: link up"); + } else { + morpheus_hwinit::serial::log_info("NET", 948, "activation complete: link down"); + } + + 0 +} + +pub(super) unsafe fn init_userspace_network_activation( + dma: morpheus_network::dma::DmaRegion, + tsc_freq: u64, +) { + state::set_activation_context(dma, tsc_freq); + morpheus_hwinit::register_net_activation(activate_network_from_userspace); +} diff --git a/bootloader/src/baremetal_ops/network/config.rs b/bootloader/src/baremetal_ops/network/config.rs new file mode 100644 index 00000000..f085b07f --- /dev/null +++ b/bootloader/src/baremetal_ops/network/config.rs @@ -0,0 +1,86 @@ +use morpheus_network::stack::NetState; + +use super::state; + +pub(super) unsafe fn net_cfg_get(buf: *mut u8) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + + let out = &mut *(buf as *mut morpheus_hwinit::NetConfigInfo); + core::ptr::write_bytes( + out as *mut _ as *mut u8, + 0, + core::mem::size_of::(), + ); + + out.state = match stack.state() { + NetState::Unconfigured => 0, + NetState::DhcpDiscovering => 1, + NetState::Ready => 2, + NetState::Error => 3, + }; + + out.flags |= 1; + + if let Some(ip) = stack.ipv4_addr() { + out.ipv4_addr = u32::from_be_bytes(ip.octets()); + out.prefix_len = 24; + } + if let Some(gw) = stack.gateway() { + out.gateway = u32::from_be_bytes(gw.octets()); + out.flags |= 1 << 1; + } + if let Some(dns) = stack.dns() { + out.dns_primary = u32::from_be_bytes(dns.octets()); + out.flags |= 1 << 2; + } + + let mac = stack.mac_address(); + out.mac[..6].copy_from_slice(&mac); + out.mtu = 1500; + state::write_hostname_to(out); + + 0 +} + +pub(super) unsafe fn net_cfg_dhcp() -> i64 { + if state::user_net_stack_mut().is_some() { + 0 + } else { + -1 + } +} + +pub(super) unsafe fn net_cfg_static_ip(_ip: u32, _prefix_len: u8, _gateway: u32) -> i64 { + -1 +} + +pub(super) unsafe fn net_cfg_hostname(name: *const u8, len: usize) -> i64 { + state::set_hostname(name, len) +} + +pub(super) unsafe fn net_poll_drive(timestamp_ms: u64) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + + stack.device_mut().refill_rx_queue(); + let activity = stack.poll(timestamp_ms); + stack.device_mut().collect_tx_completions(); + if activity { 1 } else { 0 } +} + +pub(super) unsafe fn net_poll_stats(buf: *mut u8) -> i64 { + if state::user_net_stack_mut().is_none() { + return -1; + } + let out = &mut *(buf as *mut morpheus_hwinit::NetStats); + core::ptr::write_bytes( + out as *mut _ as *mut u8, + 0, + core::mem::size_of::(), + ); + out.tcp_active = state::tcp_active_count(); + 0 +} diff --git a/bootloader/src/baremetal_ops/network/mod.rs b/bootloader/src/baremetal_ops/network/mod.rs new file mode 100644 index 00000000..dd0f3ce3 --- /dev/null +++ b/bootloader/src/baremetal_ops/network/mod.rs @@ -0,0 +1,13 @@ +mod activate; +mod config; +mod nic; +mod state; +mod tcp; +mod udp_dns; + +pub unsafe fn init_userspace_network_activation( + dma: morpheus_network::dma::DmaRegion, + tsc_freq: u64, +) { + activate::init_userspace_network_activation(dma, tsc_freq); +} diff --git a/bootloader/src/baremetal_ops/network/nic.rs b/bootloader/src/baremetal_ops/network/nic.rs new file mode 100644 index 00000000..918659f2 --- /dev/null +++ b/bootloader/src/baremetal_ops/network/nic.rs @@ -0,0 +1,113 @@ +use morpheus_network::device::NetworkDevice; + +use super::state; + +pub(super) unsafe fn log_pci_network_candidates() { + let mut found = 0u32; + for bus in 0..=255u8 { + for dev in 0..32u8 { + for func in 0..8u8 { + let addr = morpheus_hwinit::PciAddr::new(bus, dev, func); + let vendor = morpheus_hwinit::pci_cfg_read16(addr, 0x00); + if vendor == 0xFFFF { + if func == 0 { + break; + } + continue; + } + + let class = morpheus_hwinit::pci_cfg_read8(addr, 0x0B); + let subclass = morpheus_hwinit::pci_cfg_read8(addr, 0x0A); + if class != 0x02 { + if func == 0 { + let header = morpheus_hwinit::pci_cfg_read8(addr, 0x0E); + if (header & 0x80) == 0 { + break; + } + } + continue; + } + + let device = morpheus_hwinit::pci_cfg_read16(addr, 0x02); + found = found.wrapping_add(1); + morpheus_hwinit::serial::puts("[INFO] [NET] pci net candidate bdf="); + morpheus_hwinit::serial::put_hex64( + ((bus as u64) << 16) | ((dev as u64) << 8) | func as u64, + ); + morpheus_hwinit::serial::puts(" ven="); + morpheus_hwinit::serial::put_hex64(vendor as u64); + morpheus_hwinit::serial::puts(" dev="); + morpheus_hwinit::serial::put_hex64(device as u64); + morpheus_hwinit::serial::puts(" sub="); + morpheus_hwinit::serial::put_hex64(subclass as u64); + morpheus_hwinit::serial::puts("\n"); + + if func == 0 { + let header = morpheus_hwinit::pci_cfg_read8(addr, 0x0E); + if (header & 0x80) == 0 { + break; + } + } + } + } + } + + if found == 0 { + morpheus_hwinit::serial::log_warn("NET", 953, "pci scan found zero class-0x02 devices"); + } else { + morpheus_hwinit::serial::log_info("NET", 954, "pci net candidates logged"); + } +} + +pub(super) unsafe fn user_net_tx(frame: *const u8, len: usize) -> i64 { + let Some(driver) = state::user_net_driver_mut() else { + return -1; + }; + let frame = core::slice::from_raw_parts(frame, len); + if driver.transmit(frame).is_ok() { + 0 + } else { + -1 + } +} + +pub(super) unsafe fn user_net_rx(buf: *mut u8, buf_len: usize) -> i64 { + let Some(driver) = state::user_net_driver_mut() else { + return -1; + }; + let buf = core::slice::from_raw_parts_mut(buf, buf_len); + match driver.receive(buf) { + Ok(Some(n)) => n as i64, + Ok(None) => 0, + Err(_) => -1, + } +} + +pub(super) unsafe fn user_net_link_up() -> i64 { + let Some(driver) = state::user_net_driver_mut() else { + return 0; + }; + driver.link_up() as i64 +} + +pub(super) unsafe fn user_net_mac(out: *mut u8) -> i64 { + let Some(driver) = state::user_net_driver_mut() else { + return -1; + }; + let mac = driver.mac_address(); + core::ptr::copy_nonoverlapping(mac.as_ptr(), out, 6); + 0 +} + +pub(super) unsafe fn user_net_refill() -> i64 { + let Some(driver) = state::user_net_driver_mut() else { + return -1; + }; + driver.refill_rx_queue(); + driver.collect_tx_completions(); + 0 +} + +pub(super) unsafe fn user_net_ctrl(_cmd: u32, _arg: u64) -> i64 { + -1 +} diff --git a/bootloader/src/baremetal_ops/network/state.rs b/bootloader/src/baremetal_ops/network/state.rs new file mode 100644 index 00000000..2c625a55 --- /dev/null +++ b/bootloader/src/baremetal_ops/network/state.rs @@ -0,0 +1,203 @@ +use core::net::Ipv4Addr; + +use morpheus_network::device::UnifiedNetDevice; +use morpheus_network::stack::{DnsQueryHandle, NetInterface, SocketHandle}; + +pub(super) static mut USER_NET_DRIVER: Option = None; +pub(super) static mut USER_NET_STACK: Option> = None; +static mut USER_NET_DMA: Option = None; +static mut USER_NET_TSC_FREQ: u64 = 0; +static mut USER_NET_HOSTNAME: [u8; 64] = [0; 64]; +static mut USER_NET_HOSTNAME_LEN: usize = 0; + +const MAX_TCP_HANDLES: usize = 128; +const MAX_UDP_HANDLES: usize = 128; +const MAX_DNS_QUERIES: usize = 64; + +static mut USER_TCP_HANDLES: [Option; MAX_TCP_HANDLES] = [None; MAX_TCP_HANDLES]; +static mut USER_UDP_HANDLES: [Option; MAX_UDP_HANDLES] = [None; MAX_UDP_HANDLES]; +static mut USER_DNS_QUERIES: [Option; MAX_DNS_QUERIES] = [None; MAX_DNS_QUERIES]; + +#[inline(always)] +pub(super) fn ip_from_nbo(ip: u32) -> Ipv4Addr { + let [a, b, c, d] = ip.to_be_bytes(); + Ipv4Addr::new(a, b, c, d) +} + +#[inline(always)] +pub(super) fn ip_to_nbo(ip: Ipv4Addr) -> u32 { + u32::from_be_bytes(ip.octets()) +} + +#[inline(always)] +fn slot_to_user_handle(slot: usize) -> i64 { + (slot as i64) + 1 +} + +#[inline(always)] +fn user_handle_to_slot(handle: i64, max: usize) -> Option { + if handle <= 0 { + return None; + } + let idx = (handle - 1) as usize; + if idx < max { + Some(idx) + } else { + None + } +} + +pub(super) unsafe fn set_activation_context(dma: morpheus_network::dma::DmaRegion, tsc_freq: u64) { + USER_NET_DMA = Some(dma); + USER_NET_TSC_FREQ = tsc_freq; +} + +pub(super) unsafe fn activation_context() -> Option<(&'static morpheus_network::dma::DmaRegion, u64)> { + let dma = USER_NET_DMA.as_ref()?; + Some((dma, USER_NET_TSC_FREQ)) +} + +pub(super) unsafe fn user_net_driver_mut() -> Option<&'static mut UnifiedNetDevice> { + if let Some(stack) = USER_NET_STACK.as_mut() { + return Some(stack.device_mut()); + } + USER_NET_DRIVER.as_mut() +} + +pub(super) unsafe fn user_net_stack_mut() -> Option<&'static mut NetInterface> { + USER_NET_STACK.as_mut() +} + +pub(super) unsafe fn set_stack(stack: NetInterface) { + USER_NET_STACK = Some(stack); + USER_NET_DRIVER = None; +} + +pub(super) unsafe fn has_driver() -> bool { + USER_NET_DRIVER.is_some() +} + +pub(super) unsafe fn clear_net_handle_tables() { + USER_TCP_HANDLES.fill(None); + USER_UDP_HANDLES.fill(None); + USER_DNS_QUERIES.fill(None); +} + +pub(super) unsafe fn alloc_tcp_slot(handle: SocketHandle) -> Option { + for idx in 0..MAX_TCP_HANDLES { + if USER_TCP_HANDLES[idx].is_none() { + USER_TCP_HANDLES[idx] = Some(handle); + return Some(slot_to_user_handle(idx)); + } + } + None +} + +pub(super) unsafe fn get_tcp_slot(handle: i64) -> Option { + let idx = user_handle_to_slot(handle, MAX_TCP_HANDLES)?; + USER_TCP_HANDLES[idx] +} + +pub(super) unsafe fn take_tcp_slot(handle: i64) -> Option { + let idx = user_handle_to_slot(handle, MAX_TCP_HANDLES)?; + USER_TCP_HANDLES[idx].take() +} + +pub(super) unsafe fn set_tcp_slot(handle: i64, socket: SocketHandle) -> bool { + let Some(idx) = user_handle_to_slot(handle, MAX_TCP_HANDLES) else { + return false; + }; + USER_TCP_HANDLES[idx] = Some(socket); + true +} + +pub(super) unsafe fn tcp_active_count() -> u32 { + USER_TCP_HANDLES.iter().filter(|h| h.is_some()).count() as u32 +} + +pub(super) unsafe fn alloc_udp_slot(handle: SocketHandle) -> Option { + for idx in 0..MAX_UDP_HANDLES { + if USER_UDP_HANDLES[idx].is_none() { + USER_UDP_HANDLES[idx] = Some(handle); + return Some(slot_to_user_handle(idx)); + } + } + None +} + +pub(super) unsafe fn get_udp_slot(handle: i64) -> Option { + let idx = user_handle_to_slot(handle, MAX_UDP_HANDLES)?; + USER_UDP_HANDLES[idx] +} + +pub(super) unsafe fn take_udp_slot(handle: i64) -> Option { + let idx = user_handle_to_slot(handle, MAX_UDP_HANDLES)?; + USER_UDP_HANDLES[idx].take() +} + +pub(super) unsafe fn alloc_dns_query_slot(handle: DnsQueryHandle) -> Option { + for idx in 0..MAX_DNS_QUERIES { + if USER_DNS_QUERIES[idx].is_none() { + USER_DNS_QUERIES[idx] = Some(handle); + return Some(slot_to_user_handle(idx)); + } + } + None +} + +pub(super) unsafe fn get_dns_query_slot(handle: i64) -> Option { + let idx = user_handle_to_slot(handle, MAX_DNS_QUERIES)?; + USER_DNS_QUERIES[idx] +} + +pub(super) unsafe fn clear_dns_query_slot(handle: i64) { + if let Some(idx) = user_handle_to_slot(handle, MAX_DNS_QUERIES) { + USER_DNS_QUERIES[idx] = None; + } +} + +pub(super) unsafe fn set_hostname(name: *const u8, len: usize) -> i64 { + if name.is_null() || len == 0 || len > 63 { + return -1; + } + USER_NET_HOSTNAME_LEN = len; + core::ptr::copy_nonoverlapping(name, USER_NET_HOSTNAME.as_mut_ptr(), len); + USER_NET_HOSTNAME[len] = 0; + 0 +} + +pub(super) unsafe fn write_hostname_to(out: &mut morpheus_hwinit::NetConfigInfo) { + if USER_NET_HOSTNAME_LEN > 0 { + let n = USER_NET_HOSTNAME_LEN.min(63); + out.hostname[..n].copy_from_slice(&USER_NET_HOSTNAME[..n]); + out.hostname[n] = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn handle_slot_roundtrip() { + for i in 0..32usize { + let h = slot_to_user_handle(i); + assert_eq!(user_handle_to_slot(h, 64), Some(i)); + } + } + + #[test] + fn handle_slot_rejects_invalid_values() { + assert_eq!(user_handle_to_slot(0, 8), None); + assert_eq!(user_handle_to_slot(-5, 8), None); + assert_eq!(user_handle_to_slot(999, 8), None); + } + + #[test] + fn ipv4_nbo_roundtrip() { + let ip = Ipv4Addr::new(10, 0, 2, 15); + let nbo = ip_to_nbo(ip); + assert_eq!(nbo, 0x0A00_020F); + assert_eq!(ip_from_nbo(nbo), ip); + } +} diff --git a/bootloader/src/baremetal_ops/network/tcp.rs b/bootloader/src/baremetal_ops/network/tcp.rs new file mode 100644 index 00000000..4c0bb9d5 --- /dev/null +++ b/bootloader/src/baremetal_ops/network/tcp.rs @@ -0,0 +1,182 @@ +use super::state; + +pub(super) unsafe fn net_tcp_socket_impl() -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + + let Ok(socket) = stack.tcp_socket() else { + return -1; + }; + + if let Some(handle) = state::alloc_tcp_slot(socket) { + handle + } else { + stack.remove_socket(socket); + -1 + } +} + +pub(super) unsafe fn net_tcp_connect_impl(handle: i64, ip: u32, port: u16) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_tcp_slot(handle) else { + return -1; + }; + + if stack.tcp_connect(socket, state::ip_from_nbo(ip), port).is_ok() { + 0 + } else { + -1 + } +} + +pub(super) unsafe fn net_tcp_send_impl(handle: i64, buf: *const u8, len: usize) -> i64 { + if len == 0 { + return 0; + } + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_tcp_slot(handle) else { + return -1; + }; + + let data = core::slice::from_raw_parts(buf, len); + match stack.tcp_send(socket, data) { + Ok(n) => n as i64, + Err(_) => -1, + } +} + +pub(super) unsafe fn net_tcp_recv_impl(handle: i64, buf: *mut u8, len: usize) -> i64 { + if len == 0 { + return 0; + } + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_tcp_slot(handle) else { + return -1; + }; + + let data = core::slice::from_raw_parts_mut(buf, len); + match stack.tcp_recv(socket, data) { + Ok(n) => n as i64, + Err(_) => -1, + } +} + +pub(super) unsafe fn net_tcp_close_impl(handle: i64) { + let Some(stack) = state::user_net_stack_mut() else { + return; + }; + let Some(socket) = state::take_tcp_slot(handle) else { + return; + }; + + stack.tcp_close(socket); + stack.remove_socket(socket); +} + +pub(super) unsafe fn net_tcp_state_impl(handle: i64) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_tcp_slot(handle) else { + return -1; + }; + stack.tcp_state_code(socket) as i64 +} + +pub(super) unsafe fn net_tcp_listen_impl(handle: i64, port: u16) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_tcp_slot(handle) else { + return -1; + }; + + if stack.tcp_listen(socket, port).is_ok() { + 0 + } else { + -1 + } +} + +pub(super) unsafe fn net_tcp_accept_impl(listen_handle: i64) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(listen_socket) = state::get_tcp_slot(listen_handle) else { + return -1; + }; + + let state_code = stack.tcp_state_code(listen_socket); + if state_code != 4 && state_code != 7 { + return -1; + } + + let Some(local_port) = stack.tcp_local_port(listen_socket) else { + return -1; + }; + + let Ok(new_listen_socket) = stack.tcp_socket() else { + return -1; + }; + if stack.tcp_listen(new_listen_socket, local_port).is_err() { + stack.remove_socket(new_listen_socket); + return -1; + } + + if !state::set_tcp_slot(listen_handle, new_listen_socket) { + stack.remove_socket(new_listen_socket); + return -1; + } + + state::alloc_tcp_slot(listen_socket).unwrap_or_else(|| { + stack.tcp_close(listen_socket); + stack.remove_socket(listen_socket); + -1 + }) +} + +pub(super) unsafe fn net_tcp_shutdown_impl(handle: i64) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_tcp_slot(handle) else { + return -1; + }; + + if stack.tcp_shutdown(socket).is_ok() { + 0 + } else { + -1 + } +} + +pub(super) unsafe fn net_tcp_nodelay_impl(handle: i64, on: i64) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_tcp_slot(handle) else { + return -1; + }; + + stack.tcp_set_nodelay(socket, on != 0); + 0 +} + +pub(super) unsafe fn net_tcp_keepalive_impl(handle: i64, ms: u64) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_tcp_slot(handle) else { + return -1; + }; + + stack.tcp_set_keepalive(socket, ms); + 0 +} diff --git a/bootloader/src/baremetal_ops/network/udp_dns.rs b/bootloader/src/baremetal_ops/network/udp_dns.rs new file mode 100644 index 00000000..5d3d4419 --- /dev/null +++ b/bootloader/src/baremetal_ops/network/udp_dns.rs @@ -0,0 +1,150 @@ +use core::net::Ipv4Addr; + +use super::state; + +pub(super) unsafe fn net_udp_socket_impl() -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + + let Ok(socket) = stack.udp_socket() else { + return -1; + }; + + if let Some(handle) = state::alloc_udp_slot(socket) { + handle + } else { + stack.remove_socket(socket); + -1 + } +} + +pub(super) unsafe fn net_udp_send_to_impl( + handle: i64, + dest_ip: u32, + dest_port: u16, + buf: *const u8, + len: usize, +) -> i64 { + if len == 0 { + return 0; + } + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_udp_slot(handle) else { + return -1; + }; + + let data = core::slice::from_raw_parts(buf, len); + match stack.udp_send_to(socket, state::ip_from_nbo(dest_ip), dest_port, data) { + Ok(n) => n as i64, + Err(_) => -1, + } +} + +pub(super) unsafe fn net_udp_recv_from_impl( + handle: i64, + buf: *mut u8, + len: usize, + src_out: *mut u8, +) -> i64 { + if len == 0 { + return 0; + } + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(socket) = state::get_udp_slot(handle) else { + return -1; + }; + + let data = core::slice::from_raw_parts_mut(buf, len); + match stack.udp_recv_from(socket, data) { + Ok((n, ip, port)) => { + let ip_nbo = state::ip_to_nbo(ip); + core::ptr::copy_nonoverlapping((&ip_nbo as *const u32).cast::(), src_out, 4); + core::ptr::copy_nonoverlapping((&port as *const u16).cast::(), src_out.add(4), 2); + core::ptr::write_bytes(src_out.add(6), 0, 2); + n as i64 + } + Err(_) => -1, + } +} + +pub(super) unsafe fn net_udp_close_impl(handle: i64) { + let Some(stack) = state::user_net_stack_mut() else { + return; + }; + let Some(socket) = state::take_udp_slot(handle) else { + return; + }; + + stack.remove_socket(socket); +} + +pub(super) unsafe fn net_dns_start_impl(name: *const u8, len: usize) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + if name.is_null() || len == 0 { + return -1; + } + + let name_bytes = core::slice::from_raw_parts(name, len); + let Ok(hostname) = core::str::from_utf8(name_bytes) else { + return -1; + }; + + let Ok(query) = stack.start_dns_query(hostname) else { + return -1; + }; + + state::alloc_dns_query_slot(query).unwrap_or(-1) +} + +pub(super) unsafe fn net_dns_result_impl(query: i64, out: *mut u8) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + let Some(query_handle) = state::get_dns_query_slot(query) else { + return -1; + }; + + match stack.get_dns_result(query_handle) { + Ok(Some(ip)) => { + let nbo = state::ip_to_nbo(ip); + core::ptr::copy_nonoverlapping((&nbo as *const u32).cast::(), out, 4); + state::clear_dns_query_slot(query); + 0 + } + Ok(None) => 1, + Err(_) => { + state::clear_dns_query_slot(query); + -1 + } + } +} + +pub(super) unsafe fn net_dns_set_servers_impl(servers: *const u32, count: usize) -> i64 { + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + if servers.is_null() || count == 0 { + return -1; + } + + let server_nbo = core::slice::from_raw_parts(servers, count); + let mut server_list = [Ipv4Addr::new(0, 0, 0, 0); 4]; + let mut used = 0usize; + for ip in server_nbo.iter().take(server_list.len()) { + server_list[used] = state::ip_from_nbo(*ip); + used += 1; + } + + if stack.set_dns_servers(&server_list[..used]).is_ok() { + 0 + } else { + -1 + } +} diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index f1839389..624019c8 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -15,6 +15,7 @@ extern crate alloc; use core::panic::PanicInfo; mod baremetal; +mod baremetal_ops; mod bsod; mod storage; mod tui; diff --git a/bootloader/src/tui/mouse.rs b/bootloader/src/tui/mouse.rs index a719d0e9..5dd2d829 100644 --- a/bootloader/src/tui/mouse.rs +++ b/bootloader/src/tui/mouse.rs @@ -117,6 +117,20 @@ impl Mouse { self.fill = 0; let status = self.buf[0]; + let raw_dx = self.buf[1]; + let raw_dy = self.buf[2]; + + // bit3-only sync is too weak when stale bytes exist at boot. + // reject packets whose sign bits cannot describe the data bytes. + if !packet_sign_bits_match(status, raw_dx, raw_dy) { + self.desync_count = self.desync_count.saturating_add(1); + if self.desync_count >= RESYNC_THRESHOLD { + self.desync_count = 0; + } + return None; + } + + self.desync_count = 0; let buttons = status & 0x07; // 9-bit sign extension using status byte sign bits. @@ -151,6 +165,16 @@ impl Mouse { } } +#[inline(always)] +fn packet_sign_bits_match(status: u8, dx: u8, dy: u8) -> bool { + // If overflow is set, data bytes are not reliable for sign checks. + let x_overflow = (status & 0x40) != 0; + let y_overflow = (status & 0x80) != 0; + let x_ok = x_overflow || (((status & 0x10) != 0) == ((dx & 0x80) != 0)); + let y_ok = y_overflow || (((status & 0x20) != 0) == ((dy & 0x80) != 0)); + x_ok && y_ok +} + /// Send a command byte to the mouse (via 0xD4 → aux port), wait for ACK. unsafe fn mouse_cmd(byte: u8) -> u8 { asm_ps2_write_cmd(0xD4); diff --git a/compd/src/islands/input.rs b/compd/src/islands/input.rs index 668d69f1..c4967f4e 100644 --- a/compd/src/islands/input.rs +++ b/compd/src/islands/input.rs @@ -58,7 +58,7 @@ fn poll_keyboard(state: &mut CompState) { fn poll_mouse(state: &mut CompState) { let ms = hw::mouse_read(); - if ms.dx == 0 && ms.dy == 0 && ms.buttons == 0 { + if ms.dx == 0 && ms.dy == 0 && ms.buttons == state.last_buttons { return; } diff --git a/hwinit/src/mouse.rs b/hwinit/src/mouse.rs index f69c5378..206bf047 100644 --- a/hwinit/src/mouse.rs +++ b/hwinit/src/mouse.rs @@ -3,17 +3,27 @@ //! The desktop event loop (producer) calls `accumulate(dx, dy, buttons)`. //! `SYS_MOUSE_READ` (consumer) atomically drains the accumulated state. -use core::sync::atomic::{AtomicI32, AtomicU8, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering}; static DX: AtomicI32 = AtomicI32::new(0); static DY: AtomicI32 = AtomicI32::new(0); static BUTTONS: AtomicU8 = AtomicU8::new(0); +static BUTTON_EDGE_PENDING: AtomicBool = AtomicBool::new(false); +static BUTTON_EDGE_SAMPLE: AtomicU8 = AtomicU8::new(0); /// Accumulate relative motion from a PS/2 mouse packet. pub fn accumulate(dx: i16, dy: i16, buttons: u8) { DX.fetch_add(dx as i32, Ordering::Relaxed); DY.fetch_add(dy as i32, Ordering::Relaxed); - BUTTONS.store(buttons, Ordering::Relaxed); + + // Latch the first button transition until userspace drains it. + // This avoids losing fast press+release clicks between poll ticks. + let prev = BUTTONS.swap(buttons, Ordering::Relaxed); + if buttons != prev { + if !BUTTON_EDGE_PENDING.swap(true, Ordering::AcqRel) { + BUTTON_EDGE_SAMPLE.store(buttons, Ordering::Relaxed); + } + } } /// Atomically drain accumulated mouse state. Returns (dx, dy, buttons). @@ -21,6 +31,10 @@ pub fn accumulate(dx: i16, dy: i16, buttons: u8) { pub fn drain() -> (i32, i32, u8) { let dx = DX.swap(0, Ordering::Relaxed); let dy = DY.swap(0, Ordering::Relaxed); - let buttons = BUTTONS.load(Ordering::Relaxed); + let buttons = if BUTTON_EDGE_PENDING.swap(false, Ordering::AcqRel) { + BUTTON_EDGE_SAMPLE.load(Ordering::Relaxed) + } else { + BUTTONS.load(Ordering::Relaxed) + }; (dx, dy, buttons) } diff --git a/network/src/stack/interface.rs b/network/src/stack/interface.rs index 13352c47..9201c87c 100644 --- a/network/src/stack/interface.rs +++ b/network/src/stack/interface.rs @@ -67,6 +67,10 @@ use smoltcp::socket::dns::{GetQueryResultError, Socket as DnsSocket}; use smoltcp::socket::tcp::{ Socket as TcpSocket, SocketBuffer as TcpSocketBuffer, State as TcpState, }; +use smoltcp::socket::udp::{ + PacketBuffer as UdpPacketBuffer, PacketMetadata as UdpPacketMetadata, Socket as UdpSocket, +}; +use smoltcp::time::Duration; use smoltcp::time::Instant; use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, IpEndpoint, Ipv4Address, Ipv4Cidr}; @@ -142,6 +146,12 @@ pub const TCP_RX_BUFFER_SIZE: usize = 65535; /// TCP transmit buffer size. pub const TCP_TX_BUFFER_SIZE: usize = 65535; +/// UDP packet metadata entry count. +pub const UDP_PACKET_META_COUNT: usize = 8; + +/// UDP payload storage per direction. +pub const UDP_PACKET_DATA_BYTES: usize = 8192; + /// Full network interface with IP stack. /// /// Wraps a `NetworkDevice` with complete smoltcp integration. @@ -205,11 +215,8 @@ impl NetInterface { super::set_debug_stage(16); // Stage 16: SocketSet created super::debug_log(16, "SocketSet created"); - // Default DNS servers (Cloudflare and Google) - let default_dns_servers: &[IpAddress] = &[ - IpAddress::v4(1, 1, 1, 1), // Cloudflare - IpAddress::v4(8, 8, 8, 8), // Google - ]; + // Keep this list to one entry to match small DNS_MAX_SERVER_COUNT configs. + let default_dns_servers: &[IpAddress] = &[IpAddress::v4(1, 1, 1, 1)]; // Create DNS socket with default servers super::set_debug_stage(17); // Stage 17: about to create DNS socket @@ -324,6 +331,23 @@ impl NetInterface { }) } + /// Override DNS servers for the interface. + pub fn set_dns_servers(&mut self, servers: &[Ipv4Addr]) -> Result<()> { + if servers.is_empty() { + return Err(NetworkError::DnsResolutionFailed); + } + + let mut list: Vec = Vec::with_capacity(servers.len()); + for server in servers { + list.push(IpAddress::Ipv4(Ipv4Address::from_bytes(&server.octets()))); + } + + let dns_socket = self.sockets.get_mut::(self.dns_handle); + dns_socket.update_servers(&list); + self.dns = Some(Ipv4Address::from_bytes(&servers[0].octets())); + Ok(()) + } + /// Check DNS query result. Returns Ok(Some(ip)) if resolved, Ok(None) if pending, Err if failed. pub fn get_dns_result( &mut self, @@ -392,29 +416,15 @@ impl NetInterface { self.gateway = Some(router); } - // Update DNS servers: DHCP-provided first, then real-world fallbacks - // This ensures QEMU/virtual DNS works while keeping real DNS for hardware - let mut dns_addrs: Vec = - dns_servers.iter().map(|a| IpAddress::Ipv4(*a)).collect(); - - // Add real-world DNS servers as fallbacks (for real hardware) - // Only add if not already present from DHCP - let cloudflare = IpAddress::v4(1, 1, 1, 1); - let google = IpAddress::v4(8, 8, 8, 8); - if !dns_addrs.contains(&cloudflare) { - dns_addrs.push(cloudflare); - } - if !dns_addrs.contains(&google) { - dns_addrs.push(google); - } - - // Update DNS socket with combined servers + // Update DNS server with a single preferred entry to avoid + // panics when DNS_MAX_SERVER_COUNT is configured to 1. + let primary_dns = dns_servers + .first() + .copied() + .unwrap_or(Ipv4Address::new(1, 1, 1, 1)); let dns_socket = self.sockets.get_mut::(self.dns_handle); - dns_socket.update_servers(&dns_addrs); - - if !dns_servers.is_empty() { - self.dns = Some(dns_servers[0]); - } + dns_socket.update_servers(&[IpAddress::Ipv4(primary_dns)]); + self.dns = Some(primary_dns); self.state = NetState::Ready; super::debug_log(31, "DHCP state -> Ready"); @@ -511,6 +521,113 @@ impl NetInterface { socket.close(); } + /// Start listening for inbound TCP on `port`. + pub fn tcp_listen(&mut self, handle: SocketHandle, port: u16) -> Result<()> { + let socket = self.sockets.get_mut::(handle); + socket.listen(port).map_err(|_| NetworkError::ConnectionFailed) + } + + /// Returns true if the socket is currently in listening state. + pub fn tcp_is_listening(&self, handle: SocketHandle) -> bool { + let socket = self.sockets.get::(handle); + socket.is_listening() + } + + /// Returns local port if bound. + pub fn tcp_local_port(&self, handle: SocketHandle) -> Option { + let socket = self.sockets.get::(handle); + socket.local_endpoint().map(|ep| ep.port) + } + + /// Half-close TCP (write side). + pub fn tcp_shutdown(&mut self, handle: SocketHandle) -> Result<()> { + let socket = self.sockets.get_mut::(handle); + socket.close(); + Ok(()) + } + + /// Toggle Nagle algorithm. `on = true` means TCP_NODELAY. + pub fn tcp_set_nodelay(&mut self, handle: SocketHandle, on: bool) { + let socket = self.sockets.get_mut::(handle); + socket.set_nagle_enabled(!on); + } + + /// Set TCP keepalive interval in milliseconds. `0` disables. + pub fn tcp_set_keepalive(&mut self, handle: SocketHandle, interval_ms: u64) { + let socket = self.sockets.get_mut::(handle); + if interval_ms == 0 { + socket.set_keep_alive(None); + } else { + socket.set_keep_alive(Some(Duration::from_millis(interval_ms))); + } + } + + /// Create a UDP socket and return its handle. + pub fn udp_socket(&mut self) -> Result { + let rx_meta = vec![UdpPacketMetadata::EMPTY; UDP_PACKET_META_COUNT]; + let tx_meta = vec![UdpPacketMetadata::EMPTY; UDP_PACKET_META_COUNT]; + let rx_data = vec![0u8; UDP_PACKET_DATA_BYTES]; + let tx_data = vec![0u8; UDP_PACKET_DATA_BYTES]; + + let socket = UdpSocket::new( + UdpPacketBuffer::new(rx_meta, rx_data), + UdpPacketBuffer::new(tx_meta, tx_data), + ); + Ok(self.sockets.add(socket)) + } + + /// Bind a UDP socket to local port. + pub fn udp_bind(&mut self, handle: SocketHandle, port: u16) -> Result<()> { + let socket = self.sockets.get_mut::(handle); + socket.bind(port).map_err(|_| NetworkError::ConnectionFailed) + } + + /// Send a UDP datagram. + pub fn udp_send_to( + &mut self, + handle: SocketHandle, + remote_ip: Ipv4Addr, + remote_port: u16, + data: &[u8], + ) -> Result { + let remote = Ipv4Address::from_bytes(&remote_ip.octets()); + let endpoint = IpEndpoint::new(IpAddress::Ipv4(remote), remote_port); + let local_port = self.ephemeral_port(); + + let socket = self.sockets.get_mut::(handle); + if !socket.is_open() { + socket + .bind(local_port) + .map_err(|_| NetworkError::ConnectionFailed)?; + } + + socket + .send_slice(data, endpoint) + .map(|_| data.len()) + .map_err(|_| NetworkError::SendFailed) + } + + /// Receive a UDP datagram. + pub fn udp_recv_from( + &mut self, + handle: SocketHandle, + buffer: &mut [u8], + ) -> Result<(usize, Ipv4Addr, u16)> { + let socket = self.sockets.get_mut::(handle); + match socket.recv_slice(buffer) { + Ok((n, meta)) => { + let IpAddress::Ipv4(v4) = meta.endpoint.addr; + let octets = v4.as_bytes(); + Ok(( + n, + Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]), + meta.endpoint.port, + )) + } + Err(_) => Ok((0, Ipv4Addr::new(0, 0, 0, 0), 0)), + } + } + /// Remove a socket from the set. pub fn remove_socket(&mut self, handle: SocketHandle) { self.sockets.remove(handle); @@ -521,6 +638,23 @@ impl NetInterface { self.sockets.get::(handle).state() } + /// Get TCP state as syscall ABI ordinal (0..10). + pub fn tcp_state_code(&self, handle: SocketHandle) -> u8 { + match self.tcp_state(handle) { + TcpState::Closed => 0, + TcpState::Listen => 1, + TcpState::SynSent => 2, + TcpState::SynReceived => 3, + TcpState::Established => 4, + TcpState::FinWait1 => 5, + TcpState::FinWait2 => 6, + TcpState::CloseWait => 7, + TcpState::Closing => 8, + TcpState::LastAck => 9, + TcpState::TimeWait => 10, + } + } + /// Generate an ephemeral port number. fn ephemeral_port(&self) -> u16 { // Simple ephemeral port allocation based on timestamp @@ -541,3 +675,4 @@ impl NetInterface { &mut self.device.inner } } + diff --git a/network/src/stack/mod.rs b/network/src/stack/mod.rs index af1d1021..0a916b8e 100644 --- a/network/src/stack/mod.rs +++ b/network/src/stack/mod.rs @@ -42,6 +42,8 @@ use smoltcp::phy::{Device, DeviceCapabilities, Medium, RxToken, TxToken}; use smoltcp::time::Instant; pub use crate::device::pci::ecam_bases; +pub use smoltcp::iface::SocketHandle; +pub use smoltcp::socket::dns::QueryHandle as DnsQueryHandle; pub use interface::{NetConfig, NetInterface, NetState, MAX_TCP_SOCKETS}; const MTU: usize = 1536; diff --git a/persistent/src/pe/embedded_reloc_data.rs b/persistent/src/pe/embedded_reloc_data.rs index fa7ea13f..9ab250f3 100644 --- a/persistent/src/pe/embedded_reloc_data.rs +++ b/persistent/src/pe/embedded_reloc_data.rs @@ -8,74 +8,111 @@ //! Run: ./tools/extract-reloc-data.sh after each build /// Original .reloc section RVA -pub const RELOC_RVA: u32 = 0x0061b000; +pub const RELOC_RVA: u32 = 0x00633000; /// Original .reloc section size -pub const RELOC_SIZE: u32 = 0x000002a8; +pub const RELOC_SIZE: u32 = 0x00000468; /// Original ImageBase from linker script pub const ORIGINAL_IMAGE_BASE: u64 = 0x0000004001000000; -/// Hardcoded .reloc section data (680 bytes) -/// Extracted from morpheus-bootloader.efi at file offset 0x001de000 +/// Hardcoded .reloc section data (1128 bytes) +/// Extracted from morpheus-bootloader.efi at file offset 0x001f4c00 #[allow(dead_code)] -pub const RELOC_DATA: [u8; 680] = [ - 0x00, 0x80, 0x05, 0x00, 0x38, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, - 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xd8, 0xa6, 0xf0, 0xa6, - 0x08, 0xa7, 0x60, 0xa7, 0x78, 0xa7, 0x90, 0xa7, 0x20, 0xa8, 0x28, 0xa9, - 0x30, 0xab, 0x48, 0xab, 0x50, 0xac, 0x28, 0xad, 0x40, 0xad, 0x58, 0xad, - 0x70, 0xad, 0x88, 0xad, 0xa0, 0xad, 0xb8, 0xad, 0x00, 0x90, 0x05, 0x00, - 0x44, 0x00, 0x00, 0x00, 0x00, 0xa3, 0x18, 0xa3, 0x30, 0xa3, 0xf8, 0xa6, - 0x10, 0xa7, 0x68, 0xa7, 0x80, 0xa7, 0xd0, 0xa7, 0xe8, 0xa7, 0x00, 0xa8, - 0x38, 0xa8, 0x68, 0xa8, 0x60, 0xaa, 0x78, 0xaa, 0x10, 0xab, 0x28, 0xab, - 0x40, 0xab, 0x58, 0xab, 0x70, 0xab, 0x80, 0xac, 0xe0, 0xac, 0x58, 0xad, - 0x50, 0xae, 0x68, 0xae, 0x80, 0xae, 0x98, 0xae, 0xb0, 0xae, 0xe0, 0xae, - 0x00, 0xaf, 0x00, 0x00, 0x00, 0xa0, 0x05, 0x00, 0x50, 0x00, 0x00, 0x00, - 0x48, 0xa4, 0x60, 0xa4, 0x78, 0xa4, 0x90, 0xa4, 0xa8, 0xa4, 0xc0, 0xa4, - 0xe0, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0x88, 0xa6, 0xc0, 0xa6, 0x30, 0xa9, - 0x48, 0xa9, 0x60, 0xa9, 0x78, 0xa9, 0x90, 0xa9, 0xa8, 0xa9, 0xc0, 0xa9, - 0x10, 0xaa, 0x70, 0xaa, 0x80, 0xab, 0xc0, 0xac, 0x28, 0xad, 0x48, 0xad, - 0x60, 0xad, 0x78, 0xad, 0x90, 0xad, 0xa8, 0xad, 0xb0, 0xae, 0x20, 0xaf, - 0x38, 0xaf, 0x50, 0xaf, 0x68, 0xaf, 0x80, 0xaf, 0x98, 0xaf, 0xd8, 0xaf, - 0x00, 0xb0, 0x05, 0x00, 0x4c, 0x00, 0x00, 0x00, 0x68, 0xa0, 0xd8, 0xab, - 0x30, 0xad, 0x40, 0xad, 0x50, 0xad, 0x60, 0xad, 0x70, 0xad, 0x80, 0xad, - 0x90, 0xad, 0xa0, 0xad, 0xb0, 0xad, 0xc0, 0xad, 0xd0, 0xad, 0xe0, 0xad, - 0xf0, 0xad, 0x00, 0xae, 0x10, 0xae, 0x20, 0xae, 0x30, 0xae, 0x40, 0xae, - 0x50, 0xae, 0x60, 0xae, 0x70, 0xae, 0x80, 0xae, 0x90, 0xae, 0xa0, 0xae, - 0xb0, 0xae, 0xc0, 0xae, 0xd0, 0xae, 0xe0, 0xae, 0xf0, 0xae, 0x00, 0xaf, - 0x10, 0xaf, 0x20, 0xaf, 0x00, 0xc0, 0x05, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x00, 0xa1, 0x00, 0x00, 0x00, 0xd0, 0x05, 0x00, 0x10, 0x00, 0x00, 0x00, - 0xc0, 0xa2, 0xd8, 0xa2, 0x28, 0xa3, 0x80, 0xa3, 0x00, 0xe0, 0x05, 0x00, - 0x74, 0x00, 0x00, 0x00, 0xe8, 0xa0, 0x00, 0xa1, 0x38, 0xa1, 0x68, 0xa1, - 0x80, 0xa1, 0x98, 0xa1, 0xb0, 0xa1, 0xc8, 0xa1, 0xe0, 0xa1, 0xf8, 0xa1, - 0x10, 0xa2, 0x50, 0xa2, 0x68, 0xa2, 0x80, 0xa2, 0x98, 0xa2, 0xb0, 0xa2, - 0xe8, 0xa2, 0x18, 0xa3, 0x30, 0xa3, 0x48, 0xa3, 0x60, 0xa3, 0x98, 0xa3, - 0xb0, 0xa3, 0xc8, 0xa3, 0xe0, 0xa3, 0xf8, 0xa3, 0x10, 0xa4, 0xa0, 0xa4, - 0xe0, 0xa4, 0xf8, 0xa4, 0x58, 0xa5, 0x70, 0xa5, 0x88, 0xa5, 0xa0, 0xa5, - 0xb8, 0xa5, 0xd0, 0xa5, 0x08, 0xa6, 0xb8, 0xa6, 0xd0, 0xa6, 0x20, 0xa7, - 0x30, 0xa7, 0x40, 0xa7, 0x50, 0xa7, 0x60, 0xa7, 0x90, 0xa7, 0xc8, 0xa7, - 0x10, 0xa8, 0x60, 0xa8, 0x98, 0xa8, 0xb0, 0xa8, 0xf8, 0xa8, 0x80, 0xa9, - 0xd0, 0xaf, 0xe0, 0xaf, 0x00, 0xf0, 0x05, 0x00, 0x4c, 0x00, 0x00, 0x00, - 0x10, 0xa0, 0x28, 0xa0, 0x70, 0xa0, 0x80, 0xa0, 0x90, 0xa0, 0xb8, 0xa0, - 0xf0, 0xa0, 0x30, 0xa6, 0x40, 0xa6, 0x50, 0xa6, 0x60, 0xa6, 0xb0, 0xa6, - 0xc0, 0xa6, 0xd0, 0xa6, 0xe0, 0xa6, 0xf0, 0xa6, 0x18, 0xa7, 0x28, 0xa7, - 0x38, 0xa7, 0xa0, 0xa7, 0xb0, 0xa7, 0xc0, 0xa7, 0x08, 0xa8, 0x18, 0xa8, - 0x50, 0xa8, 0x60, 0xa8, 0x88, 0xa8, 0x98, 0xa8, 0xd0, 0xa8, 0xe8, 0xa8, - 0x40, 0xa9, 0xb0, 0xaf, 0xc8, 0xaf, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, - 0x74, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x50, 0xa0, 0x98, 0xa0, 0xa8, 0xa0, - 0xc8, 0xa1, 0xa0, 0xa2, 0xf8, 0xa2, 0x10, 0xa3, 0x28, 0xa3, 0x40, 0xa3, - 0x58, 0xa3, 0x70, 0xa3, 0x88, 0xa3, 0x18, 0xa4, 0xc8, 0xa4, 0xe0, 0xa4, - 0xf8, 0xa4, 0x68, 0xa5, 0x98, 0xa5, 0x60, 0xa6, 0x98, 0xa6, 0xb0, 0xa6, - 0x78, 0xa7, 0x90, 0xa7, 0x40, 0xa8, 0x58, 0xa8, 0xa8, 0xa8, 0xb8, 0xa8, - 0xe8, 0xa8, 0x20, 0xa9, 0x30, 0xa9, 0x80, 0xa9, 0x90, 0xa9, 0xd8, 0xa9, - 0xe8, 0xa9, 0x78, 0xaa, 0x88, 0xaa, 0xc0, 0xaa, 0xd0, 0xaa, 0x00, 0xab, - 0x10, 0xab, 0x28, 0xab, 0x68, 0xab, 0x78, 0xab, 0x90, 0xab, 0xe8, 0xab, - 0xf8, 0xab, 0x10, 0xac, 0x28, 0xac, 0x40, 0xac, 0x78, 0xac, 0x90, 0xac, - 0xa8, 0xac, 0xc0, 0xac, 0x00, 0x20, 0x06, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0xd8, 0xab, 0xe0, 0xab, 0x00, 0xc0, 0x1c, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0x40, 0xac, 0x00, 0x00, 0x00, 0xd0, 0x1c, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x88, 0xa8, 0x90, 0xa8, 0x98, 0xa8, 0x00, 0x00, 0x00, 0xf0, 0x1d, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0xd0, 0xae, 0x00, 0x00, 0x00, 0xa0, 0x61, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, +pub const RELOC_DATA: [u8; 1128] = [ + 0x00, 0xb0, 0x06, 0x00, 0x30, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, + 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xf0, 0xa8, 0x08, 0xa9, + 0x20, 0xa9, 0x38, 0xa9, 0x50, 0xa9, 0x10, 0xaa, 0x40, 0xaa, 0xc0, 0xac, + 0xd8, 0xac, 0xf0, 0xac, 0x48, 0xad, 0x60, 0xad, 0x78, 0xad, 0xf8, 0xad, + 0x00, 0xc0, 0x06, 0x00, 0x74, 0x00, 0x00, 0x00, 0x68, 0xa1, 0x80, 0xa1, + 0x98, 0xa1, 0x20, 0xa3, 0xa0, 0xa3, 0xb8, 0xa3, 0xd0, 0xa3, 0x10, 0xa4, + 0x20, 0xa4, 0x68, 0xa4, 0x80, 0xa4, 0x98, 0xa4, 0xb0, 0xa4, 0x50, 0xa5, + 0x68, 0xa5, 0x80, 0xa5, 0x10, 0xa6, 0x28, 0xa6, 0x40, 0xa6, 0x58, 0xa6, + 0x70, 0xa6, 0x88, 0xa6, 0xa0, 0xa6, 0xf0, 0xa8, 0x08, 0xa9, 0x20, 0xa9, + 0x38, 0xa9, 0x50, 0xa9, 0x68, 0xa9, 0xe8, 0xa9, 0x00, 0xaa, 0x88, 0xaa, + 0xe8, 0xaa, 0x00, 0xab, 0x18, 0xab, 0xd0, 0xad, 0xe8, 0xad, 0x00, 0xae, + 0x18, 0xae, 0x30, 0xae, 0x48, 0xae, 0x60, 0xae, 0x78, 0xae, 0x90, 0xae, + 0x10, 0xaf, 0x28, 0xaf, 0x40, 0xaf, 0x58, 0xaf, 0x70, 0xaf, 0x88, 0xaf, + 0xa0, 0xaf, 0xb8, 0xaf, 0xd0, 0xaf, 0xe8, 0xaf, 0x00, 0xd0, 0x06, 0x00, + 0x74, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x18, 0xa0, 0x30, 0xa0, 0x48, 0xa0, + 0x60, 0xa0, 0x78, 0xa0, 0x90, 0xa0, 0x10, 0xa1, 0xa0, 0xa1, 0xc0, 0xa3, + 0xb0, 0xa4, 0xb8, 0xa5, 0xd0, 0xa7, 0xe8, 0xa7, 0x28, 0xa8, 0x40, 0xa8, + 0x58, 0xa8, 0x70, 0xa8, 0xf8, 0xa8, 0x10, 0xa9, 0x50, 0xa9, 0x68, 0xa9, + 0xc8, 0xa9, 0xe0, 0xa9, 0xf8, 0xa9, 0x10, 0xaa, 0x28, 0xaa, 0x40, 0xaa, + 0x78, 0xaa, 0xa8, 0xab, 0xe0, 0xac, 0xf8, 0xac, 0x48, 0xad, 0xc0, 0xad, + 0xd8, 0xad, 0xf0, 0xad, 0x08, 0xae, 0x20, 0xae, 0x38, 0xae, 0x50, 0xae, + 0x68, 0xae, 0x80, 0xae, 0x98, 0xae, 0xb0, 0xae, 0xc8, 0xae, 0xe0, 0xae, + 0xf8, 0xae, 0x10, 0xaf, 0x78, 0xaf, 0x90, 0xaf, 0xa8, 0xaf, 0xc0, 0xaf, + 0xd8, 0xaf, 0xf0, 0xaf, 0x00, 0xe0, 0x06, 0x00, 0x90, 0x00, 0x00, 0x00, + 0x08, 0xa0, 0x20, 0xa0, 0x38, 0xa0, 0x50, 0xa0, 0x68, 0xa0, 0xb8, 0xa0, + 0xc8, 0xa0, 0x20, 0xa1, 0x30, 0xa1, 0x10, 0xa3, 0x28, 0xa3, 0x40, 0xa3, + 0x58, 0xa3, 0xd8, 0xa3, 0xf0, 0xa3, 0x08, 0xa4, 0x20, 0xa4, 0x38, 0xa4, + 0x50, 0xa4, 0x68, 0xa4, 0x80, 0xa4, 0x98, 0xa4, 0xc8, 0xa4, 0x68, 0xa5, + 0x80, 0xa5, 0x00, 0xa6, 0x80, 0xa6, 0x98, 0xa6, 0xb0, 0xa6, 0xc8, 0xa6, + 0xe0, 0xa6, 0xf8, 0xa6, 0x80, 0xa7, 0xa8, 0xa8, 0xb8, 0xa8, 0xc8, 0xa8, + 0x40, 0xa9, 0x58, 0xa9, 0x70, 0xa9, 0x88, 0xa9, 0xa0, 0xa9, 0xb8, 0xa9, + 0xd0, 0xa9, 0xe8, 0xa9, 0x00, 0xaa, 0x18, 0xaa, 0x30, 0xaa, 0x48, 0xaa, + 0x60, 0xaa, 0x78, 0xaa, 0x90, 0xaa, 0xa8, 0xaa, 0x50, 0xac, 0x68, 0xac, + 0x80, 0xac, 0x98, 0xac, 0xb0, 0xac, 0xc8, 0xac, 0xe0, 0xac, 0xf8, 0xac, + 0x10, 0xad, 0x98, 0xad, 0xd8, 0xad, 0xe8, 0xad, 0xa8, 0xae, 0x38, 0xaf, + 0x78, 0xaf, 0xc0, 0xaf, 0x00, 0xf0, 0x06, 0x00, 0x68, 0x00, 0x00, 0x00, + 0x58, 0xa0, 0xa0, 0xa0, 0xb0, 0xa0, 0xc0, 0xa0, 0xd0, 0xa0, 0x50, 0xa1, + 0x68, 0xa1, 0xe8, 0xa1, 0x88, 0xa2, 0x98, 0xa2, 0xa8, 0xa2, 0x40, 0xa3, + 0x78, 0xa3, 0x90, 0xa3, 0xa8, 0xa3, 0xc0, 0xa3, 0x40, 0xa4, 0x80, 0xa4, + 0x98, 0xa4, 0xd8, 0xa5, 0x08, 0xa6, 0x20, 0xa6, 0x78, 0xa6, 0x90, 0xa6, + 0xe0, 0xa6, 0xf8, 0xa6, 0x10, 0xa7, 0x48, 0xa7, 0x78, 0xa7, 0xb0, 0xa7, + 0xa8, 0xa9, 0xc0, 0xa9, 0x58, 0xaa, 0x70, 0xaa, 0x88, 0xaa, 0xa0, 0xaa, + 0xb8, 0xaa, 0xc8, 0xab, 0x28, 0xac, 0xa0, 0xac, 0x98, 0xad, 0xb0, 0xad, + 0xc8, 0xad, 0xe0, 0xad, 0xf8, 0xad, 0x28, 0xae, 0x48, 0xae, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x00, 0x50, 0x00, 0x00, 0x00, 0x90, 0xa3, 0xa8, 0xa3, + 0xc0, 0xa3, 0xd8, 0xa3, 0xf0, 0xa3, 0x08, 0xa4, 0x28, 0xa5, 0x40, 0xa5, + 0x58, 0xa5, 0xd0, 0xa5, 0x08, 0xa6, 0x78, 0xa8, 0x90, 0xa8, 0xa8, 0xa8, + 0xc0, 0xa8, 0xd8, 0xa8, 0xf0, 0xa8, 0x08, 0xa9, 0x40, 0xa9, 0x50, 0xaa, + 0x90, 0xab, 0xf8, 0xab, 0x18, 0xac, 0x30, 0xac, 0x48, 0xac, 0x60, 0xac, + 0x78, 0xac, 0x80, 0xad, 0xf0, 0xad, 0x08, 0xae, 0x20, 0xae, 0x38, 0xae, + 0x50, 0xae, 0x68, 0xae, 0xa8, 0xae, 0x38, 0xaf, 0x00, 0x10, 0x07, 0x00, + 0x4c, 0x00, 0x00, 0x00, 0xd0, 0xa9, 0x00, 0xab, 0x58, 0xac, 0x68, 0xac, + 0x78, 0xac, 0x88, 0xac, 0x98, 0xac, 0xa8, 0xac, 0xb8, 0xac, 0xc8, 0xac, + 0xd8, 0xac, 0xe8, 0xac, 0xf8, 0xac, 0x08, 0xad, 0x18, 0xad, 0x28, 0xad, + 0x38, 0xad, 0x48, 0xad, 0x58, 0xad, 0x68, 0xad, 0x78, 0xad, 0x88, 0xad, + 0x98, 0xad, 0xa8, 0xad, 0xb8, 0xad, 0xc8, 0xad, 0xd8, 0xad, 0xe8, 0xad, + 0xf8, 0xad, 0x08, 0xae, 0x18, 0xae, 0x28, 0xae, 0x38, 0xae, 0x48, 0xae, + 0x00, 0x20, 0x07, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x28, 0xa0, 0x00, 0x00, + 0x00, 0x30, 0x07, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x08, 0xa2, 0x60, 0xa2, + 0xd0, 0xa3, 0xe8, 0xa3, 0x38, 0xa4, 0x48, 0xa4, 0x58, 0xa4, 0x68, 0xa4, + 0x78, 0xa4, 0xa8, 0xa4, 0xe0, 0xa4, 0x28, 0xa5, 0x78, 0xa5, 0xb0, 0xa5, + 0xc8, 0xa5, 0x10, 0xa6, 0x98, 0xa6, 0x00, 0x00, 0x00, 0x40, 0x07, 0x00, + 0x54, 0x00, 0x00, 0x00, 0x50, 0xa3, 0x68, 0xa3, 0xa0, 0xa3, 0xd0, 0xa3, + 0xe8, 0xa3, 0x00, 0xa4, 0x18, 0xa4, 0x30, 0xa4, 0x48, 0xa4, 0x60, 0xa4, + 0x78, 0xa4, 0xb8, 0xa4, 0xd0, 0xa4, 0xe8, 0xa4, 0x00, 0xa5, 0x18, 0xa5, + 0x50, 0xa5, 0x80, 0xa5, 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, 0x00, 0xa6, + 0x18, 0xa6, 0x30, 0xa6, 0x48, 0xa6, 0x60, 0xa6, 0x78, 0xa6, 0x08, 0xa7, + 0x50, 0xad, 0x60, 0xad, 0x90, 0xad, 0xa8, 0xad, 0xf0, 0xad, 0x00, 0xae, + 0x10, 0xae, 0x38, 0xae, 0x70, 0xae, 0x00, 0x00, 0x00, 0x50, 0x07, 0x00, + 0x48, 0x00, 0x00, 0x00, 0xb0, 0xa3, 0xc0, 0xa3, 0xd0, 0xa3, 0xe0, 0xa3, + 0x30, 0xa4, 0x40, 0xa4, 0x50, 0xa4, 0x60, 0xa4, 0x70, 0xa4, 0x98, 0xa4, + 0xa8, 0xa4, 0xb8, 0xa4, 0x20, 0xa5, 0x30, 0xa5, 0x40, 0xa5, 0x88, 0xa5, + 0x98, 0xa5, 0xd0, 0xa5, 0xe0, 0xa5, 0x08, 0xa6, 0x18, 0xa6, 0x50, 0xa6, + 0x68, 0xa6, 0xc0, 0xa6, 0x30, 0xad, 0x48, 0xad, 0x80, 0xad, 0xd0, 0xad, + 0x18, 0xae, 0x28, 0xae, 0xb0, 0xaf, 0xe8, 0xaf, 0x00, 0x60, 0x07, 0x00, + 0x88, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xc8, 0xa0, 0xe0, 0xa0, 0x90, 0xa1, + 0xa8, 0xa1, 0xf8, 0xa1, 0x08, 0xa2, 0x38, 0xa2, 0x70, 0xa2, 0x80, 0xa2, + 0xd0, 0xa2, 0xe0, 0xa2, 0x28, 0xa3, 0x38, 0xa3, 0xc8, 0xa3, 0xd8, 0xa3, + 0x10, 0xa4, 0x20, 0xa4, 0x50, 0xa4, 0x60, 0xa4, 0x78, 0xa4, 0xb8, 0xa4, + 0xc8, 0xa4, 0xe0, 0xa4, 0x38, 0xa5, 0x48, 0xa5, 0x60, 0xa5, 0x78, 0xa5, + 0x90, 0xa5, 0xc8, 0xa5, 0xe0, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0xa8, 0xa6, + 0xc0, 0xa6, 0xd8, 0xa6, 0xf0, 0xa6, 0x08, 0xa7, 0x20, 0xa7, 0x38, 0xa7, + 0x08, 0xa9, 0x20, 0xa9, 0x60, 0xa9, 0x78, 0xa9, 0x90, 0xa9, 0xa8, 0xa9, + 0xe8, 0xa9, 0x68, 0xaa, 0x80, 0xaa, 0x98, 0xaa, 0xb0, 0xaa, 0xc8, 0xaa, + 0xe0, 0xaa, 0xf8, 0xaa, 0x10, 0xab, 0x28, 0xab, 0x40, 0xab, 0x58, 0xab, + 0x70, 0xab, 0x88, 0xab, 0xd8, 0xad, 0xf0, 0xad, 0xe8, 0xae, 0x90, 0xaf, + 0x00, 0x70, 0x07, 0x00, 0x20, 0x00, 0x00, 0x00, 0x40, 0xa0, 0x58, 0xa0, + 0x70, 0xa0, 0x88, 0xa0, 0xa0, 0xa0, 0xb8, 0xa0, 0xd0, 0xa0, 0x38, 0xa1, + 0x50, 0xa1, 0x68, 0xa1, 0xd8, 0xa1, 0x08, 0xa2, 0x00, 0x90, 0x07, 0x00, + 0x14, 0x00, 0x00, 0x00, 0xf0, 0xa6, 0xf8, 0xa6, 0x00, 0xa7, 0x08, 0xa7, + 0x10, 0xa7, 0x18, 0xa7, 0x00, 0x30, 0x1e, 0x00, 0x14, 0x00, 0x00, 0x00, + 0x80, 0xa7, 0x90, 0xa8, 0x98, 0xa8, 0xa0, 0xa8, 0xa8, 0xa8, 0x00, 0x00, + 0x00, 0x50, 0x1f, 0x00, 0x0c, 0x00, 0x00, 0x00, 0xe0, 0xae, 0x00, 0x00, + 0x00, 0x20, 0x63, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, ]; diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs index 0f9b6724..2f62b173 100644 --- a/settings/src/chambers/net_obs.rs +++ b/settings/src/chambers/net_obs.rs @@ -8,6 +8,8 @@ use crate::widgets; use libmorpheus::net; +const ENODEV: u64 = u64::MAX - 19; + // editable field indices const FIELD_MODE_DHCP: usize = 0; const FIELD_MODE_STATIC: usize = 1; @@ -37,6 +39,7 @@ pub struct NetObsChamber { pub hostname_len: usize, pub link_up: bool, pub mtu: u32, + pub stack_available: bool, // stats pub tx_packets: u64, @@ -77,6 +80,7 @@ impl NetObsChamber { hostname_len: 0, link_up: false, mtu: 0, + stack_available: false, tx_packets: 0, rx_packets: 0, tx_bytes: 0, @@ -114,7 +118,10 @@ impl NetObsChamber { self.hostname[..hlen].copy_from_slice(&cfg.hostname[..hlen]); self.hostname_len = hlen; - self.edit_dhcp = (cfg.flags & net::NET_FLAG_DHCP) != 0; + // keep DHCP default-on when stack is still unconfigured. + if cfg.state != 0 { + self.edit_dhcp = (cfg.flags & net::NET_FLAG_DHCP) != 0; + } // populate edit fields from live state self.sync_edit_from_live(); @@ -124,11 +131,17 @@ impl NetObsChamber { self.link_up = info.link_up != 0; } - if let Ok(stats) = net::net_stats() { - self.tx_packets = stats.tx_packets; - self.rx_packets = stats.rx_packets; - self.tx_bytes = stats.tx_bytes; - self.rx_bytes = stats.rx_bytes; + match net::net_stats() { + Ok(stats) => { + self.tx_packets = stats.tx_packets; + self.rx_packets = stats.rx_packets; + self.tx_bytes = stats.tx_bytes; + self.rx_bytes = stats.rx_bytes; + self.stack_available = true; + } + Err(_) => { + self.stack_available = false; + } } } @@ -139,7 +152,7 @@ impl NetObsChamber { // ip self.edit_ip_len = widgets::format_ip(self.ip, &mut self.edit_ip); - self.edit_prefix = self.prefix_len; + self.edit_prefix = if self.prefix_len == 0 { 24 } else { self.prefix_len }; self.edit_gw_len = widgets::format_ip(self.gateway, &mut self.edit_gateway); self.edit_dns1_len = widgets::format_ip(self.dns1, &mut self.edit_dns1); self.edit_dns2_len = widgets::format_ip(self.dns2, &mut self.edit_dns2); @@ -187,6 +200,64 @@ impl NetObsChamber { } } +fn run_dhcp_request(app: &mut SettingsApp, source: &'static str) -> bool { + // force mode to DHCP for this action regardless of previous UI mode. + app.net_obs.edit_dhcp = true; + libmorpheus::println!("[settings/net] dhcp request source={}", source); + + match net::net_dhcp() { + Ok(()) => { + // keep driving the stack for a short burst to surface lease progress. + for _ in 0..256 { + let _ = net::nic_refill(); + let _ = net::net_poll_drive(libmorpheus::time::uptime_ms()); + } + app.net_obs.refresh(); + + let mut ip_buf = [0u8; 16]; + let ip_len = widgets::format_ip(app.net_obs.ip, &mut ip_buf); + let ip = core::str::from_utf8(&ip_buf[..ip_len]).unwrap_or("0.0.0.0"); + if app.net_obs.ip != 0 { + app.set_status("DHCP lease updated", false); + libmorpheus::println!( + "[settings/net] dhcp lease ip={} gw=0x{:08x} dns1=0x{:08x} state={}", + ip, + app.net_obs.gateway, + app.net_obs.dns1, + app.net_obs.state + ); + } else { + app.set_status("DHCP requested (no lease yet)", true); + libmorpheus::println!( + "[settings/net] dhcp pending state={} flags=0x{:08x}", + app.net_obs.state, + app.net_obs.flags + ); + } + true + } + Err(e) => { + if e == ENODEV { + // NIC path is active, but stack ops are not registered in kernel yet. + libmorpheus::println!( + "[settings/net] dhcp unavailable: ip stack not registered err=0x{:x}", + e + ); + app.set_status("DHCP unavailable: IP stack not registered", true); + } else { + libmorpheus::println!("[settings/net] dhcp request failed err=0x{:x}", e); + app.set_status("DHCP request failed", true); + } + false + } + } +} + +#[inline] +fn is_text_field(idx: usize) -> bool { + matches!(idx, FIELD_HOSTNAME | FIELD_IP | FIELD_GATEWAY | FIELD_DNS1 | FIELD_DNS2) +} + pub fn activate(app: &mut SettingsApp, idx: usize) { match idx { FIELD_MODE_DHCP => { @@ -198,38 +269,7 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { app.mark_edited(Route::NetObservatory, "dhcp"); } FIELD_DHCP_REQUEST => { - // DHCP can take a few polls to settle; drive it inline so UI reflects results now. - app.net_obs.edit_dhcp = true; - libmorpheus::io::print("[settings/net] dhcp request requested\n"); - match net::net_dhcp() { - Ok(()) => { - for _ in 0..128 { - let _ = net::nic_refill(); - let _ = net::net_poll_drive(0); - } - app.net_obs.refresh(); - - let mut ip_buf = [0u8; 16]; - let ip_len = widgets::format_ip(app.net_obs.ip, &mut ip_buf); - let ip = core::str::from_utf8(&ip_buf[..ip_len]).unwrap_or("0.0.0.0"); - if app.net_obs.ip != 0 { - app.set_status("DHCP requested (lease info refreshed)", false); - libmorpheus::println!( - "[settings/net] dhcp lease ip={} gw=0x{:08x} dns1=0x{:08x}", - ip, - app.net_obs.gateway, - app.net_obs.dns1 - ); - } else { - app.set_status("DHCP requested (no lease yet)", true); - libmorpheus::println!("[settings/net] dhcp no lease yet"); - } - } - Err(e) => { - libmorpheus::println!("[settings/net] dhcp request failed err=0x{:x}", e); - app.set_status("DHCP request failed", true); - } - } + let _ = run_dhcp_request(app, "button"); } FIELD_HOSTNAME | FIELD_IP | FIELD_GATEWAY | FIELD_DNS1 | FIELD_DNS2 => { app.net_obs.editing_field = Some(idx); @@ -293,16 +333,19 @@ pub fn apply(app: &mut SettingsApp) -> bool { let dhcp = app.net_obs.edit_dhcp; if dhcp { - if let Err(_) = net::net_dhcp() { - app.set_status("DHCP request failed", true); + if !run_dhcp_request(app, "apply") { return false; } app.log_change(Route::NetObservatory, "mode", "Switched to DHCP", false); } else { let ip_len = app.net_obs.edit_ip_len; let gw_len = app.net_obs.edit_gw_len; - let prefix = app.net_obs.edit_prefix; - if prefix == 0 || prefix > 32 { + let mut prefix = app.net_obs.edit_prefix; + if prefix == 0 { + prefix = 24; + app.net_obs.edit_prefix = 24; + } + if prefix > 32 { app.set_status("Invalid prefix length", true); return false; } @@ -377,6 +420,27 @@ pub fn handle_key(app: &mut SettingsApp, scancode: u8) { } } app.frame_dirty = true; + return; + } + + // start text editing immediately when the focused field gets typed into. + if is_text_field(app.pane_focus) { + match scancode { + 0x0E => { + app.net_obs.editing_field = Some(app.pane_focus); + app.net_obs.text_backspace(app.pane_focus); + app.mark_edited(Route::NetObservatory, field_name(app.pane_focus)); + app.frame_dirty = true; + } + _ => { + if let Some(ch) = scancode_to_char(scancode) { + app.net_obs.editing_field = Some(app.pane_focus); + app.net_obs.text_insert(app.pane_focus, ch); + app.mark_edited(Route::NetObservatory, field_name(app.pane_focus)); + app.frame_dirty = true; + } + } + } } } @@ -400,6 +464,11 @@ pub fn render(app: &SettingsApp) { layout::draw_kv(app, px, cy, "Link:", link_str, link_color); cy += r2; + let stack_str = if net.stack_available { "ONLINE" } else { "OFFLINE" }; + let stack_color = if net.stack_available { t.success } else { t.warning }; + layout::draw_kv(app, px, cy, "IP Stack:", stack_str, stack_color); + cy += r2; + let mut mac_buf = [0u8; 17]; let mac_len = widgets::format_mac(&net.mac, &mut mac_buf); let mac_str = core::str::from_utf8(&mac_buf[..mac_len]).unwrap_or("??"); @@ -444,10 +513,8 @@ pub fn render(app: &SettingsApp) { layout::draw_button_row(app, px, cy, static_label, FIELD_MODE_STATIC, t.glyph); cy += r8; - if net.edit_dhcp { - layout::draw_button_row(app, px, cy, "Request IP (DHCP)", FIELD_DHCP_REQUEST, t.warning); - cy += r8; - } + layout::draw_button_row(app, px, cy, "Request IP (DHCP)", FIELD_DHCP_REQUEST, t.warning); + cy += r8; // hostname let hn = core::str::from_utf8(&net.edit_hostname[..net.edit_hostname_len]).unwrap_or(""); diff --git a/settings/src/state.rs b/settings/src/state.rs index 3c0923b3..509b0730 100644 --- a/settings/src/state.rs +++ b/settings/src/state.rs @@ -306,7 +306,8 @@ impl SettingsApp { // mouse let ms = libmorpheus::hw::mouse_read(); - if ms.dx != 0 || ms.dy != 0 || ms.buttons != 0 { + let buttons_changed = ms.buttons != self.last_buttons; + if ms.dx != 0 || ms.dy != 0 || buttons_changed { self.mouse_x = (self.mouse_x + ms.dx as i32).clamp(0, self.fb_w as i32 - 1); self.mouse_y = (self.mouse_y + ms.dy as i32).clamp(0, self.fb_h as i32 - 1); From 1ea4fdc59ab4e88b6be7918844e046143ffc0885 Mon Sep 17 00:00:00 2001 From: "T. Andrew Davis" Date: Thu, 12 Mar 2026 17:58:38 +0100 Subject: [PATCH 12/12] Refactor network handling and appearance management - Update `NetObsChamber` to prevent clobbering in-progress typing during periodic refresh. - Prefer NIC hardware counters when available for network statistics. - Enhance `SettingsApp` initialization to sync global settings. - Introduce network mode selection in `setup-dev.sh` with support for bridge and user NAT modes. - Implement bridge ACL checks and helper availability in `setup-dev.sh`. - Add `netcheck` application for network diagnostics, including DHCP lease acquisition and DNS resolution. - Refactor appearance management in `shelld` to use a shared `DesktopAppearance` struct for consistent theming. - Update various islands in `shelld` to utilize dynamic color settings from `ShellState`. - Introduce persistence for desktop appearance settings in `libmorpheus`. --- Cargo.lock | 7 + Cargo.toml | 1 + .../src/baremetal_ops/network/config.rs | 13 +- compd/src/islands/mod.rs | 16 +- compd/src/islands/renderer.rs | 14 +- compd/src/main.rs | 14 ++ libmorpheus/src/desktop.rs | 154 +++++++++++++ libmorpheus/src/lib.rs | 1 + network/src/stack/interface.rs | 19 ++ persistent/src/pe/embedded_reloc_data.rs | 196 ++++++++-------- settings/src/chambers/mirror.rs | 106 +++++++-- settings/src/chambers/net_obs.rs | 18 +- settings/src/state.rs | 1 + setup-dev.sh | 215 ++++++++++++++++-- shelld/src/islands/launcher.rs | 19 +- shelld/src/islands/mod.rs | 58 ++++- shelld/src/islands/panel.rs | 23 +- shelld/src/islands/wallpaper.rs | 4 +- shelld/src/main.rs | 14 ++ tests/netcheck/Cargo.toml | 7 + tests/netcheck/src/main.rs | 208 +++++++++++++++++ 21 files changed, 911 insertions(+), 197 deletions(-) create mode 100644 libmorpheus/src/desktop.rs create mode 100644 tests/netcheck/Cargo.toml create mode 100644 tests/netcheck/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 599a9168..f433f252 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -289,6 +289,13 @@ dependencies = [ "libmorpheus", ] +[[package]] +name = "netcheck" +version = "0.1.0" +dependencies = [ + "libmorpheus", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index edb4ab1b..e9beb65f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "shelld", "settings", "tests/syscall-e2e", + "tests/netcheck", "tests/spinning-cube", "tests/system-visualizer", ] diff --git a/bootloader/src/baremetal_ops/network/config.rs b/bootloader/src/baremetal_ops/network/config.rs index f085b07f..766c0456 100644 --- a/bootloader/src/baremetal_ops/network/config.rs +++ b/bootloader/src/baremetal_ops/network/config.rs @@ -45,10 +45,13 @@ pub(super) unsafe fn net_cfg_get(buf: *mut u8) -> i64 { } pub(super) unsafe fn net_cfg_dhcp() -> i64 { - if state::user_net_stack_mut().is_some() { - 0 - } else { - -1 + let Some(stack) = state::user_net_stack_mut() else { + return -1; + }; + + match stack.restart_dhcp() { + Ok(()) => 0, + Err(_) => -1, } } @@ -81,6 +84,8 @@ pub(super) unsafe fn net_poll_stats(buf: *mut u8) -> i64 { 0, core::mem::size_of::(), ); + out.tx_packets = morpheus_network::stack::tx_packet_count() as u64; + out.rx_packets = morpheus_network::stack::rx_packet_count() as u64; out.tcp_active = state::tcp_active_count(); 0 } diff --git a/compd/src/islands/mod.rs b/compd/src/islands/mod.rs index 9fc07a2e..8c714229 100644 --- a/compd/src/islands/mod.rs +++ b/compd/src/islands/mod.rs @@ -13,12 +13,9 @@ pub const TITLE_H: u32 = 22; pub const BORDER: u32 = 1; pub const CASCADE_STEP: i32 = 28; -pub const TITLE_FOCUSED_RGB: (u8, u8, u8) = (0, 85, 0); pub const TITLE_UNFOCUSED_RGB: (u8, u8, u8) = (40, 40, 40); pub const TITLE_TEXT_RGB: (u8, u8, u8) = (255, 255, 255); -pub const BORDER_FOCUSED_RGB: (u8, u8, u8) = (0, 170, 0); pub const BORDER_UNFOCUSED_RGB: (u8, u8, u8) = (85, 85, 85); -pub const DESKTOP_RGB: (u8, u8, u8) = (26, 26, 46); pub const CURSOR_RGB: (u8, u8, u8) = (255, 255, 255); // shelld's panel height. hardcoded here so compd can re-blit it above windows. @@ -93,6 +90,10 @@ pub struct CompState { pub last_buttons: u8, pub capture: Option, + pub desktop_rgb: (u8, u8, u8), + pub title_focused_rgb: (u8, u8, u8), + pub border_focused_rgb: (u8, u8, u8), + // --- channels (SPSC) --- pub ch_input_to_focus: Channel, pub ch_mouse_spatial: Channel, @@ -117,6 +118,9 @@ impl CompState { mouse_y: (fb_h / 2) as i32, last_buttons: 0, capture: None, + desktop_rgb: (26, 26, 46), + title_focused_rgb: (0, 85, 0), + border_focused_rgb: (0, 170, 0), ch_input_to_focus: Channel::new(), ch_mouse_spatial: Channel::new(), ch_mouse_route: Channel::new(), @@ -132,6 +136,12 @@ impl CompState { (r as u32) | ((g as u32) << 8) | ((b as u32) << 16) } } + + pub fn apply_desktop_appearance(&mut self, a: &libmorpheus::desktop::DesktopAppearance) { + self.desktop_rgb = a.desktop_rgb; + self.title_focused_rgb = a.title_focus_rgb; + self.border_focused_rgb = a.border_focus_rgb; + } } pub const fn zeroed_surface_entry() -> compsys::SurfaceEntry { diff --git a/compd/src/islands/renderer.rs b/compd/src/islands/renderer.rs index d3f4fda1..f0375ec1 100644 --- a/compd/src/islands/renderer.rs +++ b/compd/src/islands/renderer.rs @@ -43,7 +43,7 @@ pub fn compose(state: &mut CompState) { } } if !drew_desktop { - let (dr, dg, db) = DESKTOP_RGB; + let (dr, dg, db) = state.desktop_rgb; raw_fill( fb_ptr, state.fb_stride, @@ -148,16 +148,8 @@ fn draw_window(state: &mut CompState, idx: usize, focused: bool) { ) }; - let (tb_r, tb_g, tb_b) = if focused { - TITLE_FOCUSED_RGB - } else { - TITLE_UNFOCUSED_RGB - }; - let (br, bg, bb) = if focused { - BORDER_FOCUSED_RGB - } else { - BORDER_UNFOCUSED_RGB - }; + let (tb_r, tb_g, tb_b) = if focused { state.title_focused_rgb } else { TITLE_UNFOCUSED_RGB }; + let (br, bg, bb) = if focused { state.border_focused_rgb } else { BORDER_UNFOCUSED_RGB }; let outer_x = win_x - BORDER as i32; let outer_y = win_y - TITLE_H as i32 - BORDER as i32; diff --git a/compd/src/main.rs b/compd/src/main.rs index 850245b2..2b774cd9 100644 --- a/compd/src/main.rs +++ b/compd/src/main.rs @@ -30,8 +30,22 @@ fn main() -> i32 { let mut state = islands::CompState::new(fb_ptr, fb_info.width, fb_info.height, fb_stride_px, is_bgrx); + if let Some(a) = libmorpheus::desktop::DesktopAppearance::load() { + state.apply_desktop_appearance(&a); + } + + let mut last_appearance_poll_ms = 0u64; + // 5. enter main vsync loop loop { + let now_ms = libmorpheus::time::uptime_ms(); + if now_ms.saturating_sub(last_appearance_poll_ms) >= 400 { + if let Some(a) = libmorpheus::desktop::DesktopAppearance::load() { + state.apply_desktop_appearance(&a); + } + last_appearance_poll_ms = now_ms; + } + islands::vsync::tick(&mut state); islands::input::poll(&mut state); islands::surface_mgr::update(&mut state); diff --git a/libmorpheus/src/desktop.rs b/libmorpheus/src/desktop.rs new file mode 100644 index 00000000..676fa0cf --- /dev/null +++ b/libmorpheus/src/desktop.rs @@ -0,0 +1,154 @@ +//! Desktop environment appearance profile shared across settings, shelld, and compd. + +use crate::persist; + +pub const APPEARANCE_KEY: &str = "de_appearance_v1"; +pub const APPEARANCE_KEY_LEGACY: &str = "de.appearance.v1"; +const MAGIC: [u8; 4] = *b"MDE1"; +const VERSION: u8 = 1; +const ENCODED_LEN: usize = 26; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct DesktopAppearance { + pub dark_mode: bool, + pub accent_rgb: (u8, u8, u8), + pub desktop_rgb: (u8, u8, u8), + pub panel_rgb: (u8, u8, u8), + pub start_rgb: (u8, u8, u8), + pub title_focus_rgb: (u8, u8, u8), + pub border_focus_rgb: (u8, u8, u8), +} + +impl DesktopAppearance { + pub const fn default_dark() -> Self { + Self { + dark_mode: true, + accent_rgb: (0, 230, 118), + desktop_rgb: (26, 26, 46), + panel_rgb: (18, 20, 30), + start_rgb: (0, 85, 0), + title_focus_rgb: (0, 85, 0), + border_focus_rgb: (0, 170, 0), + } + } + + pub const fn default_light() -> Self { + Self { + dark_mode: false, + accent_rgb: (0, 132, 96), + desktop_rgb: (236, 238, 245), + panel_rgb: (220, 223, 230), + start_rgb: (0, 132, 96), + title_focus_rgb: (0, 132, 96), + border_focus_rgb: (0, 165, 120), + } + } + + pub fn from_theme_choice(dark_mode: bool, accent_rgb: (u8, u8, u8)) -> Self { + let (ar, ag, ab) = accent_rgb; + let title = scale_rgb(accent_rgb, if dark_mode { 38 } else { 68 }); + let border = scale_rgb(accent_rgb, if dark_mode { 76 } else { 92 }); + let start = scale_rgb(accent_rgb, if dark_mode { 34 } else { 74 }); + let desktop = if dark_mode { + (26, 26, 46) + } else { + (236, 238, 245) + }; + let panel = if dark_mode { + (18, 20, 30) + } else { + (220, 223, 230) + }; + + Self { + dark_mode, + accent_rgb: (ar, ag, ab), + desktop_rgb: desktop, + panel_rgb: panel, + start_rgb: start, + title_focus_rgb: title, + border_focus_rgb: border, + } + } + + pub fn load() -> Option { + let mut buf = [0u8; ENCODED_LEN]; + if let Ok(n) = persist::get(APPEARANCE_KEY, &mut buf) { + if n == ENCODED_LEN { + if let Some(v) = decode(&buf) { + return Some(v); + } + } + } + + if let Ok(n) = persist::get(APPEARANCE_KEY_LEGACY, &mut buf) { + if n == ENCODED_LEN { + return decode(&buf); + } + } + + None + } + + pub fn store(&self) -> Result<(), u64> { + let buf = encode(self); + match persist::put(APPEARANCE_KEY, &buf) { + Ok(()) => Ok(()), + Err(_) => persist::put(APPEARANCE_KEY_LEGACY, &buf), + } + } +} + +#[inline(always)] +fn scale8(v: u8, pct: u16) -> u8 { + let x = (v as u16).saturating_mul(pct) / 100; + x.min(255) as u8 +} + +#[inline(always)] +fn scale_rgb(rgb: (u8, u8, u8), pct: u16) -> (u8, u8, u8) { + (scale8(rgb.0, pct), scale8(rgb.1, pct), scale8(rgb.2, pct)) +} + +fn encode(a: &DesktopAppearance) -> [u8; ENCODED_LEN] { + let mut out = [0u8; ENCODED_LEN]; + out[0..4].copy_from_slice(&MAGIC); + out[4] = VERSION; + out[5] = if a.dark_mode { 1 } else { 0 }; + + write_rgb(&mut out, 8, a.accent_rgb); + write_rgb(&mut out, 11, a.desktop_rgb); + write_rgb(&mut out, 14, a.panel_rgb); + write_rgb(&mut out, 17, a.start_rgb); + write_rgb(&mut out, 20, a.title_focus_rgb); + write_rgb(&mut out, 23, a.border_focus_rgb); + out +} + +fn decode(buf: &[u8; ENCODED_LEN]) -> Option { + if buf[0..4] != MAGIC || buf[4] != VERSION { + return None; + } + + Some(DesktopAppearance { + dark_mode: buf[5] != 0, + accent_rgb: read_rgb(buf, 8), + desktop_rgb: read_rgb(buf, 11), + panel_rgb: read_rgb(buf, 14), + start_rgb: read_rgb(buf, 17), + title_focus_rgb: read_rgb(buf, 20), + border_focus_rgb: read_rgb(buf, 23), + }) +} + +#[inline(always)] +fn write_rgb(buf: &mut [u8; ENCODED_LEN], i: usize, rgb: (u8, u8, u8)) { + buf[i] = rgb.0; + buf[i + 1] = rgb.1; + buf[i + 2] = rgb.2; +} + +#[inline(always)] +fn read_rgb(buf: &[u8; ENCODED_LEN], i: usize) -> (u8, u8, u8) { + (buf[i], buf[i + 1], buf[i + 2]) +} diff --git a/libmorpheus/src/lib.rs b/libmorpheus/src/lib.rs index a7fdb586..23e29aa4 100644 --- a/libmorpheus/src/lib.rs +++ b/libmorpheus/src/lib.rs @@ -16,6 +16,7 @@ extern crate alloc; // buddy.rs registers #[global_allocator] → Vec/Box/String pub mod buddy; pub mod compositor; +pub mod desktop; pub mod entry; pub mod env; pub mod error; diff --git a/network/src/stack/interface.rs b/network/src/stack/interface.rs index 9201c87c..d1b1d1e2 100644 --- a/network/src/stack/interface.rs +++ b/network/src/stack/interface.rs @@ -315,6 +315,25 @@ impl NetInterface { }) } + /// Restart DHCP discovery on an existing DHCP-enabled interface. + pub fn restart_dhcp(&mut self) -> Result<()> { + let Some(dhcp_handle) = self.dhcp_handle else { + return Err(NetworkError::ProtocolNotAvailable); + }; + + let dhcp = self.sockets.get_mut::(dhcp_handle); + dhcp.reset(); + + // Drop current lease immediately so userspace sees discovery state. + self.iface.update_ip_addrs(|addrs| addrs.clear()); + self.iface.routes_mut().remove_default_ipv4_route(); + self.gateway = None; + self.dns = None; + self.state = NetState::DhcpDiscovering; + + Ok(()) + } + /// Start a DNS query for a hostname. Returns a query handle. pub fn start_dns_query(&mut self, hostname: &str) -> Result { super::debug_log(80, "start_dns_query"); diff --git a/persistent/src/pe/embedded_reloc_data.rs b/persistent/src/pe/embedded_reloc_data.rs index 9ab250f3..a185047e 100644 --- a/persistent/src/pe/embedded_reloc_data.rs +++ b/persistent/src/pe/embedded_reloc_data.rs @@ -11,108 +11,108 @@ pub const RELOC_RVA: u32 = 0x00633000; /// Original .reloc section size -pub const RELOC_SIZE: u32 = 0x00000468; +pub const RELOC_SIZE: u32 = 0x00000464; /// Original ImageBase from linker script pub const ORIGINAL_IMAGE_BASE: u64 = 0x0000004001000000; -/// Hardcoded .reloc section data (1128 bytes) -/// Extracted from morpheus-bootloader.efi at file offset 0x001f4c00 +/// Hardcoded .reloc section data (1124 bytes) +/// Extracted from morpheus-bootloader.efi at file offset 0x001f4a00 #[allow(dead_code)] -pub const RELOC_DATA: [u8; 1128] = [ - 0x00, 0xb0, 0x06, 0x00, 0x30, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, - 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xf0, 0xa8, 0x08, 0xa9, - 0x20, 0xa9, 0x38, 0xa9, 0x50, 0xa9, 0x10, 0xaa, 0x40, 0xaa, 0xc0, 0xac, - 0xd8, 0xac, 0xf0, 0xac, 0x48, 0xad, 0x60, 0xad, 0x78, 0xad, 0xf8, 0xad, - 0x00, 0xc0, 0x06, 0x00, 0x74, 0x00, 0x00, 0x00, 0x68, 0xa1, 0x80, 0xa1, - 0x98, 0xa1, 0x20, 0xa3, 0xa0, 0xa3, 0xb8, 0xa3, 0xd0, 0xa3, 0x10, 0xa4, - 0x20, 0xa4, 0x68, 0xa4, 0x80, 0xa4, 0x98, 0xa4, 0xb0, 0xa4, 0x50, 0xa5, - 0x68, 0xa5, 0x80, 0xa5, 0x10, 0xa6, 0x28, 0xa6, 0x40, 0xa6, 0x58, 0xa6, - 0x70, 0xa6, 0x88, 0xa6, 0xa0, 0xa6, 0xf0, 0xa8, 0x08, 0xa9, 0x20, 0xa9, - 0x38, 0xa9, 0x50, 0xa9, 0x68, 0xa9, 0xe8, 0xa9, 0x00, 0xaa, 0x88, 0xaa, - 0xe8, 0xaa, 0x00, 0xab, 0x18, 0xab, 0xd0, 0xad, 0xe8, 0xad, 0x00, 0xae, - 0x18, 0xae, 0x30, 0xae, 0x48, 0xae, 0x60, 0xae, 0x78, 0xae, 0x90, 0xae, - 0x10, 0xaf, 0x28, 0xaf, 0x40, 0xaf, 0x58, 0xaf, 0x70, 0xaf, 0x88, 0xaf, - 0xa0, 0xaf, 0xb8, 0xaf, 0xd0, 0xaf, 0xe8, 0xaf, 0x00, 0xd0, 0x06, 0x00, - 0x74, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x18, 0xa0, 0x30, 0xa0, 0x48, 0xa0, - 0x60, 0xa0, 0x78, 0xa0, 0x90, 0xa0, 0x10, 0xa1, 0xa0, 0xa1, 0xc0, 0xa3, - 0xb0, 0xa4, 0xb8, 0xa5, 0xd0, 0xa7, 0xe8, 0xa7, 0x28, 0xa8, 0x40, 0xa8, - 0x58, 0xa8, 0x70, 0xa8, 0xf8, 0xa8, 0x10, 0xa9, 0x50, 0xa9, 0x68, 0xa9, - 0xc8, 0xa9, 0xe0, 0xa9, 0xf8, 0xa9, 0x10, 0xaa, 0x28, 0xaa, 0x40, 0xaa, - 0x78, 0xaa, 0xa8, 0xab, 0xe0, 0xac, 0xf8, 0xac, 0x48, 0xad, 0xc0, 0xad, - 0xd8, 0xad, 0xf0, 0xad, 0x08, 0xae, 0x20, 0xae, 0x38, 0xae, 0x50, 0xae, - 0x68, 0xae, 0x80, 0xae, 0x98, 0xae, 0xb0, 0xae, 0xc8, 0xae, 0xe0, 0xae, - 0xf8, 0xae, 0x10, 0xaf, 0x78, 0xaf, 0x90, 0xaf, 0xa8, 0xaf, 0xc0, 0xaf, - 0xd8, 0xaf, 0xf0, 0xaf, 0x00, 0xe0, 0x06, 0x00, 0x90, 0x00, 0x00, 0x00, - 0x08, 0xa0, 0x20, 0xa0, 0x38, 0xa0, 0x50, 0xa0, 0x68, 0xa0, 0xb8, 0xa0, - 0xc8, 0xa0, 0x20, 0xa1, 0x30, 0xa1, 0x10, 0xa3, 0x28, 0xa3, 0x40, 0xa3, - 0x58, 0xa3, 0xd8, 0xa3, 0xf0, 0xa3, 0x08, 0xa4, 0x20, 0xa4, 0x38, 0xa4, - 0x50, 0xa4, 0x68, 0xa4, 0x80, 0xa4, 0x98, 0xa4, 0xc8, 0xa4, 0x68, 0xa5, - 0x80, 0xa5, 0x00, 0xa6, 0x80, 0xa6, 0x98, 0xa6, 0xb0, 0xa6, 0xc8, 0xa6, - 0xe0, 0xa6, 0xf8, 0xa6, 0x80, 0xa7, 0xa8, 0xa8, 0xb8, 0xa8, 0xc8, 0xa8, - 0x40, 0xa9, 0x58, 0xa9, 0x70, 0xa9, 0x88, 0xa9, 0xa0, 0xa9, 0xb8, 0xa9, - 0xd0, 0xa9, 0xe8, 0xa9, 0x00, 0xaa, 0x18, 0xaa, 0x30, 0xaa, 0x48, 0xaa, - 0x60, 0xaa, 0x78, 0xaa, 0x90, 0xaa, 0xa8, 0xaa, 0x50, 0xac, 0x68, 0xac, - 0x80, 0xac, 0x98, 0xac, 0xb0, 0xac, 0xc8, 0xac, 0xe0, 0xac, 0xf8, 0xac, - 0x10, 0xad, 0x98, 0xad, 0xd8, 0xad, 0xe8, 0xad, 0xa8, 0xae, 0x38, 0xaf, - 0x78, 0xaf, 0xc0, 0xaf, 0x00, 0xf0, 0x06, 0x00, 0x68, 0x00, 0x00, 0x00, - 0x58, 0xa0, 0xa0, 0xa0, 0xb0, 0xa0, 0xc0, 0xa0, 0xd0, 0xa0, 0x50, 0xa1, - 0x68, 0xa1, 0xe8, 0xa1, 0x88, 0xa2, 0x98, 0xa2, 0xa8, 0xa2, 0x40, 0xa3, - 0x78, 0xa3, 0x90, 0xa3, 0xa8, 0xa3, 0xc0, 0xa3, 0x40, 0xa4, 0x80, 0xa4, - 0x98, 0xa4, 0xd8, 0xa5, 0x08, 0xa6, 0x20, 0xa6, 0x78, 0xa6, 0x90, 0xa6, - 0xe0, 0xa6, 0xf8, 0xa6, 0x10, 0xa7, 0x48, 0xa7, 0x78, 0xa7, 0xb0, 0xa7, - 0xa8, 0xa9, 0xc0, 0xa9, 0x58, 0xaa, 0x70, 0xaa, 0x88, 0xaa, 0xa0, 0xaa, - 0xb8, 0xaa, 0xc8, 0xab, 0x28, 0xac, 0xa0, 0xac, 0x98, 0xad, 0xb0, 0xad, - 0xc8, 0xad, 0xe0, 0xad, 0xf8, 0xad, 0x28, 0xae, 0x48, 0xae, 0x00, 0x00, - 0x00, 0x00, 0x07, 0x00, 0x50, 0x00, 0x00, 0x00, 0x90, 0xa3, 0xa8, 0xa3, - 0xc0, 0xa3, 0xd8, 0xa3, 0xf0, 0xa3, 0x08, 0xa4, 0x28, 0xa5, 0x40, 0xa5, - 0x58, 0xa5, 0xd0, 0xa5, 0x08, 0xa6, 0x78, 0xa8, 0x90, 0xa8, 0xa8, 0xa8, - 0xc0, 0xa8, 0xd8, 0xa8, 0xf0, 0xa8, 0x08, 0xa9, 0x40, 0xa9, 0x50, 0xaa, - 0x90, 0xab, 0xf8, 0xab, 0x18, 0xac, 0x30, 0xac, 0x48, 0xac, 0x60, 0xac, - 0x78, 0xac, 0x80, 0xad, 0xf0, 0xad, 0x08, 0xae, 0x20, 0xae, 0x38, 0xae, - 0x50, 0xae, 0x68, 0xae, 0xa8, 0xae, 0x38, 0xaf, 0x00, 0x10, 0x07, 0x00, - 0x4c, 0x00, 0x00, 0x00, 0xd0, 0xa9, 0x00, 0xab, 0x58, 0xac, 0x68, 0xac, - 0x78, 0xac, 0x88, 0xac, 0x98, 0xac, 0xa8, 0xac, 0xb8, 0xac, 0xc8, 0xac, - 0xd8, 0xac, 0xe8, 0xac, 0xf8, 0xac, 0x08, 0xad, 0x18, 0xad, 0x28, 0xad, - 0x38, 0xad, 0x48, 0xad, 0x58, 0xad, 0x68, 0xad, 0x78, 0xad, 0x88, 0xad, - 0x98, 0xad, 0xa8, 0xad, 0xb8, 0xad, 0xc8, 0xad, 0xd8, 0xad, 0xe8, 0xad, - 0xf8, 0xad, 0x08, 0xae, 0x18, 0xae, 0x28, 0xae, 0x38, 0xae, 0x48, 0xae, - 0x00, 0x20, 0x07, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x28, 0xa0, 0x00, 0x00, - 0x00, 0x30, 0x07, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x08, 0xa2, 0x60, 0xa2, - 0xd0, 0xa3, 0xe8, 0xa3, 0x38, 0xa4, 0x48, 0xa4, 0x58, 0xa4, 0x68, 0xa4, - 0x78, 0xa4, 0xa8, 0xa4, 0xe0, 0xa4, 0x28, 0xa5, 0x78, 0xa5, 0xb0, 0xa5, - 0xc8, 0xa5, 0x10, 0xa6, 0x98, 0xa6, 0x00, 0x00, 0x00, 0x40, 0x07, 0x00, - 0x54, 0x00, 0x00, 0x00, 0x50, 0xa3, 0x68, 0xa3, 0xa0, 0xa3, 0xd0, 0xa3, - 0xe8, 0xa3, 0x00, 0xa4, 0x18, 0xa4, 0x30, 0xa4, 0x48, 0xa4, 0x60, 0xa4, - 0x78, 0xa4, 0xb8, 0xa4, 0xd0, 0xa4, 0xe8, 0xa4, 0x00, 0xa5, 0x18, 0xa5, - 0x50, 0xa5, 0x80, 0xa5, 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, 0x00, 0xa6, - 0x18, 0xa6, 0x30, 0xa6, 0x48, 0xa6, 0x60, 0xa6, 0x78, 0xa6, 0x08, 0xa7, - 0x50, 0xad, 0x60, 0xad, 0x90, 0xad, 0xa8, 0xad, 0xf0, 0xad, 0x00, 0xae, - 0x10, 0xae, 0x38, 0xae, 0x70, 0xae, 0x00, 0x00, 0x00, 0x50, 0x07, 0x00, - 0x48, 0x00, 0x00, 0x00, 0xb0, 0xa3, 0xc0, 0xa3, 0xd0, 0xa3, 0xe0, 0xa3, - 0x30, 0xa4, 0x40, 0xa4, 0x50, 0xa4, 0x60, 0xa4, 0x70, 0xa4, 0x98, 0xa4, - 0xa8, 0xa4, 0xb8, 0xa4, 0x20, 0xa5, 0x30, 0xa5, 0x40, 0xa5, 0x88, 0xa5, - 0x98, 0xa5, 0xd0, 0xa5, 0xe0, 0xa5, 0x08, 0xa6, 0x18, 0xa6, 0x50, 0xa6, - 0x68, 0xa6, 0xc0, 0xa6, 0x30, 0xad, 0x48, 0xad, 0x80, 0xad, 0xd0, 0xad, - 0x18, 0xae, 0x28, 0xae, 0xb0, 0xaf, 0xe8, 0xaf, 0x00, 0x60, 0x07, 0x00, - 0x88, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xc8, 0xa0, 0xe0, 0xa0, 0x90, 0xa1, - 0xa8, 0xa1, 0xf8, 0xa1, 0x08, 0xa2, 0x38, 0xa2, 0x70, 0xa2, 0x80, 0xa2, - 0xd0, 0xa2, 0xe0, 0xa2, 0x28, 0xa3, 0x38, 0xa3, 0xc8, 0xa3, 0xd8, 0xa3, - 0x10, 0xa4, 0x20, 0xa4, 0x50, 0xa4, 0x60, 0xa4, 0x78, 0xa4, 0xb8, 0xa4, - 0xc8, 0xa4, 0xe0, 0xa4, 0x38, 0xa5, 0x48, 0xa5, 0x60, 0xa5, 0x78, 0xa5, - 0x90, 0xa5, 0xc8, 0xa5, 0xe0, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0xa8, 0xa6, - 0xc0, 0xa6, 0xd8, 0xa6, 0xf0, 0xa6, 0x08, 0xa7, 0x20, 0xa7, 0x38, 0xa7, - 0x08, 0xa9, 0x20, 0xa9, 0x60, 0xa9, 0x78, 0xa9, 0x90, 0xa9, 0xa8, 0xa9, - 0xe8, 0xa9, 0x68, 0xaa, 0x80, 0xaa, 0x98, 0xaa, 0xb0, 0xaa, 0xc8, 0xaa, - 0xe0, 0xaa, 0xf8, 0xaa, 0x10, 0xab, 0x28, 0xab, 0x40, 0xab, 0x58, 0xab, - 0x70, 0xab, 0x88, 0xab, 0xd8, 0xad, 0xf0, 0xad, 0xe8, 0xae, 0x90, 0xaf, - 0x00, 0x70, 0x07, 0x00, 0x20, 0x00, 0x00, 0x00, 0x40, 0xa0, 0x58, 0xa0, - 0x70, 0xa0, 0x88, 0xa0, 0xa0, 0xa0, 0xb8, 0xa0, 0xd0, 0xa0, 0x38, 0xa1, - 0x50, 0xa1, 0x68, 0xa1, 0xd8, 0xa1, 0x08, 0xa2, 0x00, 0x90, 0x07, 0x00, - 0x14, 0x00, 0x00, 0x00, 0xf0, 0xa6, 0xf8, 0xa6, 0x00, 0xa7, 0x08, 0xa7, - 0x10, 0xa7, 0x18, 0xa7, 0x00, 0x30, 0x1e, 0x00, 0x14, 0x00, 0x00, 0x00, - 0x80, 0xa7, 0x90, 0xa8, 0x98, 0xa8, 0xa0, 0xa8, 0xa8, 0xa8, 0x00, 0x00, - 0x00, 0x50, 0x1f, 0x00, 0x0c, 0x00, 0x00, 0x00, 0xe0, 0xae, 0x00, 0x00, - 0x00, 0x20, 0x63, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, +pub const RELOC_DATA: [u8; 1124] = [ + 0x00, 0xb0, 0x06, 0x00, 0x64, 0x00, 0x00, 0x00, 0x50, 0xa4, 0xe0, 0xa5, + 0x38, 0xa6, 0x50, 0xa6, 0x68, 0xa6, 0x80, 0xa6, 0xd8, 0xa6, 0xf0, 0xa6, + 0x08, 0xa7, 0x20, 0xa7, 0x38, 0xa7, 0x50, 0xa7, 0x68, 0xa7, 0xe8, 0xa7, + 0x00, 0xa8, 0x18, 0xa8, 0x30, 0xa8, 0x48, 0xa8, 0x60, 0xa8, 0x78, 0xa8, + 0x90, 0xa8, 0xa8, 0xa8, 0x28, 0xa9, 0x40, 0xa9, 0x58, 0xa9, 0x70, 0xa9, + 0x88, 0xa9, 0xa0, 0xa9, 0xb8, 0xa9, 0xd0, 0xa9, 0xe8, 0xa9, 0x00, 0xaa, + 0x18, 0xaa, 0x30, 0xaa, 0x48, 0xaa, 0x60, 0xaa, 0x78, 0xaa, 0x90, 0xaa, + 0xa8, 0xaa, 0x28, 0xab, 0x40, 0xab, 0x58, 0xab, 0x70, 0xab, 0x88, 0xab, + 0x08, 0xac, 0x78, 0xaf, 0x00, 0xc0, 0x06, 0x00, 0x38, 0x00, 0x00, 0x00, + 0xe8, 0xa3, 0x80, 0xa6, 0x98, 0xa6, 0xb0, 0xa6, 0xf8, 0xa8, 0x00, 0xaa, + 0xe8, 0xab, 0x00, 0xac, 0x18, 0xac, 0x70, 0xac, 0x88, 0xac, 0xa0, 0xac, + 0x20, 0xad, 0x38, 0xad, 0x50, 0xad, 0x50, 0xae, 0x68, 0xae, 0x80, 0xae, + 0x98, 0xae, 0xb0, 0xae, 0xc8, 0xae, 0x48, 0xaf, 0x60, 0xaf, 0xe8, 0xaf, + 0x00, 0xd0, 0x06, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x48, 0xa0, 0x60, 0xa0, + 0x78, 0xa0, 0xd8, 0xa0, 0x58, 0xa1, 0x70, 0xa1, 0x88, 0xa1, 0xc8, 0xa1, + 0xd8, 0xa1, 0x20, 0xa2, 0x38, 0xa2, 0x50, 0xa2, 0x68, 0xa2, 0x08, 0xa3, + 0xd8, 0xa5, 0xf0, 0xa5, 0x30, 0xa6, 0x48, 0xa6, 0x60, 0xa6, 0x78, 0xa6, + 0x00, 0xa7, 0x18, 0xa7, 0xd8, 0xa7, 0x08, 0xa8, 0x68, 0xa8, 0x80, 0xa8, + 0xe0, 0xa8, 0xf8, 0xa8, 0x10, 0xa9, 0x28, 0xa9, 0x40, 0xa9, 0x58, 0xa9, + 0x90, 0xa9, 0xc0, 0xaa, 0xf8, 0xab, 0x10, 0xac, 0x60, 0xac, 0xd8, 0xac, + 0xf0, 0xac, 0x08, 0xad, 0x20, 0xad, 0x38, 0xad, 0x50, 0xad, 0x68, 0xad, + 0x80, 0xad, 0x98, 0xad, 0xb0, 0xad, 0xc8, 0xad, 0xe0, 0xad, 0xf8, 0xad, + 0x10, 0xae, 0x28, 0xae, 0x90, 0xae, 0xa8, 0xae, 0xc0, 0xae, 0xd8, 0xae, + 0xf0, 0xae, 0x08, 0xaf, 0x20, 0xaf, 0x38, 0xaf, 0x50, 0xaf, 0x68, 0xaf, + 0x80, 0xaf, 0xd0, 0xaf, 0xe0, 0xaf, 0x00, 0x00, 0x00, 0xe0, 0x06, 0x00, + 0x8c, 0x00, 0x00, 0x00, 0x38, 0xa0, 0x48, 0xa0, 0x28, 0xa2, 0x40, 0xa2, + 0x58, 0xa2, 0x70, 0xa2, 0xf0, 0xa2, 0x08, 0xa3, 0x20, 0xa3, 0x38, 0xa3, + 0x50, 0xa3, 0x68, 0xa3, 0x80, 0xa3, 0x98, 0xa3, 0xb0, 0xa3, 0xe0, 0xa3, + 0x80, 0xa4, 0x98, 0xa4, 0x18, 0xa5, 0x98, 0xa5, 0xb0, 0xa5, 0xc8, 0xa5, + 0xe0, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0x98, 0xa6, 0xc0, 0xa7, 0xd0, 0xa7, + 0xe0, 0xa7, 0x58, 0xa8, 0x70, 0xa8, 0x88, 0xa8, 0xa0, 0xa8, 0xb8, 0xa8, + 0xd0, 0xa8, 0xe8, 0xa8, 0x00, 0xa9, 0x18, 0xa9, 0x30, 0xa9, 0x48, 0xa9, + 0x60, 0xa9, 0x78, 0xa9, 0x90, 0xa9, 0xa8, 0xa9, 0xc0, 0xa9, 0x68, 0xab, + 0x80, 0xab, 0x98, 0xab, 0xb0, 0xab, 0xc8, 0xab, 0xe0, 0xab, 0xf8, 0xab, + 0x10, 0xac, 0x28, 0xac, 0xb0, 0xac, 0xf0, 0xac, 0x00, 0xad, 0xc0, 0xad, + 0x50, 0xae, 0x90, 0xae, 0xd8, 0xae, 0x70, 0xaf, 0xb8, 0xaf, 0xc8, 0xaf, + 0xd8, 0xaf, 0xe8, 0xaf, 0x00, 0xf0, 0x06, 0x00, 0x5c, 0x00, 0x00, 0x00, + 0x68, 0xa0, 0x80, 0xa0, 0x00, 0xa1, 0xa0, 0xa1, 0xb0, 0xa1, 0xc0, 0xa1, + 0x58, 0xa2, 0x90, 0xa2, 0xa8, 0xa2, 0xc0, 0xa2, 0xd8, 0xa2, 0x58, 0xa3, + 0x98, 0xa3, 0xb0, 0xa3, 0xf0, 0xa4, 0x20, 0xa5, 0x38, 0xa5, 0x90, 0xa5, + 0xa8, 0xa5, 0xf8, 0xa5, 0x10, 0xa6, 0x28, 0xa6, 0x60, 0xa6, 0x90, 0xa6, + 0xc8, 0xa6, 0xc0, 0xa8, 0xd8, 0xa8, 0x70, 0xa9, 0x88, 0xa9, 0xa0, 0xa9, + 0xb8, 0xa9, 0xd0, 0xa9, 0xe0, 0xaa, 0x40, 0xab, 0xb8, 0xab, 0xb0, 0xac, + 0xc8, 0xac, 0xe0, 0xac, 0xf8, 0xac, 0x10, 0xad, 0x40, 0xad, 0x60, 0xad, + 0x00, 0x00, 0x07, 0x00, 0x50, 0x00, 0x00, 0x00, 0xa8, 0xa2, 0xc0, 0xa2, + 0xd8, 0xa2, 0xf0, 0xa2, 0x08, 0xa3, 0x20, 0xa3, 0x40, 0xa4, 0x58, 0xa4, + 0x70, 0xa4, 0xe8, 0xa4, 0x20, 0xa5, 0x90, 0xa7, 0xa8, 0xa7, 0xc0, 0xa7, + 0xd8, 0xa7, 0xf0, 0xa7, 0x08, 0xa8, 0x20, 0xa8, 0x58, 0xa8, 0x68, 0xa9, + 0xa8, 0xaa, 0x10, 0xab, 0x30, 0xab, 0x48, 0xab, 0x60, 0xab, 0x78, 0xab, + 0x90, 0xab, 0x98, 0xac, 0x08, 0xad, 0x20, 0xad, 0x38, 0xad, 0x50, 0xad, + 0x68, 0xad, 0x80, 0xad, 0xc0, 0xad, 0x50, 0xae, 0x00, 0x10, 0x07, 0x00, + 0x50, 0x00, 0x00, 0x00, 0xe8, 0xa8, 0x18, 0xaa, 0x70, 0xab, 0x80, 0xab, + 0x90, 0xab, 0xa0, 0xab, 0xb0, 0xab, 0xc0, 0xab, 0xd0, 0xab, 0xe0, 0xab, + 0xf0, 0xab, 0x00, 0xac, 0x10, 0xac, 0x20, 0xac, 0x30, 0xac, 0x40, 0xac, + 0x50, 0xac, 0x60, 0xac, 0x70, 0xac, 0x80, 0xac, 0x90, 0xac, 0xa0, 0xac, + 0xb0, 0xac, 0xc0, 0xac, 0xd0, 0xac, 0xe0, 0xac, 0xf0, 0xac, 0x00, 0xad, + 0x10, 0xad, 0x20, 0xad, 0x30, 0xad, 0x40, 0xad, 0x50, 0xad, 0x60, 0xad, + 0x40, 0xaf, 0x00, 0x00, 0x00, 0x30, 0x07, 0x00, 0x2c, 0x00, 0x00, 0x00, + 0x20, 0xa1, 0x78, 0xa1, 0xe8, 0xa2, 0x00, 0xa3, 0x50, 0xa3, 0x60, 0xa3, + 0x70, 0xa3, 0x80, 0xa3, 0x90, 0xa3, 0xc0, 0xa3, 0xf8, 0xa3, 0x40, 0xa4, + 0x90, 0xa4, 0xc8, 0xa4, 0xe0, 0xa4, 0x28, 0xa5, 0xb0, 0xa5, 0x00, 0x00, + 0x00, 0x40, 0x07, 0x00, 0x54, 0x00, 0x00, 0x00, 0x68, 0xa2, 0x80, 0xa2, + 0xb8, 0xa2, 0xe8, 0xa2, 0x00, 0xa3, 0x18, 0xa3, 0x30, 0xa3, 0x48, 0xa3, + 0x60, 0xa3, 0x78, 0xa3, 0x90, 0xa3, 0xd0, 0xa3, 0xe8, 0xa3, 0x00, 0xa4, + 0x18, 0xa4, 0x30, 0xa4, 0x68, 0xa4, 0x98, 0xa4, 0xb0, 0xa4, 0xc8, 0xa4, + 0xe0, 0xa4, 0x18, 0xa5, 0x30, 0xa5, 0x48, 0xa5, 0x60, 0xa5, 0x78, 0xa5, + 0x90, 0xa5, 0x20, 0xa6, 0x68, 0xac, 0x78, 0xac, 0xa8, 0xac, 0xc0, 0xac, + 0x08, 0xad, 0x18, 0xad, 0x28, 0xad, 0x50, 0xad, 0x88, 0xad, 0x00, 0x00, + 0x00, 0x50, 0x07, 0x00, 0x50, 0x00, 0x00, 0x00, 0xc8, 0xa2, 0xd8, 0xa2, + 0xe8, 0xa2, 0xf8, 0xa2, 0x48, 0xa3, 0x58, 0xa3, 0x68, 0xa3, 0x78, 0xa3, + 0x88, 0xa3, 0xb0, 0xa3, 0xc0, 0xa3, 0xd0, 0xa3, 0x38, 0xa4, 0x48, 0xa4, + 0x58, 0xa4, 0xa0, 0xa4, 0xb0, 0xa4, 0xe8, 0xa4, 0xf8, 0xa4, 0x20, 0xa5, + 0x30, 0xa5, 0x68, 0xa5, 0x80, 0xa5, 0xd8, 0xa5, 0x48, 0xac, 0x60, 0xac, + 0x98, 0xac, 0xe8, 0xac, 0x30, 0xad, 0x40, 0xad, 0xc8, 0xae, 0x00, 0xaf, + 0x18, 0xaf, 0xe0, 0xaf, 0xf8, 0xaf, 0x00, 0x00, 0x00, 0x60, 0x07, 0x00, + 0x90, 0x00, 0x00, 0x00, 0xa8, 0xa0, 0xc0, 0xa0, 0x10, 0xa1, 0x20, 0xa1, + 0x50, 0xa1, 0x88, 0xa1, 0x98, 0xa1, 0xe8, 0xa1, 0xf8, 0xa1, 0x40, 0xa2, + 0x50, 0xa2, 0xe0, 0xa2, 0xf0, 0xa2, 0x28, 0xa3, 0x38, 0xa3, 0x68, 0xa3, + 0x78, 0xa3, 0x90, 0xa3, 0xd0, 0xa3, 0xe0, 0xa3, 0xf8, 0xa3, 0x50, 0xa4, + 0x60, 0xa4, 0x78, 0xa4, 0x90, 0xa4, 0xa8, 0xa4, 0xe0, 0xa4, 0xf8, 0xa4, + 0x10, 0xa5, 0x28, 0xa5, 0xc0, 0xa5, 0xd8, 0xa5, 0xf0, 0xa5, 0x08, 0xa6, + 0x20, 0xa6, 0x38, 0xa6, 0x50, 0xa6, 0x20, 0xa8, 0x38, 0xa8, 0x78, 0xa8, + 0x90, 0xa8, 0xa8, 0xa8, 0xc0, 0xa8, 0x00, 0xa9, 0x80, 0xa9, 0x98, 0xa9, + 0xb0, 0xa9, 0xc8, 0xa9, 0xe0, 0xa9, 0xf8, 0xa9, 0x10, 0xaa, 0x28, 0xaa, + 0x40, 0xaa, 0x58, 0xaa, 0x70, 0xaa, 0x88, 0xaa, 0xa0, 0xaa, 0xf0, 0xac, + 0x08, 0xad, 0x00, 0xae, 0xa8, 0xae, 0x58, 0xaf, 0x70, 0xaf, 0x88, 0xaf, + 0xa0, 0xaf, 0xb8, 0xaf, 0xd0, 0xaf, 0xe8, 0xaf, 0x00, 0x70, 0x07, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x50, 0xa0, 0x68, 0xa0, 0x80, 0xa0, 0xf0, 0xa0, + 0x20, 0xa1, 0x00, 0x00, 0x00, 0x90, 0x07, 0x00, 0x14, 0x00, 0x00, 0x00, + 0xf0, 0xa6, 0xf8, 0xa6, 0x00, 0xa7, 0x08, 0xa7, 0x10, 0xa7, 0x18, 0xa7, + 0x00, 0x30, 0x1e, 0x00, 0x14, 0x00, 0x00, 0x00, 0x80, 0xa7, 0x90, 0xa8, + 0x98, 0xa8, 0xa0, 0xa8, 0xa8, 0xa8, 0x00, 0x00, 0x00, 0x50, 0x1f, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0xe0, 0xae, 0x00, 0x00, 0x00, 0x20, 0x63, 0x00, + 0x0c, 0x00, 0x00, 0x00, 0x20, 0xa0, 0x00, 0x00, ]; diff --git a/settings/src/chambers/mirror.rs b/settings/src/chambers/mirror.rs index b0720b4e..d3d268bb 100644 --- a/settings/src/chambers/mirror.rs +++ b/settings/src/chambers/mirror.rs @@ -1,11 +1,10 @@ -// mirror basin — appearance controls. -// dark/light toggle, accent color selection, intensity tweaking. -// purely local state — changes apply to the running app instance only. +// mirror basin — DE appearance controls. use crate::layout::{self, PANE_PAD, RAIL_WIDTH, STRIP_HEIGHT}; use crate::state::{Route, SettingsApp}; use crate::theme::OneiricTheme; use crate::widgets; +use libmorpheus::desktop::DesktopAppearance; const FIELD_THEME_TOGGLE: usize = 0; const FIELD_ACCENT_PREV: usize = 1; @@ -14,7 +13,6 @@ const FIELD_APPLY: usize = 3; const FIELD_REVERT: usize = 4; const FIELD_COUNT: usize = 5; -// preset accent palettes const ACCENT_COUNT: usize = 6; const ACCENTS: [(u8, u8, u8, &str); ACCENT_COUNT] = [ (0, 230, 118, "Morpheus Green"), @@ -28,7 +26,6 @@ const ACCENTS: [(u8, u8, u8, &str); ACCENT_COUNT] = [ pub struct MirrorChamber { pub dark_mode: bool, pub accent_idx: usize, - // snapshot for revert pub saved_dark: bool, pub saved_accent: usize, } @@ -63,33 +60,68 @@ impl MirrorChamber { } } +pub fn sync_from_global(app: &mut SettingsApp) { + if let Some(a) = DesktopAppearance::load() { + app.mirror.dark_mode = a.dark_mode; + app.mirror.accent_idx = nearest_accent_idx(a.accent_rgb); + app.mirror.saved_dark = app.mirror.dark_mode; + app.mirror.saved_accent = app.mirror.accent_idx; + rebuild_theme(app); + } +} + pub fn activate(app: &mut SettingsApp, idx: usize) { match idx { FIELD_THEME_TOGGLE => { app.mirror.dark_mode = !app.mirror.dark_mode; rebuild_theme(app); + if let Err(code) = sync_global_from_local(app) { + libmorpheus::println!("[settings/mirror] de appearance sync failed err=0x{:x}", code); + app.set_status("DE appearance sync failed", true); + } app.mark_edited(Route::MirrorBasin, "theme_mode"); } FIELD_ACCENT_PREV => { - app.mirror.accent_idx = if app.mirror.accent_idx == 0 { ACCENT_COUNT - 1 } else { app.mirror.accent_idx - 1 }; + app.mirror.accent_idx = if app.mirror.accent_idx == 0 { + ACCENT_COUNT - 1 + } else { + app.mirror.accent_idx - 1 + }; rebuild_theme(app); + if let Err(code) = sync_global_from_local(app) { + libmorpheus::println!("[settings/mirror] de appearance sync failed err=0x{:x}", code); + app.set_status("DE appearance sync failed", true); + } app.mark_edited(Route::MirrorBasin, "accent"); } FIELD_ACCENT_NEXT => { app.mirror.accent_idx = (app.mirror.accent_idx + 1) % ACCENT_COUNT; rebuild_theme(app); + if let Err(code) = sync_global_from_local(app) { + libmorpheus::println!("[settings/mirror] de appearance sync failed err=0x{:x}", code); + app.set_status("DE appearance sync failed", true); + } app.mark_edited(Route::MirrorBasin, "accent"); } FIELD_APPLY => { let accent_name = ACCENTS[app.mirror.accent_idx].3; - app.mirror.apply(); - app.clear_pending_for(Route::MirrorBasin); - app.set_status("Appearance applied", false); - app.log_change(Route::MirrorBasin, "appearance", accent_name, false); + match sync_global_from_local(app) { + Ok(()) => { + app.mirror.apply(); + app.clear_pending_for(Route::MirrorBasin); + app.set_status("DE appearance applied", false); + app.log_change(Route::MirrorBasin, "appearance", accent_name, false); + } + Err(code) => { + libmorpheus::println!("[settings/mirror] de appearance apply failed err=0x{:x}", code); + app.set_status("DE appearance apply failed", true); + } + } } FIELD_REVERT => { app.mirror.revert(); rebuild_theme(app); + let _ = sync_global_from_local(app); app.clear_pending_for(Route::MirrorBasin); app.set_status("Appearance reverted", false); } @@ -100,7 +132,11 @@ pub fn activate(app: &mut SettingsApp, idx: usize) { fn rebuild_theme(app: &mut SettingsApp) { let dark = app.mirror.dark_mode; let ai = app.mirror.accent_idx; - let base = if dark { OneiricTheme::dark() } else { OneiricTheme::light() }; + let base = if dark { + OneiricTheme::dark() + } else { + OneiricTheme::light() + }; let (r, g, b, _) = ACCENTS[ai]; let accent = crate::theme::pack(r, g, b); app.theme.signal = accent; @@ -118,6 +154,28 @@ fn rebuild_theme(app: &mut SettingsApp) { app.frame_dirty = true; } +fn nearest_accent_idx(target: (u8, u8, u8)) -> usize { + let mut best = 0usize; + let mut best_dist = u32::MAX; + for (i, (r, g, b, _)) in ACCENTS.iter().enumerate() { + let dr = (*r as i32 - target.0 as i32) as i64; + let dg = (*g as i32 - target.1 as i32) as i64; + let db = (*b as i32 - target.2 as i32) as i64; + let d = (dr * dr + dg * dg + db * db) as u32; + if d < best_dist { + best_dist = d; + best = i; + } + } + best +} + +fn sync_global_from_local(app: &SettingsApp) -> Result<(), u64> { + let (r, g, b, _) = ACCENTS[app.mirror.accent_idx]; + let profile = DesktopAppearance::from_theme_choice(app.mirror.dark_mode, (r, g, b)); + profile.store() +} + pub fn handle_key(_app: &mut SettingsApp, _scancode: u8) {} pub fn render(app: &SettingsApp) { @@ -131,22 +189,23 @@ pub fn render(app: &SettingsApp) { let r8 = layout::row_step(app, 8); let r12 = layout::row_step(app, 12); - // theme mode layout::draw_section(app, px, cy, "Theme Mode"); cy += r4; - let mode_label = if mirror.dark_mode { "[X] Dark [ ] Light" } else { "[ ] Dark [X] Light" }; + let mode_label = if mirror.dark_mode { + "[X] Dark [ ] Light" + } else { + "[ ] Dark [X] Light" + }; layout::draw_button_row(app, px, cy, mode_label, FIELD_THEME_TOGGLE, t.glyph); cy += r8; - // accent color layout::draw_section(app, px, cy, "Accent Color"); cy += r4; let (ar, ag, ab, name) = ACCENTS[mirror.accent_idx]; let accent = crate::theme::pack(ar, ag, ab); - // show all accents in a row with selection marker for i in 0..ACCENT_COUNT { let (r, g, b, _) = ACCENTS[i]; let c = crate::theme::pack(r, g, b); @@ -154,26 +213,32 @@ pub fn render(app: &SettingsApp) { let swatch_y = cy; widgets::fill_rect(app.surface, app.fb_stride, sx, swatch_y, 24, 16, c, app.fb_w, app.fb_h); if i == mirror.accent_idx { - widgets::rect_outline(app.surface, app.fb_stride, sx.saturating_sub(1), swatch_y.saturating_sub(1), 26, 18, t.focus_ring, app.fb_w, app.fb_h); + widgets::rect_outline( + app.surface, + app.fb_stride, + sx.saturating_sub(1), + swatch_y.saturating_sub(1), + 26, + 18, + t.focus_ring, + app.fb_w, + app.fb_h, + ); } } cy += 20; - // selected name widgets::draw_str(app.surface, app.fb_stride, px, cy, name, accent, t.substrate, app.fb_w, app.fb_h); cy += r4; - // nav buttons layout::draw_button_row(app, px, cy, "<< Previous Accent", FIELD_ACCENT_PREV, t.glyph); cy += r4; layout::draw_button_row(app, px, cy, ">> Next Accent", FIELD_ACCENT_NEXT, t.glyph); cy += r12; - // preview section layout::draw_section(app, px, cy, "Preview"); cy += r4; - // color swatch grid showing all theme tokens let tokens: [(&str, u32); 10] = [ ("substrate", t.substrate), ("glyph", t.glyph), @@ -195,7 +260,6 @@ pub fn render(app: &SettingsApp) { cy += 8; - // action buttons layout::draw_button_row(app, px, cy, "Apply Appearance", FIELD_APPLY, t.signal); cy += r4; layout::draw_button_row(app, px, cy, "Revert", FIELD_REVERT, t.glyph_dim); diff --git a/settings/src/chambers/net_obs.rs b/settings/src/chambers/net_obs.rs index 2f62b173..8f11e8b5 100644 --- a/settings/src/chambers/net_obs.rs +++ b/settings/src/chambers/net_obs.rs @@ -123,8 +123,10 @@ impl NetObsChamber { self.edit_dhcp = (cfg.flags & net::NET_FLAG_DHCP) != 0; } - // populate edit fields from live state - self.sync_edit_from_live(); + // Don't clobber in-progress typing with periodic refresh. + if self.editing_field.is_none() { + self.sync_edit_from_live(); + } } if let Ok(info) = net::nic_info() { @@ -143,6 +145,14 @@ impl NetObsChamber { self.stack_available = false; } } + + // Prefer NIC hardware counters when available. + if let Ok(hw) = net::nic_hw_stats() { + self.tx_packets = hw.tx_packets; + self.rx_packets = hw.rx_packets; + self.tx_bytes = hw.tx_bytes; + self.rx_bytes = hw.rx_bytes; + } } fn sync_edit_from_live(&mut self) { @@ -702,9 +712,7 @@ fn parse_ipv4_strict(buf: &[u8]) -> Result { } pub fn scancode_to_char(sc: u8) -> Option { - if sc.is_ascii_graphic() || sc == b' ' { - return Some(sc.to_ascii_lowercase()); - } + // Input here is keyboard scancode, not ASCII. Map explicitly. match sc { 0x02..=0x0A => Some(b'1' + (sc - 0x02)), 0x0B => Some(b'0'), diff --git a/settings/src/state.rs b/settings/src/state.rs index 509b0730..93ad7b05 100644 --- a/settings/src/state.rs +++ b/settings/src/state.rs @@ -256,6 +256,7 @@ impl SettingsApp { pub fn init(&mut self) { self.frame_dirty = true; + crate::chambers::mirror::sync_from_global(self); self.net_obs.refresh(); self.sys_obs.refresh(); self.mist.refresh(); diff --git a/setup-dev.sh b/setup-dev.sh index 924102dc..2c90726f 100755 --- a/setup-dev.sh +++ b/setup-dev.sh @@ -29,6 +29,8 @@ readonly SYM_BULLET="•" INTERACTIVE=false FORCE_MODE=false SKIP_QEMU=false +NET_MODE="${MORPHEUS_NET_MODE:-bridge}" +NET_BRIDGE_IF="${MORPHEUS_BRIDGE_IF:-br0}" log_info() { printf "${C_BLUE}${SYM_ARROW} %s${C_RESET}\n" "$1"; } log_success() { printf "${C_GREEN}${SYM_CHECK} %s${C_RESET}\n" "$1"; } @@ -39,6 +41,118 @@ die() { log_error "$1"; exit 1; } has_cmd() { command -v "$1" &>/dev/null; } +bridge_acl_allows() { + local bridge="$1" + local conf="/etc/qemu/bridge.conf" + + [[ -f "$conf" ]] || return 1 + + # If ACL exists but is unreadable to this user, don't guess deny here. + # qemu-bridge-helper will enforce policy with its own privileges. + if [[ ! -r "$conf" ]]; then + return 2 + fi + + # Accept either explicit bridge or allow-all policy. + if grep -Eq "^[[:space:]]*allow[[:space:]]+(${bridge}|all)[[:space:]]*$" "$conf"; then + return 0 + fi + + return 1 +} + +bridge_helper_available() { + local helper="" + if [[ -x /usr/lib/qemu/qemu-bridge-helper ]]; then + helper="/usr/lib/qemu/qemu-bridge-helper" + elif [[ -x /usr/libexec/qemu-bridge-helper ]]; then + helper="/usr/libexec/qemu-bridge-helper" + else + return 1 + fi + + # Helper needs privilege elevation (setuid root or cap_net_admin). + if [[ -u "$helper" ]]; then + return 0 + fi + if has_cmd getcap && getcap "$helper" 2>/dev/null | grep -q 'cap_net_admin'; then + return 0 + fi + + return 1 +} + +bridge_interface_exists() { + local bridge="$1" + if has_cmd ip; then + ip link show "$bridge" >/dev/null 2>&1 + return $? + fi + [[ -d "/sys/class/net/${bridge}" ]] +} + +pick_net_mode() { + case "${NET_MODE}" in + bridge|user) ;; + *) + log_warn "Unknown network mode '${NET_MODE}', falling back to user" + NET_MODE="user" + ;; + esac + + if [[ "${NET_MODE}" == "bridge" ]]; then + if ! bridge_interface_exists "${NET_BRIDGE_IF}"; then + if bridge_interface_exists "virbr0"; then + log_warn "Bridge '${NET_BRIDGE_IF}' not found; using 'virbr0' instead" + NET_BRIDGE_IF="virbr0" + else + log_warn "Bridge '${NET_BRIDGE_IF}' not found on host; falling back to user NAT" + log_warn "Fix: create bridge '${NET_BRIDGE_IF}' or run with --bridge " + NET_MODE="user" + return + fi + fi + + local acl_status=0 + if bridge_acl_allows "${NET_BRIDGE_IF}"; then + acl_status=0 + else + acl_status=$? + fi + if [[ $acl_status -eq 1 ]]; then + log_warn "Bridge ACL does not allow '${NET_BRIDGE_IF}' in /etc/qemu/bridge.conf; falling back to user NAT" + log_warn "Fix: add 'allow ${NET_BRIDGE_IF}' to /etc/qemu/bridge.conf" + NET_MODE="user" + return + elif [[ $acl_status -eq 2 ]]; then + log_warn "Bridge ACL file exists but is unreadable by current user; attempting bridge anyway" + log_warn "Tip: chmod 0644 /etc/qemu/bridge.conf to silence this preflight warning" + fi + + if ! bridge_helper_available; then + log_warn "qemu-bridge-helper missing privileges; falling back to user NAT" + log_warn "Fix: ensure qemu-bridge-helper is setuid root (or has cap_net_admin)" + NET_MODE="user" + fi + fi +} + +run_qemu_command() { + local rc=0 + set +e + "$@" + rc=$? + set -e + + if [[ $rc -ne 0 && "${NET_MODE}" == "bridge" ]]; then + log_warn "Bridge launch failed (exit ${rc}); retrying with user NAT" + NET_MODE="user" + return 88 + fi + + return $rc +} + ask() { [[ "${INTERACTIVE}" != "true" ]] && return 0 printf "${C_YELLOW}%s [Y/n] ${C_RESET}" "$1" @@ -274,6 +388,7 @@ USER_APPS=( "shelld,/bin/shelld" "settings,/bin/settings" "syscall-e2e,/bin/syscall-e2e" + "netcheck,/bin/netcheck" "msh,/bin/msh" "spinning-cube,/bin/spinning-cube" "system-visualizer,/bin/sysvis" @@ -388,6 +503,22 @@ do_launch_qemu() { log_info "Press Ctrl+A X to exit QEMU" printf "\n" + pick_net_mode + local -a net_args + if [[ "${NET_MODE}" == "bridge" ]]; then + log_info "Network mode: bridge (${NET_BRIDGE_IF})" + net_args=( + -device virtio-net-pci,netdev=net0,disable-legacy=on + -netdev bridge,id=net0,br="${NET_BRIDGE_IF}" + ) + else + log_info "Network mode: user NAT" + net_args=( + -device virtio-net-pci,netdev=net0,disable-legacy=on + -netdev user,id=net0,hostfwd=tcp::2222-:22 + ) + fi + if [[ -f "${TESTING_DIR}/test-disk-50g.img" ]]; then # Build/refresh the boot ESP image that UEFI will load BOOTX64.EFI from. # The data disk (test-disk-50g.img) is raw HelixFS with no partition table @@ -408,7 +539,7 @@ do_launch_qemu() { fi # Disk 0 (virtio): ESP FAT32 image — UEFI boots BOOTX64.EFI from here # Disk 1 (virtio): raw HelixFS image — kernel mounts this as data storage - qemu-system-x86_64 \ + if ! run_qemu_command qemu-system-x86_64 \ -enable-kvm \ -machine q35,accel=kvm,i8042=on,usb=off \ -cpu host \ @@ -418,15 +549,21 @@ do_launch_qemu() { -device virtio-blk-pci,drive=disk0,disable-legacy=on,bootindex=1 \ -drive file="${TESTING_DIR}/test-disk-50g.img",format=raw,if=none,id=disk1,cache=writeback \ -device virtio-blk-pci,drive=disk1,disable-legacy=on,iothread=iothread0 \ - -device virtio-net-pci,netdev=net0,disable-legacy=on \ - -netdev user,id=net0,hostfwd=tcp::2222-:22 \ + "${net_args[@]}" \ -smp 8 \ -m 12G \ -vga virtio \ -display sdl,gl=on,grab-mod=rctrl,show-cursor=off \ -no-reboot \ -d cpu_reset,int -D /tmp/qemu-int.log \ - -serial stdio + -serial stdio; then + local rc=$? + if [[ $rc -eq 88 ]]; then + do_launch_qemu + return + fi + return $rc + fi else # Create a temp ESP image for virtio-blk local esp_img="${TESTING_DIR}/esp-temp.img" @@ -441,7 +578,7 @@ do_launch_qemu() { sudo umount "$mnt" rmdir "$mnt" fi - qemu-system-x86_64 \ + if ! run_qemu_command qemu-system-x86_64 \ -enable-kvm \ -machine q35,accel=kvm,i8042=on,usb=off \ -cpu host \ @@ -449,14 +586,20 @@ do_launch_qemu() { -object iothread,id=iothread0 \ -drive file="$esp_img",format=raw,if=none,id=disk0,cache=writeback \ -device virtio-blk-pci,drive=disk0,disable-legacy=on,iothread=iothread0 \ - -device virtio-net-pci,netdev=net0,disable-legacy=on \ - -netdev user,id=net0,hostfwd=tcp::2222-:22 \ + "${net_args[@]}" \ -smp 8 \ -m 12G \ -vga virtio \ -display sdl,gl=on,grab-mod=rctrl,show-cursor=off \ -no-reboot \ - -serial stdio + -serial stdio; then + local rc=$? + if [[ $rc -eq 88 ]]; then + do_launch_qemu + return + fi + return $rc + fi fi } @@ -636,8 +779,24 @@ do_launch_thinkpad() { log_info "Press Ctrl+A X to exit QEMU" printf "\n" + pick_net_mode + local -a net_args + if [[ "${NET_MODE}" == "bridge" ]]; then + log_info "Network mode: bridge (${NET_BRIDGE_IF})" + net_args=( + -device e1000,netdev=net0 + -netdev bridge,id=net0,br="${NET_BRIDGE_IF}" + ) + else + log_info "Network mode: user NAT" + net_args=( + -device e1000,netdev=net0 + -netdev user,id=net0 + ) + fi + if [[ -f "${TESTING_DIR}/test-disk-50g.img" ]]; then - qemu-system-x86_64 \ + if ! run_qemu_command qemu-system-x86_64 \ -enable-kvm \ -machine q35,accel=kvm,i8042=on,usb=off \ -cpu host \ @@ -650,14 +809,20 @@ do_launch_thinkpad() { -drive file="${TESTING_DIR}/test-disk-50g.img",format=raw,if=none,id=disk0,cache=writeback \ -device ich9-ahci,id=ahci0 \ -device ide-hd,drive=disk0,bus=ahci0.0,bootindex=1 \ - -device e1000,netdev=net0 \ - -netdev user,id=net0 \ + "${net_args[@]}" \ -smp 8 \ -m 12G \ -vga virtio \ -display sdl,gl=on,grab-mod=rctrl,show-cursor=off \ -no-reboot \ - -serial stdio + -serial stdio; then + local rc=$? + if [[ $rc -eq 88 ]]; then + do_launch_thinkpad + return + fi + return $rc + fi else # Create a temp ESP image for AHCI local esp_img="${TESTING_DIR}/esp-temp.img" @@ -672,7 +837,7 @@ do_launch_thinkpad() { sudo umount "$mnt" rmdir "$mnt" fi - qemu-system-x86_64 \ + if ! run_qemu_command qemu-system-x86_64 \ -enable-kvm \ -machine q35,accel=kvm,i8042=on,usb=off \ -cpu host \ @@ -685,17 +850,22 @@ do_launch_thinkpad() { -drive file="$esp_img",format=raw,if=none,id=disk0,cache=writeback \ -device ich9-ahci,id=ahci0 \ -device ide-hd,drive=disk0,bus=ahci0.0,bootindex=1 \ - -device e1000,netdev=net0 \ - -netdev user,id=net0 \ + "${net_args[@]}" \ -smp 8 \ -m 12G \ -vga virtio \ -display sdl,gl=on,grab-mod=rctrl,show-cursor=off \ -no-reboot \ - -serial stdio + -serial stdio; then + local rc=$? + if [[ $rc -eq 88 ]]; then + do_launch_thinkpad + return + fi + return $rc + fi fi } - cmd_thinkpad() { print_banner check_bootloader || die "Bootloader not built. Run: $0 build" @@ -769,6 +939,8 @@ usage() { printf " ${C_CYAN}-i, --interactive${C_RESET} Ask at each step what to do\n" printf " ${C_CYAN}-f, --force${C_RESET} Force rebuild/recreate\n" printf " ${C_CYAN}-n, --no-qemu${C_RESET} Setup everything but don't launch QEMU\n" + printf " ${C_CYAN}--bridge [br]${C_RESET} Use bridge netdev (default: br0)\n" + printf " ${C_CYAN}--usernet${C_RESET} Use QEMU user-mode NAT networking\n" printf " ${C_CYAN}-h, --help${C_RESET} Show this help\n" printf "\n${C_BOLD}Commands:${C_RESET} (for power users)\n" @@ -807,6 +979,15 @@ main() { -i|--interactive) INTERACTIVE=true; shift ;; -f|--force) FORCE_MODE=true; shift ;; -n|--no-qemu) SKIP_QEMU=true; shift ;; + --bridge) + NET_MODE="bridge" + if [[ $# -gt 1 && "${2:-}" != -* ]]; then + NET_BRIDGE_IF="$2" + shift + fi + shift + ;; + --usernet) NET_MODE="user"; shift ;; -h|--help) usage; exit 0 ;; -*) die "Unknown option: $1" ;; *) [[ -z "$cmd" ]] && cmd="$1" || args+=("$1"); shift ;; diff --git a/shelld/src/islands/launcher.rs b/shelld/src/islands/launcher.rs index 576cf8a9..7a4cfaf8 100644 --- a/shelld/src/islands/launcher.rs +++ b/shelld/src/islands/launcher.rs @@ -1,7 +1,4 @@ -use crate::islands::{ - draw_text, raw_fill, ShellState, ICON_BG_RGB, ICON_INNER_RGB, ICON_SIZE, LAUNCHER_BG_RGB, - LAUNCHER_H, LAUNCHER_W, PANEL_H, START_RGB, -}; +use crate::islands::{draw_text, raw_fill, ShellState, ICON_SIZE, LAUNCHER_H, LAUNCHER_W, PANEL_H}; use libmorpheus::{io, process}; /// launcher island. application launcher overlay triggered by clicking START. @@ -14,7 +11,7 @@ pub fn tick(state: &mut ShellState) { let ly = state.fb_h.saturating_sub(PANEL_H + LAUNCHER_H + 8); // launcher background - let (lr, lg, lb) = LAUNCHER_BG_RGB; + let (lr, lg, lb) = state.launcher_bg_rgb; let launcher_bg = state.pack(lr, lg, lb); raw_fill( state.surface_ptr, @@ -27,7 +24,7 @@ pub fn tick(state: &mut ShellState) { ); // launcher title bar - let (sr, sg, sb) = START_RGB; + let (sr, sg, sb) = state.start_rgb; let title_bg = state.pack(sr, sg, sb); raw_fill( state.surface_ptr, @@ -44,13 +41,13 @@ pub fn tick(state: &mut ShellState) { ly + 4, "Launcher", (255, 255, 255), - START_RGB, + state.start_rgb, ); // shell icon let icon_x = lx + 16; let icon_y = ly + 40; - let (ir, ig, ib) = ICON_BG_RGB; + let (ir, ig, ib) = state.icon_bg_rgb; raw_fill( state.surface_ptr, state.fb_stride, @@ -60,7 +57,7 @@ pub fn tick(state: &mut ShellState) { ICON_SIZE, state.pack(ir, ig, ib), ); - let (iir, iig, iib) = ICON_INNER_RGB; + let (iir, iig, iib) = state.icon_inner_rgb; raw_fill( state.surface_ptr, state.fb_stride, @@ -76,7 +73,7 @@ pub fn tick(state: &mut ShellState) { icon_y + 20, ">_", (255, 255, 255), - ICON_INNER_RGB, + state.icon_inner_rgb, ); draw_text( state, @@ -84,7 +81,7 @@ pub fn tick(state: &mut ShellState) { icon_y + ICON_SIZE + 8, "Shell", (230, 230, 230), - LAUNCHER_BG_RGB, + state.launcher_bg_rgb, ); state.launcher_dirty = false; diff --git a/shelld/src/islands/mod.rs b/shelld/src/islands/mod.rs index 09dc8957..8daa9377 100644 --- a/shelld/src/islands/mod.rs +++ b/shelld/src/islands/mod.rs @@ -8,14 +8,6 @@ pub const LAUNCHER_W: u32 = 300; pub const LAUNCHER_H: u32 = 220; pub const ICON_SIZE: u32 = 56; -pub const DESKTOP_RGB: (u8, u8, u8) = (26, 26, 46); -pub const PANEL_BG_RGB: (u8, u8, u8) = (18, 20, 30); -pub const START_RGB: (u8, u8, u8) = (0, 85, 0); -pub const START_ACTIVE_RGB: (u8, u8, u8) = (0, 110, 42); -pub const LAUNCHER_BG_RGB: (u8, u8, u8) = (28, 30, 42); -pub const ICON_BG_RGB: (u8, u8, u8) = (40, 60, 90); -pub const ICON_INNER_RGB: (u8, u8, u8) = (18, 24, 40); - pub struct ShellState { // surface buffer — private offscreen FB given by kernel because compd owns the real one pub surface_ptr: *mut u32, @@ -34,6 +26,15 @@ pub struct ShellState { pub launcher_open: bool, pub launcher_dirty: bool, + // shared DE appearance profile + pub desktop_rgb: (u8, u8, u8), + pub panel_rgb: (u8, u8, u8), + pub start_rgb: (u8, u8, u8), + pub start_active_rgb: (u8, u8, u8), + pub launcher_bg_rgb: (u8, u8, u8), + pub icon_bg_rgb: (u8, u8, u8), + pub icon_inner_rgb: (u8, u8, u8), + // input state — shelld reads forwarded mouse from compd pub mouse_x: i32, pub mouse_y: i32, @@ -58,6 +59,13 @@ impl ShellState { panel_dirty: true, launcher_open: false, launcher_dirty: false, + desktop_rgb: (26, 26, 46), + panel_rgb: (18, 20, 30), + start_rgb: (0, 85, 0), + start_active_rgb: (0, 110, 42), + launcher_bg_rgb: (28, 30, 42), + icon_bg_rgb: (40, 60, 90), + icon_inner_rgb: (18, 24, 40), // init at center. same as compd. so when compd forwards deltas, our position tracks. mouse_x: (fb_w / 2) as i32, mouse_y: (fb_h / 2) as i32, @@ -74,6 +82,40 @@ impl ShellState { (r as u32) | ((g as u32) << 8) | ((b as u32) << 16) } } + + pub fn apply_desktop_appearance(&mut self, a: &libmorpheus::desktop::DesktopAppearance) { + self.desktop_rgb = a.desktop_rgb; + self.panel_rgb = a.panel_rgb; + self.start_rgb = a.start_rgb; + self.start_active_rgb = lighten(a.start_rgb, 28); + self.launcher_bg_rgb = darken(self.desktop_rgb, 8); + self.icon_bg_rgb = darken(self.start_rgb, 22); + self.icon_inner_rgb = darken(self.icon_bg_rgb, 42); + + self.wallpaper_dirty = true; + self.panel_dirty = true; + self.launcher_dirty = true; + } +} + +#[inline(always)] +fn darken(rgb: (u8, u8, u8), pct: u8) -> (u8, u8, u8) { + let k = 100u16.saturating_sub(pct as u16); + ( + ((rgb.0 as u16 * k) / 100) as u8, + ((rgb.1 as u16 * k) / 100) as u8, + ((rgb.2 as u16 * k) / 100) as u8, + ) +} + +#[inline(always)] +fn lighten(rgb: (u8, u8, u8), pct: u8) -> (u8, u8, u8) { + let p = pct as u16; + ( + (rgb.0 as u16 + ((255 - rgb.0 as u16) * p) / 100) as u8, + (rgb.1 as u16 + ((255 - rgb.1 as u16) * p) / 100) as u8, + (rgb.2 as u16 + ((255 - rgb.2 as u16) * p) / 100) as u8, + ) } // --- raw pixel primitives for writing into shelld's own surface buffer --- diff --git a/shelld/src/islands/panel.rs b/shelld/src/islands/panel.rs index 10cde68a..c2feec8d 100644 --- a/shelld/src/islands/panel.rs +++ b/shelld/src/islands/panel.rs @@ -1,7 +1,4 @@ -use crate::islands::{ - draw_text, raw_fill, ShellState, PANEL_BG_RGB, PANEL_H, START_ACTIVE_RGB, START_BTN_W, - START_RGB, -}; +use crate::islands::{draw_text, raw_fill, ShellState, PANEL_H, START_BTN_W}; use libmorpheus::time; /// panel island. taskbar at the bottom of the screen. @@ -12,7 +9,7 @@ pub fn tick(state: &mut ShellState) { } let panel_y = state.fb_h.saturating_sub(PANEL_H); - let (pr, pg, pb) = PANEL_BG_RGB; + let (pr, pg, pb) = state.panel_rgb; let panel_bg = state.pack(pr, pg, pb); // panel background bar @@ -27,11 +24,7 @@ pub fn tick(state: &mut ShellState) { ); // START button - let (sr, sg, sb) = if state.launcher_open { - START_ACTIVE_RGB - } else { - START_RGB - }; + let (sr, sg, sb) = if state.launcher_open { state.start_active_rgb } else { state.start_rgb }; let start_bg = state.pack(sr, sg, sb); raw_fill( state.surface_ptr, @@ -43,11 +36,7 @@ pub fn tick(state: &mut ShellState) { start_bg, ); let start_fg = (255u8, 255u8, 255u8); - let start_bg_rgb = if state.launcher_open { - START_ACTIVE_RGB - } else { - START_RGB - }; + let start_bg_rgb = if state.launcher_open { state.start_active_rgb } else { state.start_rgb }; draw_text(state, 10, panel_y + 7, "START", start_fg, start_bg_rgb); // status label @@ -57,7 +46,7 @@ pub fn tick(state: &mut ShellState) { panel_y + 7, "MorpheusX DE", (200, 200, 210), - PANEL_BG_RGB, + state.panel_rgb, ); // uptime clock on the right side @@ -76,7 +65,7 @@ pub fn tick(state: &mut ShellState) { panel_y + 7, clock_str, (180, 220, 180), - PANEL_BG_RGB, + state.panel_rgb, ); } diff --git a/shelld/src/islands/wallpaper.rs b/shelld/src/islands/wallpaper.rs index 03cbe45a..3b47529f 100644 --- a/shelld/src/islands/wallpaper.rs +++ b/shelld/src/islands/wallpaper.rs @@ -1,4 +1,4 @@ -use crate::islands::{raw_fill, ShellState, DESKTOP_RGB}; +use crate::islands::{raw_fill, ShellState}; /// wallpaper island. renders a solid color desktop background into the /// shell's surface buffer. compd blends it at z-layer 0 (background). @@ -7,7 +7,7 @@ pub fn tick(state: &mut ShellState) { if !state.wallpaper_dirty { return; } - let (r, g, b) = DESKTOP_RGB; + let (r, g, b) = state.desktop_rgb; let px = state.pack(r, g, b); raw_fill( state.surface_ptr, diff --git a/shelld/src/main.rs b/shelld/src/main.rs index bb8ec49f..430fd994 100644 --- a/shelld/src/main.rs +++ b/shelld/src/main.rs @@ -60,7 +60,21 @@ fn main() -> i32 { is_bgrx, ); + if let Some(a) = libmorpheus::desktop::DesktopAppearance::load() { + state.apply_desktop_appearance(&a); + } + + let mut last_appearance_poll_ms = 0u64; + loop { + let now_ms = libmorpheus::time::uptime_ms(); + if now_ms.saturating_sub(last_appearance_poll_ms) >= 400 { + if let Some(a) = libmorpheus::desktop::DesktopAppearance::load() { + state.apply_desktop_appearance(&a); + } + last_appearance_poll_ms = now_ms; + } + // render wallpaper (only when dirty — first frame) islands::wallpaper::tick(&mut state); diff --git a/tests/netcheck/Cargo.toml b/tests/netcheck/Cargo.toml new file mode 100644 index 00000000..eabe082c --- /dev/null +++ b/tests/netcheck/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "netcheck" +version = "0.1.0" +edition = "2021" + +[dependencies] +libmorpheus = { path = "../../libmorpheus" } diff --git a/tests/netcheck/src/main.rs b/tests/netcheck/src/main.rs new file mode 100644 index 00000000..e952ac6f --- /dev/null +++ b/tests/netcheck/src/main.rs @@ -0,0 +1,208 @@ +#![no_std] +#![no_main] + +use libmorpheus::entry; +use libmorpheus::io::{print, println}; +use libmorpheus::net::{self, Ipv4Addr, TcpState, TcpStream}; +use libmorpheus::time; + +entry!(main); + +fn main() -> i32 { + println("[netcheck] start"); + + match net::net_activate() { + Ok(rc) => { + if rc == 0 { + println("[netcheck] network activated"); + } else { + println("[netcheck] network already active"); + } + } + Err(_) => { + println("[netcheck] activation failed"); + return 1; + } + } + + if net::net_dhcp().is_err() { + println("[netcheck] dhcp request failed"); + return 2; + } + + let ip = match wait_for_lease(6000) { + Some(ip) => ip, + None => { + println("[netcheck] no dhcp lease"); + return 3; + } + }; + println("[netcheck] lease acquired"); + print("[netcheck] ip="); + print_ipv4(ip); + println(""); + + let pre = net::net_stats().ok(); + + let dns_ip = match resolve_with_timeout("example.com", 6000) { + Some(ip) => ip, + None => { + println("[netcheck] dns resolve failed"); + return 4; + } + }; + print("[netcheck] dns example.com="); + print_ipv4(dns_ip); + println(""); + + let stream = match TcpStream::connect(Ipv4Addr::from_nbo(dns_ip), 80) { + Ok(s) => s, + Err(_) => { + println("[netcheck] tcp connect failed"); + return 5; + } + }; + + if !wait_connected(&stream, 5000) { + println("[netcheck] tcp handshake timeout"); + return 6; + } + + let req = b"GET / HTTP/1.0\r\nHost: example.com\r\nConnection: close\r\n\r\n"; + if stream.send_all(req).is_err() { + println("[netcheck] tcp send failed"); + return 7; + } + + let mut buf = [0u8; 512]; + let n = match recv_with_timeout(&stream, &mut buf, 5000) { + Some(n) if n > 0 => n, + _ => { + println("[netcheck] tcp recv timeout"); + return 8; + } + }; + + if n >= 4 && &buf[0..4] == b"HTTP" { + println("[netcheck] http response received"); + } else { + println("[netcheck] recv data but no http prefix"); + } + + if let (Some(a), Ok(b)) = (pre, net::net_stats()) { + let tx = b.tx_packets.saturating_sub(a.tx_packets); + let rx = b.rx_packets.saturating_sub(a.rx_packets); + print("[netcheck] delta tx="); + print_u64(tx); + print(" rx="); + print_u64(rx); + println(""); + } + + println("[netcheck] PASS"); + 0 +} + +fn wait_for_lease(timeout_ms: u64) -> Option { + let start = time::uptime_ms(); + loop { + let now = time::uptime_ms(); + let _ = net::nic_refill(); + let _ = net::net_poll_drive(now); + if let Ok(cfg) = net::net_config() { + if cfg.ipv4_addr != 0 { + return Some(cfg.ipv4_addr); + } + } + if now.saturating_sub(start) >= timeout_ms { + return None; + } + libmorpheus::process::sleep(10); + } +} + +fn resolve_with_timeout(host: &str, timeout_ms: u64) -> Option { + let q = net::dns_start(host).ok()?; + let start = time::uptime_ms(); + loop { + let now = time::uptime_ms(); + let _ = net::nic_refill(); + let _ = net::net_poll_drive(now); + match net::dns_poll(q) { + Ok(Some(ip)) => return Some(ip), + Ok(None) => {} + Err(_) => return None, + } + if now.saturating_sub(start) >= timeout_ms { + return None; + } + libmorpheus::process::sleep(10); + } +} + +fn wait_connected(stream: &TcpStream, timeout_ms: u64) -> bool { + let start = time::uptime_ms(); + loop { + let now = time::uptime_ms(); + let _ = net::net_poll_drive(now); + match stream.state() { + Ok(TcpState::Established) => return true, + Ok(TcpState::Closed) => return false, + _ => {} + } + if now.saturating_sub(start) >= timeout_ms { + return false; + } + libmorpheus::process::sleep(10); + } +} + +fn recv_with_timeout(stream: &TcpStream, buf: &mut [u8], timeout_ms: u64) -> Option { + let start = time::uptime_ms(); + loop { + let now = time::uptime_ms(); + let _ = net::net_poll_drive(now); + match net::tcp_recv(stream.handle(), buf) { + Ok(n) if n > 0 => return Some(n), + Ok(_) => {} + Err(_) => return None, + } + if now.saturating_sub(start) >= timeout_ms { + return None; + } + libmorpheus::process::sleep(10); + } +} + +fn print_u64(v: u64) { + if v == 0 { + print("0"); + return; + } + let mut n = v; + let mut buf = [0u8; 20]; + let mut i = buf.len(); + while n > 0 { + i -= 1; + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + } + if let Ok(s) = core::str::from_utf8(&buf[i..]) { + print(s); + } +} + +fn print_u8(v: u8) { + print_u64(v as u64); +} + +fn print_ipv4(nbo: u32) { + let ip = Ipv4Addr::from_nbo(nbo).octets(); + print_u8(ip[0]); + print("."); + print_u8(ip[1]); + print("."); + print_u8(ip[2]); + print("."); + print_u8(ip[3]); +}