diff options
author | Clement Tsang <34804052+ClementTsang@users.noreply.github.com> | 2022-10-12 16:25:38 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-12 16:25:38 -0400 |
commit | 2a740f48f79498e6a812670176c45c53e0383c05 (patch) | |
tree | f6c9c017477931ad5ff8a7624e21cc2dd7fade2b /src/app/widgets/process_table | |
parent | 1e5f0ea2d9dafa49279151b565154d9acf3b59d7 (diff) |
refactor: tables V2 (#749)
* refactor: move to new data table implementation
* more work towards refactor
* move disk and temp over, fix longstanding bug with disk and temp if removing the last value and selected
* work towards porting over CPU
work towards porting over CPU
fix typo
partially port over cpu, fix some potentially inefficient concat_string calls
more work towards cpu widget migration
some refactoring
* sortable data
sortable data
more refactoring
some sort refactoring
more refactoringgggg
column refactoring
renaming and reorganizing
more refactoring regarding column logic
add sort arrows again
* move over sort menu
* port over process
port over process
precommit
temp
temp two, remember to squash
work
fix broken ltr calculation and CPU hiding
add back row styling
temp
fix a bunch of issues, get proc working
more fixes around click
fix frozen issues
* fix dd process killing
* revert some of the persistent config changes from #257
* fix colouring for trees
* fix missing entries in tree
* keep columns if there is no data
* add and remove tests
* Fix ellipsis
Diffstat (limited to 'src/app/widgets/process_table')
-rw-r--r-- | src/app/widgets/process_table/proc_widget_column.rs | 122 | ||||
-rw-r--r-- | src/app/widgets/process_table/proc_widget_data.rs | 274 | ||||
-rw-r--r-- | src/app/widgets/process_table/sort_table.rs | 42 |
3 files changed, 438 insertions, 0 deletions
diff --git a/src/app/widgets/process_table/proc_widget_column.rs b/src/app/widgets/process_table/proc_widget_column.rs new file mode 100644 index 00000000..5578598d --- /dev/null +++ b/src/app/widgets/process_table/proc_widget_column.rs @@ -0,0 +1,122 @@ +use crate::{ + components::data_table::{ColumnHeader, SortsRow}, + utils::gen_util::sort_partial_fn, +}; + +use std::{borrow::Cow, cmp::Reverse}; + +use super::ProcWidgetData; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum ProcColumn { + CpuPercent, + MemoryVal, + MemoryPercent, + Pid, + Count, + Name, + Command, + ReadPerSecond, + WritePerSecond, + TotalRead, + TotalWrite, + State, + User, +} + +impl ColumnHeader for ProcColumn { + fn text(&self) -> Cow<'static, str> { + match self { + ProcColumn::CpuPercent => "CPU%", + ProcColumn::MemoryVal => "Mem", + ProcColumn::MemoryPercent => "Mem%", + ProcColumn::Pid => "PID", + ProcColumn::Count => "Count", + ProcColumn::Name => "Name", + ProcColumn::Command => "Command", + ProcColumn::ReadPerSecond => "R/s", + ProcColumn::WritePerSecond => "W/s", + ProcColumn::TotalRead => "T.Read", + ProcColumn::TotalWrite => "T.Write", + ProcColumn::State => "State", + ProcColumn::User => "User", + } + .into() + } + + fn header(&self) -> Cow<'static, str> { + match self { + ProcColumn::CpuPercent => "CPU%(c)", + ProcColumn::MemoryVal => "Mem(m)", + ProcColumn::MemoryPercent => "Mem%(m)", + ProcColumn::Pid => "PID(p)", + ProcColumn::Count => "Count", + ProcColumn::Name => "Name(n)", + ProcColumn::Command => "Command(n)", + ProcColumn::ReadPerSecond => "R/s", + ProcColumn::WritePerSecond => "W/s", + ProcColumn::TotalRead => "T.Read", + ProcColumn::TotalWrite => "T.Write", + ProcColumn::State => "State", + ProcColumn::User => "User", + } + .into() + } +} + +impl SortsRow<ProcWidgetData> for ProcColumn { + fn sort_data(&self, data: &mut [ProcWidgetData], descending: bool) { + match self { + ProcColumn::CpuPercent => { + data.sort_by(|a, b| { + sort_partial_fn(descending)(a.cpu_usage_percent, b.cpu_usage_percent) + }); + } + ProcColumn::MemoryVal | ProcColumn::MemoryPercent => { + data.sort_by(|a, b| sort_partial_fn(descending)(&a.mem_usage, &b.mem_usage)); + } + ProcColumn::Pid => { + data.sort_by(|a, b| sort_partial_fn(descending)(a.pid, b.pid)); + } + ProcColumn::Count => { + data.sort_by(|a, b| sort_partial_fn(descending)(a.num_similar, b.num_similar)); + } + ProcColumn::Name | ProcColumn::Command => { + if descending { + data.sort_by_cached_key(|pd| Reverse(pd.id.to_lowercase())); + } else { + data.sort_by_cached_key(|pd| pd.id.to_lowercase()); + } + } + ProcColumn::ReadPerSecond => { + data.sort_by(|a, b| sort_partial_fn(descending)(a.rps, b.rps)); + } + ProcColumn::WritePerSecond => { + data.sort_by(|a, b| sort_partial_fn(descending)(a.wps, b.wps)); + } + ProcColumn::TotalRead => { + data.sort_by(|a, b| sort_partial_fn(descending)(a.total_read, b.total_read)); + } + ProcColumn::TotalWrite => { + data.sort_by(|a, b| sort_partial_fn(descending)(a.total_write, b.total_write)); + } + ProcColumn::State => { + if descending { + data.sort_by_cached_key(|pd| Reverse(pd.process_state.to_lowercase())); + } else { + data.sort_by_cached_key(|pd| pd.process_state.to_lowercase()); + } + } + ProcColumn::User => { + #[cfg(target_family = "unix")] + { + if descending { + data.sort_by_cached_key(|pd| Reverse(pd.user.to_lowercase())); + } else { + data.sort_by_cached_key(|pd| pd.user.to_lowercase()); + } + } + } + } + } +} diff --git a/src/app/widgets/process_table/proc_widget_data.rs b/src/app/widgets/process_table/proc_widget_data.rs new file mode 100644 index 00000000..4957c87f --- /dev/null +++ b/src/app/widgets/process_table/proc_widget_data.rs @@ -0,0 +1,274 @@ +use std::{ + cmp::{max, Ordering}, + fmt::Display, +}; + +use concat_string::concat_string; +use tui::{text::Text, widgets::Row}; + +use crate::{ + app::data_harvester::processes::ProcessHarvest, + canvas::Painter, + components::data_table::{DataTableColumn, DataToCell}, + data_conversion::{binary_byte_string, dec_bytes_per_second_string, dec_bytes_string}, + utils::gen_util::truncate_text, + Pid, +}; + +use super::proc_widget_column::ProcColumn; + +#[derive(Clone)] +enum IdType { + Name(String), + Command(String), +} + +#[derive(Clone)] +pub struct Id { + id_type: IdType, + prefix: Option<String>, +} + +impl Id { + /// Returns the ID as a lowercase [`String`], with no prefix. This is primarily useful for + /// cases like sorting where we treat everything as the same case (e.g. `Discord` comes before + /// `dkms`). + pub fn to_lowercase(&self) -> String { + match &self.id_type { + IdType::Name(name) => name.to_lowercase(), + IdType::Command(cmd) => cmd.to_lowercase(), + } + } + + /// Return the ID as a borrowed [`str`] with no prefix. + pub fn as_str(&self) -> &str { + match &self.id_type { + IdType::Name(name) => name.as_str(), + IdType::Command(cmd) => cmd.as_str(), + } + } + + /// Returns the ID as a [`String`] with prefix. + pub fn to_prefixed_string(&self) -> String { + if let Some(prefix) = &self.prefix { + concat_string!( + prefix, + match &self.id_type { + IdType::Name(name) => name, + IdType::Command(cmd) => cmd, + } + ) + } else { + match &self.id_type { + IdType::Name(name) => name.to_string(), + IdType::Command(cmd) => cmd.to_string(), + } + } + } +} + +impl Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +// TODO: Can reduce this to 32 bytes. +#[derive(PartialEq, Clone)] +pub enum MemUsage { + Percent(f64), + Bytes(u64), +} + +impl PartialOrd for MemUsage { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + match (self, other) { + (MemUsage::Percent(a), MemUsage::Percent(b)) => a.partial_cmp(b), + (MemUsage::Bytes(a), MemUsage::Bytes(b)) => a.partial_cmp(b), + _ => unreachable!(), + } + } +} + +impl Display for MemUsage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MemUsage::Percent(percent) => f.write_fmt(format_args!("{:.1}%", percent)), + MemUsage::Bytes(bytes) => f.write_str(&binary_byte_string(*bytes)), + } + } +} + +#[derive(Clone)] +pub struct ProcWidgetData { + pub pid: Pid, + pub ppid: Option<Pid>, + pub id: Id, + pub cpu_usage_percent: f64, + pub mem_usage: MemUsage, + pub rps: u64, + pub wps: u64, + pub total_read: u64, + pub total_write: u64, + pub process_state: String, + pub process_char: char, + #[cfg(target_family = "unix")] + pub user: String, + pub num_similar: u64, + pub disabled: bool, +} + +impl ProcWidgetData { + pub fn from_data(process: &ProcessHarvest, is_command: bool, is_mem_percent: bool) -> Self { + let id = Id { + id_type: if is_command { + IdType::Command(process.command.clone()) + } else { + IdType::Name(process.name.clone()) + }, + prefix: None, + }; + + let mem_usage = if is_mem_percent { + MemUsage::Percent(process.mem_usage_percent) + } else { + MemUsage::Bytes(process.mem_usage_bytes) + }; + + Self { + pid: process.pid, + ppid: process.parent_pid, + id, + cpu_usage_percent: process.cpu_usage_percent, + mem_usage, + rps: process.read_bytes_per_sec, + wps: process.write_bytes_per_sec, + total_read: process.total_read_bytes, + total_write: process.total_write_bytes, + process_state: process.process_state.0.clone(), + process_char: process.process_state.1, + #[cfg(target_family = "unix")] + user: process.user.to_string(), + num_similar: 1, + disabled: false, + } + } + + pub fn num_similar(mut self, num_similar: u64) -> Self { + self.num_similar = num_similar; + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + pub fn prefix(mut self, prefix: Option<String>) -> Self { + self.id.prefix = prefix; + self + } + + pub fn add(&mut self, other: &Self) { + self.cpu_usage_percent += other.cpu_usage_percent; + self.mem_usage = match (&self.mem_usage, &other.mem_usage) { + (MemUsage::Percent(a), MemUsage::Percent(b)) => MemUsage::Percent(a + b), + (MemUsage::Bytes(a), MemUsage::Bytes(b)) => MemUsage::Bytes(a + b), + (MemUsage::Percent(_), MemUsage::Bytes(_)) + | (MemUsage::Bytes(_), MemUsage::Percent(_)) => { + unreachable!("trying to add together two different memory usage types!") + } + }; + self.rps += other.rps; + self.wps += other.wps; + self.total_read += other.total_read; + self.total_write += other.total_write; + } + + fn to_string(&self, column: &ProcColumn) -> String { + match column { + ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent), + ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string(), + ProcColumn::Pid => self.pid.to_string(), + ProcColumn::Count => self.num_similar.to_string(), + ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string(), + ProcColumn::ReadPerSecond => dec_bytes_per_second_string(self.rps), + ProcColumn::WritePerSecond => dec_bytes_per_second_string(self.wps), + ProcColumn::TotalRead => dec_bytes_string(self.total_read), + ProcColumn::TotalWrite => dec_bytes_string(self.total_write), + ProcColumn::State => self.process_char.to_string(), + ProcColumn::User => { + #[cfg(target_family = "unix")] + { + self.user.clone() + } + #[cfg(not(target_family = "unix"))] + { + "".to_string() + } + } + } + } +} + +impl DataToCell<ProcColumn> for ProcWidgetData { + fn to_cell<'a>(&'a self, column: &ProcColumn, calculated_width: u16) -> Option<Text<'a>> { + Some(truncate_text( + &match column { + ProcColumn::CpuPercent => { + format!("{:.1}%", self.cpu_usage_percent) + } + ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string(), + ProcColumn::Pid => self.pid.to_string(), + ProcColumn::Count => self.num_similar.to_string(), + ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string(), + ProcColumn::ReadPerSecond => dec_bytes_per_second_string(self.rps), + ProcColumn::WritePerSecond => dec_bytes_per_second_string(self.wps), + ProcColumn::TotalRead => dec_bytes_string(self.total_read), + ProcColumn::TotalWrite => dec_bytes_string(self.total_write), + ProcColumn::State => { + if calculated_width < 8 { + self.process_char.to_string() + } else { + self.process_state.clone() + } + } + ProcColumn::User => { + #[cfg(target_family = "unix")] + { + self.user.clone() + } + #[cfg(not(target_family = "unix"))] + { + "".to_string() + } + } + }, + calculated_width, + )) + } + + #[inline(always)] + fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> { + if self.disabled { + row.style(painter.colours.disabled_text_style) + } else { + row + } + } + + fn column_widths<C: DataTableColumn<ProcColumn>>(data: &[Self], columns: &[C]) -> Vec<u16> + where + Self: Sized, + { + let mut widths = vec![0; columns.len()]; + + for d in data { + for (w, c) in widths.iter_mut().zip(columns) { + *w = max(*w, d.to_string(c.inner()).len() as u16); + } + } + + widths + } +} diff --git a/src/app/widgets/process_table/sort_table.rs b/src/app/widgets/process_table/sort_table.rs new file mode 100644 index 00000000..5ace0ecb --- /dev/null +++ b/src/app/widgets/process_table/sort_table.rs @@ -0,0 +1,42 @@ +use std::borrow::Cow; + +use tui::text::Text; + +use crate::{ + components::data_table::{ColumnHeader, DataTableColumn, DataToCell}, + utils::gen_util::truncate_text, +}; + +pub struct SortTableColumn; + +impl ColumnHeader for SortTableColumn { + fn text(&self) -> std::borrow::Cow<'static, str> { + "Sort By".into() + } +} + +impl DataToCell<SortTableColumn> for &'static str { + fn to_cell<'a>(&'a self, _column: &SortTableColumn, calculated_width: u16) -> Option<Text<'a>> { + Some(truncate_text(self, calculated_width)) + } + + fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16> + where + Self: Sized, + { + vec![data.iter().map(|d| d.len() as u16).max().unwrap_or(0)] + } +} + +impl DataToCell<SortTableColumn> for Cow<'static, str> { + fn to_cell<'a>(&'a self, _column: &SortTableColumn, calculated_width: u16) -> Option<Text<'a>> { + Some(truncate_text(self, calculated_width)) + } + + fn column_widths<C: DataTableColumn<SortTableColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16> + where + Self: Sized, + { + vec![data.iter().map(|d| d.len() as u16).max().unwrap_or(0)] + } +} |