1use crate::theme::Gradient;
7use presentar_core::{
8 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
9 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
10};
11use std::any::Any;
12use std::cmp::Ordering;
13use std::fmt::Write as _;
14use std::time::Duration;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum ProcessState {
19 Running,
21 #[default]
23 Sleeping,
24 DiskWait,
26 Zombie,
28 Stopped,
30 Idle,
32}
33
34impl ProcessState {
35 #[must_use]
37 pub fn char(&self) -> char {
38 match self {
39 Self::Running => 'R',
40 Self::Sleeping => 'S',
41 Self::DiskWait => 'D',
42 Self::Zombie => 'Z',
43 Self::Stopped => 'T',
44 Self::Idle => 'I',
45 }
46 }
47
48 #[must_use]
50 pub fn color(&self) -> Color {
51 match self {
52 Self::Running => Color::new(0.3, 0.9, 0.3, 1.0), Self::Sleeping => Color::new(0.5, 0.5, 0.5, 1.0), Self::DiskWait => Color::new(1.0, 0.7, 0.2, 1.0), Self::Zombie => Color::new(1.0, 0.3, 0.3, 1.0), Self::Stopped => Color::new(0.9, 0.9, 0.3, 1.0), Self::Idle => Color::new(0.4, 0.4, 0.4, 1.0), }
59 }
60}
61
62#[derive(Debug, Clone)]
64pub struct ProcessEntry {
65 pub pid: u32,
67 pub user: String,
69 pub cpu_percent: f32,
71 pub mem_percent: f32,
73 pub command: String,
75 pub cmdline: Option<String>,
77 pub state: ProcessState,
79 pub oom_score: Option<i32>,
81 pub cgroup: Option<String>,
83 pub nice: Option<i32>,
85 pub threads: Option<u32>,
87 pub parent_pid: Option<u32>,
89 pub tree_depth: usize,
91 pub is_last_child: bool,
93 pub tree_prefix: String,
95}
96
97impl ProcessEntry {
98 #[must_use]
100 pub fn new(
101 pid: u32,
102 user: impl Into<String>,
103 cpu: f32,
104 mem: f32,
105 command: impl Into<String>,
106 ) -> Self {
107 Self {
108 pid,
109 user: user.into(),
110 cpu_percent: cpu,
111 mem_percent: mem,
112 command: command.into(),
113 cmdline: None,
114 state: ProcessState::default(),
115 oom_score: None,
116 cgroup: None,
117 nice: None,
118 threads: None,
119 parent_pid: None,
120 tree_depth: 0,
121 is_last_child: false,
122 tree_prefix: String::new(),
123 }
124 }
125
126 #[must_use]
128 pub fn with_cmdline(mut self, cmdline: impl Into<String>) -> Self {
129 self.cmdline = Some(cmdline.into());
130 self
131 }
132
133 #[must_use]
135 pub fn with_state(mut self, state: ProcessState) -> Self {
136 self.state = state;
137 self
138 }
139
140 #[must_use]
142 pub fn with_oom_score(mut self, score: i32) -> Self {
143 self.oom_score = Some(score);
144 self
145 }
146
147 #[must_use]
149 pub fn with_cgroup(mut self, cgroup: impl Into<String>) -> Self {
150 self.cgroup = Some(cgroup.into());
151 self
152 }
153
154 #[must_use]
156 pub fn with_nice(mut self, nice: i32) -> Self {
157 self.nice = Some(nice);
158 self
159 }
160
161 #[must_use]
163 pub fn with_threads(mut self, threads: u32) -> Self {
164 self.threads = Some(threads);
165 self
166 }
167
168 #[must_use]
170 pub fn with_parent_pid(mut self, ppid: u32) -> Self {
171 self.parent_pid = Some(ppid);
172 self
173 }
174
175 pub fn set_tree_info(&mut self, depth: usize, is_last: bool, prefix: String) {
177 self.tree_depth = depth;
178 self.is_last_child = is_last;
179 self.tree_prefix = prefix;
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum ProcessSort {
186 Pid,
187 User,
188 Cpu,
189 Memory,
190 Command,
191 Oom,
192}
193
194#[derive(Debug, Clone)]
196#[allow(clippy::struct_excessive_bools)]
197pub struct ProcessTable {
198 processes: Vec<ProcessEntry>,
200 selected: usize,
202 scroll_offset: usize,
204 sort_by: ProcessSort,
206 sort_ascending: bool,
208 cpu_gradient: Gradient,
210 mem_gradient: Gradient,
212 show_cmdline: bool,
214 compact: bool,
216 show_oom: bool,
218 show_nice: bool,
220 show_threads: bool,
222 tree_view: bool,
224 bounds: Rect,
226}
227
228impl Default for ProcessTable {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234impl ProcessTable {
235 #[must_use]
237 pub fn new() -> Self {
238 Self {
239 processes: Vec::new(),
240 selected: 0,
241 scroll_offset: 0,
242 sort_by: ProcessSort::Cpu,
243 sort_ascending: false, cpu_gradient: Gradient::from_hex(&["#7aa2f7", "#e0af68", "#f7768e"]),
245 mem_gradient: Gradient::from_hex(&["#9ece6a", "#e0af68", "#f7768e"]),
246 show_cmdline: false,
247 compact: false,
248 show_oom: false,
249 show_nice: false,
250 show_threads: false,
251 tree_view: false,
252 bounds: Rect::default(),
253 }
254 }
255
256 pub fn set_processes(&mut self, processes: Vec<ProcessEntry>) {
258 self.processes = processes;
259 if self.tree_view {
261 self.build_tree();
262 } else {
263 self.sort_processes();
264 }
265 if !self.processes.is_empty() && self.selected >= self.processes.len() {
267 self.selected = self.processes.len() - 1;
268 }
269 }
270
271 pub fn add_process(&mut self, process: ProcessEntry) {
273 self.processes.push(process);
274 }
275
276 pub fn clear(&mut self) {
278 self.processes.clear();
279 self.selected = 0;
280 self.scroll_offset = 0;
281 }
282
283 #[must_use]
285 pub fn with_cpu_gradient(mut self, gradient: Gradient) -> Self {
286 self.cpu_gradient = gradient;
287 self
288 }
289
290 #[must_use]
292 pub fn with_mem_gradient(mut self, gradient: Gradient) -> Self {
293 self.mem_gradient = gradient;
294 self
295 }
296
297 #[must_use]
299 pub fn compact(mut self) -> Self {
300 self.compact = true;
301 self
302 }
303
304 #[must_use]
306 pub fn with_cmdline(mut self) -> Self {
307 self.show_cmdline = true;
308 self
309 }
310
311 #[must_use]
313 pub fn with_oom(mut self) -> Self {
314 self.show_oom = true;
315 self
316 }
317
318 #[must_use]
320 pub fn with_nice_column(mut self) -> Self {
321 self.show_nice = true;
322 self
323 }
324
325 #[must_use]
327 pub fn with_threads_column(mut self) -> Self {
328 self.show_threads = true;
329 self
330 }
331
332 #[must_use]
334 pub fn with_tree_view(mut self) -> Self {
335 self.tree_view = true;
336 self
337 }
338
339 pub fn toggle_tree_view(&mut self) {
341 self.tree_view = !self.tree_view;
342 if self.tree_view {
343 self.build_tree();
344 }
345 }
346
347 #[must_use]
349 pub fn is_tree_view(&self) -> bool {
350 self.tree_view
351 }
352
353 pub fn sort_by(&mut self, column: ProcessSort) {
355 if self.sort_by == column {
356 self.sort_ascending = !self.sort_ascending;
357 } else {
358 self.sort_by = column;
359 self.sort_ascending = !matches!(
361 column,
362 ProcessSort::Cpu | ProcessSort::Memory | ProcessSort::Oom
363 );
364 }
365 self.sort_processes();
366 }
367
368 #[must_use]
370 pub fn current_sort(&self) -> ProcessSort {
371 self.sort_by
372 }
373
374 #[must_use]
376 pub fn selected(&self) -> usize {
377 self.selected
378 }
379
380 #[must_use]
382 pub fn selected_process(&self) -> Option<&ProcessEntry> {
383 self.processes.get(self.selected)
384 }
385
386 pub fn select(&mut self, row: usize) {
388 if !self.processes.is_empty() {
389 self.selected = row.min(self.processes.len() - 1);
390 self.ensure_visible();
391 }
392 }
393
394 pub fn select_prev(&mut self) {
396 if self.selected > 0 {
397 self.selected -= 1;
398 self.ensure_visible();
399 }
400 }
401
402 pub fn select_next(&mut self) {
404 if !self.processes.is_empty() && self.selected < self.processes.len() - 1 {
405 self.selected += 1;
406 self.ensure_visible();
407 }
408 }
409
410 #[must_use]
412 pub fn len(&self) -> usize {
413 self.processes.len()
414 }
415
416 #[must_use]
418 pub fn is_empty(&self) -> bool {
419 self.processes.is_empty()
420 }
421
422 fn build_tree(&mut self) {
427 use std::collections::HashMap;
428
429 if self.processes.is_empty() {
430 return;
431 }
432
433 let pid_to_idx: HashMap<u32, usize> = self
435 .processes
436 .iter()
437 .enumerate()
438 .map(|(i, p)| (p.pid, i))
439 .collect();
440
441 let mut children: HashMap<u32, Vec<usize>> = HashMap::new();
443 let mut roots: Vec<usize> = Vec::new();
444
445 for (idx, proc) in self.processes.iter().enumerate() {
446 if let Some(ppid) = proc.parent_pid {
447 if pid_to_idx.contains_key(&ppid) {
448 children.entry(ppid).or_default().push(idx);
449 } else {
450 roots.push(idx);
452 }
453 } else {
454 roots.push(idx);
455 }
456 }
457
458 for children_list in children.values_mut() {
460 children_list.sort_by(|&a, &b| {
461 self.processes[b]
462 .cpu_percent
463 .partial_cmp(&self.processes[a].cpu_percent)
464 .unwrap_or(std::cmp::Ordering::Equal)
465 });
466 }
467
468 roots.sort_by(|&a, &b| {
470 self.processes[b]
471 .cpu_percent
472 .partial_cmp(&self.processes[a].cpu_percent)
473 .unwrap_or(std::cmp::Ordering::Equal)
474 });
475
476 let mut tree_order: Vec<(usize, usize, bool, String)> = Vec::new();
478
479 let roots_len = roots.len();
480 for (i, &root_idx) in roots.iter().enumerate() {
481 let is_last = i == roots_len - 1;
482 Self::build_tree_dfs(
483 root_idx,
484 0,
485 "",
486 is_last,
487 &self.processes,
488 &children,
489 &mut tree_order,
490 );
491 }
492
493 let old_processes = std::mem::take(&mut self.processes);
495 self.processes.reserve(tree_order.len());
496
497 for (idx, depth, is_last, prefix) in tree_order {
498 let mut proc = old_processes[idx].clone();
499 proc.set_tree_info(depth, is_last, prefix);
500 self.processes.push(proc);
501 }
502 }
503
504 fn build_tree_dfs(
505 idx: usize,
506 depth: usize,
507 prefix: &str,
508 is_last: bool,
509 processes: &[ProcessEntry],
510 children: &std::collections::HashMap<u32, Vec<usize>>,
511 tree_order: &mut Vec<(usize, usize, bool, String)>,
512 ) {
513 let proc = &processes[idx];
514 let current_prefix = if depth == 0 {
515 String::new()
516 } else if is_last {
517 format!("{prefix}└─")
518 } else {
519 format!("{prefix}├─")
520 };
521
522 tree_order.push((idx, depth, is_last, current_prefix));
523
524 let next_prefix = if depth == 0 {
526 String::new()
527 } else if is_last {
528 format!("{prefix} ")
529 } else {
530 format!("{prefix}│ ")
531 };
532
533 if let Some(child_indices) = children.get(&proc.pid) {
534 let len = child_indices.len();
535 for (i, &child_idx) in child_indices.iter().enumerate() {
536 let child_is_last = i == len - 1;
537 Self::build_tree_dfs(
538 child_idx,
539 depth + 1,
540 &next_prefix,
541 child_is_last,
542 processes,
543 children,
544 tree_order,
545 );
546 }
547 }
548 }
549
550 fn sort_processes(&mut self) {
551 let ascending = self.sort_ascending;
552 match self.sort_by {
553 ProcessSort::Pid => {
554 self.processes.sort_by(|a, b| {
555 if ascending {
556 a.pid.cmp(&b.pid)
557 } else {
558 b.pid.cmp(&a.pid)
559 }
560 });
561 }
562 ProcessSort::User => {
563 self.processes.sort_by(|a, b| {
564 if ascending {
565 a.user.cmp(&b.user)
566 } else {
567 b.user.cmp(&a.user)
568 }
569 });
570 }
571 ProcessSort::Cpu => {
572 self.processes.sort_by(|a, b| {
573 let cmp = a
574 .cpu_percent
575 .partial_cmp(&b.cpu_percent)
576 .unwrap_or(std::cmp::Ordering::Equal);
577 if ascending {
578 cmp
579 } else {
580 cmp.reverse()
581 }
582 });
583 }
584 ProcessSort::Memory => {
585 self.processes.sort_by(|a, b| {
586 let cmp = a
587 .mem_percent
588 .partial_cmp(&b.mem_percent)
589 .unwrap_or(std::cmp::Ordering::Equal);
590 if ascending {
591 cmp
592 } else {
593 cmp.reverse()
594 }
595 });
596 }
597 ProcessSort::Command => {
598 self.processes.sort_by(|a, b| {
599 if ascending {
600 a.command.cmp(&b.command)
601 } else {
602 b.command.cmp(&a.command)
603 }
604 });
605 }
606 ProcessSort::Oom => {
607 self.processes.sort_by(|a, b| {
608 let a_oom = a.oom_score.unwrap_or(0);
609 let b_oom = b.oom_score.unwrap_or(0);
610 if ascending {
611 a_oom.cmp(&b_oom)
612 } else {
613 b_oom.cmp(&a_oom)
614 }
615 });
616 }
617 }
618 }
619
620 fn ensure_visible(&mut self) {
621 let visible_rows = (self.bounds.height as usize).saturating_sub(2);
622 if visible_rows == 0 {
623 return;
624 }
625
626 if self.selected < self.scroll_offset {
627 self.scroll_offset = self.selected;
628 } else if self.selected >= self.scroll_offset + visible_rows {
629 self.scroll_offset = self.selected - visible_rows + 1;
630 }
631 }
632
633 fn truncate(s: &str, width: usize) -> String {
634 if s.chars().count() <= width {
635 format!("{s:width$}")
636 } else if width > 1 {
637 let chars: String = s.chars().take(width - 1).collect();
639 format!("{chars}…")
640 } else {
641 s.chars().take(width).collect()
642 }
643 }
644
645 fn oom_color(oom: i32) -> Color {
647 if oom > 500 {
648 Color::new(1.0, 0.3, 0.3, 1.0) } else if oom > 200 {
650 Color::new(1.0, 0.8, 0.2, 1.0) } else {
652 Color::new(0.5, 0.8, 0.5, 1.0) }
654 }
655
656 fn nice_color(ni: i32) -> Color {
658 match ni.cmp(&0) {
659 Ordering::Less => Color::new(0.3, 0.9, 0.9, 1.0), Ordering::Greater => Color::new(0.6, 0.6, 0.6, 1.0), Ordering::Equal => Color::new(0.8, 0.8, 0.8, 1.0), }
663 }
664
665 fn threads_color(th: u32) -> Color {
667 if th > 50 {
668 Color::new(0.3, 0.9, 0.9, 1.0) } else if th > 10 {
670 Color::new(1.0, 0.8, 0.2, 1.0) } else {
672 Color::new(0.8, 0.8, 0.8, 1.0) }
674 }
675
676 fn build_header(&self, cols: &ColumnWidths) -> String {
678 let sep = if self.compact { " " } else { " │ " };
679 let mut header = String::new();
680 let _ = write!(header, "{:>w$}", "PID", w = cols.pid);
681 if self.compact {
682 header.push(' ');
683 let _ = write!(header, "{:>1}", "S");
684 } else {
685 header.push_str(sep);
686 let _ = write!(header, "{:w$}", "USER", w = cols.user);
687 }
688 if self.show_oom {
689 header.push_str(sep);
690 let _ = write!(header, "{:>3}", "OOM");
691 }
692 if self.show_nice {
693 header.push_str(sep);
694 let _ = write!(header, "{:>3}", "NI");
695 }
696 if self.show_threads {
697 header.push_str(sep);
698 let _ = write!(header, "{:>3}", "TH");
699 }
700 header.push_str(sep);
701 let _ = write!(
702 header,
703 "{:>w$}",
704 if self.compact { "C%" } else { "CPU%" },
705 w = cols.cpu
706 );
707 header.push_str(sep);
708 let _ = write!(
709 header,
710 "{:>w$}",
711 if self.compact { "M%" } else { "MEM%" },
712 w = cols.mem
713 );
714 header.push_str(sep);
715 let _ = write!(header, "{:w$}", "COMMAND", w = cols.cmd);
716 header
717 }
718
719 #[allow(clippy::too_many_arguments)]
721 fn draw_row(
722 &self,
723 canvas: &mut dyn Canvas,
724 proc: &ProcessEntry,
725 y: f32,
726 is_selected: bool,
727 cols: &ColumnWidths,
728 default_style: &TextStyle,
729 ) {
730 let sep = if self.compact { 1.0 } else { 3.0 };
731 let mut x = self.bounds.x;
732 canvas.draw_text(
734 &format!("{:>w$}", proc.pid, w = cols.pid),
735 Point::new(x, y),
736 default_style,
737 );
738 x += cols.pid as f32;
739 if self.compact {
741 x += 1.0;
742 canvas.draw_text(
743 &proc.state.char().to_string(),
744 Point::new(x, y),
745 &TextStyle {
746 color: proc.state.color(),
747 ..Default::default()
748 },
749 );
750 x += 1.0;
751 } else {
752 x += sep;
753 canvas.draw_text(
754 &Self::truncate(&proc.user, cols.user),
755 Point::new(x, y),
756 default_style,
757 );
758 x += cols.user as f32;
759 }
760 if self.show_oom {
762 x += sep;
763 let oom = proc.oom_score.unwrap_or(0);
764 canvas.draw_text(
765 &format!("{oom:>3}"),
766 Point::new(x, y),
767 &TextStyle {
768 color: Self::oom_color(oom),
769 ..Default::default()
770 },
771 );
772 x += 3.0;
773 }
774 if self.show_nice {
776 x += sep;
777 let ni = proc.nice.unwrap_or(0);
778 canvas.draw_text(
779 &format!("{ni:>3}"),
780 Point::new(x, y),
781 &TextStyle {
782 color: Self::nice_color(ni),
783 ..Default::default()
784 },
785 );
786 x += 3.0;
787 }
788 if self.show_threads {
790 x += sep;
791 let th = proc.threads.unwrap_or(1);
792 canvas.draw_text(
793 &format!("{th:>3}"),
794 Point::new(x, y),
795 &TextStyle {
796 color: Self::threads_color(th),
797 ..Default::default()
798 },
799 );
800 x += 3.0;
801 }
802 x += sep;
804 canvas.draw_text(
805 &format!("{:>5.1}%", proc.cpu_percent),
806 Point::new(x, y),
807 &TextStyle {
808 color: self.cpu_gradient.for_percent(proc.cpu_percent as f64),
809 ..Default::default()
810 },
811 );
812 x += cols.cpu as f32;
813 x += sep;
815 canvas.draw_text(
816 &format!("{:>5.1}%", proc.mem_percent),
817 Point::new(x, y),
818 &TextStyle {
819 color: self.mem_gradient.for_percent(proc.mem_percent as f64),
820 ..Default::default()
821 },
822 );
823 x += cols.mem as f32;
824 x += sep;
826 self.draw_command(canvas, proc, x, y, is_selected, cols.cmd, default_style);
827 }
828
829 #[allow(clippy::too_many_arguments)]
831 fn draw_command(
832 &self,
833 canvas: &mut dyn Canvas,
834 proc: &ProcessEntry,
835 x: f32,
836 y: f32,
837 is_selected: bool,
838 cmd_w: usize,
839 default_style: &TextStyle,
840 ) {
841 let cmd = if self.show_cmdline {
842 proc.cmdline.as_deref().unwrap_or(&proc.command)
843 } else {
844 &proc.command
845 };
846 let cmd_style = if is_selected {
847 TextStyle {
848 color: Color::new(1.0, 1.0, 1.0, 1.0),
849 ..Default::default()
850 }
851 } else {
852 default_style.clone()
853 };
854 if self.tree_view && !proc.tree_prefix.is_empty() {
855 let prefix_len = proc.tree_prefix.chars().count();
856 canvas.draw_text(
857 &proc.tree_prefix,
858 Point::new(x, y),
859 &TextStyle {
860 color: Color::new(0.4, 0.5, 0.6, 1.0),
861 ..Default::default()
862 },
863 );
864 canvas.draw_text(
865 &Self::truncate(cmd, cmd_w.saturating_sub(prefix_len)),
866 Point::new(x + prefix_len as f32, y),
867 &cmd_style,
868 );
869 } else {
870 canvas.draw_text(&Self::truncate(cmd, cmd_w), Point::new(x, y), &cmd_style);
871 }
872 }
873}
874
875#[allow(dead_code)]
877struct ColumnWidths {
878 pid: usize,
879 state: usize,
880 oom: usize,
881 nice: usize,
882 threads: usize,
883 user: usize,
884 cpu: usize,
885 mem: usize,
886 sep: usize,
887 cmd: usize,
888 num_seps: usize,
889}
890
891impl ColumnWidths {
892 fn new(table: &ProcessTable, width: usize) -> Self {
893 let pid = 7;
894 let state = if table.compact { 2 } else { 0 };
895 let oom = if table.show_oom { 4 } else { 0 };
896 let nice = if table.show_nice { 4 } else { 0 };
897 let threads = if table.show_threads { 4 } else { 0 };
898 let user = if table.compact { 0 } else { 8 };
899 let cpu = 6;
900 let mem = 6;
901 let sep = if table.compact { 1 } else { 3 };
902 let extra_cols = usize::from(table.show_oom)
903 + usize::from(table.show_nice)
904 + usize::from(table.show_threads);
905 let num_seps = if table.compact { 3 } else { 4 } + extra_cols;
906 let fixed = pid + state + oom + nice + threads + user + cpu + mem + sep * num_seps;
907 let cmd = width.saturating_sub(fixed);
908 Self {
909 pid,
910 state,
911 oom,
912 nice,
913 threads,
914 user,
915 cpu,
916 mem,
917 sep,
918 cmd,
919 num_seps,
920 }
921 }
922}
923
924impl Brick for ProcessTable {
925 fn brick_name(&self) -> &'static str {
926 "process_table"
927 }
928
929 fn assertions(&self) -> &[BrickAssertion] {
930 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
931 ASSERTIONS
932 }
933
934 fn budget(&self) -> BrickBudget {
935 BrickBudget::uniform(16)
936 }
937
938 fn verify(&self) -> BrickVerification {
939 let passed = if self.processes.is_empty() || self.selected < self.processes.len() {
940 vec![BrickAssertion::max_latency_ms(16)]
941 } else {
942 vec![]
943 };
944 let failed = if !self.processes.is_empty() && self.selected >= self.processes.len() {
945 vec![(
946 BrickAssertion::max_latency_ms(16),
947 format!(
948 "Selected {} >= process count {}",
949 self.selected,
950 self.processes.len()
951 ),
952 )]
953 } else {
954 vec![]
955 };
956
957 BrickVerification {
958 passed,
959 failed,
960 verification_time: Duration::from_micros(10),
961 }
962 }
963
964 fn to_html(&self) -> String {
965 String::new()
966 }
967
968 fn to_css(&self) -> String {
969 String::new()
970 }
971}
972
973impl Widget for ProcessTable {
974 fn type_id(&self) -> TypeId {
975 TypeId::of::<Self>()
976 }
977
978 fn measure(&self, constraints: Constraints) -> Size {
979 let min_width = if self.compact { 40.0 } else { 60.0 };
980 let width = constraints.max_width.max(min_width);
981 let height = (self.processes.len() + 2).min(30) as f32;
982 constraints.constrain(Size::new(width, height.max(3.0)))
983 }
984
985 fn layout(&mut self, bounds: Rect) -> LayoutResult {
986 self.bounds = bounds;
987 self.ensure_visible();
988 LayoutResult {
989 size: Size::new(bounds.width, bounds.height),
990 }
991 }
992
993 fn paint(&self, canvas: &mut dyn Canvas) {
994 let width = self.bounds.width as usize;
995 let height = self.bounds.height as usize;
996 if width == 0 || height == 0 {
997 return;
998 }
999
1000 let cols = ColumnWidths::new(self, width);
1001
1002 let header_style = TextStyle {
1004 color: Color::new(0.0, 1.0, 1.0, 1.0),
1005 weight: presentar_core::FontWeight::Bold,
1006 ..Default::default()
1007 };
1008 canvas.draw_text(
1009 &self.build_header(&cols),
1010 Point::new(self.bounds.x, self.bounds.y),
1011 &header_style,
1012 );
1013
1014 if height > 1 {
1016 canvas.draw_text(
1017 &"─".repeat(width),
1018 Point::new(self.bounds.x, self.bounds.y + 1.0),
1019 &TextStyle {
1020 color: Color::new(0.3, 0.3, 0.4, 1.0),
1021 ..Default::default()
1022 },
1023 );
1024 }
1025
1026 let default_style = TextStyle {
1028 color: Color::new(0.8, 0.8, 0.8, 1.0),
1029 ..Default::default()
1030 };
1031 let visible_rows = height.saturating_sub(2);
1032 for (i, proc_idx) in (self.scroll_offset..self.processes.len())
1033 .take(visible_rows)
1034 .enumerate()
1035 {
1036 let proc = &self.processes[proc_idx];
1037 let y = self.bounds.y + 2.0 + i as f32;
1038 let is_selected = proc_idx == self.selected;
1039 if is_selected {
1040 canvas.fill_rect(
1041 Rect::new(self.bounds.x, y, self.bounds.width, 1.0),
1042 Color::new(0.2, 0.2, 0.4, 0.5),
1043 );
1044 }
1045 self.draw_row(canvas, proc, y, is_selected, &cols, &default_style);
1046 }
1047
1048 if self.processes.is_empty() && height > 2 {
1050 canvas.draw_text(
1051 "No processes",
1052 Point::new(self.bounds.x + 1.0, self.bounds.y + 2.0),
1053 &TextStyle {
1054 color: Color::new(0.5, 0.5, 0.5, 1.0),
1055 ..Default::default()
1056 },
1057 );
1058 }
1059 }
1060
1061 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
1062 match event {
1063 Event::KeyDown { key } => {
1064 match key {
1065 Key::Up | Key::K => self.select_prev(),
1066 Key::Down | Key::J => self.select_next(),
1067 Key::C => self.sort_by(ProcessSort::Cpu),
1068 Key::M => self.sort_by(ProcessSort::Memory),
1069 Key::P => self.sort_by(ProcessSort::Pid),
1070 Key::N => self.sort_by(ProcessSort::Command),
1071 Key::O => self.sort_by(ProcessSort::Oom),
1072 Key::T => self.toggle_tree_view(), _ => {}
1074 }
1075 None
1076 }
1077 _ => None,
1078 }
1079 }
1080
1081 fn children(&self) -> &[Box<dyn Widget>] {
1082 &[]
1083 }
1084
1085 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
1086 &mut []
1087 }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::*;
1093
1094 fn sample_processes() -> Vec<ProcessEntry> {
1095 vec![
1096 ProcessEntry::new(1, "root", 0.5, 0.1, "systemd"),
1097 ProcessEntry::new(1234, "noah", 25.0, 5.5, "firefox"),
1098 ProcessEntry::new(5678, "noah", 80.0, 12.3, "rustc"),
1099 ]
1100 }
1101
1102 #[test]
1103 fn test_process_table_new() {
1104 let table = ProcessTable::new();
1105 assert!(table.is_empty());
1106 }
1107
1108 #[test]
1109 fn test_process_table_set_processes() {
1110 let mut table = ProcessTable::new();
1111 table.set_processes(sample_processes());
1112 assert_eq!(table.len(), 3);
1113 }
1114
1115 #[test]
1116 fn test_process_table_add_process() {
1117 let mut table = ProcessTable::new();
1118 table.add_process(ProcessEntry::new(1, "root", 0.0, 0.0, "init"));
1119 assert_eq!(table.len(), 1);
1120 }
1121
1122 #[test]
1123 fn test_process_table_clear() {
1124 let mut table = ProcessTable::new();
1125 table.set_processes(sample_processes());
1126 table.select(1);
1127 table.clear();
1128 assert!(table.is_empty());
1129 assert_eq!(table.selected(), 0);
1130 }
1131
1132 #[test]
1133 fn test_process_table_selection() {
1134 let mut table = ProcessTable::new();
1135 table.set_processes(sample_processes());
1136 assert_eq!(table.selected(), 0);
1137
1138 table.select_next();
1139 assert_eq!(table.selected(), 1);
1140
1141 table.select_prev();
1142 assert_eq!(table.selected(), 0);
1143 }
1144
1145 #[test]
1146 fn test_process_table_select_bounds() {
1147 let mut table = ProcessTable::new();
1148 table.set_processes(sample_processes());
1149
1150 table.select(100);
1151 assert_eq!(table.selected(), 2);
1152
1153 table.select_prev();
1154 table.select_prev();
1155 table.select_prev();
1156 assert_eq!(table.selected(), 0);
1157 }
1158
1159 #[test]
1160 fn test_process_table_sort_cpu() {
1161 let mut table = ProcessTable::new();
1162 table.set_processes(sample_processes());
1163 assert_eq!(table.processes[0].command, "rustc");
1165 }
1166
1167 #[test]
1168 fn test_process_table_sort_toggle() {
1169 let mut table = ProcessTable::new();
1170 table.set_processes(sample_processes());
1171 table.sort_by(ProcessSort::Cpu); assert_eq!(table.processes[0].command, "systemd");
1173 }
1174
1175 #[test]
1176 fn test_process_table_sort_pid() {
1177 let mut table = ProcessTable::new();
1178 table.set_processes(sample_processes());
1179 table.sort_by(ProcessSort::Pid);
1180 assert_eq!(table.processes[0].pid, 1);
1181 }
1182
1183 #[test]
1184 fn test_process_table_sort_memory() {
1185 let mut table = ProcessTable::new();
1186 table.set_processes(sample_processes());
1187 table.sort_by(ProcessSort::Memory);
1188 assert_eq!(table.processes[0].command, "rustc");
1189 }
1190
1191 #[test]
1192 fn test_process_table_selected_process() {
1193 let mut table = ProcessTable::new();
1194 table.set_processes(sample_processes());
1195 let proc = table.selected_process().unwrap();
1196 assert_eq!(proc.command, "rustc"); }
1198
1199 #[test]
1200 fn test_process_entry_with_cmdline() {
1201 let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "bash").with_cmdline("/bin/bash --login");
1202 assert_eq!(proc.cmdline.as_deref(), Some("/bin/bash --login"));
1203 }
1204
1205 #[test]
1206 fn test_process_table_compact() {
1207 let table = ProcessTable::new().compact();
1208 assert!(table.compact);
1209 }
1210
1211 #[test]
1212 fn test_process_table_with_cmdline() {
1213 let table = ProcessTable::new().with_cmdline();
1214 assert!(table.show_cmdline);
1215 }
1216
1217 #[test]
1218 fn test_process_table_verify() {
1219 let mut table = ProcessTable::new();
1220 table.set_processes(sample_processes());
1221 assert!(table.verify().is_valid());
1222 }
1223
1224 #[test]
1225 fn test_process_table_verify_invalid() {
1226 let mut table = ProcessTable::new();
1227 table.set_processes(sample_processes());
1228 table.selected = 100;
1229 assert!(!table.verify().is_valid());
1230 }
1231
1232 #[test]
1233 fn test_process_table_measure() {
1234 let mut table = ProcessTable::new();
1235 table.set_processes(sample_processes());
1236 let size = table.measure(Constraints::new(0.0, 100.0, 0.0, 50.0));
1237 assert!(size.width >= 60.0);
1238 assert!(size.height >= 3.0);
1239 }
1240
1241 #[test]
1242 fn test_process_table_layout() {
1243 let mut table = ProcessTable::new();
1244 table.set_processes(sample_processes());
1245 let result = table.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
1246 assert_eq!(result.size.width, 80.0);
1247 }
1248
1249 #[test]
1250 fn test_process_table_event_keys() {
1251 let mut table = ProcessTable::new();
1252 table.set_processes(sample_processes());
1253
1254 table.event(&Event::KeyDown { key: Key::J });
1255 assert_eq!(table.selected(), 1);
1256
1257 table.event(&Event::KeyDown { key: Key::K });
1258 assert_eq!(table.selected(), 0);
1259
1260 table.event(&Event::KeyDown { key: Key::P });
1261 assert_eq!(table.current_sort(), ProcessSort::Pid);
1262 }
1263
1264 #[test]
1265 fn test_process_table_brick_name() {
1266 let table = ProcessTable::new();
1267 assert_eq!(table.brick_name(), "process_table");
1268 }
1269
1270 #[test]
1271 fn test_process_table_default() {
1272 let table = ProcessTable::default();
1273 assert!(table.is_empty());
1274 }
1275
1276 #[test]
1277 fn test_process_table_children() {
1278 let table = ProcessTable::new();
1279 assert!(table.children().is_empty());
1280 }
1281
1282 #[test]
1283 fn test_process_table_children_mut() {
1284 let mut table = ProcessTable::new();
1285 assert!(table.children_mut().is_empty());
1286 }
1287
1288 #[test]
1289 fn test_process_table_truncate() {
1290 assert_eq!(ProcessTable::truncate("hello", 10), "hello ");
1291 assert_eq!(ProcessTable::truncate("hello world", 8), "hello w…");
1292 assert_eq!(ProcessTable::truncate("hi", 2), "hi");
1293 assert!(ProcessTable::truncate("long text here", 6).ends_with('…'));
1295 }
1296
1297 #[test]
1298 fn test_process_table_type_id() {
1299 let table = ProcessTable::new();
1300 assert_eq!(Widget::type_id(&table), TypeId::of::<ProcessTable>());
1301 }
1302
1303 #[test]
1304 fn test_process_table_to_html() {
1305 let table = ProcessTable::new();
1306 assert!(table.to_html().is_empty());
1307 }
1308
1309 #[test]
1310 fn test_process_table_to_css() {
1311 let table = ProcessTable::new();
1312 assert!(table.to_css().is_empty());
1313 }
1314
1315 #[test]
1316 fn test_process_table_sort_command() {
1317 let mut table = ProcessTable::new();
1318 table.set_processes(sample_processes());
1319 table.sort_by(ProcessSort::Command);
1320 assert_eq!(table.processes[0].command, "firefox");
1321 }
1322
1323 #[test]
1324 fn test_process_table_sort_user() {
1325 let mut table = ProcessTable::new();
1326 table.set_processes(sample_processes());
1327 table.sort_by(ProcessSort::User);
1328 assert_eq!(table.processes[0].user, "noah");
1329 }
1330
1331 #[test]
1332 fn test_process_table_sort_oom() {
1333 let mut table = ProcessTable::new();
1334 let entries = vec![
1336 ProcessEntry::new(1, "user", 10.0, 5.0, "low_oom").with_oom_score(100),
1337 ProcessEntry::new(2, "user", 10.0, 5.0, "high_oom").with_oom_score(800),
1338 ProcessEntry::new(3, "user", 10.0, 5.0, "med_oom").with_oom_score(400),
1339 ];
1340 table.set_processes(entries);
1341
1342 table.sort_by(ProcessSort::Oom);
1344
1345 assert_eq!(table.processes[0].command, "high_oom");
1347 assert_eq!(table.processes[1].command, "med_oom");
1348 assert_eq!(table.processes[2].command, "low_oom");
1349 }
1350
1351 #[test]
1352 fn test_process_table_sort_oom_toggle_ascending() {
1353 let mut table = ProcessTable::new();
1354 let entries = vec![
1355 ProcessEntry::new(1, "user", 10.0, 5.0, "low_oom").with_oom_score(100),
1356 ProcessEntry::new(2, "user", 10.0, 5.0, "high_oom").with_oom_score(800),
1357 ];
1358 table.set_processes(entries);
1359
1360 table.sort_by(ProcessSort::Oom);
1362 table.sort_by(ProcessSort::Oom);
1363
1364 assert_eq!(table.processes[0].command, "low_oom");
1366 assert_eq!(table.processes[1].command, "high_oom");
1367 }
1368
1369 struct MockCanvas {
1374 texts: Vec<(String, Point)>,
1375 rects: Vec<Rect>,
1376 }
1377
1378 impl MockCanvas {
1379 fn new() -> Self {
1380 Self {
1381 texts: vec![],
1382 rects: vec![],
1383 }
1384 }
1385 }
1386
1387 impl Canvas for MockCanvas {
1388 fn fill_rect(&mut self, rect: Rect, _color: Color) {
1389 self.rects.push(rect);
1390 }
1391 fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
1392 fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
1393 self.texts.push((text.to_string(), position));
1394 }
1395 fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
1396 fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
1397 fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
1398 fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
1399 fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
1400 fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
1401 fn push_clip(&mut self, _rect: Rect) {}
1402 fn pop_clip(&mut self) {}
1403 fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
1404 fn pop_transform(&mut self) {}
1405 }
1406
1407 #[test]
1408 fn test_process_table_paint_basic() {
1409 let mut table = ProcessTable::new();
1410 table.set_processes(sample_processes());
1411 table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1412
1413 let mut canvas = MockCanvas::new();
1414 table.paint(&mut canvas);
1415
1416 assert!(!canvas.texts.is_empty());
1418 assert!(canvas.texts.iter().any(|(t, _)| t.contains("PID")));
1420 }
1421
1422 #[test]
1423 fn test_process_table_paint_compact() {
1424 let mut table = ProcessTable::new().compact();
1425 table.set_processes(sample_processes());
1426 table.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
1427
1428 let mut canvas = MockCanvas::new();
1429 table.paint(&mut canvas);
1430
1431 assert!(canvas.texts.iter().any(|(t, _)| t.contains("C%")));
1433 assert!(canvas.texts.iter().any(|(t, _)| t.contains("M%")));
1434 }
1435
1436 #[test]
1437 fn test_process_table_paint_with_oom() {
1438 let mut table = ProcessTable::new().with_oom();
1439 let entries = vec![
1440 ProcessEntry::new(1, "user", 10.0, 5.0, "low_oom").with_oom_score(100),
1441 ProcessEntry::new(2, "user", 10.0, 5.0, "high_oom").with_oom_score(800),
1442 ProcessEntry::new(3, "user", 10.0, 5.0, "med_oom").with_oom_score(400),
1443 ];
1444 table.set_processes(entries);
1445 table.bounds = Rect::new(0.0, 0.0, 100.0, 20.0);
1446
1447 let mut canvas = MockCanvas::new();
1448 table.paint(&mut canvas);
1449
1450 assert!(canvas.texts.iter().any(|(t, _)| t.contains("OOM")));
1452 assert!(canvas.texts.iter().any(|(t, _)| t.contains("100")));
1454 assert!(canvas.texts.iter().any(|(t, _)| t.contains("800")));
1455 }
1456
1457 #[test]
1458 fn test_process_table_paint_with_nice() {
1459 let mut table = ProcessTable::new().with_nice_column();
1460 let entries = vec![
1461 ProcessEntry::new(1, "user", 10.0, 5.0, "high_pri").with_nice(-10),
1462 ProcessEntry::new(2, "user", 10.0, 5.0, "low_pri").with_nice(10),
1463 ProcessEntry::new(3, "user", 10.0, 5.0, "normal").with_nice(0),
1464 ];
1465 table.set_processes(entries);
1466 table.bounds = Rect::new(0.0, 0.0, 100.0, 20.0);
1467
1468 let mut canvas = MockCanvas::new();
1469 table.paint(&mut canvas);
1470
1471 assert!(canvas.texts.iter().any(|(t, _)| t.contains("NI")));
1473 }
1474
1475 #[test]
1476 fn test_process_table_paint_with_selection() {
1477 let mut table = ProcessTable::new();
1478 table.set_processes(sample_processes());
1479 table.select(1);
1480 table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1481
1482 let mut canvas = MockCanvas::new();
1483 table.paint(&mut canvas);
1484
1485 assert!(!canvas.rects.is_empty());
1487 }
1488
1489 #[test]
1490 fn test_process_table_paint_empty() {
1491 let mut table = ProcessTable::new();
1492 table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
1493
1494 let mut canvas = MockCanvas::new();
1495 table.paint(&mut canvas);
1496
1497 assert!(canvas.texts.iter().any(|(t, _)| t.contains("No processes")));
1499 }
1500
1501 #[test]
1502 fn test_process_table_paint_zero_bounds() {
1503 let mut table = ProcessTable::new();
1504 table.set_processes(sample_processes());
1505 table.bounds = Rect::new(0.0, 0.0, 0.0, 0.0);
1506
1507 let mut canvas = MockCanvas::new();
1508 table.paint(&mut canvas);
1509
1510 assert!(canvas.texts.is_empty());
1512 }
1513
1514 #[test]
1515 fn test_process_table_paint_with_cmdline() {
1516 let mut table = ProcessTable::new().with_cmdline();
1517 let entries = vec![
1518 ProcessEntry::new(1, "root", 0.5, 0.1, "bash").with_cmdline("/bin/bash --login -i")
1519 ];
1520 table.set_processes(entries);
1521 table.bounds = Rect::new(0.0, 0.0, 100.0, 20.0);
1522
1523 let mut canvas = MockCanvas::new();
1524 table.paint(&mut canvas);
1525
1526 assert!(canvas
1528 .texts
1529 .iter()
1530 .any(|(t, _)| t.contains("/bin/bash") || t.contains("--login")));
1531 }
1532
1533 #[test]
1534 fn test_process_table_paint_compact_with_state() {
1535 let mut table = ProcessTable::new().compact();
1536 let entries = vec![
1537 ProcessEntry::new(1, "root", 50.0, 10.0, "running").with_state(ProcessState::Running),
1538 ProcessEntry::new(2, "user", 0.0, 0.5, "sleeping").with_state(ProcessState::Sleeping),
1539 ProcessEntry::new(3, "user", 0.0, 0.1, "zombie").with_state(ProcessState::Zombie),
1540 ];
1541 table.set_processes(entries);
1542 table.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
1543
1544 let mut canvas = MockCanvas::new();
1545 table.paint(&mut canvas);
1546
1547 assert!(canvas.texts.iter().any(|(t, _)| t == "R")); assert!(canvas.texts.iter().any(|(t, _)| t == "S")); }
1551
1552 #[test]
1553 fn test_process_state_char() {
1554 assert_eq!(ProcessState::Running.char(), 'R');
1555 assert_eq!(ProcessState::Sleeping.char(), 'S');
1556 assert_eq!(ProcessState::DiskWait.char(), 'D');
1557 assert_eq!(ProcessState::Zombie.char(), 'Z');
1558 assert_eq!(ProcessState::Stopped.char(), 'T');
1559 assert_eq!(ProcessState::Idle.char(), 'I');
1560 }
1561
1562 #[test]
1563 fn test_process_state_color() {
1564 let running = ProcessState::Running.color();
1566 let sleeping = ProcessState::Sleeping.color();
1567 let zombie = ProcessState::Zombie.color();
1568 assert_ne!(running, sleeping);
1569 assert_ne!(running, zombie);
1570 }
1571
1572 #[test]
1573 fn test_process_state_default() {
1574 assert_eq!(ProcessState::default(), ProcessState::Sleeping);
1575 }
1576
1577 #[test]
1578 fn test_process_entry_with_state() {
1579 let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_state(ProcessState::Running);
1580 assert_eq!(proc.state, ProcessState::Running);
1581 }
1582
1583 #[test]
1584 fn test_process_entry_with_cgroup() {
1585 let proc =
1586 ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_cgroup("/user.slice/user-1000");
1587 assert_eq!(proc.cgroup.as_deref(), Some("/user.slice/user-1000"));
1588 }
1589
1590 #[test]
1591 fn test_process_entry_with_nice() {
1592 let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_nice(-5);
1593 assert_eq!(proc.nice, Some(-5));
1594 }
1595
1596 #[test]
1597 fn test_process_entry_with_oom_score() {
1598 let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_oom_score(500);
1599 assert_eq!(proc.oom_score, Some(500));
1600 }
1601
1602 #[test]
1603 fn test_process_entry_with_threads() {
1604 let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_threads(42);
1605 assert_eq!(proc.threads, Some(42));
1606 }
1607
1608 #[test]
1609 fn test_process_table_with_threads_column() {
1610 let table = ProcessTable::new().with_threads_column();
1611 assert!(table.show_threads);
1612 }
1613
1614 #[test]
1615 fn test_process_table_scroll() {
1616 let mut table = ProcessTable::new();
1617 let entries: Vec<ProcessEntry> = (0..50)
1619 .map(|i| ProcessEntry::new(i, "user", i as f32, 0.0, format!("proc{i}")))
1620 .collect();
1621 table.set_processes(entries);
1622 table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0); table.layout(table.bounds);
1624
1625 table.select(45);
1627 assert!(table.scroll_offset > 0);
1629 }
1630
1631 #[test]
1632 fn test_process_table_ensure_visible_up() {
1633 let mut table = ProcessTable::new();
1634 let entries: Vec<ProcessEntry> = (0..20)
1635 .map(|i| ProcessEntry::new(i, "user", 0.0, 0.0, format!("proc{i}")))
1636 .collect();
1637 table.set_processes(entries);
1638 table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1639 table.scroll_offset = 10;
1640 table.selected = 5; table.ensure_visible();
1643 assert!(table.scroll_offset <= table.selected);
1644 }
1645
1646 #[test]
1647 fn test_process_table_select_empty() {
1648 let mut table = ProcessTable::new();
1649 table.select(5);
1651 table.select_next();
1652 table.select_prev();
1653 assert_eq!(table.selected(), 0);
1654 }
1655
1656 #[test]
1657 fn test_process_table_selected_process_empty() {
1658 let table = ProcessTable::new();
1659 assert!(table.selected_process().is_none());
1660 }
1661
1662 #[test]
1663 fn test_process_table_budget() {
1664 let table = ProcessTable::new();
1665 let budget = table.budget();
1666 assert!(budget.paint_ms > 0);
1667 }
1668
1669 #[test]
1670 fn test_process_table_assertions() {
1671 let table = ProcessTable::new();
1672 assert!(!table.assertions().is_empty());
1673 }
1674
1675 #[test]
1676 fn test_process_table_set_processes_clamp_selection() {
1677 let mut table = ProcessTable::new();
1678 table.set_processes(sample_processes());
1679 table.selected = 2; table.set_processes(vec![ProcessEntry::new(1, "root", 0.0, 0.0, "test")]);
1682 assert_eq!(table.selected(), 0);
1684 }
1685
1686 #[test]
1687 fn test_process_table_event_down() {
1688 let mut table = ProcessTable::new();
1689 table.set_processes(sample_processes());
1690
1691 table.event(&Event::KeyDown { key: Key::Down });
1692 assert_eq!(table.selected(), 1);
1693 }
1694
1695 #[test]
1696 fn test_process_table_event_up() {
1697 let mut table = ProcessTable::new();
1698 table.set_processes(sample_processes());
1699 table.select(2);
1700
1701 table.event(&Event::KeyDown { key: Key::Up });
1702 assert_eq!(table.selected(), 1);
1703 }
1704
1705 #[test]
1706 fn test_process_table_event_c() {
1707 let mut table = ProcessTable::new();
1708 table.set_processes(sample_processes());
1709 table.sort_by(ProcessSort::Pid);
1711
1712 table.event(&Event::KeyDown { key: Key::C });
1713 assert_eq!(table.current_sort(), ProcessSort::Cpu);
1714 }
1715
1716 #[test]
1717 fn test_process_table_event_m() {
1718 let mut table = ProcessTable::new();
1719 table.set_processes(sample_processes());
1720
1721 table.event(&Event::KeyDown { key: Key::M });
1722 assert_eq!(table.current_sort(), ProcessSort::Memory);
1723 }
1724
1725 #[test]
1726 fn test_process_table_event_n() {
1727 let mut table = ProcessTable::new();
1728 table.set_processes(sample_processes());
1729
1730 table.event(&Event::KeyDown { key: Key::N });
1731 assert_eq!(table.current_sort(), ProcessSort::Command);
1732 }
1733
1734 #[test]
1735 fn test_process_table_event_o() {
1736 let mut table = ProcessTable::new();
1737 table.set_processes(sample_processes());
1738
1739 table.event(&Event::KeyDown { key: Key::O });
1740 assert_eq!(table.current_sort(), ProcessSort::Oom);
1741 }
1742
1743 #[test]
1744 fn test_process_table_event_other() {
1745 let mut table = ProcessTable::new();
1746 table.set_processes(sample_processes());
1747 let prev_selected = table.selected();
1748
1749 table.event(&Event::KeyDown { key: Key::A });
1751 assert_eq!(table.selected(), prev_selected);
1752 }
1753
1754 #[test]
1755 fn test_process_table_event_non_keydown() {
1756 let mut table = ProcessTable::new();
1757 table.set_processes(sample_processes());
1758
1759 let result = table.event(&Event::Resize {
1761 width: 100.0,
1762 height: 50.0,
1763 });
1764 assert!(result.is_none());
1765 }
1766
1767 #[test]
1768 fn test_process_table_with_cpu_gradient() {
1769 let gradient = Gradient::from_hex(&["#0000FF", "#FF0000"]);
1770 let table = ProcessTable::new().with_cpu_gradient(gradient);
1771 assert!(!table.is_empty() || table.is_empty());
1773 }
1774
1775 #[test]
1776 fn test_process_table_with_mem_gradient() {
1777 let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
1778 let table = ProcessTable::new().with_mem_gradient(gradient);
1779 assert!(!table.is_empty() || table.is_empty());
1780 }
1781
1782 #[test]
1783 fn test_process_table_measure_compact() {
1784 let table = ProcessTable::new().compact();
1785 let size = table.measure(Constraints::new(0.0, 100.0, 0.0, 50.0));
1786 assert!(size.width >= 40.0); }
1788
1789 #[test]
1790 fn test_process_table_truncate_exact() {
1791 assert_eq!(ProcessTable::truncate("exact", 5), "exact");
1792 }
1793
1794 #[test]
1795 fn test_process_table_truncate_width_1() {
1796 assert_eq!(ProcessTable::truncate("hello", 1), "h");
1797 }
1798
1799 #[test]
1800 fn test_process_table_paint_all_columns() {
1801 let mut table = ProcessTable::new()
1803 .compact()
1804 .with_oom()
1805 .with_nice_column()
1806 .with_cmdline();
1807
1808 let entries = vec![
1809 ProcessEntry::new(1, "root", 50.0, 10.0, "bash")
1810 .with_state(ProcessState::Running)
1811 .with_oom_score(100)
1812 .with_nice(-5)
1813 .with_cmdline("/bin/bash"),
1814 ProcessEntry::new(2, "user", 30.0, 5.0, "vim")
1815 .with_state(ProcessState::Sleeping)
1816 .with_oom_score(600)
1817 .with_nice(10)
1818 .with_cmdline("/usr/bin/vim"),
1819 ];
1820 table.set_processes(entries);
1821 table.bounds = Rect::new(0.0, 0.0, 120.0, 20.0);
1822
1823 let mut canvas = MockCanvas::new();
1824 table.paint(&mut canvas);
1825
1826 assert!(canvas.texts.iter().any(|(t, _)| t.contains("PID")));
1828 assert!(canvas.texts.iter().any(|(t, _)| t.contains("OOM")));
1829 assert!(canvas.texts.iter().any(|(t, _)| t.contains("NI")));
1830 }
1831
1832 #[test]
1833 fn test_process_entry_clone() {
1834 let proc = ProcessEntry::new(1, "root", 50.0, 10.0, "test")
1835 .with_state(ProcessState::Running)
1836 .with_oom_score(100);
1837 let cloned = proc.clone();
1838 assert_eq!(cloned.pid, proc.pid);
1839 assert_eq!(cloned.state, proc.state);
1840 }
1841
1842 #[test]
1843 fn test_process_entry_debug() {
1844 let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test");
1845 let debug = format!("{:?}", proc);
1846 assert!(debug.contains("ProcessEntry"));
1847 }
1848
1849 #[test]
1850 fn test_process_sort_debug() {
1851 let sort = ProcessSort::Cpu;
1852 let debug = format!("{:?}", sort);
1853 assert!(debug.contains("Cpu"));
1854 }
1855
1856 #[test]
1857 fn test_process_table_clone() {
1858 let mut table = ProcessTable::new();
1859 table.set_processes(sample_processes());
1860 let cloned = table.clone();
1861 assert_eq!(cloned.len(), table.len());
1862 }
1863
1864 #[test]
1865 fn test_process_table_debug() {
1866 let table = ProcessTable::new();
1867 let debug = format!("{:?}", table);
1868 assert!(debug.contains("ProcessTable"));
1869 }
1870
1871 #[test]
1872 fn test_process_state_debug() {
1873 let state = ProcessState::Running;
1874 let debug = format!("{:?}", state);
1875 assert!(debug.contains("Running"));
1876 }
1877
1878 #[test]
1883 fn test_process_entry_with_parent_pid() {
1884 let proc = ProcessEntry::new(100, "user", 10.0, 5.0, "child").with_parent_pid(1);
1885 assert_eq!(proc.parent_pid, Some(1));
1886 }
1887
1888 #[test]
1889 fn test_process_entry_set_tree_info() {
1890 let mut proc = ProcessEntry::new(100, "user", 10.0, 5.0, "child");
1891 proc.set_tree_info(2, true, "│ └─".to_string());
1892 assert_eq!(proc.tree_depth, 2);
1893 assert!(proc.is_last_child);
1894 assert_eq!(proc.tree_prefix, "│ └─");
1895 }
1896
1897 #[test]
1898 fn test_process_table_with_tree_view() {
1899 let table = ProcessTable::new().with_tree_view();
1900 assert!(table.is_tree_view());
1901 }
1902
1903 #[test]
1904 fn test_process_table_toggle_tree_view() {
1905 let mut table = ProcessTable::new();
1906 assert!(!table.is_tree_view());
1907
1908 table.toggle_tree_view();
1909 assert!(table.is_tree_view());
1910
1911 table.toggle_tree_view();
1912 assert!(!table.is_tree_view());
1913 }
1914
1915 #[test]
1916 fn test_process_table_tree_view_builds_tree() {
1917 let mut table = ProcessTable::new().with_tree_view();
1918
1919 let entries = vec![
1923 ProcessEntry::new(200, "user", 5.0, 1.0, "vim").with_parent_pid(100),
1924 ProcessEntry::new(100, "user", 10.0, 2.0, "bash").with_parent_pid(1),
1925 ProcessEntry::new(101, "root", 1.0, 0.5, "sshd").with_parent_pid(1),
1926 ProcessEntry::new(1, "root", 0.5, 0.1, "systemd"),
1927 ];
1928 table.set_processes(entries);
1929
1930 assert_eq!(table.processes[0].command, "systemd");
1932
1933 assert_eq!(table.processes[0].tree_prefix, "");
1936 }
1939
1940 #[test]
1941 fn test_process_table_tree_view_prefix_chars() {
1942 let mut table = ProcessTable::new().with_tree_view();
1943
1944 let entries = vec![
1946 ProcessEntry::new(3, "user", 5.0, 1.0, "grandchild").with_parent_pid(2),
1947 ProcessEntry::new(2, "user", 10.0, 2.0, "child").with_parent_pid(1),
1948 ProcessEntry::new(1, "root", 0.5, 0.1, "parent"),
1949 ];
1950 table.set_processes(entries);
1951
1952 assert_eq!(table.processes[0].tree_prefix, "");
1954 assert!(
1956 table.processes[1].tree_prefix.contains('└')
1957 || table.processes[1].tree_prefix.contains('├')
1958 );
1959 }
1960
1961 #[test]
1962 fn test_process_table_event_t_toggles_tree() {
1963 let mut table = ProcessTable::new();
1964 table.set_processes(sample_processes());
1965 assert!(!table.is_tree_view());
1966
1967 table.event(&Event::KeyDown { key: Key::T });
1968 assert!(table.is_tree_view());
1969
1970 table.event(&Event::KeyDown { key: Key::T });
1971 assert!(!table.is_tree_view());
1972 }
1973
1974 #[test]
1975 fn test_process_table_tree_view_paint() {
1976 let mut table = ProcessTable::new().with_tree_view();
1977
1978 let entries = vec![
1979 ProcessEntry::new(2, "user", 10.0, 2.0, "child").with_parent_pid(1),
1980 ProcessEntry::new(1, "root", 0.5, 0.1, "parent"),
1981 ];
1982 table.set_processes(entries);
1983 table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
1984
1985 let mut canvas = MockCanvas::new();
1986 table.paint(&mut canvas);
1987
1988 assert!(canvas
1990 .texts
1991 .iter()
1992 .any(|(t, _)| t.contains("└") || t.contains("├")));
1993 }
1994
1995 #[test]
1996 fn test_process_table_tree_empty() {
1997 let mut table = ProcessTable::new().with_tree_view();
1998 table.set_processes(vec![]);
2000 assert!(table.is_empty());
2001 }
2002
2003 #[test]
2010 fn test_f_tree_001_hierarchy_overrides_sorting() {
2011 let mut table = ProcessTable::new().with_tree_view();
2012
2013 let entries = vec![
2016 ProcessEntry::new(999, "root", 99.0, 50.0, "chrome"), ProcessEntry::new(200, "user", 0.1, 0.1, "sleep").with_parent_pid(100),
2018 ProcessEntry::new(201, "user", 0.1, 0.1, "sleep").with_parent_pid(100),
2019 ProcessEntry::new(100, "user", 1.0, 0.5, "sh"),
2020 ProcessEntry::new(1, "root", 0.5, 0.1, "systemd"),
2021 ];
2022 table.set_processes(entries);
2023
2024 let sh_idx = table
2026 .processes
2027 .iter()
2028 .position(|p| p.command == "sh")
2029 .expect("sh not found");
2030
2031 let sleep1_idx = table
2033 .processes
2034 .iter()
2035 .position(|p| p.command == "sleep" && p.pid == 200)
2036 .expect("sleep 200 not found");
2037 let sleep2_idx = table
2038 .processes
2039 .iter()
2040 .position(|p| p.command == "sleep" && p.pid == 201)
2041 .expect("sleep 201 not found");
2042
2043 assert!(
2045 sleep1_idx > sh_idx && sleep1_idx <= sh_idx + 2,
2046 "sleep 200 (idx {}) should be immediately after sh (idx {})",
2047 sleep1_idx,
2048 sh_idx
2049 );
2050 assert!(
2051 sleep2_idx > sh_idx && sleep2_idx <= sh_idx + 2,
2052 "sleep 201 (idx {}) should be immediately after sh (idx {})",
2053 sleep2_idx,
2054 sh_idx
2055 );
2056
2057 let chrome_idx = table
2059 .processes
2060 .iter()
2061 .position(|p| p.command == "chrome")
2062 .expect("chrome not found");
2063 assert!(
2064 !(chrome_idx > sh_idx && chrome_idx < sleep1_idx.max(sleep2_idx)),
2065 "Unrelated process should not split parent-child hierarchy"
2066 );
2067 }
2068
2069 #[test]
2072 fn test_f_tree_002_orphan_handling() {
2073 let mut table = ProcessTable::new().with_tree_view();
2074
2075 let entries = vec![
2077 ProcessEntry::new(200, "user", 5.0, 1.0, "orphan1").with_parent_pid(100), ProcessEntry::new(201, "user", 3.0, 1.0, "orphan2").with_parent_pid(100), ProcessEntry::new(1, "root", 0.5, 0.1, "systemd"),
2080 ];
2081 table.set_processes(entries);
2082
2083 assert_eq!(table.len(), 3);
2085
2086 let orphan1 = table
2088 .processes
2089 .iter()
2090 .find(|p| p.command == "orphan1")
2091 .unwrap();
2092 let orphan2 = table
2093 .processes
2094 .iter()
2095 .find(|p| p.command == "orphan2")
2096 .unwrap();
2097
2098 assert_eq!(orphan1.tree_depth, 0);
2100 assert_eq!(orphan2.tree_depth, 0);
2101 }
2102
2103 #[test]
2106 fn test_f_tree_003_deep_nesting_15_levels() {
2107 let mut table = ProcessTable::new().with_tree_view();
2108
2109 let mut entries = vec![ProcessEntry::new(1, "root", 0.5, 0.1, "init")];
2111
2112 for depth in 1..=15 {
2113 let pid = (depth + 1) as u32;
2114 let ppid = depth as u32;
2115 entries.push(
2116 ProcessEntry::new(pid, "user", 0.1, 0.1, format!("level{depth}"))
2117 .with_parent_pid(ppid),
2118 );
2119 }
2120
2121 table.set_processes(entries);
2122
2123 assert_eq!(table.len(), 16); let deepest = table
2128 .processes
2129 .iter()
2130 .find(|p| p.command == "level15")
2131 .unwrap();
2132 assert_eq!(deepest.tree_depth, 15);
2133
2134 let prefix_segments =
2136 deepest.tree_prefix.matches("│").count() + deepest.tree_prefix.matches(" ").count();
2137 assert!(
2139 deepest.tree_prefix.len() > 20,
2140 "Deep prefix should be substantial: '{}'",
2141 deepest.tree_prefix
2142 );
2143 }
2144
2145 #[test]
2148 fn test_f_tree_004_dfs_traversal_order() {
2149 let mut table = ProcessTable::new().with_tree_view();
2150
2151 let entries = vec![
2155 ProcessEntry::new(5, "user", 1.0, 1.0, "E").with_parent_pid(2),
2156 ProcessEntry::new(4, "user", 1.0, 1.0, "D").with_parent_pid(2),
2157 ProcessEntry::new(3, "user", 1.0, 1.0, "C").with_parent_pid(1),
2158 ProcessEntry::new(2, "user", 2.0, 1.0, "B").with_parent_pid(1), ProcessEntry::new(1, "root", 0.5, 0.1, "A"),
2160 ];
2161 table.set_processes(entries);
2162
2163 let commands: Vec<&str> = table.processes.iter().map(|p| p.command.as_str()).collect();
2166
2167 assert_eq!(commands[0], "A", "Root should be first");
2168 assert_eq!(commands[1], "B", "B (higher CPU child) should be second");
2169 assert!(
2171 commands[2] == "D" || commands[2] == "E",
2172 "B's children should follow B"
2173 );
2174 assert!(
2175 commands[3] == "D" || commands[3] == "E",
2176 "B's children should follow B"
2177 );
2178 assert_eq!(commands[4], "C", "C should be after B's subtree");
2179 }
2180}