diff --git a/.cargo-husky/hooks/pre-push b/.cargo-husky/hooks/pre-push index a1e3616f..50105aa1 100755 --- a/.cargo-husky/hooks/pre-push +++ b/.cargo-husky/hooks/pre-push @@ -1,5 +1,7 @@ #!/bin/sh +set -e + echo "Running pre-push hook:" echo "Executing: cargo +nightly clippy -- -D clippy::all" diff --git a/.vscode/settings.json b/.vscode/settings.json index 8653ef8a..9a9b4df4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,10 +46,13 @@ "fract", "gnueabihf", "gotop", + "gotop's", "gtop", "haase", "heim", "hjkl", + "htop", + "indexmap", "libc", "markdownlint", "memb", @@ -62,6 +65,7 @@ "nvme", "paren", "pmem", + "ppid", "prepush", "processthreadsapi", "regexes", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b3357bd..2fbcb362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#220](https://github.com/ClementTsang/bottom/pull/220): Add ability to hide specific temperature and disk entries via config. +- [#223](https://github.com/ClementTsang/bottom/pull/223): Add tree mode for processes. + ### Changes - [#213](https://github.com/ClementTsang/bottom/pull/213), [#214](https://github.com/ClementTsang/bottom/pull/214): Updated help descriptions, added auto-complete generation. diff --git a/Cargo.lock b/Cargo.lock index 20f2221f..7893cdbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,7 @@ dependencies = [ "fern", "futures", "heim", + "indexmap", "itertools", "lazy_static", "libc", @@ -534,6 +535,12 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c" +[[package]] +name = "hashbrown" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7" + [[package]] name = "heim" version = "0.0.10" @@ -723,6 +730,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e" +[[package]] +name = "indexmap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "iovec" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index d8a83071..d2ec0dd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ ctrlc = {version = "3.1", features = ["termination"]} clap = "2.33" dirs = "3.0.1" futures = "0.3.5" +indexmap = "1.6.0" itertools = "0.9.0" libc = "0.2" regex = "1.3" diff --git a/README.md b/README.md index e504ea58..aa38c6a0 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ A cross-platform graphical process/system monitor with a customizable interface - [Processes](#processes) - [Process searching](#process-searching) - [Process sorting](#process-sorting) + - [Tree mode](#tree-mode) - [Zoom](#zoom) - [Expanding](#expanding) - [Basic mode](#basic-mode) @@ -246,23 +247,24 @@ Run using `btm`. | `s, F6` | Open process sort widget | | `I` | Invert current sort | | `%` | Toggle between values and percentages for memory usage | +| `t`, `F5` | Toggle tree mode | #### Process search bindings -| | | -| ------------ | -------------------------------------------- | -| `Tab` | Toggle between searching by PID or name | -| `Esc` | Close the search widget (retains the filter) | -| `Ctrl-a` | Skip to the start of the search query | -| `Ctrl-e` | Skip to the end of the search query | -| `Ctrl-u` | Clear the current search query | -| `Backspace` | Delete the character behind the cursor | -| `Delete` | Delete the character at the cursor | -| `Alt-c`/`F1` | Toggle matching case | -| `Alt-w`/`F2` | Toggle matching the entire word | -| `Alt-r`/`F3` | Toggle using regex | -| `Left` | Move cursor left | -| `Right` | Move cursor right | +| | | +| ------------- | -------------------------------------------- | +| `Tab` | Toggle between searching by PID or name | +| `Esc` | Close the search widget (retains the filter) | +| `Ctrl-a` | Skip to the start of the search query | +| `Ctrl-e` | Skip to the end of the search query | +| `Ctrl-u` | Clear the current search query | +| `Backspace` | Delete the character behind the cursor | +| `Delete` | Delete the character at the cursor | +| `Alt-c`, `F1` | Toggle matching case | +| `Alt-w`, `F2` | Toggle matching the entire word | +| `Alt-r`, `F3` | Toggle using regex | +| `Left` | Move cursor left | +| `Right` | Move cursor right | ### Process sort bindings @@ -424,6 +426,23 @@ You can sort the processes list by any column you want by pressing `s` while on ![sorting](assets/sort.png) +#### Tree mode + +Use `t` or `F5` to toggle tree mode in a process widget. This is somewhat similar to htop's tree +mode. + +![Standard tree](assets/trees_1.png) + +Sorting works as well, but it is done per groups of siblings. For example, by CPU%: + +![Standard tree](assets/trees_2.png) + +You can also still filter processes. Branches that entirely do not match the query are pruned out, +but if a branch contains an element that does match the query, any non-matching elements will instead +just be greyed out, so the tree structure is still maintained: + +![Standard tree](assets/trees_3.png) + ### Zoom Using the `+`/`-` keys or the scroll wheel will move the current time intervals of the currently selected widget, and `=` to reset the zoom levels to the default. diff --git a/assets/trees_1.png b/assets/trees_1.png new file mode 100644 index 00000000..cfe7e6b2 Binary files /dev/null and b/assets/trees_1.png differ diff --git a/assets/trees_2.png b/assets/trees_2.png new file mode 100644 index 00000000..e6c1063c Binary files /dev/null and b/assets/trees_2.png differ diff --git a/assets/trees_3.png b/assets/trees_3.png new file mode 100644 index 00000000..741116b1 Binary files /dev/null and b/assets/trees_3.png differ diff --git a/src/app.rs b/src/app.rs index 1aa390f7..4d07f4d0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,6 +13,7 @@ pub use states::*; use crate::{ canvas, constants, utils::error::{BottomError, Result}, + Pid, }; pub mod data_farmer; @@ -67,7 +68,7 @@ pub struct App { pub dd_err: Option, #[builder(default, setter(skip))] - to_delete_process_list: Option<(String, Vec)>, + to_delete_process_list: Option<(String, Vec)>, #[builder(default = false, setter(skip))] pub is_frozen: bool, @@ -265,37 +266,40 @@ impl App { .proc_state .get_mut_widget_state(self.current_widget.widget_id) { - // Toggles process widget grouping state - proc_widget_state.is_grouped = !(proc_widget_state.is_grouped); + // Do NOT allow when in tree mode! + if !proc_widget_state.is_tree_mode { + // Toggles process widget grouping state + proc_widget_state.is_grouped = !(proc_widget_state.is_grouped); - // Forcefully switch off column if we were on it... - if (proc_widget_state.is_grouped - && proc_widget_state.process_sorting_type - == data_harvester::processes::ProcessSorting::Pid) - || (!proc_widget_state.is_grouped + // Forcefully switch off column if we were on it... + if (proc_widget_state.is_grouped && proc_widget_state.process_sorting_type - == data_harvester::processes::ProcessSorting::Count) - { - proc_widget_state.process_sorting_type = - data_harvester::processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group - proc_widget_state.process_sorting_reverse = true; + == data_harvester::processes::ProcessSorting::Pid) + || (!proc_widget_state.is_grouped + && proc_widget_state.process_sorting_type + == data_harvester::processes::ProcessSorting::Count) + { + proc_widget_state.process_sorting_type = + data_harvester::processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group + proc_widget_state.is_process_sort_descending = true; + } + + proc_widget_state + .columns + .column_mapping + .get_mut(&processes::ProcessSorting::State) + .unwrap() + .enabled = !(proc_widget_state.is_grouped); + + proc_widget_state + .columns + .toggle(&processes::ProcessSorting::Count); + proc_widget_state + .columns + .toggle(&processes::ProcessSorting::Pid); + + self.proc_state.force_update = Some(self.current_widget.widget_id); } - - proc_widget_state - .columns - .column_mapping - .get_mut(&processes::ProcessSorting::State) - .unwrap() - .enabled = !(proc_widget_state.is_grouped); - - proc_widget_state - .columns - .toggle(&processes::ProcessSorting::Count); - proc_widget_state - .columns - .toggle(&processes::ProcessSorting::Pid); - - self.proc_state.force_update = Some(self.current_widget.widget_id); } } _ => {} @@ -384,8 +388,8 @@ impl App { }; if let Some(proc_widget_state) = self.proc_state.get_mut_widget_state(widget_id) { - proc_widget_state.process_sorting_reverse = - !proc_widget_state.process_sorting_reverse; + proc_widget_state.is_process_sort_descending = + !proc_widget_state.is_process_sort_descending; self.proc_state.force_update = Some(widget_id); } @@ -483,6 +487,24 @@ impl App { } } + pub fn toggle_tree_mode(&mut self) { + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get_mut(&(self.current_widget.widget_id)) + { + proc_widget_state.is_tree_mode = !proc_widget_state.is_tree_mode; + + if proc_widget_state.is_tree_mode { + // We enabled... set PID sort type to ascending. + proc_widget_state.process_sorting_type = processes::ProcessSorting::Pid; + proc_widget_state.is_process_sort_descending = false; + } + + self.proc_state.force_update = Some(self.current_widget.widget_id); + } + } + /// One of two functions allowed to run while in a dialog... pub fn on_enter(&mut self) { if self.delete_dialog_state.is_showing_dd { @@ -889,7 +911,7 @@ impl App { if proc_widget_state.scroll_state.current_scroll_position < corresponding_filtered_process_list.len() { - let current_process: (String, Vec); + let current_process: (String, Vec); if self.is_grouped(self.current_widget.widget_id) { if let Some(process) = &corresponding_filtered_process_list .get(proc_widget_state.scroll_state.current_scroll_position) @@ -1069,13 +1091,13 @@ impl App { { match proc_widget_state.process_sorting_type { processes::ProcessSorting::CpuPercent => { - proc_widget_state.process_sorting_reverse = - !proc_widget_state.process_sorting_reverse + proc_widget_state.is_process_sort_descending = + !proc_widget_state.is_process_sort_descending } _ => { proc_widget_state.process_sorting_type = processes::ProcessSorting::CpuPercent; - proc_widget_state.process_sorting_reverse = true; + proc_widget_state.is_process_sort_descending = true; } } self.proc_state.force_update = Some(self.current_widget.widget_id); @@ -1092,13 +1114,13 @@ impl App { { match proc_widget_state.process_sorting_type { processes::ProcessSorting::MemPercent => { - proc_widget_state.process_sorting_reverse = - !proc_widget_state.process_sorting_reverse + proc_widget_state.is_process_sort_descending = + !proc_widget_state.is_process_sort_descending } _ => { proc_widget_state.process_sorting_type = processes::ProcessSorting::MemPercent; - proc_widget_state.process_sorting_reverse = true; + proc_widget_state.is_process_sort_descending = true; } } self.proc_state.force_update = Some(self.current_widget.widget_id); @@ -1116,13 +1138,13 @@ impl App { if !proc_widget_state.is_grouped { match proc_widget_state.process_sorting_type { processes::ProcessSorting::Pid => { - proc_widget_state.process_sorting_reverse = - !proc_widget_state.process_sorting_reverse + proc_widget_state.is_process_sort_descending = + !proc_widget_state.is_process_sort_descending } _ => { proc_widget_state.process_sorting_type = processes::ProcessSorting::Pid; - proc_widget_state.process_sorting_reverse = false; + proc_widget_state.is_process_sort_descending = false; } } self.proc_state.force_update = Some(self.current_widget.widget_id); @@ -1168,8 +1190,8 @@ impl App { match proc_widget_state.process_sorting_type { processes::ProcessSorting::ProcessName | processes::ProcessSorting::Command => { - proc_widget_state.process_sorting_reverse = - !proc_widget_state.process_sorting_reverse + proc_widget_state.is_process_sort_descending = + !proc_widget_state.is_process_sort_descending } _ => { proc_widget_state.process_sorting_type = @@ -1178,7 +1200,7 @@ impl App { } else { processes::ProcessSorting::ProcessName }; - proc_widget_state.process_sorting_reverse = false; + proc_widget_state.is_process_sort_descending = false; } } self.proc_state.force_update = Some(self.current_widget.widget_id); @@ -1194,6 +1216,7 @@ impl App { 'L' | 'D' => self.move_widget_selection(&WidgetDirection::Right), 'K' | 'W' => self.move_widget_selection(&WidgetDirection::Up), 'J' | 'S' => self.move_widget_selection(&WidgetDirection::Down), + 't' => self.toggle_tree_mode(), '+' => self.zoom_in(), '-' => self.zoom_out(), '=' => self.reset_zoom(), @@ -1228,7 +1251,7 @@ impl App { } } - pub fn get_to_delete_processes(&self) -> Option<(String, Vec)> { + pub fn get_to_delete_processes(&self) -> Option<(String, Vec)> { self.to_delete_process_list.clone() } diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index eb5b0add..e7deca52 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -72,7 +72,7 @@ pub struct DataCollector { pub data: Data, sys: System, #[cfg(target_os = "linux")] - pid_mapping: HashMap, + pid_mapping: HashMap, #[cfg(target_os = "linux")] prev_idle: f64, #[cfg(target_os = "linux")] diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs index dcd6d647..8604bf8f 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes.rs @@ -1,3 +1,4 @@ +use crate::Pid; use std::path::PathBuf; use sysinfo::ProcessStatus; @@ -59,7 +60,8 @@ impl Default for ProcessSorting { #[derive(Debug, Clone, Default)] pub struct ProcessHarvest { - pub pid: u32, + pub pid: Pid, + pub parent_pid: Option, // Remember, parent_pid 0 is root... pub cpu_usage_percent: f64, pub mem_usage_percent: f64, pub mem_usage_bytes: u64, @@ -89,7 +91,7 @@ pub struct PrevProcDetails { } impl PrevProcDetails { - pub fn new(pid: u32) -> Self { + pub fn new(pid: Pid) -> Self { PrevProcDetails { proc_io_path: PathBuf::from(format!("/proc/{}/io", pid)), proc_exe_path: PathBuf::from(format!("/proc/{}/exe", pid)), @@ -200,7 +202,7 @@ fn read_path_contents(path: &PathBuf) -> std::io::Result { #[cfg(target_os = "linux")] fn get_linux_process_state(stat: &[&str]) -> (char, String) { - // The -2 offset is because of us cutting off name + pid + // The -2 offset is because of us cutting off name + pid, normally it's 2 if let Some(first_char) = stat[0].chars().collect::>().first() { ( *first_char, @@ -241,8 +243,8 @@ fn get_linux_cpu_usage( #[allow(clippy::too_many_arguments)] #[cfg(target_os = "linux")] fn read_proc( - pid: u32, cpu_usage: f64, cpu_fraction: f64, - pid_mapping: &mut HashMap, use_current_cpu_total: bool, + pid: Pid, cpu_usage: f64, cpu_fraction: f64, + pid_mapping: &mut HashMap, use_current_cpu_total: bool, time_difference_in_secs: u64, mem_total_kb: u64, page_file_kb: u64, ) -> error::Result { let pid_stat = pid_mapping @@ -282,6 +284,7 @@ fn read_proc( &mut pid_stat.cpu_time, use_current_cpu_total, )?; + let parent_pid = stat[1].parse::().ok(); let (_vsize, rss) = get_linux_process_vsize_rss(&stat); let mem_usage_kb = rss * page_file_kb; let mem_usage_percent = mem_usage_kb as f64 / mem_total_kb as f64 * 100.0; @@ -320,6 +323,7 @@ fn read_proc( Ok(ProcessHarvest { pid, + parent_pid, name, command, mem_usage_percent, @@ -337,14 +341,16 @@ fn read_proc( #[cfg(target_os = "linux")] pub fn linux_get_processes_list( prev_idle: &mut f64, prev_non_idle: &mut f64, - pid_mapping: &mut HashMap, use_current_cpu_total: bool, + pid_mapping: &mut HashMap, use_current_cpu_total: bool, time_difference_in_secs: u64, mem_total_kb: u64, page_file_kb: u64, ) -> crate::utils::error::Result> { + // TODO: [PROC THREADS] Add threads + if let Ok((cpu_usage, cpu_fraction)) = cpu_usage_calculation(prev_idle, prev_non_idle) { let process_vector: Vec = std::fs::read_dir("/proc")? .filter_map(|dir| { if let Ok(dir) = dir { - let pid = dir.file_name().to_string_lossy().trim().parse::(); + let pid = dir.file_name().to_string_lossy().trim().parse::(); if let Ok(pid) = pid { // I skip checking if the path is also a directory, it's not needed I think? if let Ok(process_object) = read_proc( @@ -424,7 +430,8 @@ pub fn windows_macos_get_processes_list( let disk_usage = process_val.disk_usage(); process_vector.push(ProcessHarvest { - pid: process_val.pid() as u32, + pid: process_val.pid(), + parent_pid: process_val.parent(), name, command, mem_usage_percent: if mem_total_kb > 0 { diff --git a/src/app/process_killer.rs b/src/app/process_killer.rs index cb0662bc..e367d99c 100644 --- a/src/app/process_killer.rs +++ b/src/app/process_killer.rs @@ -10,6 +10,7 @@ use winapi::{ /// This file is meant to house (OS specific) implementations on how to kill processes. use crate::utils::error::BottomError; +use crate::Pid; #[cfg(target_os = "windows")] struct Process(HANDLE); @@ -31,9 +32,9 @@ impl Process { } /// Kills a process, given a PID. -pub fn kill_process_given_pid(pid: u32) -> crate::utils::error::Result<()> { - if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - #[cfg(any(target_os = "linux", target_os = "macos"))] +pub fn kill_process_given_pid(pid: Pid) -> crate::utils::error::Result<()> { + if cfg!(target_family = "unix") { + #[cfg(any(target_family = "unix"))] { let output = unsafe { libc::kill(pid as i32, libc::SIGTERM) }; if output != 0 { @@ -59,8 +60,8 @@ pub fn kill_process_given_pid(pid: u32) -> crate::utils::error::Result<()> { }; } } - } else if cfg!(target_os = "windows") { - #[cfg(target_os = "windows")] + } else if cfg!(target_family = "windows") { + #[cfg(target_family = "windows")] { let process = Process::open(pid as DWORD)?; process.kill()?; diff --git a/src/app/states.rs b/src/app/states.rs index cf9123db..318b2175 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -9,6 +9,7 @@ use crate::{ constants, data_harvester::processes::{self, ProcessSorting}, }; +use ProcessSorting::*; #[derive(Debug)] pub enum ScrollDirection { @@ -159,7 +160,6 @@ pub struct ProcColumn { impl Default for ProcColumn { fn default() -> Self { - use ProcessSorting::*; let ordered_columns = vec![ Count, Pid, @@ -352,11 +352,12 @@ pub struct ProcWidgetState { pub is_grouped: bool, pub scroll_state: AppScrollWidgetState, pub process_sorting_type: processes::ProcessSorting, - pub process_sorting_reverse: bool, + pub is_process_sort_descending: bool, pub is_using_command: bool, pub current_column_index: usize, pub is_sort_open: bool, pub columns: ProcColumn, + pub is_tree_mode: bool, } impl ProcWidgetState { @@ -390,11 +391,12 @@ impl ProcWidgetState { is_grouped, scroll_state: AppScrollWidgetState::default(), process_sorting_type, - process_sorting_reverse: true, + is_process_sort_descending: true, is_using_command: false, current_column_index: 0, is_sort_open: false, columns, + is_tree_mode: false, } } @@ -422,7 +424,7 @@ impl ProcWidgetState { if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) { if *new_sort_type == self.process_sorting_type { // Just reverse the search if we're reselecting! - self.process_sorting_reverse = !(self.process_sorting_reverse); + self.is_process_sort_descending = !(self.is_process_sort_descending); } else { self.process_sorting_type = new_sort_type.clone(); match self.process_sorting_type { @@ -431,7 +433,7 @@ impl ProcWidgetState { | ProcessSorting::ProcessName | ProcessSorting::Command => { // Also invert anything that uses alphabetical sorting by default. - self.process_sorting_reverse = false; + self.is_process_sort_descending = false; } _ => {} } diff --git a/src/canvas.rs b/src/canvas.rs index 61f74e4f..e276e097 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -42,7 +42,6 @@ pub struct DisplayableData { pub disk_data: Vec>, pub temp_sensor_data: Vec>, pub single_process_data: Vec, // Contains single process data - pub process_data: Vec, // Not the final value, may be grouped or single pub finalized_process_data_map: HashMap>, // What's actually displayed pub mem_label_percent: String, pub swap_label_percent: String, diff --git a/src/canvas/canvas_colours.rs b/src/canvas/canvas_colours.rs index 98932d5d..2876e24b 100644 --- a/src/canvas/canvas_colours.rs +++ b/src/canvas/canvas_colours.rs @@ -28,6 +28,7 @@ pub struct CanvasColours { // Full, Medium, Low pub battery_bar_styles: Vec