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