Skip to main content

procutils_top/
lib.rs

1use clap::Parser;
2use crossterm::event::{self, Event, KeyCode, KeyEventKind, MouseEventKind};
3use procfs::prelude::*;
4use procutils_common::{MAX_TERM_WIDTH, man::ManContent};
5use ratatui::{
6    DefaultTerminal, Frame,
7    layout::{Constraint, Layout},
8    style::{Color, Modifier, Style},
9    text::{Line, Span},
10    widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState},
11};
12use std::{
13    collections::HashMap, io::Write as _, process::ExitCode, time::Duration,
14};
15
16pub const MAN: ManContent = ManContent {
17    description: Some(include_str!("../man/description.man")),
18    extra_sections: &[
19        ("KEY COMMANDS", include_str!("../man/key_commands.man")),
20        (
21            "FIELD DESCRIPTIONS",
22            include_str!("../man/field_descriptions.man"),
23        ),
24        ("EXAMPLES", include_str!("../man/examples.man")),
25        ("NOTES", include_str!("../man/notes.man")),
26        ("SEE ALSO", include_str!("../man/see_also.man")),
27    ],
28};
29
30/// Display Linux processes.
31#[derive(Parser)]
32#[command(name = "top", version, about, max_term_width = MAX_TERM_WIDTH)]
33pub struct Args {
34    /// Update delay in seconds.
35    #[arg(short, long, default_value = "3.0")]
36    delay: f64,
37
38    /// Exit after this many display updates.
39    #[arg(short = 'n', long)]
40    iterations: Option<u64>,
41
42    /// Batch mode (non-interactive, for piping).
43    #[arg(short, long)]
44    batch: bool,
45
46    /// Monitor only these PIDs (comma-separated).
47    #[arg(short, long, value_delimiter = ',')]
48    pid: Option<Vec<i32>>,
49
50    /// Show only processes for this user.
51    #[arg(short, long)]
52    user: Option<String>,
53
54    /// Start with per-CPU display.
55    #[arg(short = '1')]
56    per_cpu: bool,
57}
58
59#[derive(Clone, Copy, PartialEq)]
60enum SortField {
61    Cpu,
62    Mem,
63    Pid,
64    Time,
65}
66
67struct ProcessInfo {
68    pid: i32,
69    user: String,
70    priority: i64,
71    nice: i64,
72    virt_kb: u64,
73    res_kb: u64,
74    shr_kb: u64,
75    state: char,
76    cpu_pct: f64,
77    mem_pct: f64,
78    time_ticks: u64,
79    command: String,
80    cmdline: String,
81}
82
83use procutils_common::{fmt::format_kb, uid::UidCache, utmp};
84
85fn format_time_plus(ticks: u64, tps: u64) -> String {
86    if tps == 0 {
87        return "0:00.00".to_string();
88    }
89    let total_secs = ticks / tps;
90    let hundredths = (ticks % tps) * 100 / tps;
91    let mins = total_secs / 60;
92    let secs = total_secs % 60;
93    format!("{mins}:{secs:02}.{hundredths:02}")
94}
95
96fn format_uptime(secs: f64) -> String {
97    let total = secs as u64;
98    let days = total / 86400;
99    let hours = (total % 86400) / 3600;
100    let mins = (total % 3600) / 60;
101
102    if days > 0 {
103        format!(
104            "up {days} day{}, {hours:2}:{mins:02}",
105            if days == 1 { "" } else { "s" }
106        )
107    } else if hours > 0 {
108        format!("up {hours:2}:{mins:02}")
109    } else {
110        format!("up {mins} min")
111    }
112}
113
114fn count_users() -> usize {
115    utmp::read(utmp::DEFAULT_UTMP_PATH)
116        .map(|entries| utmp::count_user_processes(&entries))
117        .unwrap_or(0)
118}
119
120struct CpuDelta {
121    user: f64,
122    nice: f64,
123    system: f64,
124    idle: f64,
125    iowait: f64,
126    irq: f64,
127    softirq: f64,
128    steal: f64,
129}
130
131fn cpu_delta(prev: &procfs::CpuTime, cur: &procfs::CpuTime) -> CpuDelta {
132    let d = |c: u64, p: u64| c.saturating_sub(p) as f64;
133    let user = d(cur.user, prev.user);
134    let nice = d(cur.nice, prev.nice);
135    let system = d(cur.system, prev.system);
136    let idle = d(cur.idle, prev.idle);
137    let iowait = d(cur.iowait.unwrap_or(0), prev.iowait.unwrap_or(0));
138    let irq = d(cur.irq.unwrap_or(0), prev.irq.unwrap_or(0));
139    let softirq = d(cur.softirq.unwrap_or(0), prev.softirq.unwrap_or(0));
140    let steal = d(cur.steal.unwrap_or(0), prev.steal.unwrap_or(0));
141
142    let total = user + nice + system + idle + iowait + irq + softirq + steal;
143    if total == 0.0 {
144        return CpuDelta {
145            user: 0.0,
146            nice: 0.0,
147            system: 0.0,
148            idle: 100.0,
149            iowait: 0.0,
150            irq: 0.0,
151            softirq: 0.0,
152            steal: 0.0,
153        };
154    }
155
156    let pct = |v: f64| v / total * 100.0;
157    CpuDelta {
158        user: pct(user),
159        nice: pct(nice),
160        system: pct(system),
161        idle: pct(idle),
162        iowait: pct(iowait),
163        irq: pct(irq),
164        softirq: pct(softirq),
165        steal: pct(steal),
166    }
167}
168
169fn cpu_pct_cumulative(ct: &procfs::CpuTime) -> CpuDelta {
170    let user = ct.user as f64;
171    let nice = ct.nice as f64;
172    let system = ct.system as f64;
173    let idle = ct.idle as f64;
174    let iowait = ct.iowait.unwrap_or(0) as f64;
175    let irq = ct.irq.unwrap_or(0) as f64;
176    let softirq = ct.softirq.unwrap_or(0) as f64;
177    let steal = ct.steal.unwrap_or(0) as f64;
178
179    let total = user + nice + system + idle + iowait + irq + softirq + steal;
180    if total == 0.0 {
181        return CpuDelta {
182            user: 0.0,
183            nice: 0.0,
184            system: 0.0,
185            idle: 100.0,
186            iowait: 0.0,
187            irq: 0.0,
188            softirq: 0.0,
189            steal: 0.0,
190        };
191    }
192
193    let pct = |v: f64| v / total * 100.0;
194    CpuDelta {
195        user: pct(user),
196        nice: pct(nice),
197        system: pct(system),
198        idle: pct(idle),
199        iowait: pct(iowait),
200        irq: pct(irq),
201        softirq: pct(softirq),
202        steal: pct(steal),
203    }
204}
205
206fn cpu_time_total_ticks(ct: &procfs::CpuTime) -> u64 {
207    ct.user
208        + ct.nice
209        + ct.system
210        + ct.idle
211        + ct.iowait.unwrap_or(0)
212        + ct.irq.unwrap_or(0)
213        + ct.softirq.unwrap_or(0)
214        + ct.steal.unwrap_or(0)
215}
216
217struct App {
218    processes: Vec<ProcessInfo>,
219    sort_field: SortField,
220    sort_reverse: bool,
221    delay: Duration,
222    table_state: TableState,
223    show_full_cmd: bool,
224    show_per_cpu: bool,
225
226    prev_kernel: Option<procfs::KernelStats>,
227    prev_proc_times: HashMap<i32, u64>,
228
229    uid_cache: UidCache,
230    total_mem_kb: u64,
231    page_size: u64,
232    tps: u64,
233    num_cpus: usize,
234
235    pid_filter: Option<Vec<i32>>,
236    uid_filter: Option<u32>,
237
238    iterations_remaining: Option<u64>,
239
240    // Cached summary data from latest refresh
241    uptime_secs: f64,
242    load_avg: [f64; 3],
243    task_total: usize,
244    task_running: usize,
245    task_sleeping: usize,
246    task_stopped: usize,
247    task_zombie: usize,
248    cpu_deltas: Vec<CpuDelta>,
249    mem_total_kb: u64,
250    mem_free_kb: u64,
251    mem_used_kb: u64,
252    mem_bufcache_kb: u64,
253    swap_total_kb: u64,
254    swap_free_kb: u64,
255    swap_used_kb: u64,
256    mem_avail_kb: u64,
257}
258
259impl App {
260    fn new(args: &Args) -> Self {
261        let tps = procfs::ticks_per_second();
262        let page_size = procfs::page_size();
263
264        let uid_filter = args.user.as_ref().and_then(|u| {
265            u.parse::<u32>()
266                .ok()
267                .or_else(|| procutils_common::uid::resolve_uid(u))
268        });
269
270        Self {
271            processes: Vec::new(),
272            sort_field: SortField::Cpu,
273            sort_reverse: false,
274            delay: Duration::from_secs_f64(args.delay.max(0.1)),
275            table_state: TableState::default(),
276            show_full_cmd: false,
277            show_per_cpu: args.per_cpu,
278
279            prev_kernel: None,
280            prev_proc_times: HashMap::new(),
281
282            uid_cache: UidCache::new(),
283            total_mem_kb: 0,
284            page_size,
285            tps,
286            num_cpus: 0,
287
288            pid_filter: args.pid.clone(),
289            uid_filter,
290
291            iterations_remaining: args.iterations,
292
293            uptime_secs: 0.0,
294            load_avg: [0.0; 3],
295            task_total: 0,
296            task_running: 0,
297            task_sleeping: 0,
298            task_stopped: 0,
299            task_zombie: 0,
300            cpu_deltas: Vec::new(),
301            mem_total_kb: 0,
302            mem_free_kb: 0,
303            mem_used_kb: 0,
304            mem_bufcache_kb: 0,
305            swap_total_kb: 0,
306            swap_free_kb: 0,
307            swap_used_kb: 0,
308            mem_avail_kb: 0,
309        }
310    }
311
312    fn refresh(&mut self) {
313        // Read system-wide data
314        let kernel = match procfs::KernelStats::current() {
315            Ok(k) => k,
316            Err(_) => return,
317        };
318
319        let meminfo = match procfs::Meminfo::current() {
320            Ok(m) => m,
321            Err(_) => return,
322        };
323
324        if let Ok(la) = procfs::LoadAverage::current() {
325            self.load_avg = [la.one as f64, la.five as f64, la.fifteen as f64];
326        }
327
328        if let Ok(up) = procfs::Uptime::current() {
329            self.uptime_secs = up.uptime;
330        }
331
332        // Memory info (procfs returns bytes)
333        self.mem_total_kb = meminfo.mem_total / 1024;
334        self.total_mem_kb = self.mem_total_kb;
335        self.mem_free_kb = meminfo.mem_free / 1024;
336        let buffers_kb = meminfo.buffers / 1024;
337        let cached_kb = meminfo.cached / 1024;
338        let sreclaimable_kb = meminfo.s_reclaimable.unwrap_or(0) / 1024;
339        self.mem_bufcache_kb = buffers_kb + cached_kb + sreclaimable_kb;
340        self.mem_used_kb =
341            self.mem_total_kb - self.mem_free_kb - self.mem_bufcache_kb;
342        self.swap_total_kb = meminfo.swap_total / 1024;
343        self.swap_free_kb = meminfo.swap_free / 1024;
344        self.swap_used_kb = self.swap_total_kb - self.swap_free_kb;
345        self.mem_avail_kb =
346            meminfo.mem_available.unwrap_or(meminfo.mem_free) / 1024;
347
348        // CPU deltas
349        self.num_cpus = kernel.cpu_time.len();
350        self.cpu_deltas.clear();
351
352        if let Some(ref prev) = self.prev_kernel {
353            // Total CPU line
354            self.cpu_deltas.push(cpu_delta(&prev.total, &kernel.total));
355            // Per-CPU lines
356            if self.show_per_cpu {
357                for (i, cur_cpu) in kernel.cpu_time.iter().enumerate() {
358                    if let Some(prev_cpu) = prev.cpu_time.get(i) {
359                        self.cpu_deltas.push(cpu_delta(prev_cpu, cur_cpu));
360                    }
361                }
362            }
363        } else {
364            // First sample: show cumulative percentages
365            self.cpu_deltas.push(cpu_pct_cumulative(&kernel.total));
366            if self.show_per_cpu {
367                for cur_cpu in &kernel.cpu_time {
368                    self.cpu_deltas.push(cpu_pct_cumulative(cur_cpu));
369                }
370            }
371        }
372
373        // Compute total CPU ticks delta for per-process %CPU
374        let total_ticks_delta = if let Some(ref prev) = self.prev_kernel {
375            cpu_time_total_ticks(&kernel.total)
376                .saturating_sub(cpu_time_total_ticks(&prev.total))
377        } else {
378            cpu_time_total_ticks(&kernel.total)
379        };
380
381        // Read processes
382        let all_procs = match procfs::process::all_processes() {
383            Ok(iter) => iter,
384            Err(_) => {
385                self.prev_kernel = Some(kernel);
386                return;
387            }
388        };
389
390        let mut new_processes = Vec::new();
391        let mut new_proc_times = HashMap::new();
392        let mut task_running = 0usize;
393        let mut task_sleeping = 0usize;
394        let mut task_stopped = 0usize;
395        let mut task_zombie = 0usize;
396
397        for proc_result in all_procs {
398            let proc = match proc_result {
399                Ok(p) => p,
400                Err(_) => continue,
401            };
402
403            let stat = match proc.stat() {
404                Ok(s) => s,
405                Err(_) => continue,
406            };
407
408            // PID filter
409            if let Some(ref pids) = self.pid_filter
410                && !pids.contains(&stat.pid)
411            {
412                continue;
413            }
414
415            let status = match proc.status() {
416                Ok(s) => s,
417                Err(_) => continue,
418            };
419
420            // User filter
421            if let Some(uid) = self.uid_filter
422                && status.euid != uid
423            {
424                continue;
425            }
426
427            // Count task states
428            match stat.state {
429                'R' => task_running += 1,
430                'S' | 'I' => task_sleeping += 1,
431                'T' | 't' => task_stopped += 1,
432                'Z' => task_zombie += 1,
433                _ => task_sleeping += 1,
434            }
435
436            let proc_ticks = stat.utime + stat.stime;
437            new_proc_times.insert(stat.pid, proc_ticks);
438
439            // Compute per-process CPU%
440            let cpu_pct = if total_ticks_delta > 0 {
441                let prev_ticks =
442                    self.prev_proc_times.get(&stat.pid).copied().unwrap_or(0);
443                let delta = proc_ticks.saturating_sub(prev_ticks);
444                // Scale by num_cpus so a single busy core = ~100%
445                delta as f64 / total_ticks_delta as f64
446                    * self.num_cpus.max(1) as f64
447                    * 100.0
448            } else {
449                0.0
450            };
451
452            let res_kb = stat.rss * self.page_size / 1024;
453            let shr_kb = (status.rssfile.unwrap_or(0)
454                + status.rssshmem.unwrap_or(0))
455                / 1024;
456            let mem_pct = if self.total_mem_kb > 0 {
457                res_kb as f64 / self.total_mem_kb as f64 * 100.0
458            } else {
459                0.0
460            };
461
462            let cmdline =
463                proc.cmdline().ok().map(|v| v.join(" ")).unwrap_or_default();
464
465            new_processes.push(ProcessInfo {
466                pid: stat.pid,
467                user: self.uid_cache.get(status.euid).to_string(),
468                priority: stat.priority,
469                nice: stat.nice,
470                virt_kb: stat.vsize / 1024,
471                res_kb,
472                shr_kb,
473                state: stat.state,
474                cpu_pct,
475                mem_pct,
476                time_ticks: proc_ticks,
477                command: stat.comm.clone(),
478                cmdline,
479            });
480        }
481
482        self.task_total = new_processes.len();
483        self.task_running = task_running;
484        self.task_sleeping = task_sleeping;
485        self.task_stopped = task_stopped;
486        self.task_zombie = task_zombie;
487
488        // Sort
489        sort_processes(&mut new_processes, self.sort_field, self.sort_reverse);
490
491        self.processes = new_processes;
492        self.prev_kernel = Some(kernel);
493        self.prev_proc_times = new_proc_times;
494    }
495
496    fn scroll_up(&mut self, n: u16) {
497        let offset = self.table_state.offset_mut();
498        *offset = offset.saturating_sub(n as usize);
499    }
500
501    fn scroll_down(&mut self, n: u16) {
502        let offset = self.table_state.offset_mut();
503        *offset = offset.saturating_add(n as usize);
504    }
505
506    fn scroll_home(&mut self) {
507        *self.table_state.offset_mut() = 0;
508    }
509
510    fn scroll_end(&mut self) {
511        *self.table_state.offset_mut() = self.processes.len().saturating_sub(1);
512    }
513
514    fn summary_height(&self) -> u16 {
515        // Line 1: uptime, Line 2: tasks, Line 3+: CPU, Line N-1: Mem, Line N: Swap
516        // Plus 1 for the border
517        let cpu_lines = if self.show_per_cpu {
518            self.num_cpus.max(1) as u16
519        } else {
520            1
521        };
522        cpu_lines + 5
523    }
524
525    fn draw(&mut self, frame: &mut Frame) {
526        let area = frame.area();
527        let summary_h = self.summary_height();
528
529        let chunks = Layout::vertical([
530            Constraint::Length(summary_h),
531            Constraint::Min(3),
532        ])
533        .split(area);
534
535        self.draw_summary(frame, chunks[0]);
536        self.draw_table(frame, chunks[1]);
537    }
538
539    fn draw_summary(&self, frame: &mut Frame, area: ratatui::layout::Rect) {
540        let label = Style::default().fg(Color::Cyan);
541        let mut lines = Vec::new();
542
543        // Line 1: top - HH:MM:SS up X days, HH:MM, N users, load average: ...
544        let now = chrono_free_time();
545        let users = count_users();
546        lines.push(Line::from(vec![
547            Span::styled(" top - ", label),
548            Span::raw(format!(
549                "{now}  {}, {users} user{}, load average: {:.2}, {:.2}, {:.2}",
550                format_uptime(self.uptime_secs),
551                if users == 1 { "" } else { "s" },
552                self.load_avg[0],
553                self.load_avg[1],
554                self.load_avg[2],
555            )),
556        ]));
557
558        // Line 2: Tasks
559        lines.push(Line::from(vec![
560            Span::styled(" Tasks: ", label),
561            Span::raw(format!(
562                "{} total, {} running, {} sleeping, {} stopped, {} zombie",
563                self.task_total,
564                self.task_running,
565                self.task_sleeping,
566                self.task_stopped,
567                self.task_zombie,
568            )),
569        ]));
570
571        // CPU lines
572        for (i, d) in self.cpu_deltas.iter().enumerate() {
573            let cpu_label = if i == 0 && !self.show_per_cpu {
574                " %Cpu(s): ".to_string()
575            } else if i == 0 && self.show_per_cpu {
576                // Skip the aggregate line when showing per-cpu
577                continue;
578            } else {
579                format!(" %Cpu{:>2}: ", i - 1)
580            };
581
582            lines.push(Line::from(vec![
583                Span::styled(cpu_label, label),
584                Span::raw(format!(
585                    "{:5.1} us, {:5.1} sy, {:5.1} ni, {:5.1} id, {:5.1} wa, {:5.1} hi, {:5.1} si, {:5.1} st",
586                    d.user, d.system, d.nice, d.idle, d.iowait, d.irq, d.softirq, d.steal,
587                )),
588            ]));
589        }
590
591        // When per_cpu is on but we have no deltas yet, show a placeholder
592        if self.cpu_deltas.is_empty() {
593            lines.push(Line::from(vec![
594                Span::styled(" %Cpu(s): ", label),
595                Span::raw("  0.0 us,   0.0 sy,   0.0 ni, 100.0 id,   0.0 wa,   0.0 hi,   0.0 si,   0.0 st"),
596            ]));
597        }
598
599        // Memory line
600        let mib = |kb: u64| kb as f64 / 1024.0;
601        lines.push(Line::from(vec![
602            Span::styled(" MiB Mem: ", label),
603            Span::raw(format!(
604                "{:10.1} total, {:10.1} free, {:10.1} used, {:10.1} buff/cache",
605                mib(self.mem_total_kb),
606                mib(self.mem_free_kb),
607                mib(self.mem_used_kb),
608                mib(self.mem_bufcache_kb),
609            )),
610        ]));
611
612        // Swap line
613        lines.push(Line::from(vec![
614            Span::styled(" MiB Swap:", label),
615            Span::raw(format!(
616                "{:10.1} total, {:10.1} free, {:10.1} used. {:10.1} avail Mem",
617                mib(self.swap_total_kb),
618                mib(self.swap_free_kb),
619                mib(self.swap_used_kb),
620                mib(self.mem_avail_kb),
621            )),
622        ]));
623
624        frame.render_widget(
625            Paragraph::new(lines)
626                .block(Block::default().borders(Borders::BOTTOM)),
627            area,
628        );
629    }
630
631    fn draw_table(&mut self, frame: &mut Frame, area: ratatui::layout::Rect) {
632        let header_style = Style::default()
633            .add_modifier(Modifier::BOLD)
634            .add_modifier(Modifier::REVERSED);
635
636        let sort_indicator = |field: SortField| {
637            if self.sort_field == field {
638                if self.sort_reverse { " ^" } else { " v" }
639            } else {
640                ""
641            }
642        };
643
644        let header = Row::new(vec![
645            Cell::from(format!("  PID{}", sort_indicator(SortField::Pid))),
646            Cell::from("USER"),
647            Cell::from(" PR"),
648            Cell::from("  NI"),
649            Cell::from("    VIRT"),
650            Cell::from("     RES"),
651            Cell::from("     SHR"),
652            Cell::from("S"),
653            Cell::from(format!(" %CPU{}", sort_indicator(SortField::Cpu))),
654            Cell::from(format!(" %MEM{}", sort_indicator(SortField::Mem))),
655            Cell::from(format!("    TIME+{}", sort_indicator(SortField::Time))),
656            Cell::from("COMMAND"),
657        ])
658        .style(header_style);
659
660        let rows: Vec<Row> = self
661            .processes
662            .iter()
663            .map(|p| {
664                let cmd = if p.cmdline.is_empty() {
665                    // Kernel thread — only comm is available
666                    &p.command
667                } else if self.show_full_cmd {
668                    &p.cmdline
669                } else {
670                    &p.command
671                };
672
673                let state_color = match p.state {
674                    'R' => Color::Green,
675                    'Z' => Color::Red,
676                    'T' | 't' => Color::Yellow,
677                    _ => Color::default(),
678                };
679
680                Row::new(vec![
681                    Cell::from(format!("{:>5}", p.pid)),
682                    Cell::from(format!("{:<8}", truncate(&p.user, 8))),
683                    Cell::from(format!(
684                        "{:>3}",
685                        if p.priority < -99 {
686                            "rt".to_string()
687                        } else {
688                            p.priority.to_string()
689                        }
690                    )),
691                    Cell::from(format!("{:>4}", p.nice)),
692                    Cell::from(format!("{:>8}", format_kb(p.virt_kb))),
693                    Cell::from(format!("{:>8}", format_kb(p.res_kb))),
694                    Cell::from(format!("{:>8}", format_kb(p.shr_kb))),
695                    Cell::from(format!("{}", p.state))
696                        .style(Style::default().fg(state_color)),
697                    Cell::from(format!("{:>5.1}", p.cpu_pct)),
698                    Cell::from(format!("{:>5.1}", p.mem_pct)),
699                    Cell::from(format!(
700                        "{:>9}",
701                        format_time_plus(p.time_ticks, self.tps)
702                    )),
703                    Cell::from(cmd.to_string()),
704                ])
705            })
706            .collect();
707
708        let widths = [
709            Constraint::Length(5),
710            Constraint::Length(8),
711            Constraint::Length(3),
712            Constraint::Length(4),
713            Constraint::Length(8),
714            Constraint::Length(8),
715            Constraint::Length(8),
716            Constraint::Length(1),
717            Constraint::Length(5),
718            Constraint::Length(5),
719            Constraint::Length(9),
720            Constraint::Fill(1),
721        ];
722
723        let table = Table::new(rows, widths).header(header).column_spacing(1);
724
725        frame.render_stateful_widget(table, area, &mut self.table_state);
726    }
727}
728
729fn sort_processes(
730    procs: &mut [ProcessInfo],
731    sort_field: SortField,
732    sort_reverse: bool,
733) {
734    let cmp = |a: &ProcessInfo, b: &ProcessInfo| -> std::cmp::Ordering {
735        match sort_field {
736            SortField::Cpu => b
737                .cpu_pct
738                .partial_cmp(&a.cpu_pct)
739                .unwrap_or(std::cmp::Ordering::Equal),
740            SortField::Mem => b
741                .mem_pct
742                .partial_cmp(&a.mem_pct)
743                .unwrap_or(std::cmp::Ordering::Equal),
744            SortField::Pid => a.pid.cmp(&b.pid),
745            SortField::Time => b.time_ticks.cmp(&a.time_ticks),
746        }
747    };
748
749    if sort_reverse {
750        procs.sort_by(|a, b| cmp(a, b).reverse());
751    } else {
752        procs.sort_by(cmp);
753    }
754}
755
756fn truncate(s: &str, max: usize) -> &str {
757    if s.len() <= max { s } else { &s[..max] }
758}
759
760fn chrono_free_time() -> String {
761    // Get current time without chrono dependency
762    let ts = std::time::SystemTime::now()
763        .duration_since(std::time::UNIX_EPOCH)
764        .unwrap_or_default()
765        .as_secs();
766
767    // Simple UTC-to-local is hard without libc, so read /etc/localtime offset
768    // from procfs. For simplicity, just format as UTC-based local using libc-free approach.
769    // Actually, read from /proc/self: use the same trick as coreutils.
770    // Simplest: use the elapsed seconds and compute HH:MM:SS.
771    let secs_in_day = ts % 86400;
772
773    // Try to get timezone offset from TZ or default to reading /etc/localtime
774    // For simplicity, use a basic approach:
775    let offset = local_tz_offset();
776    let local_secs = (secs_in_day as i64 + offset).rem_euclid(86400) as u64;
777
778    let h = local_secs / 3600;
779    let m = (local_secs % 3600) / 60;
780    let s = local_secs % 60;
781    format!("{h:02}:{m:02}:{s:02}")
782}
783
784fn local_tz_offset() -> i64 {
785    // Try TZ environment variable first, then /etc/localtime
786    // For a simple implementation, parse /etc/timezone or use a fixed heuristic
787    // This is a best-effort approach — top doesn't need perfect timezone handling
788    if let Ok(tz) = std::env::var("TZ")
789        && let Some(offset) = parse_posix_tz_offset(&tz)
790    {
791        return offset;
792    }
793
794    // Read the TZif file at /etc/localtime
795    if let Ok(data) = std::fs::read("/etc/localtime")
796        && let Some(offset) = parse_tzif_current_offset(&data)
797    {
798        return offset;
799    }
800
801    0 // fallback to UTC
802}
803
804fn parse_posix_tz_offset(tz: &str) -> Option<i64> {
805    // Handle simple POSIX TZ like "EST5EDT" or "UTC" or "EST-5"
806    // Skip alphabetic prefix
807    let rest = tz.trim_start_matches(|c: char| c.is_ascii_alphabetic());
808    if rest.is_empty() {
809        return Some(0);
810    }
811    // Parse offset hours (note: POSIX TZ offset sign is inverted)
812    let hours: i64 = rest
813        .split(|c: char| !c.is_ascii_digit() && c != '-' && c != '+')
814        .next()?
815        .parse()
816        .ok()?;
817    Some(-hours * 3600)
818}
819
820fn parse_tzif_current_offset(data: &[u8]) -> Option<i64> {
821    // Minimal TZif parser — just get the last transition's UTC offset
822    if data.len() < 44 || &data[0..4] != b"TZif" {
823        return None;
824    }
825
826    // Check for TZif2/3 (version byte at offset 4)
827    let version = data[4];
828
829    if version == b'2' || version == b'3' {
830        // Skip v1 data block to find v2/v3 header
831        let tzh_ttisutcnt =
832            u32::from_be_bytes(data[20..24].try_into().ok()?) as usize;
833        let tzh_ttisstdcnt =
834            u32::from_be_bytes(data[24..28].try_into().ok()?) as usize;
835        let tzh_leapcnt =
836            u32::from_be_bytes(data[28..32].try_into().ok()?) as usize;
837        let tzh_timecnt =
838            u32::from_be_bytes(data[32..36].try_into().ok()?) as usize;
839        let tzh_typecnt =
840            u32::from_be_bytes(data[36..40].try_into().ok()?) as usize;
841        let tzh_charcnt =
842            u32::from_be_bytes(data[40..44].try_into().ok()?) as usize;
843
844        let v1_datablock_size = tzh_timecnt * 4
845            + tzh_timecnt
846            + tzh_typecnt * 6
847            + tzh_charcnt
848            + tzh_leapcnt * 8
849            + tzh_ttisstdcnt
850            + tzh_ttisutcnt;
851
852        let v2_header_start = 44 + v1_datablock_size;
853        if data.len() < v2_header_start + 44 {
854            return None;
855        }
856
857        return parse_tzif_v2(data, v2_header_start);
858    }
859
860    // v1: parse directly
861    parse_tzif_v1(data)
862}
863
864fn parse_tzif_v1(data: &[u8]) -> Option<i64> {
865    let tzh_timecnt =
866        u32::from_be_bytes(data[32..36].try_into().ok()?) as usize;
867    let tzh_typecnt =
868        u32::from_be_bytes(data[36..40].try_into().ok()?) as usize;
869
870    if tzh_typecnt == 0 {
871        return None;
872    }
873
874    let times_start = 44;
875    let types_start = times_start + tzh_timecnt * 4;
876    let ttinfos_start = types_start + tzh_timecnt;
877
878    let now = std::time::SystemTime::now()
879        .duration_since(std::time::UNIX_EPOCH)
880        .unwrap_or_default()
881        .as_secs() as i64;
882
883    // Find the most recent transition before now
884    let mut type_idx = 0u8;
885    for i in (0..tzh_timecnt).rev() {
886        let offset = times_start + i * 4;
887        if data.len() < offset + 4 {
888            continue;
889        }
890        let trans_time =
891            i32::from_be_bytes(data[offset..offset + 4].try_into().ok()?)
892                as i64;
893        if trans_time <= now {
894            type_idx = data[types_start + i];
895            break;
896        }
897    }
898
899    // Read the ttinfo for this type
900    let ttinfo_offset = ttinfos_start + type_idx as usize * 6;
901    if data.len() < ttinfo_offset + 6 {
902        return None;
903    }
904    let utoff = i32::from_be_bytes(
905        data[ttinfo_offset..ttinfo_offset + 4].try_into().ok()?,
906    );
907    Some(utoff as i64)
908}
909
910fn parse_tzif_v2(data: &[u8], header_start: usize) -> Option<i64> {
911    let h = header_start;
912    if data.len() < h + 44 || &data[h..h + 4] != b"TZif" {
913        return None;
914    }
915
916    let _tzh_leapcnt =
917        u32::from_be_bytes(data[h + 28..h + 32].try_into().ok()?) as usize;
918    let tzh_timecnt =
919        u32::from_be_bytes(data[h + 32..h + 36].try_into().ok()?) as usize;
920    let tzh_typecnt =
921        u32::from_be_bytes(data[h + 36..h + 40].try_into().ok()?) as usize;
922
923    if tzh_typecnt == 0 {
924        return None;
925    }
926
927    let times_start = h + 44;
928    let types_start = times_start + tzh_timecnt * 8; // v2 uses 8-byte timestamps
929    let ttinfos_start = types_start + tzh_timecnt;
930
931    let now = std::time::SystemTime::now()
932        .duration_since(std::time::UNIX_EPOCH)
933        .unwrap_or_default()
934        .as_secs() as i64;
935
936    let mut type_idx = 0u8;
937    for i in (0..tzh_timecnt).rev() {
938        let offset = times_start + i * 8;
939        if data.len() < offset + 8 {
940            continue;
941        }
942        let trans_time =
943            i64::from_be_bytes(data[offset..offset + 8].try_into().ok()?);
944        if trans_time <= now {
945            type_idx = data[types_start + i];
946            break;
947        }
948    }
949
950    let ttinfo_offset = ttinfos_start + type_idx as usize * 6;
951    if data.len() < ttinfo_offset + 6 {
952        return None;
953    }
954    let utoff = i32::from_be_bytes(
955        data[ttinfo_offset..ttinfo_offset + 4].try_into().ok()?,
956    );
957    Some(utoff as i64)
958}
959
960fn print_batch_summary(app: &App) {
961    let now = chrono_free_time();
962    let users = count_users();
963
964    println!(
965        "top - {}  {}, {} user{}, load average: {:.2}, {:.2}, {:.2}",
966        now,
967        format_uptime(app.uptime_secs),
968        users,
969        if users == 1 { "" } else { "s" },
970        app.load_avg[0],
971        app.load_avg[1],
972        app.load_avg[2],
973    );
974
975    println!(
976        "Tasks: {} total, {} running, {} sleeping, {} stopped, {} zombie",
977        app.task_total,
978        app.task_running,
979        app.task_sleeping,
980        app.task_stopped,
981        app.task_zombie,
982    );
983
984    for (i, d) in app.cpu_deltas.iter().enumerate() {
985        let label = if i == 0 && !app.show_per_cpu {
986            "%Cpu(s):".to_string()
987        } else if i == 0 && app.show_per_cpu {
988            continue;
989        } else {
990            format!("%Cpu{:>2}:", i - 1)
991        };
992        println!(
993            "{label} {:5.1} us, {:5.1} sy, {:5.1} ni, {:5.1} id, {:5.1} wa, {:5.1} hi, {:5.1} si, {:5.1} st",
994            d.user,
995            d.system,
996            d.nice,
997            d.idle,
998            d.iowait,
999            d.irq,
1000            d.softirq,
1001            d.steal,
1002        );
1003    }
1004
1005    let mib = |kb: u64| kb as f64 / 1024.0;
1006    println!(
1007        "MiB Mem: {:10.1} total, {:10.1} free, {:10.1} used, {:10.1} buff/cache",
1008        mib(app.mem_total_kb),
1009        mib(app.mem_free_kb),
1010        mib(app.mem_used_kb),
1011        mib(app.mem_bufcache_kb),
1012    );
1013    println!(
1014        "MiB Swap:{:10.1} total, {:10.1} free, {:10.1} used. {:10.1} avail Mem",
1015        mib(app.swap_total_kb),
1016        mib(app.swap_free_kb),
1017        mib(app.swap_used_kb),
1018        mib(app.mem_avail_kb),
1019    );
1020    println!();
1021
1022    println!(
1023        "    PID USER       PR   NI     VIRT      RES      SHR S  %CPU  %MEM     TIME+ COMMAND",
1024    );
1025
1026    for p in &app.processes {
1027        let pr = if p.priority < -99 {
1028            "rt".to_string()
1029        } else {
1030            p.priority.to_string()
1031        };
1032        println!(
1033            "{:>7} {:<9} {:>3} {:>4} {:>8} {:>8} {:>8} {} {:>5.1} {:>5.1} {:>9} {}",
1034            p.pid,
1035            truncate(&p.user, 9),
1036            pr,
1037            p.nice,
1038            format_kb(p.virt_kb),
1039            format_kb(p.res_kb),
1040            format_kb(p.shr_kb),
1041            p.state,
1042            p.cpu_pct,
1043            p.mem_pct,
1044            format_time_plus(p.time_ticks, app.tps),
1045            if p.cmdline.is_empty() {
1046                &p.command
1047            } else if app.show_full_cmd {
1048                &p.cmdline
1049            } else {
1050                &p.command
1051            },
1052        );
1053    }
1054
1055    println!();
1056}
1057
1058fn run_batch(args: &Args) -> ExitCode {
1059    let mut app = App::new(args);
1060    let mut iteration = 0u64;
1061
1062    loop {
1063        if let Some(max) = app.iterations_remaining
1064            && iteration >= max
1065        {
1066            break;
1067        }
1068
1069        if iteration > 0 {
1070            std::thread::sleep(app.delay);
1071        }
1072
1073        app.refresh();
1074        print_batch_summary(&app);
1075        let _ = std::io::stdout().flush();
1076
1077        iteration += 1;
1078    }
1079
1080    ExitCode::SUCCESS
1081}
1082
1083pub fn run(args: Args) -> ExitCode {
1084    if args.batch {
1085        return run_batch(&args);
1086    }
1087
1088    crossterm::execute!(
1089        std::io::stdout(),
1090        crossterm::event::EnableMouseCapture
1091    )
1092    .ok();
1093
1094    let mut terminal = match ratatui::try_init() {
1095        Ok(t) => t,
1096        Err(e) => {
1097            eprintln!("top: failed to initialize terminal: {e}");
1098            return ExitCode::FAILURE;
1099        }
1100    };
1101
1102    let result = run_app(&mut terminal, &args);
1103
1104    ratatui::restore();
1105    crossterm::execute!(
1106        std::io::stdout(),
1107        crossterm::event::DisableMouseCapture
1108    )
1109    .ok();
1110
1111    match result {
1112        Ok(()) => ExitCode::SUCCESS,
1113        Err(e) => {
1114            eprintln!("top: {e}");
1115            ExitCode::FAILURE
1116        }
1117    }
1118}
1119
1120fn run_app(
1121    terminal: &mut DefaultTerminal,
1122    args: &Args,
1123) -> Result<(), Box<dyn std::error::Error>> {
1124    let mut app = App::new(args);
1125    let mut iteration = 0u64;
1126    let mut needs_refresh = true;
1127
1128    loop {
1129        if let Some(max) = app.iterations_remaining
1130            && iteration >= max
1131        {
1132            return Ok(());
1133        }
1134
1135        if needs_refresh {
1136            app.refresh();
1137            iteration += 1;
1138            needs_refresh = false;
1139        }
1140
1141        terminal.draw(|frame| app.draw(frame))?;
1142
1143        if event::poll(app.delay)? {
1144            // Drain all pending events before redrawing, so queued
1145            // mouse moves don't each trigger an expensive /proc rescan.
1146            let mut needs_redraw = false;
1147            while event::poll(Duration::ZERO)? {
1148                match event::read()? {
1149                    Event::Key(key) if key.kind == KeyEventKind::Press => {
1150                        match key.code {
1151                            KeyCode::Char('q') | KeyCode::Char('Q') => {
1152                                return Ok(());
1153                            }
1154                            KeyCode::Char(' ') => {
1155                                needs_refresh = true;
1156                                break;
1157                            }
1158                            KeyCode::Char('P') => {
1159                                app.sort_field = SortField::Cpu;
1160                                needs_redraw = true;
1161                            }
1162                            KeyCode::Char('M') => {
1163                                app.sort_field = SortField::Mem;
1164                                needs_redraw = true;
1165                            }
1166                            KeyCode::Char('N') => {
1167                                app.sort_field = SortField::Pid;
1168                                needs_redraw = true;
1169                            }
1170                            KeyCode::Char('T') => {
1171                                app.sort_field = SortField::Time;
1172                                needs_redraw = true;
1173                            }
1174                            KeyCode::Char('R') => {
1175                                app.sort_reverse = !app.sort_reverse;
1176                                needs_redraw = true;
1177                            }
1178                            KeyCode::Char('c') => {
1179                                app.show_full_cmd = !app.show_full_cmd;
1180                                needs_redraw = true;
1181                            }
1182                            KeyCode::Char('1') => {
1183                                app.show_per_cpu = !app.show_per_cpu;
1184                                needs_refresh = true;
1185                            }
1186                            KeyCode::Up | KeyCode::Char('k') => {
1187                                app.scroll_up(1);
1188                                needs_redraw = true;
1189                            }
1190                            KeyCode::Down | KeyCode::Char('j') => {
1191                                app.scroll_down(1);
1192                                needs_redraw = true;
1193                            }
1194                            KeyCode::PageUp => {
1195                                app.scroll_up(20);
1196                                needs_redraw = true;
1197                            }
1198                            KeyCode::PageDown => {
1199                                app.scroll_down(20);
1200                                needs_redraw = true;
1201                            }
1202                            KeyCode::Home => {
1203                                app.scroll_home();
1204                                needs_redraw = true;
1205                            }
1206                            KeyCode::End => {
1207                                app.scroll_end();
1208                                needs_redraw = true;
1209                            }
1210                            _ => {}
1211                        }
1212                    }
1213                    Event::Mouse(mouse) => match mouse.kind {
1214                        MouseEventKind::ScrollUp => {
1215                            app.scroll_up(3);
1216                            needs_redraw = true;
1217                        }
1218                        MouseEventKind::ScrollDown => {
1219                            app.scroll_down(3);
1220                            needs_redraw = true;
1221                        }
1222                        _ => {}
1223                    },
1224                    _ => {}
1225                }
1226            }
1227
1228            // Re-sort if sort changed, without a full /proc rescan
1229            if needs_redraw {
1230                let sort_field = app.sort_field;
1231                let sort_reverse = app.sort_reverse;
1232                sort_processes(&mut app.processes, sort_field, sort_reverse);
1233            }
1234        } else {
1235            // Timer expired — time for a fresh /proc read
1236            needs_refresh = true;
1237        }
1238    }
1239}