Skip to main content

strat9_kernel/arch/x86_64/vga/
status_line.rs

1use alloc::{format, string::String};
2use core::sync::atomic::{AtomicU64, Ordering};
3use spin::Mutex;
4
5use super::{
6    current_fps, is_available, with_writer, TextAlign, TextOptions, UiTheme, VgaWriter, VGA_WRITER,
7};
8
9static STATUS_LAST_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
10const STATUS_REFRESH_PERIOD_TICKS: u64 = 100; // 100Hz timer => 1s
11static STATUS_LAST_IP_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
12const STATUS_IP_REFRESH_PERIOD_TICKS: u64 = 3_000; // 100Hz timer => 30s
13
14#[derive(Debug, Clone)]
15struct StatusLineInfo {
16    hostname: String,
17    ip: String,
18}
19
20static STATUS_LINE_INFO: Mutex<Option<StatusLineInfo>> = Mutex::new(None);
21
22const STATUS_NET_INFO_DEFAULT: &str = "n/a | n/a";
23const STATUS_HOSTNAME_BUF_CAP: usize = 64;
24const STATUS_NET_BUF_CAP: usize = 96;
25const STATUS_LEFT_TEXT_CAP: usize = 128;
26const STATUS_RIGHT_TEXT_CAP: usize = 320;
27const STATUS_BAR_LEFT_MIN_COLS: usize = 8;
28const STATUS_BAR_GAP_COLS: usize = 2;
29
30/// Performs the status line info operation.
31fn status_line_info() -> StatusLineInfo {
32    let mut guard = STATUS_LINE_INFO.lock();
33    if guard.is_none() {
34        *guard = Some(StatusLineInfo {
35            hostname: String::from("strat9"),
36            ip: String::from(STATUS_NET_INFO_DEFAULT),
37        });
38    }
39    guard.as_ref().cloned().unwrap_or(StatusLineInfo {
40        hostname: String::from("strat9"),
41        ip: String::from(STATUS_NET_INFO_DEFAULT),
42    })
43}
44
45/// Stack-only string buffer to avoid heap allocation in the status-line path.
46struct StackStr<const N: usize> {
47    buf: [u8; N],
48    len: usize,
49}
50
51impl<const N: usize> StackStr<N> {
52    /// Creates a new instance.
53    const fn new() -> Self {
54        Self {
55            buf: [0; N],
56            len: 0,
57        }
58    }
59
60    fn is_empty(&self) -> bool {
61        self.len == 0
62    }
63
64    fn char_len(&self) -> usize {
65        self.as_str().chars().count()
66    }
67
68    fn push_str(&mut self, s: &str) {
69        let _ = core::fmt::Write::write_str(self, s);
70    }
71
72    fn push_char(&mut self, ch: char) {
73        let mut buf = [0u8; 4];
74        self.push_str(ch.encode_utf8(&mut buf));
75    }
76
77    /// Returns this as str.
78    fn as_str(&self) -> &str {
79        unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
80    }
81}
82
83impl<const N: usize> core::fmt::Write for StackStr<N> {
84    /// Writes str.
85    fn write_str(&mut self, s: &str) -> core::fmt::Result {
86        let bytes = s.as_bytes();
87        let avail = N - self.len;
88        let n = bytes.len().min(avail);
89        self.buf[self.len..self.len + n].copy_from_slice(&bytes[..n]);
90        self.len += n;
91        Ok(())
92    }
93}
94
95impl<const N: usize> core::fmt::Display for StackStr<N> {
96    /// Performs the fmt operation.
97    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98        f.write_str(self.as_str())
99    }
100}
101
102struct StatusLineRender {
103    left: StackStr<STATUS_LEFT_TEXT_CAP>,
104    right: StackStr<STATUS_RIGHT_TEXT_CAP>,
105}
106
107impl StatusLineRender {
108    fn new(hostname: &str, ip: &str, tick: u64) -> Self {
109        use core::fmt::Write;
110
111        let total_secs = tick / 100;
112        let h = total_secs / 3600;
113        let m = (total_secs % 3600) / 60;
114        let s = total_secs % 60;
115        let fps = current_fps(tick);
116        let mem = format_mem_usage_stack();
117
118        let mut left = StackStr::<STATUS_LEFT_TEXT_CAP>::new();
119        let _ = write!(left, " {} ", hostname);
120
121        let mut right = StackStr::<STATUS_RIGHT_TEXT_CAP>::new();
122        let _ = write!(
123            right,
124            "ip:{}  ver:{}  up:{:02}:{:02}:{:02}  ticks:{}  fps:{}  load:n/a  mem:{} ",
125            ip,
126            env!("CARGO_PKG_VERSION"),
127            h,
128            m,
129            s,
130            tick,
131            fps,
132            mem.as_str()
133        );
134
135        Self { left, right }
136    }
137}
138
139fn fit_status_bar_text<const N: usize>(text: &str, max_cols: usize) -> StackStr<N> {
140    let mut out = StackStr::<N>::new();
141    if max_cols == 0 {
142        return out;
143    }
144
145    let total_cols = text.chars().count();
146    if total_cols <= max_cols {
147        out.push_str(text);
148        return out;
149    }
150
151    if max_cols <= 3 {
152        for _ in 0..max_cols {
153            out.push_char('.');
154        }
155        return out;
156    }
157
158    for ch in text.chars().take(max_cols - 3) {
159        out.push_char(ch);
160    }
161    out.push_str("...");
162    out
163}
164
165/// Performs the draw status bar inner operation.
166fn draw_status_bar_inner(w: &mut VgaWriter, left: &str, right: &str, theme: UiTheme) {
167    let saved_clip = w.clip;
168    w.reset_clip_rect();
169
170    let (gw, gh) = w.glyph_size();
171    if gh == 0 || gw == 0 {
172        w.clip = saved_clip;
173        return;
174    }
175
176    let total_cols = w.width() / gw;
177    if total_cols == 0 {
178        w.clip = saved_clip;
179        return;
180    }
181
182    // Ensure we draw through the back buffer so present() doesn't overwrite us.
183    let (prev_draw, prev_track) = w.begin_viewport_render();
184
185    let bar_h = gh;
186    let y = w.height().saturating_sub(bar_h);
187    w.fill_rect(0, y, w.width(), bar_h, theme.status_bg);
188
189    let available_cols = total_cols.saturating_sub(STATUS_BAR_GAP_COLS);
190    let left_cols = left.chars().count();
191    let right_cols = right.chars().count();
192
193    let right_budget_cols = if left_cols.saturating_add(right_cols) > available_cols {
194        available_cols.saturating_sub(core::cmp::min(left_cols, STATUS_BAR_LEFT_MIN_COLS))
195    } else {
196        right_cols
197    };
198    let fitted_right = fit_status_bar_text::<STATUS_RIGHT_TEXT_CAP>(right, right_budget_cols);
199    let fitted_right_cols = fitted_right.char_len();
200    let left_budget_cols = available_cols.saturating_sub(fitted_right_cols);
201    let fitted_left = fit_status_bar_text::<STATUS_LEFT_TEXT_CAP>(left, left_budget_cols);
202
203    let left_width = fitted_left.char_len().saturating_mul(gw);
204    let right_width = fitted_right.char_len().saturating_mul(gw);
205
206    if !fitted_left.is_empty() {
207        let left_opts = TextOptions {
208            fg: theme.status_text,
209            bg: theme.status_bg,
210            align: TextAlign::Left,
211            wrap: false,
212            max_width: Some(left_width),
213        };
214        w.draw_text(0, y, fitted_left.as_str(), left_opts);
215    }
216
217    if !fitted_right.is_empty() {
218        let right_opts = TextOptions {
219            fg: theme.status_text,
220            bg: theme.status_bg,
221            align: TextAlign::Right,
222            wrap: false,
223            max_width: Some(right_width),
224        };
225        let right_x = w.width().saturating_sub(right_width);
226        w.draw_text(right_x, y, fitted_right.as_str(), right_opts);
227    }
228
229    w.clip = saved_clip;
230    w.end_viewport_render(prev_draw, prev_track);
231}
232
233/// Performs the ui draw status bar operation.
234pub fn ui_draw_status_bar(left: &str, right: &str, theme: UiTheme) {
235    let _ = with_writer(|w| {
236        draw_status_bar_inner(w, left, right, theme);
237    });
238}
239
240/// Sets status hostname.
241pub fn set_status_hostname(hostname: &str) {
242    let mut guard = STATUS_LINE_INFO.lock();
243    if guard.is_none() {
244        *guard = Some(StatusLineInfo {
245            hostname: String::new(),
246            ip: String::from(STATUS_NET_INFO_DEFAULT),
247        });
248    }
249    if let Some(info) = guard.as_mut() {
250        info.hostname.clear();
251        info.hostname.push_str(hostname);
252    }
253}
254
255/// Sets status ip.
256pub fn set_status_ip(ip: &str) {
257    let mut guard = STATUS_LINE_INFO.lock();
258    if guard.is_none() {
259        *guard = Some(StatusLineInfo {
260            hostname: String::from("strat9"),
261            ip: String::new(),
262        });
263    }
264    if let Some(info) = guard.as_mut() {
265        info.ip.clear();
266        info.ip.push_str(ip);
267    }
268}
269
270/// Performs the draw system status line operation.
271pub fn draw_system_status_line(theme: UiTheme) {
272    let info = status_line_info();
273    let tick = crate::process::scheduler::ticks();
274    let render = StatusLineRender::new(&info.hostname, &info.ip, tick);
275    ui_draw_status_bar(render.left.as_str(), render.right.as_str(), theme);
276}
277
278/// Performs the draw boot status line operation.
279pub(super) fn draw_boot_status_line(theme: UiTheme) {
280    let _ = with_writer(|w| {
281        draw_status_bar_inner(
282            w,
283            " strat9 ",
284            "ip:n/a | n/a  ver:boot  up:00:00:00  ticks:0  load:n/a  mem:n/a ",
285            theme,
286        );
287    });
288}
289
290fn read_status_net_value(paths: &[&str], invalid_values: &[&str]) -> Option<String> {
291    for path in paths {
292        let fd = match crate::vfs::open(path, crate::vfs::OpenFlags::READ) {
293            Ok(fd) => fd,
294            Err(_) => continue,
295        };
296        let mut buf = [0u8; 96];
297        let read_res = crate::vfs::read(fd, &mut buf);
298        let _ = crate::vfs::close(fd);
299        let n = match read_res {
300            Ok(n) => n,
301            Err(_) => continue,
302        };
303        if n == 0 {
304            continue;
305        }
306        let Ok(text) = core::str::from_utf8(&buf[..n]) else {
307            continue;
308        };
309        let mut value = text.trim();
310        if let Some(slash) = value.find('/') {
311            value = &value[..slash];
312        }
313        if value.is_empty() || invalid_values.iter().any(|invalid| value == *invalid) {
314            continue;
315        }
316        return Some(String::from(value));
317    }
318    None
319}
320
321/// Performs the refresh status ip from net scheme operation.
322fn refresh_status_ip_from_net_scheme() {
323    let tick = crate::process::scheduler::ticks();
324    let last = STATUS_LAST_IP_REFRESH_TICK.load(Ordering::Relaxed);
325    if tick.saturating_sub(last) < STATUS_IP_REFRESH_PERIOD_TICKS {
326        return;
327    }
328    if STATUS_LAST_IP_REFRESH_TICK
329        .compare_exchange(last, tick, Ordering::Relaxed, Ordering::Relaxed)
330        .is_err()
331    {
332        return;
333    }
334
335    let ipv4 = read_status_net_value(&["/net/address", "/net/ip"], &["0.0.0.0", "169.254.0.0"])
336        .unwrap_or_else(|| String::from("n/a"));
337    let ipv6 = read_status_net_value(&["/net/ip6/address", "/net/ip6"], &["::"])
338        .unwrap_or_else(|| String::from("n/a"));
339    let display = format!("{} | {}", ipv4, ipv6);
340    set_status_ip(&display);
341}
342
343fn try_status_line_snapshot() -> Option<(
344    StackStr<STATUS_HOSTNAME_BUF_CAP>,
345    StackStr<STATUS_NET_BUF_CAP>,
346)> {
347    let guard = STATUS_LINE_INFO.try_lock()?;
348    let (hostname, ip) = if let Some(info) = guard.as_ref() {
349        (info.hostname.as_str(), info.ip.as_str())
350    } else {
351        ("strat9", STATUS_NET_INFO_DEFAULT)
352    };
353
354    let mut hostname_buf = StackStr::<STATUS_HOSTNAME_BUF_CAP>::new();
355    hostname_buf.push_str(hostname);
356
357    let mut ip_buf = StackStr::<STATUS_NET_BUF_CAP>::new();
358    ip_buf.push_str(ip);
359
360    Some((hostname_buf, ip_buf))
361}
362
363/// Performs the maybe refresh system status line operation.
364pub fn maybe_refresh_system_status_line(theme: UiTheme) {
365    if !is_available() {
366        return;
367    }
368
369    let tick = crate::process::scheduler::ticks();
370    let last = STATUS_LAST_REFRESH_TICK.load(Ordering::Relaxed);
371    if tick.saturating_sub(last) < STATUS_REFRESH_PERIOD_TICKS {
372        return;
373    }
374    if STATUS_LAST_REFRESH_TICK
375        .compare_exchange(last, tick, Ordering::Relaxed, Ordering::Relaxed)
376        .is_err()
377    {
378        return;
379    }
380    refresh_status_ip_from_net_scheme();
381
382    let Some((hostname, ip)) = try_status_line_snapshot() else {
383        return;
384    };
385
386    let render = StatusLineRender::new(hostname.as_str(), ip.as_str(), tick);
387
388    if let Some(mut writer) = VGA_WRITER.try_lock() {
389        draw_status_bar_inner(
390            &mut writer,
391            render.left.as_str(),
392            render.right.as_str(),
393            theme,
394        );
395        writer.flush_display();
396    }
397}
398
399fn format_mem_usage_stack() -> StackStr<32> {
400    use core::fmt::Write;
401
402    let lock = crate::memory::buddy::get_allocator();
403    let Some(guard) = lock.try_lock() else {
404        let mut buf = StackStr::<32>::new();
405        buf.push_str("n/a");
406        return buf;
407    };
408    let Some(alloc) = guard.as_ref() else {
409        let mut buf = StackStr::<32>::new();
410        buf.push_str("n/a");
411        return buf;
412    };
413
414    let (total_pages, allocated_pages) = alloc.page_totals();
415    let page_size = 4096usize;
416    let total = total_pages.saturating_mul(page_size);
417    let used = allocated_pages.saturating_mul(page_size);
418    let free = total.saturating_sub(used);
419
420    let mut buf = StackStr::<32>::new();
421    let _ = write!(
422        buf,
423        "{}/{}",
424        format_size_stack(free),
425        format_size_stack(total)
426    );
427    buf
428}
429
430/// Performs the format size stack operation.
431fn format_size_stack(bytes: usize) -> StackStr<16> {
432    use core::fmt::Write;
433    const KB: usize = 1024;
434    const MB: usize = 1024 * KB;
435    const GB: usize = 1024 * MB;
436    let mut buf = StackStr::<16>::new();
437    if bytes >= GB {
438        let _ = write!(buf, "{}G", bytes / GB);
439    } else if bytes >= MB {
440        let _ = write!(buf, "{}M", bytes / MB);
441    } else if bytes >= KB {
442        let _ = write!(buf, "{}K", bytes / KB);
443    } else {
444        let _ = write!(buf, "{}B", bytes);
445    }
446    buf
447}
448
449/// Performs the status line task main operation.
450pub extern "C" fn status_line_task_main() -> ! {
451    let mut last_tick = 0u64;
452    let mut diag_counter = 0u64;
453    loop {
454        let tick = crate::process::scheduler::ticks();
455        // In quiet mode, skip all rendering and heartbeat output.
456        if !crate::debug_cfg::is_quiet() && tick != last_tick {
457            last_tick = tick;
458            // Fallback if creation of the dedicated console task failed.
459            if !crate::arch::x86_64::vgabuf::console_task_active() {
460                crate::arch::x86_64::vgabuf::vgabuf_flush_to_framebuffer();
461            }
462            maybe_refresh_system_status_line(UiTheme::OCEAN_STATUS);
463        }
464        diag_counter += 1;
465        if !crate::debug_cfg::is_quiet() && diag_counter % 5000 == 0 {
466            crate::serial_println!(
467                "[status-line] heartbeat tick={} vga={}",
468                tick,
469                is_available()
470            );
471        }
472        crate::process::yield_task();
473    }
474}