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)]
1091#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
1092#[path = "process_table_tests.rs"]
1093mod tests;