Nothing to see here, move along
1use core::fmt::Write;
2
3use lancer_log::phase::PhaseStatus;
4
5pub static mut BOOT_DASHBOARD: Dashboard = Dashboard::new();
6#[allow(clippy::deref_addrof)]
7pub fn dash() -> &'static mut Dashboard {
8 unsafe { &mut *(&raw mut BOOT_DASHBOARD) }
9}
10
11#[allow(dead_code)]
12pub fn domain_to_section(domain: &str) -> Option<usize> {
13 let d = dash();
14 let bytes = domain.as_bytes();
15 (0..d.section_count).fold(None, |result, i| {
16 let section = &d.sections[i];
17 match section.label_len == bytes.len() && section.label[..section.label_len] == *bytes {
18 true => Some(i),
19 false => result,
20 }
21 })
22}
23
24const MAX_SECTIONS: usize = 24;
25const GUTTER_WIDTH: usize = 7;
26const MAX_LABEL_LEN: usize = GUTTER_WIDTH - 1;
27const VIEWPORT_LINE_LEN: usize = 160;
28const MAX_VIEWPORT_LINES: usize = 8;
29#[allow(dead_code)]
30const TERMINAL_ROWS: usize = 50;
31
32#[derive(Clone, Copy)]
33struct ViewportLine {
34 buf: [u8; VIEWPORT_LINE_LEN],
35 len: usize,
36}
37
38impl ViewportLine {
39 const fn empty() -> Self {
40 Self {
41 buf: [0u8; VIEWPORT_LINE_LEN],
42 len: 0,
43 }
44 }
45
46 fn set(&mut self, s: &str) {
47 let copy_len = s.len().min(VIEWPORT_LINE_LEN);
48 self.buf[..copy_len].copy_from_slice(&s.as_bytes()[..copy_len]);
49 self.len = copy_len;
50 }
51
52 fn as_str(&self) -> &str {
53 unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
54 }
55}
56
57#[derive(Clone, Copy)]
58struct Section {
59 label: [u8; MAX_LABEL_LEN],
60 label_len: usize,
61 row_budget: u8,
62 status: PhaseStatus,
63 viewport: [ViewportLine; MAX_VIEWPORT_LINES],
64 viewport_count: usize,
65 viewport_head: usize,
66 start_row: u16,
67 active_rows: u8,
68}
69
70impl Section {
71 const fn empty() -> Self {
72 Self {
73 label: [0u8; MAX_LABEL_LEN],
74 label_len: 0,
75 row_budget: 1,
76 status: PhaseStatus::Pending,
77 viewport: [const { ViewportLine::empty() }; MAX_VIEWPORT_LINES],
78 viewport_count: 0,
79 viewport_head: 0,
80 start_row: 0,
81 active_rows: 0,
82 }
83 }
84
85 fn set_label(&mut self, name: &str) {
86 let copy_len = name.len().min(MAX_LABEL_LEN);
87 self.label[..copy_len].copy_from_slice(&name.as_bytes()[..copy_len]);
88 self.label_len = copy_len;
89 }
90
91 fn label_str(&self) -> &str {
92 unsafe { core::str::from_utf8_unchecked(&self.label[..self.label_len]) }
93 }
94
95 fn push_line(&mut self, line: &str) {
96 let idx = match self.viewport_count < MAX_VIEWPORT_LINES {
97 true => {
98 let i = self.viewport_count;
99 self.viewport_count += 1;
100 i
101 }
102 false => {
103 let i = self.viewport_head;
104 self.viewport_head = (self.viewport_head + 1) % MAX_VIEWPORT_LINES;
105 i
106 }
107 };
108 self.viewport[idx].set(line);
109 }
110
111 fn visible_lines(&self, budget: usize) -> VisibleLines<'_> {
112 let total = self.viewport_count;
113 let visible = total.min(budget);
114 let start = match total <= budget {
115 true => 0,
116 false => (self.viewport_head + total - visible) % MAX_VIEWPORT_LINES,
117 };
118 VisibleLines {
119 viewport: &self.viewport,
120 count: self.viewport_count,
121 start,
122 remaining: visible,
123 }
124 }
125}
126
127struct VisibleLines<'a> {
128 viewport: &'a [ViewportLine; MAX_VIEWPORT_LINES],
129 count: usize,
130 start: usize,
131 remaining: usize,
132}
133
134impl<'a> Iterator for VisibleLines<'a> {
135 type Item = &'a str;
136
137 fn next(&mut self) -> Option<Self::Item> {
138 match self.remaining {
139 0 => None,
140 _ => {
141 let idx = self.start % self.count.max(1);
142 self.start += 1;
143 self.remaining -= 1;
144 Some(self.viewport[idx].as_str())
145 }
146 }
147 }
148}
149
150pub struct Dashboard {
151 sections: [Section; MAX_SECTIONS],
152 section_count: usize,
153 streaming: bool,
154 altscreen: bool,
155 last_streaming_label: [u8; MAX_LABEL_LEN],
156 last_streaming_label_len: usize,
157}
158
159impl Dashboard {
160 pub const fn new() -> Self {
161 Self {
162 sections: [const { Section::empty() }; MAX_SECTIONS],
163 section_count: 0,
164 streaming: false,
165 altscreen: false,
166 last_streaming_label: [0u8; MAX_LABEL_LEN],
167 last_streaming_label_len: 0,
168 }
169 }
170
171 pub fn add_section(&mut self, label: &str, row_budget: u8) -> usize {
172 let idx = self.section_count;
173 assert!(idx < MAX_SECTIONS, "too many dashboard sections");
174 self.sections[idx].set_label(label);
175 self.sections[idx].row_budget = row_budget;
176 self.section_count += 1;
177 idx
178 }
179
180 pub fn section_status(&self, idx: usize) -> PhaseStatus {
181 self.sections[idx].status
182 }
183
184 pub fn begin_phase(&mut self, idx: usize) {
185 self.sections[idx].status = PhaseStatus::Active;
186 self.relayout();
187 self.render_full();
188 }
189
190 pub fn log(&mut self, idx: usize, msg: &str) {
191 match self.streaming {
192 true => self.stream_log(idx, msg),
193 false => {
194 self.sections[idx].push_line(msg);
195 self.render_section(idx);
196 }
197 }
198 }
199
200 pub fn end_phase(&mut self, idx: usize) {
201 self.sections[idx].status = PhaseStatus::Done;
202 self.sections[idx].active_rows = 1;
203 self.relayout();
204 self.render_full();
205 }
206
207 pub fn fail_phase(&mut self, idx: usize) {
208 self.sections[idx].status = PhaseStatus::Failed;
209 self.sections[idx].active_rows = 1;
210 self.relayout();
211 self.render_full();
212 }
213
214 pub fn transition_to_streaming(&mut self) {
215 self.streaming = true;
216 match self.altscreen {
217 true => {
218 crate::kprint!("\x1b[?1049l\x1b[?25h\x1b[J");
219 self.altscreen = false;
220 }
221 false => crate::kprint!("\x1b[?25h"),
222 }
223 let w = crate::log::KLOG_GUTTER;
224 (0..self.section_count).fold((), |(), i| {
225 let section = &self.sections[i];
226 let label = section.label_str();
227 let dim = matches!(section.status, PhaseStatus::Pending | PhaseStatus::Done);
228 write_gutter_colored(label, dim, w);
229 match section.status {
230 PhaseStatus::Pending => crate::kprint!("\x1b[2m--\x1b[0m\n"),
231 PhaseStatus::Active => crate::kprint!("...\n"),
232 PhaseStatus::Done => crate::kprint!(
233 "{}ok{}\n",
234 lancer_log::color::Fg(lancer_log::palette::DONE_GREEN),
235 lancer_log::color::Reset
236 ),
237 PhaseStatus::Failed => crate::kprint!(
238 "{}FAILED{}\n",
239 lancer_log::color::Fg(lancer_log::palette::ERROR_RED),
240 lancer_log::color::Reset
241 ),
242 }
243 });
244 }
245
246 fn relayout(&mut self) {
247 let mut row: u16 = 1;
248 (0..self.section_count).fold((), |(), i| {
249 let section = &mut self.sections[i];
250 section.start_row = row;
251 let rows = match section.status {
252 PhaseStatus::Pending => 1u8,
253 PhaseStatus::Active => section.row_budget,
254 PhaseStatus::Done | PhaseStatus::Failed => 1,
255 };
256 section.active_rows = rows;
257 row += rows as u16;
258 });
259 }
260
261 fn render_full(&mut self) {
262 match self.altscreen {
263 false => {
264 crate::kprint!("\x1b[?1049h");
265 self.altscreen = true;
266 }
267 true => {}
268 }
269 crate::kprint!("\x1b[?25l");
270 (0..self.section_count).fold((), |(), i| {
271 self.render_section(i);
272 });
273 }
274
275 fn render_section(&self, idx: usize) {
276 let section = &self.sections[idx];
277 let label = section.label_str();
278 let start = section.start_row;
279 let rows = section.active_rows as usize;
280
281 match section.status {
282 PhaseStatus::Pending => {
283 crate::kprint!("\x1b[{};1H", start);
284 write_gutter_colored(label, true, GUTTER_WIDTH);
285 crate::kprint!("\x1b[2m--\x1b[0m\x1b[K");
286 }
287 PhaseStatus::Active => {
288 let mut line_idx = 0usize;
289 section.visible_lines(rows).fold((), |(), line| {
290 crate::kprint!("\x1b[{};1H", start as usize + line_idx);
291 match line_idx {
292 0 => write_gutter_colored(label, false, GUTTER_WIDTH),
293 _ => write_continuation_colored(GUTTER_WIDTH),
294 }
295 crate::kprint!("{}\x1b[K", line);
296 line_idx += 1;
297 });
298 (line_idx..rows).fold((), |(), r| {
299 crate::kprint!("\x1b[{};1H", start as usize + r);
300 match r {
301 0 => write_gutter_colored(label, false, GUTTER_WIDTH),
302 _ => write_continuation_colored(GUTTER_WIDTH),
303 }
304 crate::kprint!("\x1b[K");
305 });
306 }
307 PhaseStatus::Done => {
308 crate::kprint!("\x1b[{};1H", start);
309 write_gutter_colored(label, true, GUTTER_WIDTH);
310 crate::kprint!(
311 "{}ok{}\x1b[K",
312 lancer_log::color::Fg(lancer_log::palette::DONE_GREEN),
313 lancer_log::color::Reset
314 );
315 }
316 PhaseStatus::Failed => {
317 crate::kprint!("\x1b[{};1H", start);
318 write_gutter_colored(label, false, GUTTER_WIDTH);
319 crate::kprint!(
320 "{}FAILED{}\x1b[K",
321 lancer_log::color::Fg(lancer_log::palette::ERROR_RED),
322 lancer_log::color::Reset
323 );
324 }
325 }
326 }
327
328 fn stream_log(&mut self, idx: usize, msg: &str) {
329 let section = &self.sections[idx];
330 let label = section.label_str();
331 let same_label = self.last_streaming_label_len == label.len()
332 && self.last_streaming_label[..self.last_streaming_label_len]
333 == section.label[..section.label_len];
334
335 let w = crate::log::KLOG_GUTTER;
336 match same_label {
337 true => write_continuation_colored(w),
338 false => {
339 write_gutter_colored(label, false, w);
340 let copy_len = label.len().min(MAX_LABEL_LEN);
341 self.last_streaming_label[..copy_len]
342 .copy_from_slice(&label.as_bytes()[..copy_len]);
343 self.last_streaming_label_len = copy_len;
344 }
345 }
346 crate::kprint!("{}\n", msg);
347 }
348}
349
350fn write_gutter_colored(label: &str, dim: bool, width: usize) {
351 let mut w = crate::arch::serial::SerialWriter;
352 let _ = lancer_log::format::write_gutter_label(&mut w, label, dim, width);
353}
354
355fn write_continuation_colored(width: usize) {
356 let mut w = crate::arch::serial::SerialWriter;
357 let _ = lancer_log::format::write_continuation(&mut w, width);
358}
359
360pub struct FmtBuf {
361 buf: [u8; VIEWPORT_LINE_LEN],
362 pos: usize,
363}
364
365impl FmtBuf {
366 pub const fn new() -> Self {
367 Self {
368 buf: [0u8; VIEWPORT_LINE_LEN],
369 pos: 0,
370 }
371 }
372
373 pub fn as_str(&self) -> &str {
374 unsafe { core::str::from_utf8_unchecked(&self.buf[..self.pos]) }
375 }
376
377 #[allow(dead_code)]
378 pub fn clear(&mut self) {
379 self.pos = 0;
380 }
381}
382
383impl Write for FmtBuf {
384 fn write_str(&mut self, s: &str) -> core::fmt::Result {
385 let bytes = s.as_bytes();
386 let copy_len = bytes.len().min(VIEWPORT_LINE_LEN - self.pos);
387 self.buf[self.pos..self.pos + copy_len].copy_from_slice(&bytes[..copy_len]);
388 self.pos += copy_len;
389 Ok(())
390 }
391}