Skip to main content

strat9_kernel/shell/commands/top/
mod.rs

1//! Top command with Ratatui no_std backend.
2//!
3//! This command keeps Chevron shell as default UX and only uses Ratatui while `top` is running.
4
5#[cfg(target_arch = "x86_64")]
6pub(crate) mod ratatui_backend;
7
8use crate::{arch::vga, shell::ShellError, shell_println};
9use alloc::{format, string::String, vec, vec::Vec};
10use core::sync::atomic::Ordering;
11use ratatui::{
12    layout::{Constraint, Direction, Layout},
13    style::{Color, Modifier, Style},
14    widgets::{Block, Borders, Cell, Gauge, Paragraph, Row, Table, TableState},
15    Terminal,
16};
17pub(crate) use ratatui_backend::Strat9RatatuiBackend;
18
19const TOP_REFRESH_TICKS: u64 = 10; // 100ms at 100Hz
20const MAX_CPU_GAUGES: usize = 8;
21
22#[derive(Clone)]
23struct TaskRowData {
24    pid: String,
25    name: String,
26    state: &'static str,
27    priority: String,
28    ticks: u64,
29}
30
31#[derive(Clone)]
32struct SiloRowData {
33    sid: String,
34    name: String,
35    state: String,
36    tasks: String,
37    label: String,
38}
39
40#[derive(Clone)]
41struct StrateRowData {
42    name: String,
43    silos: String,
44}
45
46struct TopSnapshot {
47    cpu_count: usize,
48    total_pages: usize,
49    used_pages: usize,
50    tasks: Vec<TaskRowData>,
51    silos: Vec<SiloRowData>,
52    strates: Vec<StrateRowData>,
53    scheduler: crate::process::SchedulerStateSnapshot,
54}
55
56#[derive(Clone, Copy)]
57struct CpuUsageWindow {
58    per_cpu_ratio: [f64; crate::arch::percpu::MAX_CPUS],
59    avg_ratio: f64,
60}
61
62#[derive(Clone, Copy)]
63struct SchedulerMetricsWindow {
64    rt_ratio: f64,
65    fair_ratio: f64,
66    idle_ratio: f64,
67    switch_delta: u64,
68    preempt_delta: u64,
69    steal_in_delta: u64,
70    steal_out_delta: u64,
71    deferred_raised_delta: u32,
72    deferred_processed_delta: u32,
73}
74
75fn collect_silos_from_proc_scheme() -> Option<(Vec<SiloRowData>, Vec<StrateRowData>)> {
76    let fd = crate::vfs::open("/proc/silos", crate::vfs::OpenFlags::READ).ok()?;
77    let bytes = match crate::vfs::read_all(fd) {
78        Ok(b) => b,
79        Err(_) => {
80            let _ = crate::vfs::close(fd);
81            return None;
82        }
83    };
84    let _ = crate::vfs::close(fd);
85    let body = core::str::from_utf8(&bytes).ok()?;
86
87    let mut silos = Vec::new();
88    let mut strate_index: Vec<(String, Vec<String>)> = Vec::new();
89    for (line_idx, line) in body.lines().enumerate() {
90        if line_idx == 0 || line.is_empty() {
91            continue;
92        }
93        let mut fields = line.split('\t');
94        let sid = fields.next()?;
95        let state = fields.next()?;
96        let tasks = fields.next()?;
97        let _mem_used = fields.next()?;
98        let _mem_min = fields.next()?;
99        let _mem_max = fields.next()?;
100        let _gfx_flags = fields.next()?;
101        let _gfx_sessions = fields.next()?;
102        let _gfx_ttl = fields.next()?;
103        let label = fields.next()?;
104        let name = fields.next()?;
105
106        let strate_name = if label != "-" && !label.is_empty() {
107            String::from(label)
108        } else {
109            String::from(name)
110        };
111        if let Some((_, belongs)) = strate_index
112            .iter_mut()
113            .find(|(entry_name, _)| *entry_name == strate_name)
114        {
115            if !belongs.iter().any(|x| x == name) {
116                belongs.push(String::from(name));
117            }
118        } else {
119            strate_index.push((strate_name, vec![String::from(name)]));
120        }
121
122        silos.push(SiloRowData {
123            sid: String::from(sid),
124            name: String::from(name),
125            state: String::from(state),
126            tasks: String::from(tasks),
127            label: String::from(label),
128        });
129    }
130
131    silos.sort_by_key(|s| s.sid.parse::<u32>().unwrap_or(u32::MAX));
132    strate_index.sort_by(|a, b| a.0.cmp(&b.0));
133
134    let mut strates = Vec::with_capacity(strate_index.len());
135    for (name, belongs) in strate_index {
136        let mut silos_csv = String::new();
137        for (i, silo_name) in belongs.iter().enumerate() {
138            if i != 0 {
139                silos_csv.push_str(", ");
140            }
141            silos_csv.push_str(silo_name);
142        }
143        strates.push(StrateRowData {
144            name,
145            silos: silos_csv,
146        });
147    }
148
149    Some((silos, strates))
150}
151
152/// Performs the collect snapshot operation.
153fn collect_snapshot() -> TopSnapshot {
154    let cpu_count = crate::arch::percpu::cpu_count();
155    let (total_pages, used_pages) = {
156        let guard = crate::memory::buddy::get_allocator().lock();
157        guard.as_ref().map(|a| a.page_totals()).unwrap_or((0, 0))
158    };
159
160    let mut tasks = Vec::new();
161    if let Some(all_tasks) = crate::process::get_all_tasks() {
162        for task in all_tasks {
163            let state = task.get_state();
164            let state_str = match state {
165                crate::process::TaskState::Ready => "Ready",
166                crate::process::TaskState::Running => "Running",
167                crate::process::TaskState::Blocked => "Blocked",
168                crate::process::TaskState::Dead => "Dead",
169            };
170            tasks.push(TaskRowData {
171                pid: format!("{}", task.pid),
172                name: String::from(task.name),
173                state: state_str,
174                priority: format!("{:?}", task.priority),
175                ticks: task.ticks.load(Ordering::Relaxed),
176            });
177        }
178    }
179
180    // Top-like behavior: most CPU-consumed tasks first.
181    tasks.sort_by(|a, b| b.ticks.cmp(&a.ticks));
182
183    let (silos, strates) = collect_silos_from_proc_scheme().unwrap_or_else(|| {
184        let mut silos = Vec::new();
185        let mut strate_index: Vec<(String, Vec<String>)> = Vec::new();
186        let mut silo_snapshots = crate::silo::list_silos_snapshot();
187        silo_snapshots.sort_by_key(|s| s.id);
188
189        for s in silo_snapshots {
190            let label = s.strate_label.unwrap_or_default();
191            let strate_name = if !label.is_empty() {
192                label.clone()
193            } else {
194                s.name.clone()
195            };
196            if let Some((_, belongs)) = strate_index
197                .iter_mut()
198                .find(|(name, _)| *name == strate_name)
199            {
200                if !belongs.iter().any(|x| x == &s.name) {
201                    belongs.push(s.name.clone());
202                }
203            } else {
204                strate_index.push((strate_name, vec![s.name.clone()]));
205            }
206            silos.push(SiloRowData {
207                sid: format!("{}", s.id),
208                name: s.name,
209                state: format!("{:?}", s.state),
210                tasks: format!("{}", s.task_count),
211                label: if label.is_empty() {
212                    String::from("-")
213                } else {
214                    label
215                },
216            });
217        }
218
219        strate_index.sort_by(|a, b| a.0.cmp(&b.0));
220        let mut strates = Vec::with_capacity(strate_index.len());
221        for (name, belongs) in strate_index {
222            let mut silos_csv = String::new();
223            for (i, silo_name) in belongs.iter().enumerate() {
224                if i != 0 {
225                    silos_csv.push_str(", ");
226                }
227                silos_csv.push_str(silo_name);
228            }
229            strates.push(StrateRowData {
230                name,
231                silos: silos_csv,
232            });
233        }
234
235        (silos, strates)
236    });
237
238    TopSnapshot {
239        cpu_count,
240        total_pages,
241        used_pages,
242        tasks,
243        silos,
244        strates,
245        scheduler: crate::process::scheduler_state_snapshot(),
246    }
247}
248
249/// Performs the compute cpu usage window operation.
250fn compute_cpu_usage_window(
251    prev: &crate::process::CpuUsageSnapshot,
252    now: &crate::process::CpuUsageSnapshot,
253) -> CpuUsageWindow {
254    let cpu_count = now.cpu_count.min(crate::arch::percpu::MAX_CPUS);
255    let mut ratios = [0.0f64; crate::arch::percpu::MAX_CPUS];
256    let mut sum = 0.0;
257
258    for i in 0..cpu_count {
259        let delta_total = now.total_ticks[i].saturating_sub(prev.total_ticks[i]);
260        let delta_idle = now.idle_ticks[i].saturating_sub(prev.idle_ticks[i]);
261        let ratio = if delta_total == 0 {
262            0.0
263        } else {
264            let busy = delta_total.saturating_sub(delta_idle);
265            (busy as f64 / delta_total as f64).clamp(0.0, 1.0)
266        };
267        ratios[i] = ratio;
268        sum += ratio;
269    }
270
271    CpuUsageWindow {
272        per_cpu_ratio: ratios,
273        avg_ratio: if cpu_count == 0 {
274            0.0
275        } else {
276            (sum / cpu_count as f64).clamp(0.0, 1.0)
277        },
278    }
279}
280
281/// Performs the compute scheduler metrics window operation.
282fn compute_scheduler_metrics_window(
283    prev: &crate::process::SchedulerMetricsSnapshot,
284    now: &crate::process::SchedulerMetricsSnapshot,
285) -> SchedulerMetricsWindow {
286    let cpu_count = now.cpu_count.min(crate::arch::percpu::MAX_CPUS);
287    let mut rt_delta = 0u64;
288    let mut fair_delta = 0u64;
289    let mut idle_delta = 0u64;
290    let mut switch_delta = 0u64;
291    let mut preempt_delta = 0u64;
292    let mut steal_in_delta = 0u64;
293    let mut steal_out_delta = 0u64;
294    let mut deferred_raised_delta = 0u32;
295    let mut deferred_processed_delta = 0u32;
296    for i in 0..cpu_count {
297        rt_delta = rt_delta
298            .saturating_add(now.rt_runtime_ticks[i].saturating_sub(prev.rt_runtime_ticks[i]));
299        fair_delta = fair_delta
300            .saturating_add(now.fair_runtime_ticks[i].saturating_sub(prev.fair_runtime_ticks[i]));
301        idle_delta = idle_delta
302            .saturating_add(now.idle_runtime_ticks[i].saturating_sub(prev.idle_runtime_ticks[i]));
303        switch_delta =
304            switch_delta.saturating_add(now.switch_count[i].saturating_sub(prev.switch_count[i]));
305        preempt_delta = preempt_delta
306            .saturating_add(now.preempt_count[i].saturating_sub(prev.preempt_count[i]));
307        steal_in_delta = steal_in_delta
308            .saturating_add(now.steal_in_count[i].saturating_sub(prev.steal_in_count[i]));
309        steal_out_delta = steal_out_delta
310            .saturating_add(now.steal_out_count[i].saturating_sub(prev.steal_out_count[i]));
311        deferred_raised_delta = deferred_raised_delta.saturating_add(
312            now.deferred_work_raised[i].saturating_sub(prev.deferred_work_raised[i]),
313        );
314        deferred_processed_delta = deferred_processed_delta.saturating_add(
315            now.deferred_work_processed[i].saturating_sub(prev.deferred_work_processed[i]),
316        );
317    }
318    let total = rt_delta
319        .saturating_add(fair_delta)
320        .saturating_add(idle_delta);
321    let to_ratio = |v: u64| {
322        if total == 0 {
323            0.0
324        } else {
325            (v as f64 / total as f64).clamp(0.0, 1.0)
326        }
327    };
328    SchedulerMetricsWindow {
329        rt_ratio: to_ratio(rt_delta),
330        fair_ratio: to_ratio(fair_delta),
331        idle_ratio: to_ratio(idle_delta),
332        switch_delta,
333        preempt_delta,
334        steal_in_delta,
335        steal_out_delta,
336        deferred_raised_delta,
337        deferred_processed_delta,
338    }
339}
340
341/// Performs the scheduler runtime lines operation.
342fn scheduler_runtime_lines(
343    s: &crate::process::SchedulerStateSnapshot,
344    w: &SchedulerMetricsWindow,
345) -> (String, String, String, String) {
346    let line1 = format!(
347        "Win: RT {:>3}% | FAIR {:>3}% | IDLE {:>3}% | sw {} | pre {} | st+ {} | st- {}",
348        (w.rt_ratio * 100.0) as u16,
349        (w.fair_ratio * 100.0) as u16,
350        (w.idle_ratio * 100.0) as u16,
351        w.switch_delta,
352        w.preempt_delta,
353        w.steal_in_delta,
354        w.steal_out_delta
355    );
356    let line2 = format!(
357        "Cfg: init={} blocked={} pick=[{},{},{}] steal=[{},{}]",
358        s.initialized,
359        s.blocked_tasks,
360        s.pick_order[0].as_str(),
361        s.pick_order[1].as_str(),
362        s.pick_order[2].as_str(),
363        s.steal_order[0].as_str(),
364        s.steal_order[1].as_str()
365    );
366    let cpu_count = s.cpu_count.min(crate::arch::percpu::MAX_CPUS);
367
368    let line3 = if cpu_count == 0 {
369        String::from("CPU: n/a")
370    } else {
371        let c0 = format!(
372            "cpu0 cur={} rq={}/{}/{} nr={}",
373            s.current_task[0], s.rq_rt[0], s.rq_fair[0], s.rq_idle[0], s.need_resched[0]
374        );
375
376        if cpu_count == 1 {
377            format!("CPU: {}", c0)
378        } else {
379            let c1 = format!(
380                "cpu1 cur={} rq={}/{}/{} nr={}",
381                s.current_task[1], s.rq_rt[1], s.rq_fair[1], s.rq_idle[1], s.need_resched[1]
382            );
383            format!("CPU: {} | {}", c0, c1)
384        }
385    };
386    let line4 = format!(
387        "DWork: raised={} processed={} (window +{}/+{})",
388        s.deferred_work_raised.iter().copied().sum::<u32>(),
389        s.deferred_work_processed.iter().copied().sum::<u32>(),
390        w.deferred_raised_delta,
391        w.deferred_processed_delta,
392    );
393    (line1, line2, line3, line4)
394}
395
396/// Top command main loop
397#[cfg(target_arch = "x86_64")]
398pub fn cmd_top(_args: &[alloc::string::String]) -> Result<(), ShellError> {
399    if !vga::is_available() {
400        shell_println!("Error: 'top' requires a graphical framebuffer console.");
401        return Ok(());
402    }
403
404    // Switch to double buffering for flicker-free updates.
405    let was_db = vga::double_buffer_mode();
406    vga::set_double_buffer_mode(true);
407    let backend = Strat9RatatuiBackend::new().map_err(|_| ShellError::ExecutionFailed)?;
408    let mut terminal = Terminal::new(backend).map_err(|_| ShellError::ExecutionFailed)?;
409    terminal.clear().map_err(|_| ShellError::ExecutionFailed)?;
410
411    let mut last_refresh_tick = crate::process::scheduler::ticks();
412    let boot_tick = last_refresh_tick;
413    let mut prev_cpu_sample = crate::process::cpu_usage_snapshot();
414    let mut prev_sched_sample = crate::process::scheduler_metrics_snapshot();
415    let mut selected_task: usize = 0;
416
417    loop {
418        let ticks = crate::process::scheduler::ticks();
419
420        // Keep input responsive even between render ticks.
421        if let Some(ch) = crate::arch::keyboard::read_char() {
422            match ch {
423                b'q' | 0x1B | 0x03 => break,
424                crate::arch::keyboard::KEY_UP => {
425                    selected_task = selected_task.saturating_sub(1);
426                }
427                crate::arch::keyboard::KEY_DOWN => {
428                    selected_task = selected_task.saturating_add(1);
429                }
430                _ => {}
431            }
432        }
433
434        // Refresh every 100ms with a 100Hz timer.
435        if ticks.saturating_sub(last_refresh_tick) < TOP_REFRESH_TICKS {
436            crate::process::yield_task();
437            continue;
438        }
439        last_refresh_tick = ticks;
440        let snapshot = collect_snapshot();
441        let cpu_sample = crate::process::cpu_usage_snapshot();
442        let cpu_window = compute_cpu_usage_window(&prev_cpu_sample, &cpu_sample);
443        prev_cpu_sample = cpu_sample;
444        let sched_sample = crate::process::scheduler_metrics_snapshot();
445        let sched_window = compute_scheduler_metrics_window(&prev_sched_sample, &sched_sample);
446        prev_sched_sample = sched_sample;
447        let mem_ratio = if snapshot.total_pages > 0 {
448            (snapshot.used_pages as f64) / (snapshot.total_pages as f64)
449        } else {
450            0.0
451        };
452
453        let rows: Vec<Row> = snapshot
454            .tasks
455            .iter()
456            .map(|task| {
457                Row::new(vec![
458                    Cell::from(task.pid.as_str()),
459                    Cell::from(task.name.as_str()),
460                    Cell::from(task.state),
461                    Cell::from(task.priority.as_str()),
462                    Cell::from(format!("{}", task.ticks)),
463                ])
464            })
465            .collect();
466        let row_count = rows.len();
467        if row_count == 0 {
468            selected_task = 0;
469        } else if selected_task >= row_count {
470            selected_task = row_count - 1;
471        }
472        let mut table_state = TableState::default();
473        if row_count > 0 {
474            table_state.select(Some(selected_task));
475        }
476
477        let uptime_secs = ticks.saturating_sub(boot_tick) / 100;
478
479        let frame_started = vga::begin_frame();
480        terminal
481            .draw(|frame| {
482                let title_style = Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD);
483                let primary_text = Style::default().fg(Color::White);
484                let muted_text = Style::default().fg(Color::Gray);
485                let header_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD);
486
487                let area = frame.area();
488                let vertical = Layout::default()
489                    .direction(Direction::Vertical)
490                    .constraints([
491                        Constraint::Length(2),
492                        Constraint::Length(3),
493                        Constraint::Length(6),
494                        Constraint::Length(6),
495                        Constraint::Min(8),
496                        Constraint::Length(1),
497                    ])
498                    .split(area);
499
500                let title = Paragraph::new("Strat9 system monitor")
501                    .style(title_style)
502                    .block(Block::default().borders(Borders::BOTTOM).title("Top"));
503                frame.render_widget(title, vertical[0]);
504
505                let stats_line = Paragraph::new(format!(
506                    "CPUs: {} | Tasks: {} | Silos: {} | Strates: {} | CPU(avg): {:>3}% | Uptime: {}s",
507                    snapshot.cpu_count,
508                    snapshot.tasks.len(),
509                    snapshot.silos.len(),
510                    snapshot.strates.len(),
511                    (cpu_window.avg_ratio * 100.0) as u16,
512                    uptime_secs
513                ))
514                .style(primary_text)
515                .block(Block::default().borders(Borders::BOTTOM).title("Stats"));
516                frame.render_widget(stats_line, vertical[1]);
517
518                let (sched_line1, sched_line2, sched_line3, sched_line4) =
519                    scheduler_runtime_lines(&snapshot.scheduler, &sched_window);
520                let sched_line = Paragraph::new(format!(
521                    "{}\n{}\n{}\n{}",
522                    sched_line1, sched_line2, sched_line3, sched_line4
523                ))
524                .style(primary_text)
525                .block(Block::default().borders(Borders::BOTTOM).title("Scheduler"));
526                frame.render_widget(sched_line, vertical[2]);
527
528                let cpu_split = Layout::default()
529                    .direction(Direction::Horizontal)
530                    .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
531                    .split(vertical[3]);
532
533                let mem_gauge = Gauge::default()
534                    .block(
535                        Block::default()
536                            .borders(Borders::TOP | Borders::BOTTOM)
537                            .title(format!(
538                                "Memory {} / {} pages",
539                                snapshot.used_pages, snapshot.total_pages
540                            )),
541                    )
542                    .gauge_style(Style::default().fg(Color::Blue))
543                    .use_unicode(false)
544                    .ratio(mem_ratio.clamp(0.0, 1.0))
545                    .label(format!("{:.1}%", mem_ratio * 100.0));
546                frame.render_widget(mem_gauge, cpu_split[0]);
547
548                let cpu_gauge_count = snapshot.cpu_count.min(MAX_CPU_GAUGES);
549                if cpu_gauge_count > 0 {
550                    let mut constraints = Vec::with_capacity(cpu_gauge_count);
551                    for _ in 0..cpu_gauge_count {
552                        constraints.push(Constraint::Length(1));
553                    }
554                    let cpu_rows = Layout::default()
555                        .direction(Direction::Vertical)
556                        .constraints(constraints)
557                        .split(cpu_split[1]);
558
559                    for i in 0..cpu_gauge_count {
560                        let ratio = cpu_window.per_cpu_ratio[i];
561                        let gauge = Gauge::default()
562                            .block(Block::default().title(format!("CPU{}", i)).borders(Borders::NONE))
563                            .gauge_style(Style::default().fg(Color::Green))
564                            .use_unicode(false)
565                            .ratio(ratio)
566                            .label(format!("{:>3}%", (ratio * 100.0) as u16));
567                        frame.render_widget(gauge, cpu_rows[i]);
568                    }
569                }
570
571                let main_split = Layout::default()
572                    .direction(Direction::Horizontal)
573                    .constraints([Constraint::Percentage(64), Constraint::Percentage(36)])
574                    .split(vertical[4]);
575
576                let task_table = Table::new(
577                    rows.iter().cloned(),
578                    [
579                        Constraint::Length(5),  // PID
580                        Constraint::Min(18),    // Name (takes remaining width)
581                        Constraint::Length(9),  // State
582                        Constraint::Length(8),  // Prio
583                        Constraint::Length(10), // Ticks
584                    ],
585                )
586                .header(
587                    Row::new(vec!["PID", "Name", "State", "Prio", "Ticks"]).style(header_style),
588                )
589                .column_spacing(1)
590                .style(primary_text)
591                .row_highlight_style(
592                    Style::default()
593                        .bg(Color::White)
594                        .fg(Color::Black)
595                        .add_modifier(Modifier::BOLD),
596                )
597                .block(
598                    Block::default()
599                        .borders(Borders::TOP)
600                        .title("Tasks (sorted by ticks)"),
601                );
602                frame.render_stateful_widget(task_table, main_split[0], &mut table_state);
603
604                let right_split = Layout::default()
605                    .direction(Direction::Vertical)
606                    .constraints([Constraint::Percentage(52), Constraint::Percentage(48)])
607                    .split(main_split[1]);
608
609                let silo_rows: Vec<Row> = snapshot
610                    .silos
611                    .iter()
612                    .map(|s| {
613                        Row::new(vec![
614                            Cell::from(s.sid.as_str()),
615                            Cell::from(s.name.as_str()),
616                            Cell::from(s.state.as_str()),
617                            Cell::from(s.tasks.as_str()),
618                            Cell::from(s.label.as_str()),
619                        ])
620                    })
621                    .collect();
622                let silo_table = Table::new(
623                    silo_rows,
624                    [
625                        Constraint::Length(5),
626                        Constraint::Length(10),
627                        Constraint::Length(8),
628                        Constraint::Length(5),
629                        Constraint::Min(8),
630                    ],
631                )
632                .header(
633                    Row::new(vec!["SID", "Name", "State", "T", "Label"]).style(
634                        Style::default().fg(Color::LightGreen).add_modifier(Modifier::BOLD),
635                    ),
636                )
637                .column_spacing(1)
638                .style(primary_text)
639                .block(Block::default().borders(Borders::TOP).title("Silos"));
640                frame.render_widget(silo_table, right_split[0]);
641
642                let strate_rows: Vec<Row> = snapshot
643                    .strates
644                    .iter()
645                    .map(|s| Row::new(vec![Cell::from(s.name.as_str()), Cell::from(s.silos.as_str())]))
646                    .collect();
647                let strate_table = Table::new(
648                    strate_rows,
649                    [Constraint::Length(12), Constraint::Min(10)],
650                )
651                .header(
652                    Row::new(vec!["Strate", "BelongsTo"]).style(
653                        Style::default().fg(Color::LightCyan).add_modifier(Modifier::BOLD),
654                    ),
655                )
656                .column_spacing(1)
657                .style(primary_text)
658                .block(Block::default().borders(Borders::TOP).title("Strates"));
659                frame.render_widget(strate_table, right_split[1]);
660
661                let footer = Paragraph::new("[Up/Down] Select process | [q|Esc] Exit")
662                    .style(muted_text)
663                    .block(Block::default().borders(Borders::TOP));
664                frame.render_widget(footer, vertical[5]);
665            })
666            .map_err(|_| ShellError::ExecutionFailed)?;
667
668        if frame_started {
669            vga::end_frame();
670        } else {
671            vga::present();
672        }
673
674        crate::process::yield_task();
675    }
676
677    // Clean exit.
678    vga::set_double_buffer_mode(was_db);
679    crate::shell::output::clear_screen();
680    vga::set_text_cursor(0, 0);
681    shell_println!("Top exited.");
682    Ok(())
683}
684
685/// Serial-only fallback for `top` on non-x86 targets (R6: full TUI via virtio).
686#[cfg(not(target_arch = "x86_64"))]
687pub fn cmd_top(_args: &[alloc::string::String]) -> Result<(), ShellError> {
688    shell_println!("top: TUI requires a framebuffer console (not available yet)");
689    Ok(())
690}