feature: User info in proc widget for Unix-based systems (#425)

Adds users into the process widget (for Unix-based systems).  This shows only in non-grouped modes, similar to state.  Search is also supported.

In addition, a quick fix to prevent users from being in grouped mode when they tried to enter tree mode while grouped.
This commit is contained in:
Clement Tsang 2021-02-28 17:40:55 -05:00 committed by GitHub
parent c406d95699
commit 53d8bdae32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 265 additions and 48 deletions

View file

@ -67,11 +67,13 @@
"cvars", "cvars",
"czvf", "czvf",
"denylist", "denylist",
"doctest",
"dont", "dont",
"eselect", "eselect",
"fedoracentos", "fedoracentos",
"fpath", "fpath",
"fract", "fract",
"getpwuid",
"gnueabihf", "gnueabihf",
"gotop", "gotop",
"gotop's", "gotop's",

View file

@ -9,19 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Features ## 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 ## 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. - [#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 ## [0.5.7] - 2021-01-30
## Bug Fixes ## Bug Fixes

View file

@ -17,6 +17,11 @@ name = "btm"
path = "src/bin/main.rs" path = "src/bin/main.rs"
doc = false doc = false
[lib]
test = false
doctest = false
doc = false
[profile.release] [profile.release]
debug = 0 debug = 0
lto = true lto = true

View file

@ -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 | | `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 | | `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 | | `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 | | `state` | `state=running` | Matches by state; supports regex |
#### Supported comparison operators #### Supported comparison operators
@ -464,7 +465,7 @@ As yet _another_ process/system visualization and management application, bottom
- Display temperatures from sensors - 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) - Process management (well, if process killing is all you need)

View file

@ -121,6 +121,10 @@ pub struct App {
#[builder(default = false, setter(skip))] #[builder(default = false, setter(skip))]
pub did_config_fail_to_save: bool, 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 cpu_state: CpuState,
pub mem_state: MemState, pub mem_state: MemState,
pub net_state: NetState, pub net_state: NetState,
@ -310,23 +314,35 @@ impl App {
// Forcefully switch off column if we were on it... // Forcefully switch off column if we were on it...
if (proc_widget_state.is_grouped if (proc_widget_state.is_grouped
&& proc_widget_state.process_sorting_type && (proc_widget_state.process_sorting_type
== data_harvester::processes::ProcessSorting::Pid) == 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.is_grouped
&& proc_widget_state.process_sorting_type && proc_widget_state.process_sorting_type
== data_harvester::processes::ProcessSorting::Count) == processes::ProcessSorting::Count)
{ {
proc_widget_state.process_sorting_type = 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.is_process_sort_descending = true;
} }
proc_widget_state proc_widget_state.columns.set_to_sorted_index_from_type(
.columns &proc_widget_state.process_sorting_type,
.column_mapping );
.get_mut(&processes::ProcessSorting::State)
.unwrap() proc_widget_state.columns.try_set(
.enabled = !(proc_widget_state.is_grouped); &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 proc_widget_state
.columns .columns
@ -657,6 +673,26 @@ impl App {
proc_widget_state.is_tree_mode = !proc_widget_state.is_tree_mode; proc_widget_state.is_tree_mode = !proc_widget_state.is_tree_mode;
if 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. // We enabled... set PID sort type to ascending.
proc_widget_state.process_sorting_type = processes::ProcessSorting::Pid; proc_widget_state.process_sorting_type = processes::ProcessSorting::Pid;
proc_widget_state.is_process_sort_descending = false; proc_widget_state.is_process_sort_descending = false;

View file

@ -2,8 +2,11 @@ use crate::Pid;
use std::path::PathBuf; use std::path::PathBuf;
use sysinfo::ProcessStatus; use sysinfo::ProcessStatus;
#[cfg(target_family = "unix")]
use crate::utils::error;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use crate::utils::error::{self, BottomError}; use crate::utils::error::BottomError;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use fnv::{FnvHashMap, FnvHashSet}; use fnv::{FnvHashMap, FnvHashSet};
@ -29,28 +32,29 @@ pub enum ProcessSorting {
TotalRead, TotalRead,
TotalWrite, TotalWrite,
State, State,
User,
Count, Count,
} }
impl std::fmt::Display for ProcessSorting { impl std::fmt::Display for ProcessSorting {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ProcessSorting::*;
write!( write!(
f, f,
"{}", "{}",
match &self { match &self {
CpuPercent => "CPU%", ProcessSorting::CpuPercent => "CPU%",
MemPercent => "Mem%", ProcessSorting::MemPercent => "Mem%",
Mem => "Mem", ProcessSorting::Mem => "Mem",
ReadPerSecond => "R/s", ProcessSorting::ReadPerSecond => "R/s",
WritePerSecond => "W/s", ProcessSorting::WritePerSecond => "W/s",
TotalRead => "T.Read", ProcessSorting::TotalRead => "T.Read",
TotalWrite => "T.Write", ProcessSorting::TotalWrite => "T.Write",
State => "State", ProcessSorting::State => "State",
ProcessName => "Name", ProcessSorting::ProcessName => "Name",
Command => "Command", ProcessSorting::Command => "Command",
Pid => "PID", ProcessSorting::Pid => "PID",
Count => "Count", ProcessSorting::Count => "Count",
ProcessSorting::User => "User",
} }
) )
} }
@ -81,9 +85,13 @@ pub struct ProcessHarvest {
pub process_state_char: char, pub process_state_char: char,
/// This is the *effective* user ID. /// This is the *effective* user ID.
pub uid: Option<u32>, #[cfg(target_family = "unix")]
// pub real_uid: Option<u32>, // TODO: Add real user ID pub uid: Option<libc::uid_t>,
pub gid: Option<u32>,
// TODO: Add real user ID
// pub real_uid: Option<u32>,
#[cfg(target_family = "unix")]
pub gid: Option<libc::gid_t>,
} }
#[derive(Debug, Default, Clone)] #[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<libc::uid_t, String>,
}
#[cfg(target_family = "unix")]
impl UserTable {
pub fn get_uid_to_username_mapping(&mut self, uid: libc::uid_t) -> error::Result<String> {
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")] #[cfg(target_os = "linux")]
fn cpu_usage_calculation( fn cpu_usage_calculation(
prev_idle: &mut f64, prev_non_idle: &mut f64, 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, total_write_bytes: disk_usage.total_written_bytes,
process_state: process_val.status().to_string().to_string(), process_state: process_val.status().to_string().to_string(),
process_state_char: convert_process_status_to_char(process_val.status()), process_state_char: convert_process_status_to_char(process_val.status()),
uid: None,
gid: None,
}); });
} }
} }

View file

@ -26,7 +26,8 @@ pub trait ProcessQuery {
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant). /// - 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. /// - 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. /// - 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. /// - Read/s: Use prefix `r`. Can compare.
/// - Write/s: Use prefix `w`. Can compare. /// - Write/s: Use prefix `w`. Can compare.
/// - Total read: Use prefix `read`. Can compare. /// - Total read: Use prefix `read`. Can compare.
@ -128,8 +129,6 @@ impl ProcessQuery for ProcWidgetState {
fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> { fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> {
if let Some(queue_top) = query.pop_front() { if let Some(queue_top) = query.pop_front() {
// debug!("Prefix QT: {:?}", queue_top);
if inside_quotation { if inside_quotation {
if queue_top == "\"" { if queue_top == "\"" {
// This means we hit something like "". Return an empty prefix, and to deal with // 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, compare_prefix: None,
}) })
} }
PrefixType::Pid | PrefixType::State => { PrefixType::Pid | PrefixType::State | PrefixType::User => {
// We have to check if someone put an "="... // We have to check if someone put an "="...
if content == "=" { if content == "=" {
// Check next string if possible // Check next string if possible
if let Some(queue_next) = query.pop_front() { 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 { return Ok(Prefix {
or: None, or: None,
regex_prefix: Some(( regex_prefix: Some((
@ -580,6 +588,7 @@ pub enum PrefixType {
TWrite, TWrite,
Name, Name,
State, State,
User,
__Nonexhaustive, __Nonexhaustive,
} }
@ -602,6 +611,7 @@ impl std::str::FromStr for PrefixType {
"twrite" | "t.write" => Ok(TWrite), "twrite" | "t.write" => Ok(TWrite),
"pid" => Ok(Pid), "pid" => Ok(Pid),
"state" => Ok(State), "state" => Ok(State),
"user" => Ok(User),
_ => Ok(Name), _ => Ok(Name),
} }
} }
@ -628,7 +638,7 @@ impl Prefix {
} else if let Some((prefix_type, StringQuery::Value(regex_string))) = &mut self.regex_prefix } else if let Some((prefix_type, StringQuery::Value(regex_string))) = &mut self.regex_prefix
{ {
match prefix_type { match prefix_type {
PrefixType::Pid | PrefixType::Name | PrefixType::State => { PrefixType::Pid | PrefixType::Name | PrefixType::State | PrefixType::User => {
let escaped_regex: String; let escaped_regex: String;
let final_regex_string = &format!( let final_regex_string = &format!(
"{}{}{}{}", "{}{}{}{}",
@ -681,6 +691,13 @@ impl Prefix {
}), }),
PrefixType::Pid => r.is_match(process.pid.to_string().as_str()), PrefixType::Pid => r.is_match(process.pid.to_string().as_str()),
PrefixType::State => r.is_match(process.process_state.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, _ => true,
} }
} else { } else {

View file

@ -174,6 +174,9 @@ impl ProcessSearchState {
pub struct ColumnInfo { pub struct ColumnInfo {
pub enabled: bool, pub enabled: bool,
pub shortcut: Option<&'static str>, pub shortcut: Option<&'static str>,
// FIXME: Move column width logic here!
// pub hard_width: Option<u16>,
// pub max_soft_width: Option<f64>,
} }
pub struct ProcColumn { pub struct ProcColumn {
@ -205,6 +208,7 @@ impl Default for ProcColumn {
WritePerSecond, WritePerSecond,
TotalRead, TotalRead,
TotalWrite, TotalWrite,
User,
State, State,
]; ];
@ -219,6 +223,8 @@ impl Default for ProcColumn {
ColumnInfo { ColumnInfo {
enabled: true, enabled: true,
shortcut: Some("c"), shortcut: Some("c"),
// hard_width: None,
// max_soft_width: None,
}, },
); );
} }
@ -228,6 +234,8 @@ impl Default for ProcColumn {
ColumnInfo { ColumnInfo {
enabled: true, enabled: true,
shortcut: Some("m"), shortcut: Some("m"),
// hard_width: None,
// max_soft_width: None,
}, },
); );
} }
@ -237,6 +245,8 @@ impl Default for ProcColumn {
ColumnInfo { ColumnInfo {
enabled: false, enabled: false,
shortcut: Some("m"), shortcut: Some("m"),
// hard_width: None,
// max_soft_width: None,
}, },
); );
} }
@ -246,6 +256,8 @@ impl Default for ProcColumn {
ColumnInfo { ColumnInfo {
enabled: true, enabled: true,
shortcut: Some("n"), shortcut: Some("n"),
// hard_width: None,
// max_soft_width: None,
}, },
); );
} }
@ -255,6 +267,8 @@ impl Default for ProcColumn {
ColumnInfo { ColumnInfo {
enabled: false, enabled: false,
shortcut: Some("n"), shortcut: Some("n"),
// hard_width: None,
// max_soft_width: None,
}, },
); );
} }
@ -264,6 +278,8 @@ impl Default for ProcColumn {
ColumnInfo { ColumnInfo {
enabled: true, enabled: true,
shortcut: Some("p"), shortcut: Some("p"),
// hard_width: None,
// max_soft_width: None,
}, },
); );
} }
@ -273,6 +289,17 @@ impl Default for ProcColumn {
ColumnInfo { ColumnInfo {
enabled: false, enabled: false,
shortcut: None, 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 { ColumnInfo {
enabled: true, enabled: true,
shortcut: None, 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<bool> {
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<bool> {
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<bool> {
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 { pub fn is_enabled(&self, column: &ProcessSorting) -> bool {
if let Some(mapping) = self.column_mapping.get(column) { if let Some(mapping) = self.column_mapping.get(column) {
mapping.enabled mapping.enabled

View file

@ -30,6 +30,8 @@ static PROCESS_HEADERS_HARD_WIDTH_NO_GROUP: Lazy<Vec<Option<u16>>> = Lazy::new(|
Some(8), Some(8),
Some(7), Some(7),
Some(8), Some(8),
#[cfg(target_family = "unix")]
None,
None, None,
] ]
}); });
@ -48,8 +50,6 @@ static PROCESS_HEADERS_HARD_WIDTH_GROUPED: Lazy<Vec<Option<u16>>> = Lazy::new(||
static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: Lazy<Vec<Option<f64>>> = static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: Lazy<Vec<Option<f64>>> =
Lazy::new(|| vec![None, Some(0.7), None, None, None, None, None, None]); Lazy::new(|| vec![None, Some(0.7), None, None, None, None, None, None]);
static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_TREE: Lazy<Vec<Option<f64>>> =
Lazy::new(|| vec![None, Some(0.5), None, None, None, None, None, None]);
static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: Lazy<Vec<Option<f64>>> = static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: Lazy<Vec<Option<f64>>> =
Lazy::new(|| vec![None, Some(0.3), None, None, None, None, None, None]); 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<Vec<Option<f64>>> =
None, None,
None, None,
None, None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2), Some(0.2),
] ]
}); });
@ -76,6 +78,8 @@ static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE: Lazy<Vec<Option<f64>>> = La
None, None,
None, None,
None, None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2), Some(0.2),
] ]
}); });
@ -89,6 +93,8 @@ static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: Lazy<Vec<Option<f64>>> = La
None, None,
None, None,
None, None,
#[cfg(target_family = "unix")]
Some(0.05),
Some(0.2), Some(0.2),
] ]
}); });
@ -344,6 +350,7 @@ impl ProcessTableWidget for Painter {
); );
// Calculate widths // 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 { let hard_widths = if proc_widget_state.is_grouped {
&*PROCESS_HEADERS_HARD_WIDTH_GROUPED &*PROCESS_HEADERS_HARD_WIDTH_GROUPED
} else { } else {
@ -394,10 +401,10 @@ impl ProcessTableWidget for Painter {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let soft_widths_max = if proc_widget_state.is_grouped { let soft_widths_max = if proc_widget_state.is_grouped {
// Note grouped trees are not a thing.
if proc_widget_state.is_using_command { if proc_widget_state.is_using_command {
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND
} else if proc_widget_state.is_tree_mode {
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_TREE
} else { } else {
&*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_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) = if let Some(proc_widget_state) =
app_state.proc_state.widget_states.get_mut(&(widget_id - 1)) app_state.proc_state.widget_states.get_mut(&(widget_id - 1))
{ {

View file

@ -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", "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", "4 - Process search widget",
"Tab Toggle between searching for PID and name", "Tab Toggle between searching for PID and name",
"Esc Close the search widget (retains the filter)", "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", "write, w/s ex: write <= 1 tb",
"tread, t.read ex: tread = 1", "tread, t.read ex: tread = 1",
"twrite, t.write ex: twrite = 1", "twrite, t.write ex: twrite = 1",
"user ex: user = root",
"state ex: state = running", "state ex: state = running",
"", "",
"Comparison operators:", "Comparison operators:",

View file

@ -62,6 +62,7 @@ pub struct ConvertedProcessData {
pub tw_f64: f64, pub tw_f64: f64,
pub process_state: String, pub process_state: String,
pub process_char: char, pub process_char: char,
pub user: Option<String>,
/// Prefix printed before the process when displayed. /// Prefix printed before the process when displayed.
pub process_description_prefix: Option<String>, pub process_description_prefix: Option<String>,
@ -482,6 +483,7 @@ pub enum ProcessNamingType {
pub fn convert_process_data( pub fn convert_process_data(
current_data: &data_farmer::DataCollection, current_data: &data_farmer::DataCollection,
existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>, existing_converted_process_data: &mut HashMap<Pid, ConvertedProcessData>,
#[cfg(target_family = "unix")] user_table: &mut data_harvester::processes::UserTable,
) { ) {
// TODO [THREAD]: Thread highlighting and hiding support // TODO [THREAD]: Thread highlighting and hiding support
// For macOS see https://github.com/hishamhm/htop/pull/848/files // 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 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) { if let Some(process_entry) = existing_converted_process_data.get_mut(&process.pid) {
complete_pid_set.remove(&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_char = process.process_state_char;
process_entry.process_description_prefix = None; process_entry.process_description_prefix = None;
process_entry.is_disabled_entry = false; process_entry.is_disabled_entry = false;
process_entry.user = user;
} else { } else {
// ...I hate that I can't combine if let and an if statement in one line... // ...I hate that I can't combine if let and an if statement in one line...
*process_entry = ConvertedProcessData { *process_entry = ConvertedProcessData {
@ -553,6 +571,7 @@ pub fn convert_process_data(
process_description_prefix: None, process_description_prefix: None,
is_disabled_entry: false, is_disabled_entry: false,
is_collapsed_entry: false, is_collapsed_entry: false,
user,
}; };
} }
} else { } else {
@ -582,6 +601,7 @@ pub fn convert_process_data(
process_description_prefix: None, process_description_prefix: None,
is_disabled_entry: false, is_disabled_entry: false,
is_collapsed_entry: false, is_collapsed_entry: false,
user,
}, },
); );
} }
@ -827,8 +847,18 @@ pub fn tree_process_data(
is_sort_descending, 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 => { 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.write_per_sec.clone(), None),
(process.total_read.clone(), None), (process.total_read.clone(), None),
(process.total_write.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(), process.process_state.clone(),
Some(process.process_char.to_string()), Some(process.process_char.to_string()),
@ -1083,6 +1122,7 @@ pub fn group_process_data(
process_char: char::default(), process_char: char::default(),
is_disabled_entry: false, is_disabled_entry: false,
is_collapsed_entry: false, is_collapsed_entry: false,
user: None,
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()

View file

@ -369,6 +369,8 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
convert_process_data( convert_process_data(
&app.data_collection, &app.data_collection,
&mut app.canvas_data.single_process_data, &mut app.canvas_data.single_process_data,
#[cfg(target_family = "unix")]
&mut app.user_table,
); );
} }
let process_filter = app.get_process_filter(widget_id); let process_filter = app.get_process_filter(widget_id);
@ -550,6 +552,16 @@ fn sort_process_data(
to_sort_vec.reverse(); 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 => { ProcessSorting::Count => {
if proc_widget_state.is_grouped { if proc_widget_state.is_grouped {
to_sort_vec.sort_by(|a, b| { to_sort_vec.sort_by(|a, b| {

View file

@ -86,6 +86,12 @@ impl From<std::str::Utf8Error> for BottomError {
} }
} }
impl From<std::string::FromUtf8Error> for BottomError {
fn from(err: std::string::FromUtf8Error) -> Self {
BottomError::ConversionError(err.to_string())
}
}
impl From<regex::Error> for BottomError { impl From<regex::Error> for BottomError {
fn from(err: regex::Error) -> Self { fn from(err: regex::Error) -> Self {
// We only really want the last part of it... so we'll do it the ugly way: // We only really want the last part of it... so we'll do it the ugly way: