strat9_kernel/arch/x86_64/vga/
status_line.rs1use 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; static STATUS_LAST_IP_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
12const STATUS_IP_REFRESH_PERIOD_TICKS: u64 = 3_000; #[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
30fn 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
45struct StackStr<const N: usize> {
47 buf: [u8; N],
48 len: usize,
49}
50
51impl<const N: usize> StackStr<N> {
52 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 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 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 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
165fn 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 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
233pub 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
240pub 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
255pub 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
270pub 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
278pub(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
321fn 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
363pub 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
430fn 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
449pub 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 if !crate::debug_cfg::is_quiet() && tick != last_tick {
457 last_tick = tick;
458 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}