summaryrefslogtreecommitdiffstats
path: root/src/app
diff options
context:
space:
mode:
authorClement Tsang <34804052+ClementTsang@users.noreply.github.com>2022-10-12 16:25:38 -0400
committerGitHub <noreply@github.com>2022-10-12 16:25:38 -0400
commit2a740f48f79498e6a812670176c45c53e0383c05 (patch)
treef6c9c017477931ad5ff8a7624e21cc2dd7fade2b /src/app
parent1e5f0ea2d9dafa49279151b565154d9acf3b59d7 (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')
-rw-r--r--src/app/data_farmer.rs45
-rw-r--r--src/app/data_harvester/cpu.rs11
-rw-r--r--src/app/data_harvester/cpu/heim.rs18
-rw-r--r--src/app/data_harvester/processes/linux.rs1
-rw-r--r--src/app/data_harvester/processes/macos/sysctl_bindings.rs6
-rw-r--r--src/app/data_harvester/temperature.rs2
-rw-r--r--src/app/frozen_state.rs44
-rw-r--r--src/app/states.rs54
-rw-r--r--src/app/widgets.rs17
-rw-r--r--src/app/widgets/cpu_graph.rs175
-rw-r--r--src/app/widgets/disk_table.rs147
-rw-r--r--src/app/widgets/disk_table_widget.rs34
-rw-r--r--src/app/widgets/process_table.rs812
-rw-r--r--src/app/widgets/process_table/proc_widget_column.rs122
-rw-r--r--src/app/widgets/process_table/proc_widget_data.rs274
-rw-r--r--src/app/widgets/process_table/sort_table.rs42
-rw-r--r--src/app/widgets/process_table_widget.rs1169
-rw-r--r--src/app/widgets/temperature_table.rs101
-rw-r--r--src/app/widgets/temperature_table_widget.rs29
19 files changed, 1756 insertions, 1347 deletions
diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs
index a6670712..e409957f 100644
--- a/src/app/data_farmer.rs
+++ b/src/app/data_farmer.rs
@@ -33,7 +33,7 @@ use regex::Regex;
pub type TimeOffset = f64;
pub type Value = f64;
-#[derive(Debug, Default)]
+#[derive(Debug, Default, Clone)]
pub struct TimedData {
pub rx_data: Value,
pub tx_data: Value,
@@ -45,19 +45,11 @@ pub struct TimedData {
pub arc_data: Option<Value>,
}
-pub type StringPidMap = FxHashMap<String, Vec<Pid>>;
-
#[derive(Clone, Debug, Default)]
pub struct ProcessData {
/// A PID to process data map.
pub process_harvest: FxHashMap<Pid, ProcessHarvest>,
- /// A mapping from a process name to any PID with that name.
- pub name_pid_map: StringPidMap,
-
- /// A mapping from a process command to any PID with that name.
- pub cmd_pid_map: StringPidMap,
-
/// A mapping between a process PID to any children process PIDs.
pub process_parent_mapping: FxHashMap<Pid, Vec<Pid>>,
@@ -68,28 +60,10 @@ pub struct ProcessData {
impl ProcessData {
fn ingest(&mut self, list_of_processes: Vec<ProcessHarvest>) {
// TODO: [Optimization] Probably more efficient to all of this in the data collection step, but it's fine for now.
- self.name_pid_map.clear();
- self.cmd_pid_map.clear();
self.process_parent_mapping.clear();
// Reverse as otherwise the pid mappings are in the wrong order.
list_of_processes.iter().rev().for_each(|process_harvest| {
- if let Some(entry) = self.name_pid_map.get_mut(&process_harvest.name) {
- entry.push(process_harvest.pid);
- } else {
- self.name_pid_map
- .insert(process_harvest.name.to_string(), vec![process_harvest.pid]);
- }
-
- if let Some(entry) = self.cmd_pid_map.get_mut(&process_harvest.command) {
- entry.push(process_harvest.pid);
- } else {
- self.cmd_pid_map.insert(
- process_harvest.command.to_string(),
- vec![process_harvest.pid],
- );
- }
-
if let Some(parent_pid) = process_harvest.parent_pid {
if let Some(entry) = self.process_parent_mapping.get_mut(&parent_pid) {
entry.push(process_harvest.pid);
@@ -100,8 +74,6 @@ impl ProcessData {
}
});
- self.name_pid_map.shrink_to_fit();
- self.cmd_pid_map.shrink_to_fit();
self.process_parent_mapping.shrink_to_fit();
let process_pid_map = list_of_processes
@@ -137,14 +109,14 @@ impl ProcessData {
/// collected, and what is needed to convert into a displayable form.
///
/// If the app is *frozen* - that is, we do not want to *display* any changing
-/// data, keep updating this, don't convert to canvas displayable data!
+/// data, keep updating this. As of 2021-09-08, we just clone the current collection
+/// when it freezes to have a snapshot floating around.
///
/// Note that with this method, the *app* thread is responsible for cleaning -
/// not the data collector.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct DataCollection {
pub current_instant: Instant,
- pub frozen_instant: Option<Instant>,
pub timed_data_vec: Vec<(Instant, TimedData)>,
pub network_harvest: network::NetworkHarvest,
pub memory_harvest: memory::MemHarvest,
@@ -167,7 +139,6 @@ impl Default for DataCollection {
fn default() -> Self {
DataCollection {
current_instant: Instant::now(),
- frozen_instant: None,
timed_data_vec: Vec::default(),
network_harvest: network::NetworkHarvest::default(),
memory_harvest: memory::MemHarvest::default(),
@@ -210,14 +181,6 @@ impl DataCollection {
}
}
- pub fn freeze(&mut self) {
- self.frozen_instant = Some(self.current_instant);
- }
-
- pub fn thaw(&mut self) {
- self.frozen_instant = None;
- }
-
pub fn clean_data(&mut self, max_time_millis: u64) {
let current_time = Instant::now();
diff --git a/src/app/data_harvester/cpu.rs b/src/app/data_harvester/cpu.rs
index 23073606..c4131876 100644
--- a/src/app/data_harvester/cpu.rs
+++ b/src/app/data_harvester/cpu.rs
@@ -16,10 +16,15 @@ cfg_if::cfg_if! {
pub type LoadAvgHarvest = [f32; 3];
-#[derive(Default, Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
+pub enum CpuDataType {
+ Avg,
+ Cpu(usize),
+}
+
+#[derive(Debug, Clone)]
pub struct CpuData {
- pub cpu_prefix: String,
- pub cpu_count: Option<usize>,
+ pub data_type: CpuDataType,
pub cpu_usage: f64,
}
diff --git a/src/app/data_harvester/cpu/heim.rs b/src/app/data_harvester/cpu/heim.rs
index be2b251f..cdb6c8c2 100644
--- a/src/app/data_harvester/cpu/heim.rs
+++ b/src/app/data_harvester/cpu/heim.rs
@@ -18,7 +18,8 @@ cfg_if::cfg_if! {
}
}
-use crate::data_harvester::cpu::{CpuData, CpuHarvest, PastCpuTotal, PastCpuWork};
+use crate::data_harvester::cpu::{CpuData, CpuDataType, CpuHarvest, PastCpuTotal, PastCpuWork};
+
use futures::StreamExt;
use std::collections::VecDeque;
@@ -62,8 +63,7 @@ pub async fn get_cpu_data_list(
let present_times = convert_cpu_times(&present);
new_cpu_times.push(present_times);
cpu_deque.push_back(CpuData {
- cpu_prefix: "CPU".to_string(),
- cpu_count: Some(itx),
+ data_type: CpuDataType::Cpu(itx),
cpu_usage: calculate_cpu_usage_percentage(
convert_cpu_times(&past),
present_times,
@@ -72,8 +72,7 @@ pub async fn get_cpu_data_list(
} else {
new_cpu_times.push((0.0, 0.0));
cpu_deque.push_back(CpuData {
- cpu_prefix: "CPU".to_string(),
- cpu_count: Some(itx),
+ data_type: CpuDataType::Cpu(itx),
cpu_usage: 0.0,
});
}
@@ -96,8 +95,7 @@ pub async fn get_cpu_data_list(
(
present_times,
CpuData {
- cpu_prefix: "CPU".to_string(),
- cpu_count: Some(itx),
+ data_type: CpuDataType::Cpu(itx),
cpu_usage: calculate_cpu_usage_percentage(
(*past_cpu_work, *past_cpu_total),
present_times,
@@ -108,8 +106,7 @@ pub async fn get_cpu_data_list(
(
(*past_cpu_work, *past_cpu_total),
CpuData {
- cpu_prefix: "CPU".to_string(),
- cpu_count: Some(itx),
+ data_type: CpuDataType::Cpu(itx),
cpu_usage: 0.0,
},
)
@@ -147,8 +144,7 @@ pub async fn get_cpu_data_list(
*previous_average_cpu_time = Some(new_average_cpu_time);
cpu_deque.push_front(CpuData {
- cpu_prefix: "AVG".to_string(),
- cpu_count: None,
+ data_type: CpuDataType::Avg,
cpu_usage,
})
}
diff --git a/src/app/data_harvester/processes/linux.rs b/src/app/data_harvester/processes/linux.rs
index 99412247..a53a3e9a 100644
--- a/src/app/data_harvester/processes/linux.rs
+++ b/src/app/data_harvester/processes/linux.rs
@@ -116,7 +116,6 @@ fn get_linux_cpu_usage(
}
}
-#[allow(clippy::too_many_arguments)]
fn read_proc(
prev_proc: &PrevProcDetails, stat: &Stat, cpu_usage: f64, cpu_fraction: f64,
use_current_cpu_total: bool, time_difference_in_secs: u64, mem_total_kb: u64,
diff --git a/src/app/data_harvester/processes/macos/sysctl_bindings.rs b/src/app/data_harvester/processes/macos/sysctl_bindings.rs
index 77b8901b..22bc07c6 100644
--- a/src/app/data_harvester/processes/macos/sysctl_bindings.rs
+++ b/src/app/data_harvester/processes/macos/sysctl_bindings.rs
@@ -60,16 +60,16 @@ pub(crate) struct extern_proc {
/// Process identifier.
pub p_pid: pid_t,
- /// Save parent pid during ptrace. XXX
+ /// Save parent pid during ptrace.
pub p_oppid: pid_t,
- /// Sideways return value from fdopen. XXX
+ /// Sideways return value from fdopen.
pub p_dupfd: i32,
/// where user stack was allocated
pub user_stack: caddr_t,
- /// XXX Which thread is exiting?
+ /// Which thread is exiting?
pub exit_thread: *mut c_void,
/// allow to debug
diff --git a/src/app/data_harvester/temperature.rs b/src/app/data_harvester/temperature.rs
index 23465588..a9855f51 100644
--- a/src/app/data_harvester/temperature.rs
+++ b/src/app/data_harvester/temperature.rs
@@ -26,7 +26,7 @@ pub struct TempHarvest {
pub temperature: f32,
}
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Copy)]
pub enum TemperatureType {
Celsius,
Kelvin,
diff --git a/src/app/frozen_state.rs b/src/app/frozen_state.rs
new file mode 100644
index 00000000..4fc7293d
--- /dev/null
+++ b/src/app/frozen_state.rs
@@ -0,0 +1,44 @@
+use super::DataCollection;
+
+/// The [`FrozenState`] indicates whether the application state should be frozen. It is either not frozen or
+/// frozen and containing a copy of the state at the time.
+pub enum FrozenState {
+ NotFrozen,
+ Frozen(Box<DataCollection>),
+}
+
+impl Default for FrozenState {
+ fn default() -> Self {
+ Self::NotFrozen
+ }
+}
+
+pub type IsFrozen = bool;
+
+impl FrozenState {
+ /// Checks whether the [`FrozenState`] is currently frozen.
+ pub fn is_frozen(&self) -> IsFrozen {
+ matches!(self, FrozenState::Frozen(_))
+ }
+
+ /// Freezes the [`FrozenState`].
+ pub fn freeze(&mut self, data: Box<DataCollection>) {
+ *self = FrozenState::Frozen(data);
+ }
+
+ /// Unfreezes the [`FrozenState`].
+ pub fn thaw(&mut self) {
+ *self = FrozenState::NotFrozen;
+ }
+
+ /// Toggles the [`FrozenState`] and returns whether it is now frozen.
+ pub fn toggle(&mut self, data: &DataCollection) -> IsFrozen {
+ if self.is_frozen() {
+ self.thaw();
+ false
+ } else {
+ self.freeze(Box::new(data.clone()));
+ true
+ }
+ }
+}
diff --git a/src/app/states.rs b/src/app/states.rs
index 2ca18c0b..a1465640 100644
--- a/src/app/states.rs
+++ b/src/app/states.rs
@@ -4,11 +4,10 @@ use unicode_segmentation::GraphemeCursor;
use crate::{
app::{layout_manager::BottomWidgetType, query::*},
- components::text_table::{CellContent, TableComponentColumn, TableComponentState, WidthBounds},
constants,
};
-use super::widgets::{DiskWidgetState, ProcWidget, TempWidgetState};
+use super::widgets::{CpuWidgetState, DiskTableWidget, ProcWidget, TempWidgetState};
#[derive(Debug)]
pub enum ScrollDirection {
@@ -30,13 +29,6 @@ pub enum CursorDirection {
Right,
}
-/// Meant for canvas operations involving table column widths.
-#[derive(Default)]
-pub struct CanvasTableWidthState {
- pub desired_column_widths: Vec<u16>,
- pub calculated_column_widths: Vec<u16>,
-}
-
#[derive(PartialEq, Eq)]
pub enum KillSignal {
Cancel,
@@ -184,42 +176,6 @@ impl NetState {
}
}
-pub struct CpuWidgetState {
- pub current_display_time: u64,
- pub is_legend_hidden: bool,
- pub autohide_timer: Option<Instant>,
- pub table_state: TableComponentState,
- pub is_multi_graph_mode: bool,
-}
-
-impl CpuWidgetState {
- pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
- const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
- const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [
- WidthBounds::soft_from_str("CPU", Some(0.5)),
- WidthBounds::soft_from_str("Use%", Some(0.5)),
- ];
-
- let table_state = TableComponentState::new(
- CPU_LEGEND_HEADER
- .iter()
- .zip(WIDTHS)
- .map(|(c, width)| {
- TableComponentColumn::new_custom(CellContent::new(*c, None), width)
- })
- .collect(),
- );
-
- CpuWidgetState {
- current_display_time,
- is_legend_hidden: false,
- autohide_timer,
- table_state,
- is_multi_graph_mode: false,
- }
- }
-}
-
pub struct CpuState {
pub force_update: Option<u64>,
pub widget_states: HashMap<u64, CpuWidgetState>,
@@ -296,19 +252,19 @@ impl TempState {
}
pub struct DiskState {
- pub widget_states: HashMap<u64, DiskWidgetState>,
+ pub widget_states: HashMap<u64, DiskTableWidget>,
}
impl DiskState {
- pub fn init(widget_states: HashMap<u64, DiskWidgetState>) -> Self {
+ pub fn init(widget_states: HashMap<u64, DiskTableWidget>) -> Self {
DiskState { widget_states }
}
- pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskWidgetState> {
+ pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut DiskTableWidget> {
self.widget_states.get_mut(&widget_id)
}
- pub fn get_widget_state(&self, widget_id: u64) -> Option<&DiskWidgetState> {
+ pub fn get_widget_state(&self, widget_id: u64) -> Option<&DiskTableWidget> {
self.widget_states.get(&widget_id)
}
}
diff --git a/src/app/widgets.rs b/src/app/widgets.rs
index 99e9e210..6bdb0e55 100644
--- a/src/app/widgets.rs
+++ b/src/app/widgets.rs
@@ -1,8 +1,13 @@
-pub mod process_table_widget;
-pub use process_table_widget::*;
+// FIXME: Move this outside of app, along with components?
-pub mod temperature_table_widget;
-pub use temperature_table_widget::*;
+pub mod process_table;
+pub use process_table::*;
-pub mod disk_table_widget;
-pub use disk_table_widget::*;
+pub mod temperature_table;
+pub use temperature_table::*;
+
+pub mod disk_table;
+pub use disk_table::*;
+
+pub mod cpu_graph;
+pub use cpu_graph::*;
diff --git a/src/app/widgets/cpu_graph.rs b/src/app/widgets/cpu_graph.rs
new file mode 100644
index 00000000..4c127bb4
--- /dev/null
+++ b/src/app/widgets/cpu_graph.rs
@@ -0,0 +1,175 @@
+use std::{borrow::Cow, time::Instant};
+
+use concat_string::concat_string;
+
+use tui::{style::Style, text::Text, widgets::Row};
+
+use crate::{
+ app::{data_harvester::cpu::CpuDataType, AppConfigFields},
+ canvas::{canvas_colours::CanvasColours, Painter},
+ components::data_table::{
+ Column, ColumnHeader, DataTable, DataTableColumn, DataTableProps, DataTableStyling,
+ DataToCell,
+ },
+ data_conversion::CpuWidgetData,
+ utils::gen_util::truncate_text,
+};
+
+#[derive(Default)]
+pub struct CpuWidgetStyling {
+ pub all: Style,
+ pub avg: Style,
+ pub entries: Vec<Style>,
+}
+
+impl CpuWidgetStyling {
+ fn from_colours(colours: &CanvasColours) -> Self {
+ let entries = if colours.cpu_colour_styles.is_empty() {
+ vec![Style::default()]
+ } else {
+ colours.cpu_colour_styles.clone()
+ };
+
+ Self {
+ all: colours.all_colour_style,
+ avg: colours.avg_colour_style,
+ entries,
+ }
+ }
+}
+
+pub enum CpuWidgetColumn {
+ CPU,
+ Use,
+}
+
+impl ColumnHeader for CpuWidgetColumn {
+ fn text(&self) -> Cow<'static, str> {
+ match self {
+ CpuWidgetColumn::CPU => "CPU".into(),
+ CpuWidgetColumn::Use => "Use%".into(),
+ }
+ }
+}
+
+impl DataToCell<CpuWidgetColumn> for CpuWidgetData {
+ fn to_cell<'a>(&'a self, column: &CpuWidgetColumn, calculated_width: u16) -> Option<Text<'a>> {
+ const CPU_HIDE_BREAKPOINT: u16 = 5;
+
+ // This is a bit of a hack, but apparently we can avoid having to do any fancy checks
+ // of showing the "All" on a specific column if the other is hidden by just always
+ // showing it on the CPU (first) column - if there isn't room for it, it will just collapse
+ // down.
+ //
+ // This is the same for the use percentages - we just *always* show them, and *always* hide the CPU column if
+ // it is too small.
+ match &self {
+ CpuWidgetData::All => match column {
+ CpuWidgetColumn::CPU => Some(truncate_text("All", calculated_width)),
+ CpuWidgetColumn::Use => None,
+ },
+ CpuWidgetData::Entry {
+ data_type,
+ data: _,
+ last_entry,
+ } => match column {
+ CpuWidgetColumn::CPU => {
+ if calculated_width == 0 {
+ None
+ } else {
+ match data_type {
+ CpuDataType::Avg => Some(truncate_text("AVG", calculated_width)),
+ CpuDataType::Cpu(index) => {
+ let index_str = index.to_string();
+ let text = if calculated_width < CPU_HIDE_BREAKPOINT {
+ truncate_text(&index_str, calculated_width)
+ } else {
+ truncate_text(
+ &concat_string!("CPU", index_str),
+ calculated_width,
+ )
+ };
+
+ Some(text)
+ }
+ }
+ }
+ }
+ CpuWidgetColumn::Use => Some(truncate_text(
+ &format!("{:.0}%", last_entry.round()),
+ calculated_width,
+ )),
+ },
+ }
+ }
+
+ #[inline(always)]
+ fn style_row<'a>(&self, row: Row<'a>, painter: &Painter) -> Row<'a> {
+ let style = match self {
+ CpuWidgetData::All => painter.colours.all_colour_style,
+ CpuWidgetData::Entry {
+ data_type,
+ data: _,
+ last_entry: _,
+ } => match data_type {
+ CpuDataType::Avg => painter.colours.avg_colour_style,
+ CpuDataType::Cpu(index) => {
+ painter.colours.cpu_colour_styles
+ [index % painter.colours.cpu_colour_styles.len()]
+ }
+ },
+ };
+
+ row.style(style)
+ }
+
+ fn column_widths<C: DataTableColumn<CpuWidgetColumn>>(
+ _data: &[Self], _columns: &[C],
+ ) -> Vec<u16>
+ where
+ Self: Sized,
+ {
+ vec![1, 3]
+ }
+}
+
+pub struct CpuWidgetState {
+ pub current_display_time: u64,
+ pub is_legend_hidden: bool,
+ pub show_avg: bool,
+ pub autohide_timer: Option<Instant>,
+ pub table: DataTable<CpuWidgetData, CpuWidgetColumn>,
+ pub styling: CpuWidgetStyling,
+}
+
+impl CpuWidgetState {
+ pub fn new(
+ config: &AppConfigFields, current_display_time: u64, autohide_timer: Option<Instant>,
+ colours: &CanvasColours,
+ ) -> Self {
+ const COLUMNS: [Column<CpuWidgetColumn>; 2] = [
+ Column::soft(CpuWidgetColumn::CPU, Some(0.5)),
+ Column::soft(CpuWidgetColumn::Use, Some(0.5)),
+ ];
+
+ let props = DataTableProps {
+ title: None,
+ table_gap: config.table_gap,
+ left_to_right: false,
+ is_basic: false,
+ show_table_scroll_position: false, // TODO: Should this be possible?
+ show_current_entry_when_unfocused: true,
+ };
+
+ let styling = DataTableStyling::from_colours(colours);
+
+ CpuWidgetState {
+ current_display_time,
+ is_legend_hidden: false,
+ show_avg: config.show_average_cpu,
+ autohide_timer,
+ table: DataTable::new(COLUMNS, props, styling),
+ styling: CpuWidgetStyling::from_colours(colours),
+ }
+ }
+}
diff --git a/src/app/widgets/disk_table.rs b/src/app/widgets/disk_table.rs
new file mode 100644
index 00000000..be444b45
--- /dev/null
+++ b/src/app/widgets/disk_table.rs
@@ -0,0 +1,147 @@
+use std::{borrow::Cow, cmp::max};
+
+use kstring::KString;
+use tui::text::Text;
+
+use crate::{
+ app::AppConfigFields,
+ canvas::canvas_colours::CanvasColours,
+ components::data_table::{
+ Column, ColumnHeader, DataTable, DataTableColumn, DataTableProps, DataTableStyling,
+ DataToCell,
+ },
+ utils::gen_util::{get_decimal_bytes, truncate_text},
+};
+
+#[derive(Clone)]
+pub struct DiskWidgetData {
+ pub name: KString,
+ pub mount_point: KString,
+ pub free_bytes: Option<u64>,
+ pub used_bytes: Option<u64>,
+ pub total_bytes: Option<u64>,
+ pub io_read: KString,
+ pub io_write: KString,
+}
+
+impl DiskWidgetData {
+ pub fn free_space(&self) -> KString {
+ if let Some(free_bytes) = self.free_bytes {
+ let converted_free_space = get_decimal_bytes(free_bytes);
+ format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into()
+ } else {
+ "N/A".into()
+ }
+ }
+
+ pub fn total_space(&self) -> KString {
+ if let Some(total_bytes) = self.total_bytes {
+ let converted_total_space = get_decimal_bytes(total_bytes);
+ format!(
+ "{:.*}{}",
+ 0, converted_total_space.0, converted_total_space.1
+ )
+ .into()
+ } else {
+ "N/A".into()
+ }
+ }
+
+ pub fn usage(&self) -> KString {
+ if let (Some(used_bytes), Some(total_bytes)) = (self.used_bytes, self.total_bytes) {
+ format!("{:.0}%", used_bytes as f64 / total_bytes as f64 * 100_f64).into()
+ } else {
+ "N/A".into()
+ }
+ }
+}
+
+pub enum DiskWidgetColumn {
+ Disk,
+ Mount,
+ Used,
+ Free,
+ Total,
+ IoRead,
+ IoWrite,
+}
+
+impl ColumnHeader for DiskWidgetColumn {
+ fn text(&self) -> Cow<'static, str> {
+ match self {
+ DiskWidgetColumn::Disk => "Disk",
+ DiskWidgetColumn::Mount => "Mount",
+ DiskWidgetColumn::Used => "Used",
+ DiskWidgetColumn::Free => "Free",
+ DiskWidgetColumn::Total => "Total",
+ DiskWidgetColumn::IoRead => "R/s",
+ DiskWidgetColumn::IoWrite => "W/s",
+ }
+ .into()
+ }
+}
+
+impl DataToCell<DiskWidgetColumn> for DiskWidgetData {
+ fn to_cell<'a>(&'a self, column: &DiskWidgetColumn, calculated_width: u16) -> Option<Text<'a>> {
+ let text = match column {
+ DiskWidgetColumn::Disk => truncate_text(&self.name, calculated_width),
+ DiskWidgetColumn::Mount => truncate_text(&self.mount_point, calculated_width),
+ DiskWidgetColumn::Used => truncate_text(&self.usage(), calculated_width),
+ DiskWidgetColumn::Free => truncate_text(&self.free_space(), calculated_width),
+ DiskWidgetColumn::Total => truncate_text(&self.total_space(), calculated_width),
+ DiskWidgetColumn::IoRead => truncate_text(&self.io_read, calculated_width),
+ DiskWidgetColumn::IoWrite => truncate_text(&self.io_write, calculated_width),
+ };
+
+ Some(text)
+ }
+
+ fn column_widths<C: DataTableColumn<DiskWidgetColumn>>(
+ data: &[Self], _columns: &[C],
+ ) -> Vec<u16>
+ where
+ Self: Sized,
+ {
+ let mut widths = vec![0; 7];
+
+ data.iter().for_each(|row| {
+ widths[0] = max(widths[0], row.name.len() as u16);
+ widths[1] = max(widths[1], row.mount_point.len() as u16);
+ });
+
+ widths
+ }
+}
+
+pub struct DiskTableWidget {
+ pub table: DataTable<DiskWidgetData, DiskWidgetColumn>,
+}
+
+impl DiskTableWidget {
+ pub fn new(config: &AppConfigFields, colours: &CanvasColours) -> Self {
+ const COLUMNS: [Column<DiskWidgetColumn>; 7] = [
+ Column::soft(DiskWidgetColumn::Disk, Some(0.2)),
+ Column::soft(DiskWidgetColumn::Mount, Some(0.2)),
+ Column::hard(DiskWidgetColumn::Used, 4),
+ Column::hard(DiskWidgetColumn::Free, 6),
+ Column::hard(DiskWidgetColumn::Total, 6),
+ Column::hard(DiskWidgetColumn::IoRead, 7),
+ Column::hard(DiskWidgetColumn::IoWrite, 7),
+ ];
+
+ let props = DataTableProps {
+ title: Some(" Disks ".into()),
+ table_gap: config.table_gap,
+ left_to_right: true,
+ is_basic: config.use_basic_mode,
+ show_table_scroll_position: config.show_table_scroll_position,
+ show_current_entry_when_unfocused: false,
+ };
+
+ let styling = DataTableStyling::from_colours(colours);
+
+ Self {
+ table: DataTable::new(COLUMNS, props, styling),
+ }
+ }
+}
diff --git a/src/app/widgets/disk_table_widget.rs b/src/app/widgets/disk_table_widget.rs
deleted file mode 100644
index ecffb8cf..00000000
--- a/src/app/widgets/disk_table_widget.rs
+++ /dev/null
@@ -1,34 +0,0 @@
-use crate::components::text_table::{
- CellContent, TableComponentColumn, TableComponentState, WidthBounds,
-};
-
-pub struct DiskWidgetState {
- pub table_state: TableComponentState,
-}
-
-impl Default for DiskWidgetState {
- fn default() -> Self {
- const DISK_HEADERS: [&str; 7] = ["Disk", "Mount", "Used", "Free", "Total", "R/s", "W/s"];
- const WIDTHS: [WidthBounds; DISK_HEADERS.len()] = [
- WidthBounds::soft_from_str(DISK_HEADERS[0], Some(0.2)),
- WidthBounds::soft_from_str(DISK_HEADERS[1], Some(0.2)),
- WidthBounds::Hard(4),
- WidthBounds::Hard(6),
- WidthBounds::Hard(6),
- WidthBounds::Hard(7),
- WidthBounds::Hard(7),
- ];
-
- DiskWidgetState {
- table_state: TableComponentState::new(
- DISK_HEADERS
- .iter()
- .zip(WIDTHS)
- .map(|(header, width)| {
- TableComponentColumn::new_custom(CellContent::new(*header, None), width)
- })
- .collect(),
- ),
- }
- }
-}
diff --git a/src/app/widgets/process_table.rs b/src/app/widgets/process_table.rs
new file mode 100644
index 00000000..967af03c
--- /dev/null
+++ b/src/app/widgets/process_table.rs
@@ -0,0 +1,812 @@
+use std::{borrow::Cow, collections::hash_map::Entry};
+
+use crate::{
+ app::{
+ data_farmer::{DataCollection, ProcessData},
+ data_harvester::processes::ProcessHarvest,
+ query::*,
+ AppConfigFields, AppSearchState,
+ },
+ canvas::canvas_colours::CanvasColours,
+ components::data_table::{
+ Column, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps,
+ DataTableStyling, SortColumn, SortDataTable, SortDataTableProps, SortOrder,
+ },
+ Pid,
+};
+
+use fxhash::{FxHashMap, FxHashSet};
+use itertools::Itertools;
+
+pub mod proc_widget_column;
+pub use proc_widget_column::*;
+
+pub mod proc_widget_data;
+pub use proc_widget_data::*;
+
+mod sort_table;
+use sort_table::SortTableColumn;
+
+/// ProcessSearchState only deals with process' search's current settings and state.
+pub struct ProcessSearchState {
+ pub search_state: AppSearchState,
+ pub is_ignoring_case: bool,
+ pub is_searching_whole_word: bool,
+ pub is_searching_with_regex: bool,