diff --git a/.vscode/settings.json b/.vscode/settings.json index a04896e8..5d567693 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,11 +67,13 @@ "cvars", "czvf", "denylist", + "doctest", "dont", "eselect", "fedoracentos", "fpath", "fract", + "getpwuid", "gnueabihf", "gotop", "gotop's", diff --git a/CHANGELOG.md b/CHANGELOG.md index 3176e72b..0cc22531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,19 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Features -- [#263](https://github.com/ClementTsang/bottom/pull/263): Adds the option for fine-grained kill signals on Unix-like systems. +- [#263](https://github.com/ClementTsang/bottom/pull/263): Added the option for fine-grained kill signals on Unix-like systems. -- [#333](https://github.com/ClementTsang/bottom/pull/333): Adds an "out of" indicator that can be enabled using `--show_table_scroll_position` (and its corresponding config option) to help keep track of scrolled position. +- [#333](https://github.com/ClementTsang/bottom/pull/333): Added an "out of" indicator that can be enabled using `--show_table_scroll_position` (and its corresponding config option) to help keep track of scrolled position. -- [#379](https://github.com/ClementTsang/bottom/pull/379): Adds `--process_command` flag and corresponding config option to default to showing a process' command. +- [#379](https://github.com/ClementTsang/bottom/pull/379): Added `--process_command` flag and corresponding config option to default to showing a process' command. -- [#381](https://github.com/ClementTsang/bottom/pull/381): Adds a filter in the config file for network interfaces. +- [#381](https://github.com/ClementTsang/bottom/pull/381): Added a filter in the config file for network interfaces. -- [#406](https://github.com/ClementTsang/bottom/pull/406): Adds the Nord colour scheme, as well as a light variant. +- [#406](https://github.com/ClementTsang/bottom/pull/406): Added the Nord colour scheme, as well as a light variant. -- [#409](https://github.com/ClementTsang/bottom/pull/409): Adds `Ctrl-w` and `Ctrl-h` shortcuts in search, to delete a word and delete a character respectively. +- [#409](https://github.com/ClementTsang/bottom/pull/409): Added `Ctrl-w` and `Ctrl-h` shortcuts in search, to delete a word and delete a character respectively. -- [#413](https://github.com/ClementTsang/bottom/pull/413): Adds mouse support for sorting process columns. +- [#413](https://github.com/ClementTsang/bottom/pull/413): Added mouse support for sorting process columns. + +- [#425](https://github.com/ClementTsang/bottom/pull/425): Added user into the process widget for Unix-based systems. ## Changes @@ -43,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#423](https://github.com/ClementTsang/bottom/pull/423): Fixes disk encryption causing the disk widget to fail or not properly map I/O statistics. +- [#425](https://github.com/ClementTsang/bottom/pull/425): Fixed a bug allowing grouped mode in tree mode if already in grouped mode. + ## [0.5.7] - 2021-01-30 ## Bug Fixes diff --git a/Cargo.toml b/Cargo.toml index 7e944d23..b9351b53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,11 @@ name = "btm" path = "src/bin/main.rs" doc = false +[lib] +test = false +doctest = false +doc = false + [profile.release] debug = 0 lto = true diff --git a/README.md b/README.md index a899ef27..f0a929ff 100644 --- a/README.md +++ b/README.md @@ -387,6 +387,7 @@ Use `btm --help` for more information. | `write`, `w/s` | `write >= 1 kb` | Matches the write/s column in terms of bytes; supports comparison operators | | `tread`, `t.read` | `tread <= 1024 gb` | Matches he total read column in terms of bytes; supports comparison operators | | `twrite`, `t.write` | `twrite > 1024 tb` | Matches the total write column in terms of bytes; supports comparison operators | +| `user` | `user=root` | Matches by user; supports regex | | `state` | `state=running` | Matches by state; supports regex | #### Supported comparison operators @@ -464,7 +465,7 @@ As yet _another_ process/system visualization and management application, bottom - Display temperatures from sensors -- Display information regarding processes, like CPU, memory, I/O usage, and process state +- Display information regarding processes, like CPU, memory, I/O usage, user, and process state - Process management (well, if process killing is all you need) diff --git a/src/app.rs b/src/app.rs index 8be7394d..3c0fc9d5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -121,6 +121,10 @@ pub struct App { #[builder(default = false, setter(skip))] pub did_config_fail_to_save: bool, + #[cfg(target_family = "unix")] + #[builder(default, setter(skip))] + pub user_table: processes::UserTable, + pub cpu_state: CpuState, pub mem_state: MemState, pub net_state: NetState, @@ -310,23 +314,35 @@ impl App { // 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.process_sorting_type + == processes::ProcessSorting::Pid + || proc_widget_state.process_sorting_type + == processes::ProcessSorting::User + || proc_widget_state.process_sorting_type + == processes::ProcessSorting::State)) || (!proc_widget_state.is_grouped && proc_widget_state.process_sorting_type - == data_harvester::processes::ProcessSorting::Count) + == processes::ProcessSorting::Count) { proc_widget_state.process_sorting_type = - data_harvester::processes::ProcessSorting::CpuPercent; // Go back to default, negate PID for group + 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.set_to_sorted_index_from_type( + &proc_widget_state.process_sorting_type, + ); + + proc_widget_state.columns.try_set( + &processes::ProcessSorting::State, + !(proc_widget_state.is_grouped), + ); + + #[cfg(target_family = "unix")] + proc_widget_state.columns.try_set( + &processes::ProcessSorting::User, + !(proc_widget_state.is_grouped), + ); proc_widget_state .columns @@ -657,6 +673,26 @@ impl App { proc_widget_state.is_tree_mode = !proc_widget_state.is_tree_mode; if proc_widget_state.is_tree_mode { + // Disable grouping if so! + proc_widget_state.is_grouped = false; + + proc_widget_state + .columns + .try_enable(&processes::ProcessSorting::State); + + #[cfg(target_family = "unix")] + proc_widget_state + .columns + .try_enable(&processes::ProcessSorting::User); + + proc_widget_state + .columns + .try_disable(&processes::ProcessSorting::Count); + + proc_widget_state + .columns + .try_enable(&processes::ProcessSorting::Pid); + // 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; diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs index c9233337..56bf09d4 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes.rs @@ -2,8 +2,11 @@ use crate::Pid; use std::path::PathBuf; use sysinfo::ProcessStatus; +#[cfg(target_family = "unix")] +use crate::utils::error; + #[cfg(target_os = "linux")] -use crate::utils::error::{self, BottomError}; +use crate::utils::error::BottomError; #[cfg(target_os = "linux")] use fnv::{FnvHashMap, FnvHashSet}; @@ -29,28 +32,29 @@ pub enum ProcessSorting { TotalRead, TotalWrite, State, + User, Count, } impl std::fmt::Display for ProcessSorting { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use ProcessSorting::*; write!( f, "{}", match &self { - CpuPercent => "CPU%", - MemPercent => "Mem%", - Mem => "Mem", - ReadPerSecond => "R/s", - WritePerSecond => "W/s", - TotalRead => "T.Read", - TotalWrite => "T.Write", - State => "State", - ProcessName => "Name", - Command => "Command", - Pid => "PID", - Count => "Count", + ProcessSorting::CpuPercent => "CPU%", + ProcessSorting::MemPercent => "Mem%", + ProcessSorting::Mem => "Mem", + ProcessSorting::ReadPerSecond => "R/s", + ProcessSorting::WritePerSecond => "W/s", + ProcessSorting::TotalRead => "T.Read", + ProcessSorting::TotalWrite => "T.Write", + ProcessSorting::State => "State", + ProcessSorting::ProcessName => "Name", + ProcessSorting::Command => "Command", + ProcessSorting::Pid => "PID", + ProcessSorting::Count => "Count", + ProcessSorting::User => "User", } ) } @@ -81,9 +85,13 @@ pub struct ProcessHarvest { pub process_state_char: char, /// This is the *effective* user ID. - pub uid: Option, - // pub real_uid: Option, // TODO: Add real user ID - pub gid: Option, + #[cfg(target_family = "unix")] + pub uid: Option, + + // TODO: Add real user ID + // pub real_uid: Option, + #[cfg(target_family = "unix")] + pub gid: Option, } #[derive(Debug, Default, Clone)] @@ -114,6 +122,29 @@ impl PrevProcDetails { } } +#[cfg(target_family = "unix")] +#[derive(Debug, Default)] +pub struct UserTable { + pub uid_user_mapping: std::collections::HashMap, +} + +#[cfg(target_family = "unix")] +impl UserTable { + pub fn get_uid_to_username_mapping(&mut self, uid: libc::uid_t) -> error::Result { + if let Some(user) = self.uid_user_mapping.get(&uid) { + Ok(user.clone()) + } else { + let passwd = unsafe { libc::getpwuid(uid) }; + let username = unsafe { std::ffi::CStr::from_ptr((*passwd).pw_name) } + .to_str()? + .to_string(); + self.uid_user_mapping.insert(uid, username.clone()); + + Ok(username) + } + } +} + #[cfg(target_os = "linux")] fn cpu_usage_calculation( prev_idle: &mut f64, prev_non_idle: &mut f64, @@ -591,8 +622,6 @@ pub fn get_process_data( total_write_bytes: disk_usage.total_written_bytes, process_state: process_val.status().to_string().to_string(), process_state_char: convert_process_status_to_char(process_val.status()), - uid: None, - gid: None, }); } } diff --git a/src/app/query.rs b/src/app/query.rs index 79f61e73..5a827aec 100644 --- a/src/app/query.rs +++ b/src/app/query.rs @@ -26,7 +26,8 @@ pub trait ProcessQuery { /// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant). /// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare. /// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare. - /// - STATE: Use prefix `state`, TODO when we update how state looks in 0.5 probably. + /// - STATE: Use prefix `state`, can use regex, match word, or case. + /// - USER: Use prefix `user`, can use regex, match word, or case. /// - Read/s: Use prefix `r`. Can compare. /// - Write/s: Use prefix `w`. Can compare. /// - Total read: Use prefix `read`. Can compare. @@ -128,8 +129,6 @@ impl ProcessQuery for ProcWidgetState { fn process_prefix(query: &mut VecDeque, inside_quotation: bool) -> Result { if let Some(queue_top) = query.pop_front() { - // debug!("Prefix QT: {:?}", queue_top); - if inside_quotation { if queue_top == "\"" { // This means we hit something like "". Return an empty prefix, and to deal with @@ -264,11 +263,20 @@ impl ProcessQuery for ProcWidgetState { compare_prefix: None, }) } - PrefixType::Pid | PrefixType::State => { + PrefixType::Pid | PrefixType::State | PrefixType::User => { // We have to check if someone put an "="... if content == "=" { // Check next string if possible if let Some(queue_next) = query.pop_front() { + // TODO: Need to consider the following cases: + // - (test) + // - (test + // - test) + // These are split into 2 to 3 different strings due to parentheses being + // delimiters in our query system. + // + // Do we want these to be valid? They should, as a string, right? + return Ok(Prefix { or: None, regex_prefix: Some(( @@ -580,6 +588,7 @@ pub enum PrefixType { TWrite, Name, State, + User, __Nonexhaustive, } @@ -602,6 +611,7 @@ impl std::str::FromStr for PrefixType { "twrite" | "t.write" => Ok(TWrite), "pid" => Ok(Pid), "state" => Ok(State), + "user" => Ok(User), _ => Ok(Name), } } @@ -628,7 +638,7 @@ impl Prefix { } else if let Some((prefix_type, StringQuery::Value(regex_string))) = &mut self.regex_prefix { match prefix_type { - PrefixType::Pid | PrefixType::Name | PrefixType::State => { + PrefixType::Pid | PrefixType::Name | PrefixType::State | PrefixType::User => { let escaped_regex: String; let final_regex_string = &format!( "{}{}{}{}", @@ -681,6 +691,13 @@ impl Prefix { }), PrefixType::Pid => r.is_match(process.pid.to_string().as_str()), PrefixType::State => r.is_match(process.process_state.as_str()), + PrefixType::User => { + if let Some(user) = &process.user { + r.is_match(user.as_str()) + } else { + false + } + } _ => true, } } else { diff --git a/src/app/states.rs b/src/app/states.rs index c2853337..39dd0222 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -174,6 +174,9 @@ impl ProcessSearchState { pub struct ColumnInfo { pub enabled: bool, pub shortcut: Option<&'static str>, + // FIXME: Move column width logic here! + // pub hard_width: Option, + // pub max_soft_width: Option, } pub struct ProcColumn { @@ -205,6 +208,7 @@ impl Default for ProcColumn { WritePerSecond, TotalRead, TotalWrite, + User, State, ]; @@ -219,6 +223,8 @@ impl Default for ProcColumn { ColumnInfo { enabled: true, shortcut: Some("c"), + // hard_width: None, + // max_soft_width: None, }, ); } @@ -228,6 +234,8 @@ impl Default for ProcColumn { ColumnInfo { enabled: true, shortcut: Some("m"), + // hard_width: None, + // max_soft_width: None, }, ); } @@ -237,6 +245,8 @@ impl Default for ProcColumn { ColumnInfo { enabled: false, shortcut: Some("m"), + // hard_width: None, + // max_soft_width: None, }, ); } @@ -246,6 +256,8 @@ impl Default for ProcColumn { ColumnInfo { enabled: true, shortcut: Some("n"), + // hard_width: None, + // max_soft_width: None, }, ); } @@ -255,6 +267,8 @@ impl Default for ProcColumn { ColumnInfo { enabled: false, shortcut: Some("n"), + // hard_width: None, + // max_soft_width: None, }, ); } @@ -264,6 +278,8 @@ impl Default for ProcColumn { ColumnInfo { enabled: true, shortcut: Some("p"), + // hard_width: None, + // max_soft_width: None, }, ); } @@ -273,6 +289,17 @@ impl Default for ProcColumn { ColumnInfo { enabled: false, shortcut: None, + // hard_width: None, + // max_soft_width: None, + }, + ); + } + User => { + column_mapping.insert( + column, + ColumnInfo { + enabled: cfg!(target_family = "unix"), + shortcut: None, }, ); } @@ -282,6 +309,8 @@ impl Default for ProcColumn { ColumnInfo { enabled: true, shortcut: None, + // hard_width: None, + // max_soft_width: None, }, ); } @@ -316,6 +345,33 @@ impl ProcColumn { } } + pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option { + if let Some(mapping) = self.column_mapping.get_mut(column) { + mapping.enabled = setting; + Some(mapping.enabled) + } else { + None + } + } + + pub fn try_enable(&mut self, column: &ProcessSorting) -> Option { + if let Some(mapping) = self.column_mapping.get_mut(column) { + mapping.enabled = true; + Some(mapping.enabled) + } else { + None + } + } + + pub fn try_disable(&mut self, column: &ProcessSorting) -> Option { + if let Some(mapping) = self.column_mapping.get_mut(column) { + mapping.enabled = false; + Some(mapping.enabled) + } else { + None + } + } + pub fn is_enabled(&self, column: &ProcessSorting) -> bool { if let Some(mapping) = self.column_mapping.get(column) { mapping.enabled diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index 316e71ae..2abab933 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -30,6 +30,8 @@ static PROCESS_HEADERS_HARD_WIDTH_NO_GROUP: Lazy>> = Lazy::new(| Some(8), Some(7), Some(8), + #[cfg(target_family = "unix")] + None, None, ] }); @@ -48,8 +50,6 @@ static PROCESS_HEADERS_HARD_WIDTH_GROUPED: Lazy>> = Lazy::new(|| static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: Lazy>> = Lazy::new(|| vec![None, Some(0.7), None, None, None, None, None, None]); -static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_TREE: Lazy>> = - Lazy::new(|| vec![None, Some(0.5), None, None, None, None, None, None]); static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: Lazy>> = Lazy::new(|| vec![None, Some(0.3), None, None, None, None, None, None]); @@ -63,6 +63,8 @@ static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND: Lazy>> = None, None, None, + #[cfg(target_family = "unix")] + Some(0.05), Some(0.2), ] }); @@ -76,6 +78,8 @@ static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE: Lazy>> = La None, None, None, + #[cfg(target_family = "unix")] + Some(0.05), Some(0.2), ] }); @@ -89,6 +93,8 @@ static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: Lazy>> = La None, None, None, + #[cfg(target_family = "unix")] + Some(0.05), Some(0.2), ] }); @@ -344,6 +350,7 @@ impl ProcessTableWidget for Painter { ); // Calculate widths + // FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths let hard_widths = if proc_widget_state.is_grouped { &*PROCESS_HEADERS_HARD_WIDTH_GROUPED } else { @@ -394,10 +401,10 @@ impl ProcessTableWidget for Painter { .collect::>(); let soft_widths_max = if proc_widget_state.is_grouped { + // Note grouped trees are not a thing. + if proc_widget_state.is_using_command { &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND - } else if proc_widget_state.is_tree_mode { - &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_TREE } else { &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE } @@ -601,6 +608,7 @@ impl ProcessTableWidget for Painter { } } + // TODO: Make the cursor scroll back if there's space! if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&(widget_id - 1)) { diff --git a/src/constants.rs b/src/constants.rs index 38c5ef6a..fa6d4b6c 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -272,7 +272,7 @@ pub const PROCESS_HELP_TEXT: [&str; 15] = [ "click on header Sorts the entries by that column, click again to invert the sort", ]; -pub const SEARCH_HELP_TEXT: [&str; 48] = [ +pub const SEARCH_HELP_TEXT: [&str; 49] = [ "4 - Process search widget", "Tab Toggle between searching for PID and name", "Esc Close the search widget (retains the filter)", @@ -299,6 +299,7 @@ pub const SEARCH_HELP_TEXT: [&str; 48] = [ "write, w/s ex: write <= 1 tb", "tread, t.read ex: tread = 1", "twrite, t.write ex: twrite = 1", + "user ex: user = root", "state ex: state = running", "", "Comparison operators:", diff --git a/src/data_conversion.rs b/src/data_conversion.rs index b3614cf1..c3665cda 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -62,6 +62,7 @@ pub struct ConvertedProcessData { pub tw_f64: f64, pub process_state: String, pub process_char: char, + pub user: Option, /// Prefix printed before the process when displayed. pub process_description_prefix: Option, @@ -482,6 +483,7 @@ pub enum ProcessNamingType { pub fn convert_process_data( current_data: &data_farmer::DataCollection, existing_converted_process_data: &mut HashMap, + #[cfg(target_family = "unix")] user_table: &mut data_harvester::processes::UserTable, ) { // TODO [THREAD]: Thread highlighting and hiding support // For macOS see https://github.com/hishamhm/htop/pull/848/files @@ -503,6 +505,21 @@ pub fn convert_process_data( 0, converted_total_write.0, converted_total_write.1 ); + let user = { + #[cfg(target_family = "unix")] + { + if let Some(uid) = process.uid { + user_table.get_uid_to_username_mapping(uid).ok() + } else { + None + } + } + #[cfg(not(target_family = "unix"))] + { + None + } + }; + if let Some(process_entry) = existing_converted_process_data.get_mut(&process.pid) { complete_pid_set.remove(&process.pid); @@ -527,6 +544,7 @@ pub fn convert_process_data( process_entry.process_char = process.process_state_char; process_entry.process_description_prefix = None; process_entry.is_disabled_entry = false; + process_entry.user = user; } else { // ...I hate that I can't combine if let and an if statement in one line... *process_entry = ConvertedProcessData { @@ -553,6 +571,7 @@ pub fn convert_process_data( process_description_prefix: None, is_disabled_entry: false, is_collapsed_entry: false, + user, }; } } else { @@ -582,6 +601,7 @@ pub fn convert_process_data( process_description_prefix: None, is_disabled_entry: false, is_collapsed_entry: false, + user, }, ); } @@ -827,8 +847,18 @@ pub fn tree_process_data( is_sort_descending, ) }), + ProcessSorting::User => to_sort_vec.sort_by(|a, b| match (&a.1.user, &b.1.user) { + (Some(user_a), Some(user_b)) => utils::gen_util::get_ordering( + user_a.to_lowercase(), + user_b.to_lowercase(), + is_sort_descending, + ), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Less, + }), ProcessSorting::Count => { - // Should never occur in this case. + // Should never occur in this case, tree mode explicitly disables grouping. } } } @@ -990,6 +1020,15 @@ pub fn stringify_process_data( (process.write_per_sec.clone(), None), (process.total_read.clone(), None), (process.total_write.clone(), None), + #[cfg(target_family = "unix")] + ( + if let Some(user) = &process.user { + user.clone() + } else { + "N/A".to_string() + }, + None, + ), ( process.process_state.clone(), Some(process.process_char.to_string()), @@ -1083,6 +1122,7 @@ pub fn group_process_data( process_char: char::default(), is_disabled_entry: false, is_collapsed_entry: false, + user: None, } }) .collect::>() diff --git a/src/lib.rs b/src/lib.rs index f5e73a27..e3be6823 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -369,6 +369,8 @@ fn update_final_process_list(app: &mut App, widget_id: u64) { convert_process_data( &app.data_collection, &mut app.canvas_data.single_process_data, + #[cfg(target_family = "unix")] + &mut app.user_table, ); } let process_filter = app.get_process_filter(widget_id); @@ -550,6 +552,16 @@ fn sort_process_data( to_sort_vec.reverse(); } } + ProcessSorting::User => to_sort_vec.sort_by(|a, b| match (&a.user, &b.user) { + (Some(user_a), Some(user_b)) => utils::gen_util::get_ordering( + user_a.to_lowercase(), + user_b.to_lowercase(), + proc_widget_state.is_process_sort_descending, + ), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Less, + }), ProcessSorting::Count => { if proc_widget_state.is_grouped { to_sort_vec.sort_by(|a, b| { diff --git a/src/utils/error.rs b/src/utils/error.rs index f0a965b0..1fc75bd7 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -86,6 +86,12 @@ impl From for BottomError { } } +impl From for BottomError { + fn from(err: std::string::FromUtf8Error) -> Self { + BottomError::ConversionError(err.to_string()) + } +} + impl From for BottomError { fn from(err: regex::Error) -> Self { // We only really want the last part of it... so we'll do it the ugly way: