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#[derive(Parser)]
32#[command(name = "top", version, about, max_term_width = MAX_TERM_WIDTH)]
33pub struct Args {
34 #[arg(short, long, default_value = "3.0")]
36 delay: f64,
37
38 #[arg(short = 'n', long)]
40 iterations: Option<u64>,
41
42 #[arg(short, long)]
44 batch: bool,
45
46 #[arg(short, long, value_delimiter = ',')]
48 pid: Option<Vec<i32>>,
49
50 #[arg(short, long)]
52 user: Option<String>,
53
54 #[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 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 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 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 self.num_cpus = kernel.cpu_time.len();
350 self.cpu_deltas.clear();
351
352 if let Some(ref prev) = self.prev_kernel {
353 self.cpu_deltas.push(cpu_delta(&prev.total, &kernel.total));
355 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 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 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 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 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 if let Some(uid) = self.uid_filter
422 && status.euid != uid
423 {
424 continue;
425 }
426
427 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 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 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_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 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 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 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 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 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 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 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 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 &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 let ts = std::time::SystemTime::now()
763 .duration_since(std::time::UNIX_EPOCH)
764 .unwrap_or_default()
765 .as_secs();
766
767 let secs_in_day = ts % 86400;
772
773 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 if let Ok(tz) = std::env::var("TZ")
789 && let Some(offset) = parse_posix_tz_offset(&tz)
790 {
791 return offset;
792 }
793
794 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 }
803
804fn parse_posix_tz_offset(tz: &str) -> Option<i64> {
805 let rest = tz.trim_start_matches(|c: char| c.is_ascii_alphabetic());
808 if rest.is_empty() {
809 return Some(0);
810 }
811 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 if data.len() < 44 || &data[0..4] != b"TZif" {
823 return None;
824 }
825
826 let version = data[4];
828
829 if version == b'2' || version == b'3' {
830 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 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 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 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; 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 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 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 needs_refresh = true;
1237 }
1238 }
1239}