Skip to main content

strat9_kernel/arch/x86_64/vga/
status_line.rs

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