use core::fmt::Write; use lancer_log::phase::PhaseStatus; pub static mut BOOT_DASHBOARD: Dashboard = Dashboard::new(); #[allow(clippy::deref_addrof)] pub fn dash() -> &'static mut Dashboard { unsafe { &mut *(&raw mut BOOT_DASHBOARD) } } #[allow(dead_code)] pub fn domain_to_section(domain: &str) -> Option { let d = dash(); let bytes = domain.as_bytes(); (0..d.section_count).fold(None, |result, i| { let section = &d.sections[i]; match section.label_len == bytes.len() && section.label[..section.label_len] == *bytes { true => Some(i), false => result, } }) } const MAX_SECTIONS: usize = 24; const GUTTER_WIDTH: usize = 7; const MAX_LABEL_LEN: usize = GUTTER_WIDTH - 1; const VIEWPORT_LINE_LEN: usize = 160; const MAX_VIEWPORT_LINES: usize = 8; #[allow(dead_code)] const TERMINAL_ROWS: usize = 50; #[derive(Clone, Copy)] struct ViewportLine { buf: [u8; VIEWPORT_LINE_LEN], len: usize, } impl ViewportLine { const fn empty() -> Self { Self { buf: [0u8; VIEWPORT_LINE_LEN], len: 0, } } fn set(&mut self, s: &str) { let copy_len = s.len().min(VIEWPORT_LINE_LEN); self.buf[..copy_len].copy_from_slice(&s.as_bytes()[..copy_len]); self.len = copy_len; } fn as_str(&self) -> &str { unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) } } } #[derive(Clone, Copy)] struct Section { label: [u8; MAX_LABEL_LEN], label_len: usize, row_budget: u8, status: PhaseStatus, viewport: [ViewportLine; MAX_VIEWPORT_LINES], viewport_count: usize, viewport_head: usize, start_row: u16, active_rows: u8, } impl Section { const fn empty() -> Self { Self { label: [0u8; MAX_LABEL_LEN], label_len: 0, row_budget: 1, status: PhaseStatus::Pending, viewport: [const { ViewportLine::empty() }; MAX_VIEWPORT_LINES], viewport_count: 0, viewport_head: 0, start_row: 0, active_rows: 0, } } fn set_label(&mut self, name: &str) { let copy_len = name.len().min(MAX_LABEL_LEN); self.label[..copy_len].copy_from_slice(&name.as_bytes()[..copy_len]); self.label_len = copy_len; } fn label_str(&self) -> &str { unsafe { core::str::from_utf8_unchecked(&self.label[..self.label_len]) } } fn push_line(&mut self, line: &str) { let idx = match self.viewport_count < MAX_VIEWPORT_LINES { true => { let i = self.viewport_count; self.viewport_count += 1; i } false => { let i = self.viewport_head; self.viewport_head = (self.viewport_head + 1) % MAX_VIEWPORT_LINES; i } }; self.viewport[idx].set(line); } fn visible_lines(&self, budget: usize) -> VisibleLines<'_> { let total = self.viewport_count; let visible = total.min(budget); let start = match total <= budget { true => 0, false => (self.viewport_head + total - visible) % MAX_VIEWPORT_LINES, }; VisibleLines { viewport: &self.viewport, count: self.viewport_count, start, remaining: visible, } } } struct VisibleLines<'a> { viewport: &'a [ViewportLine; MAX_VIEWPORT_LINES], count: usize, start: usize, remaining: usize, } impl<'a> Iterator for VisibleLines<'a> { type Item = &'a str; fn next(&mut self) -> Option { match self.remaining { 0 => None, _ => { let idx = self.start % self.count.max(1); self.start += 1; self.remaining -= 1; Some(self.viewport[idx].as_str()) } } } } pub struct Dashboard { sections: [Section; MAX_SECTIONS], section_count: usize, streaming: bool, altscreen: bool, last_streaming_label: [u8; MAX_LABEL_LEN], last_streaming_label_len: usize, } impl Dashboard { pub const fn new() -> Self { Self { sections: [const { Section::empty() }; MAX_SECTIONS], section_count: 0, streaming: false, altscreen: false, last_streaming_label: [0u8; MAX_LABEL_LEN], last_streaming_label_len: 0, } } pub fn add_section(&mut self, label: &str, row_budget: u8) -> usize { let idx = self.section_count; assert!(idx < MAX_SECTIONS, "too many dashboard sections"); self.sections[idx].set_label(label); self.sections[idx].row_budget = row_budget; self.section_count += 1; idx } pub fn section_status(&self, idx: usize) -> PhaseStatus { self.sections[idx].status } pub fn begin_phase(&mut self, idx: usize) { self.sections[idx].status = PhaseStatus::Active; self.relayout(); self.render_full(); } pub fn log(&mut self, idx: usize, msg: &str) { match self.streaming { true => self.stream_log(idx, msg), false => { self.sections[idx].push_line(msg); self.render_section(idx); } } } pub fn end_phase(&mut self, idx: usize) { self.sections[idx].status = PhaseStatus::Done; self.sections[idx].active_rows = 1; self.relayout(); self.render_full(); } pub fn fail_phase(&mut self, idx: usize) { self.sections[idx].status = PhaseStatus::Failed; self.sections[idx].active_rows = 1; self.relayout(); self.render_full(); } pub fn transition_to_streaming(&mut self) { self.streaming = true; match self.altscreen { true => { crate::kprint!("\x1b[?1049l\x1b[?25h\x1b[J"); self.altscreen = false; } false => crate::kprint!("\x1b[?25h"), } let w = crate::log::KLOG_GUTTER; (0..self.section_count).fold((), |(), i| { let section = &self.sections[i]; let label = section.label_str(); let dim = matches!(section.status, PhaseStatus::Pending | PhaseStatus::Done); write_gutter_colored(label, dim, w); match section.status { PhaseStatus::Pending => crate::kprint!("\x1b[2m--\x1b[0m\n"), PhaseStatus::Active => crate::kprint!("...\n"), PhaseStatus::Done => crate::kprint!( "{}ok{}\n", lancer_log::color::Fg(lancer_log::palette::DONE_GREEN), lancer_log::color::Reset ), PhaseStatus::Failed => crate::kprint!( "{}FAILED{}\n", lancer_log::color::Fg(lancer_log::palette::ERROR_RED), lancer_log::color::Reset ), } }); } fn relayout(&mut self) { let mut row: u16 = 1; (0..self.section_count).fold((), |(), i| { let section = &mut self.sections[i]; section.start_row = row; let rows = match section.status { PhaseStatus::Pending => 1u8, PhaseStatus::Active => section.row_budget, PhaseStatus::Done | PhaseStatus::Failed => 1, }; section.active_rows = rows; row += rows as u16; }); } fn render_full(&mut self) { match self.altscreen { false => { crate::kprint!("\x1b[?1049h"); self.altscreen = true; } true => {} } crate::kprint!("\x1b[?25l"); (0..self.section_count).fold((), |(), i| { self.render_section(i); }); } fn render_section(&self, idx: usize) { let section = &self.sections[idx]; let label = section.label_str(); let start = section.start_row; let rows = section.active_rows as usize; match section.status { PhaseStatus::Pending => { crate::kprint!("\x1b[{};1H", start); write_gutter_colored(label, true, GUTTER_WIDTH); crate::kprint!("\x1b[2m--\x1b[0m\x1b[K"); } PhaseStatus::Active => { let mut line_idx = 0usize; section.visible_lines(rows).fold((), |(), line| { crate::kprint!("\x1b[{};1H", start as usize + line_idx); match line_idx { 0 => write_gutter_colored(label, false, GUTTER_WIDTH), _ => write_continuation_colored(GUTTER_WIDTH), } crate::kprint!("{}\x1b[K", line); line_idx += 1; }); (line_idx..rows).fold((), |(), r| { crate::kprint!("\x1b[{};1H", start as usize + r); match r { 0 => write_gutter_colored(label, false, GUTTER_WIDTH), _ => write_continuation_colored(GUTTER_WIDTH), } crate::kprint!("\x1b[K"); }); } PhaseStatus::Done => { crate::kprint!("\x1b[{};1H", start); write_gutter_colored(label, true, GUTTER_WIDTH); crate::kprint!( "{}ok{}\x1b[K", lancer_log::color::Fg(lancer_log::palette::DONE_GREEN), lancer_log::color::Reset ); } PhaseStatus::Failed => { crate::kprint!("\x1b[{};1H", start); write_gutter_colored(label, false, GUTTER_WIDTH); crate::kprint!( "{}FAILED{}\x1b[K", lancer_log::color::Fg(lancer_log::palette::ERROR_RED), lancer_log::color::Reset ); } } } fn stream_log(&mut self, idx: usize, msg: &str) { let section = &self.sections[idx]; let label = section.label_str(); let same_label = self.last_streaming_label_len == label.len() && self.last_streaming_label[..self.last_streaming_label_len] == section.label[..section.label_len]; let w = crate::log::KLOG_GUTTER; match same_label { true => write_continuation_colored(w), false => { write_gutter_colored(label, false, w); let copy_len = label.len().min(MAX_LABEL_LEN); self.last_streaming_label[..copy_len] .copy_from_slice(&label.as_bytes()[..copy_len]); self.last_streaming_label_len = copy_len; } } crate::kprint!("{}\n", msg); } } fn write_gutter_colored(label: &str, dim: bool, width: usize) { let mut w = crate::arch::serial::SerialWriter; let _ = lancer_log::format::write_gutter_label(&mut w, label, dim, width); } fn write_continuation_colored(width: usize) { let mut w = crate::arch::serial::SerialWriter; let _ = lancer_log::format::write_continuation(&mut w, width); } pub struct FmtBuf { buf: [u8; VIEWPORT_LINE_LEN], pos: usize, } impl FmtBuf { pub const fn new() -> Self { Self { buf: [0u8; VIEWPORT_LINE_LEN], pos: 0, } } pub fn as_str(&self) -> &str { unsafe { core::str::from_utf8_unchecked(&self.buf[..self.pos]) } } #[allow(dead_code)] pub fn clear(&mut self) { self.pos = 0; } } impl Write for FmtBuf { fn write_str(&mut self, s: &str) -> core::fmt::Result { let bytes = s.as_bytes(); let copy_len = bytes.len().min(VIEWPORT_LINE_LEN - self.pos); self.buf[self.pos..self.pos + copy_len].copy_from_slice(&bytes[..copy_len]); self.pos += copy_len; Ok(()) } }