strat9_kernel/arch/x86_64/vga/
status_line.rs1use 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; static STATUS_LAST_IP_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
15const STATUS_IP_REFRESH_PERIOD_TICKS: u64 = 3_000; #[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
33fn 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
48struct StackStr<const N: usize> {
50 buf: [u8; N],
51 len: usize,
52}
53
54impl<const N: usize> StackStr<N> {
55 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 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 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 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
168fn 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 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
236pub 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
243pub 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
258pub 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
273pub 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
281pub(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
324fn 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
366pub 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
432fn 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
451pub 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 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}