From 74293aa243b6e442c25619c62a85236871d20960 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Sun, 29 Aug 2021 01:19:34 -0400 Subject: [PATCH] refactor: another pass on sorting and columns --- src/app/widgets/base/sort_text_table.rs | 169 +++++++++++++----- src/app/widgets/cpu.rs | 6 +- src/app/widgets/disk.rs | 16 +- src/app/widgets/net.rs | 2 - src/app/widgets/process.rs | 226 ++++++++++++++++++++++-- src/app/widgets/temp.rs | 6 +- 6 files changed, 350 insertions(+), 75 deletions(-) diff --git a/src/app/widgets/base/sort_text_table.rs b/src/app/widgets/base/sort_text_table.rs index b85ecf5c..4a60504a 100644 --- a/src/app/widgets/base/sort_text_table.rs +++ b/src/app/widgets/base/sort_text_table.rs @@ -52,34 +52,80 @@ fn get_shortcut_name(e: &KeyEvent) -> String { format!("({}{})", modifier, key).into() } -#[derive(Debug)] -enum SortStatus { +#[derive(Copy, Clone, Debug)] +pub enum SortStatus { NotSorting, SortAscending, SortDescending, } -/// A [`SortableColumn`] represents some column in a [`SortableTextTable`]. +/// A trait for sortable columns. +pub trait SortableColumn { + /// Returns the shortcut for the column, if it exists. + fn shortcut(&self) -> &Option<(KeyEvent, String)>; + + /// Returns whether the column defaults to sorting in descending order or not. + fn default_descending(&self) -> bool; + + /// Returns whether the column is currently selected for sorting, and if so, + /// what direction. + fn sorting_status(&self) -> SortStatus; + + /// Sets the sorting status. + fn set_sorting_status(&mut self, sorting_status: SortStatus); + + fn display_name(&self) -> Cow<'static, str>; + + fn get_desired_width(&self) -> &DesiredColumnWidth; + + fn get_x_bounds(&self) -> Option<(u16, u16)>; + + fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>); +} + +impl TableColumn for T +where + T: SortableColumn, +{ + fn display_name(&self) -> Cow<'static, str> { + self.display_name() + } + + fn get_desired_width(&self) -> &DesiredColumnWidth { + self.get_desired_width() + } + + fn get_x_bounds(&self) -> Option<(u16, u16)> { + self.get_x_bounds() + } + + fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>) { + self.set_x_bounds(x_bounds) + } +} + +/// A [`SimpleSortableColumn`] represents some column in a [`SortableTextTable`]. #[derive(Debug)] -pub struct SortableColumn { +pub struct SimpleSortableColumn { pub shortcut: Option<(KeyEvent, String)>, pub default_descending: bool, pub internal: SimpleColumn, - sorting: SortStatus, + + /// Whether this column is currently selected for sorting, and which direction. + sorting_status: SortStatus, } -impl SortableColumn { - /// Creates a new [`SortableColumn`]. +impl SimpleSortableColumn { + /// Creates a new [`SimpleSortableColumn`]. fn new( - shortcut_name: Cow<'static, str>, shortcut: Option, default_descending: bool, - desired_width: DesiredColumnWidth, + full_name: Cow<'static, str>, shortcut: Option<(KeyEvent, String)>, + default_descending: bool, desired_width: DesiredColumnWidth, ) -> Self { - let shortcut = shortcut.map(|e| (e, get_shortcut_name(&e))); Self { shortcut, default_descending, - internal: SimpleColumn::new(shortcut_name, desired_width), - sorting: SortStatus::NotSorting, + internal: SimpleColumn::new(full_name, desired_width), + sorting_status: SortStatus::NotSorting, } } @@ -89,18 +135,22 @@ impl SortableColumn { name: Cow<'static, str>, shortcut: Option, default_descending: bool, hard_length: Option, ) -> Self { - let shortcut_name = if let Some(shortcut) = shortcut { - get_shortcut_name(&shortcut).into() + let (full_name, shortcut) = if let Some(shortcut) = shortcut { + let shortcut_name = get_shortcut_name(&shortcut); + ( + format!("{}{}", name, shortcut_name).into(), + Some((shortcut, shortcut_name)), + ) } else { - name + (name, None) }; - let shortcut_name_len = shortcut_name.len(); + let full_name_len = full_name.len(); - SortableColumn::new( - shortcut_name, + SimpleSortableColumn::new( + full_name, shortcut, default_descending, - DesiredColumnWidth::Hard(hard_length.unwrap_or(shortcut_name_len as u16 + 1)), + DesiredColumnWidth::Hard(hard_length.unwrap_or(full_name_len as u16 + 1)), ) } @@ -109,33 +159,53 @@ impl SortableColumn { name: Cow<'static, str>, shortcut: Option, default_descending: bool, max_percentage: f64, ) -> Self { - let shortcut_name = if let Some(shortcut) = shortcut { - get_shortcut_name(&shortcut).into() + let (full_name, shortcut) = if let Some(shortcut) = shortcut { + let shortcut_name = get_shortcut_name(&shortcut); + ( + format!("{}{}", name, shortcut_name).into(), + Some((shortcut, shortcut_name)), + ) } else { - name + (name, None) }; - let shortcut_name_len = shortcut_name.len(); + let full_name_len = full_name.len(); - SortableColumn::new( - shortcut_name, + SimpleSortableColumn::new( + full_name, shortcut, default_descending, DesiredColumnWidth::Flex { - desired: shortcut_name_len as u16, + desired: full_name_len as u16, max_percentage, }, ) } } -impl TableColumn for SortableColumn { +impl SortableColumn for SimpleSortableColumn { + fn shortcut(&self) -> &Option<(KeyEvent, String)> { + &self.shortcut + } + + fn default_descending(&self) -> bool { + self.default_descending + } + + fn sorting_status(&self) -> SortStatus { + self.sorting_status + } + + fn set_sorting_status(&mut self, sorting_status: SortStatus) { + self.sorting_status = sorting_status; + } + fn display_name(&self) -> Cow<'static, str> { const UP_ARROW: &str = "▲"; const DOWN_ARROW: &str = "▼"; format!( "{}{}", self.internal.display_name(), - match &self.sorting { + match &self.sorting_status { SortStatus::NotSorting => "", SortStatus::SortAscending => UP_ARROW, SortStatus::SortDescending => DOWN_ARROW, @@ -158,16 +228,22 @@ impl TableColumn for SortableColumn { } /// A sortable, scrollable table with columns. -pub struct SortableTextTable { +pub struct SortableTextTable +where + S: SortableColumn, +{ /// Which index we're sorting by. sort_index: usize, /// The underlying [`TextTable`]. - pub table: TextTable, + pub table: TextTable, } -impl SortableTextTable { - pub fn new(columns: Vec) -> Self { +impl SortableTextTable +where + S: SortableColumn, +{ + pub fn new(columns: Vec) -> Self { let mut st = Self { sort_index: 0, table: TextTable::new(columns), @@ -193,32 +269,32 @@ impl SortableTextTable { fn set_sort_index(&mut self, new_index: usize) { if new_index == self.sort_index { if let Some(column) = self.table.columns.get_mut(self.sort_index) { - match column.sorting { + match column.sorting_status() { SortStatus::NotSorting => { - if column.default_descending { - column.sorting = SortStatus::SortDescending; + if column.default_descending() { + column.set_sorting_status(SortStatus::SortDescending); } else { - column.sorting = SortStatus::SortAscending; + column.set_sorting_status(SortStatus::SortAscending); } } SortStatus::SortAscending => { - column.sorting = SortStatus::SortDescending; + column.set_sorting_status(SortStatus::SortDescending); } SortStatus::SortDescending => { - column.sorting = SortStatus::SortAscending; + column.set_sorting_status(SortStatus::SortAscending); } } } } else { if let Some(column) = self.table.columns.get_mut(self.sort_index) { - column.sorting = SortStatus::NotSorting; + column.set_sorting_status(SortStatus::NotSorting); } if let Some(column) = self.table.columns.get_mut(new_index) { - if column.default_descending { - column.sorting = SortStatus::SortDescending; + if column.default_descending() { + column.set_sorting_status(SortStatus::SortDescending); } else { - column.sorting = SortStatus::SortAscending; + column.set_sorting_status(SortStatus::SortAscending); } } @@ -244,10 +320,13 @@ impl SortableTextTable { } } -impl Component for SortableTextTable { +impl Component for SortableTextTable +where + S: SortableColumn, +{ fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { for (index, column) in self.table.columns.iter().enumerate() { - if let Some((shortcut, _)) = column.shortcut { + if let &Some((shortcut, _)) = column.shortcut() { if shortcut == event { self.set_sort_index(index); return EventResult::Redraw; @@ -270,7 +349,7 @@ impl Component for SortableTextTable { if y == 0 { for (index, column) in self.table.columns.iter().enumerate() { - if let Some((start, end)) = column.internal.get_x_bounds() { + if let Some((start, end)) = column.get_x_bounds() { if x >= start && x <= end { self.set_sort_index(index); return EventResult::Redraw; diff --git a/src/app/widgets/cpu.rs b/src/app/widgets/cpu.rs index 40c2fa89..51c9d795 100644 --- a/src/app/widgets/cpu.rs +++ b/src/app/widgets/cpu.rs @@ -9,7 +9,7 @@ use tui::{ use crate::{ app::{ - event::EventResult, sort_text_table::SortableColumn, time_graph::TimeGraphData, + event::EventResult, sort_text_table::SimpleSortableColumn, time_graph::TimeGraphData, AppConfigFields, DataCollection, }, canvas::Painter, @@ -96,8 +96,8 @@ impl CpuGraph { pub fn from_config(app_config_fields: &AppConfigFields) -> Self { let graph = TimeGraph::from_config(app_config_fields); let legend = SortableTextTable::new(vec![ - SortableColumn::new_flex("CPU".into(), None, false, 0.5), - SortableColumn::new_flex("Use%".into(), None, false, 0.5), + SimpleSortableColumn::new_flex("CPU".into(), None, false, 0.5), + SimpleSortableColumn::new_flex("Use%".into(), None, false, 0.5), ]); let legend_position = if app_config_fields.left_legend { CpuGraphLegendPosition::Left diff --git a/src/app/widgets/disk.rs b/src/app/widgets/disk.rs index 28673960..39f0b415 100644 --- a/src/app/widgets/disk.rs +++ b/src/app/widgets/disk.rs @@ -9,7 +9,7 @@ use tui::{ }; use crate::{ - app::{data_farmer::DataCollection, event::EventResult, sort_text_table::SortableColumn}, + app::{data_farmer::DataCollection, event::EventResult, sort_text_table::SimpleSortableColumn}, canvas::Painter, data_conversion::convert_disk_row, }; @@ -63,13 +63,13 @@ pub struct DiskTable { impl Default for DiskTable { fn default() -> Self { let table = SortableTextTable::new(vec![ - SortableColumn::new_flex("Disk".into(), None, false, 0.2), - SortableColumn::new_flex("Mount".into(), None, false, 0.2), - SortableColumn::new_hard("Used".into(), None, false, Some(5)), - SortableColumn::new_hard("Free".into(), None, false, Some(6)), - SortableColumn::new_hard("Total".into(), None, false, Some(6)), - SortableColumn::new_hard("R/s".into(), None, false, Some(7)), - SortableColumn::new_hard("W/s".into(), None, false, Some(7)), + SimpleSortableColumn::new_flex("Disk".into(), None, false, 0.2), + SimpleSortableColumn::new_flex("Mount".into(), None, false, 0.2), + SimpleSortableColumn::new_hard("Used".into(), None, false, Some(5)), + SimpleSortableColumn::new_hard("Free".into(), None, false, Some(6)), + SimpleSortableColumn::new_hard("Total".into(), None, false, Some(6)), + SimpleSortableColumn::new_hard("R/s".into(), None, false, Some(7)), + SimpleSortableColumn::new_hard("W/s".into(), None, false, Some(7)), ]); Self { diff --git a/src/app/widgets/net.rs b/src/app/widgets/net.rs index e4bb95ae..9ec1184a 100644 --- a/src/app/widgets/net.rs +++ b/src/app/widgets/net.rs @@ -476,8 +476,6 @@ impl NetGraph { match &mut self.draw_cache { NetGraphCacheState::Uncached => { - debug!("No cache!"); - let (cached_upper_bound, labels) = adjust_network_data_point( current_max_value, &self.scale_type, diff --git a/src/app/widgets/process.rs b/src/app/widgets/process.rs index 4fa7914a..1d86f02c 100644 --- a/src/app/widgets/process.rs +++ b/src/app/widgets/process.rs @@ -23,8 +23,10 @@ use crate::{ use ProcessSorting::*; use super::{ - text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component, - CursorDirection, ScrollDirection, SortableTextTable, TextInput, TextTable, Widget, + sort_text_table::{SimpleSortableColumn, SortableColumn}, + text_table::TextTableData, + AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection, + SortableTextTable, TextInput, TextTable, Widget, }; /// AppSearchState deals with generic searching (I might do this in the future). @@ -638,10 +640,198 @@ struct SearchModifiers { enable_regex: bool, } +enum FlexColumn { + Flex(f64), + Hard(Option), +} + +pub enum ProcessSortType { + Pid, + Count, + Name, + Command, + Cpu, + Mem, + MemPercent, + Rps, + Wps, + TotalRead, + TotalWrite, + User, + State, +} + +impl ProcessSortType { + fn to_str(&self) -> &'static str { + match self { + ProcessSortType::Pid => "PID", + ProcessSortType::Count => "Count", + ProcessSortType::Name => "Name", + ProcessSortType::Command => "Command", + ProcessSortType::Cpu => "Cpu", + ProcessSortType::Mem => "Mem", + ProcessSortType::MemPercent => "Mem%", + ProcessSortType::Rps => "R/s", + ProcessSortType::Wps => "W/s", + ProcessSortType::TotalRead => "T.Read", + ProcessSortType::TotalWrite => "T.Write", + ProcessSortType::User => "User", + ProcessSortType::State => "State", + } + } + + fn shortcut(&self) -> Option { + match self { + ProcessSortType::Pid => Some(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)), + ProcessSortType::Count => None, + ProcessSortType::Name => Some(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)), + ProcessSortType::Command => None, + ProcessSortType::Cpu => Some(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)), + ProcessSortType::Mem => Some(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE)), + ProcessSortType::MemPercent => { + Some(KeyEvent::new(KeyCode::Char('m'), KeyModifiers::NONE)) + } + ProcessSortType::Rps => None, + ProcessSortType::Wps => None, + ProcessSortType::TotalRead => None, + ProcessSortType::TotalWrite => None, + ProcessSortType::User => None, + ProcessSortType::State => None, + } + } + + fn column_type(&self) -> FlexColumn { + use FlexColumn::*; + + match self { + ProcessSortType::Pid => Hard(Some(7)), + ProcessSortType::Count => Hard(Some(8)), + ProcessSortType::Name => Flex(0.3), + ProcessSortType::Command => Flex(0.7), + ProcessSortType::Cpu => Hard(Some(8)), + ProcessSortType::Mem => Hard(Some(8)), + ProcessSortType::MemPercent => Hard(Some(8)), + ProcessSortType::Rps => Hard(Some(8)), + ProcessSortType::Wps => Hard(Some(8)), + ProcessSortType::TotalRead => Hard(Some(7)), + ProcessSortType::TotalWrite => Hard(Some(8)), + ProcessSortType::User => Flex(0.1), + ProcessSortType::State => Flex(0.2), + } + } + + fn default_descending(&self) -> bool { + match self { + ProcessSortType::Pid => false, + ProcessSortType::Count => true, + ProcessSortType::Name => false, + ProcessSortType::Command => false, + ProcessSortType::Cpu => true, + ProcessSortType::Mem => true, + ProcessSortType::MemPercent => true, + ProcessSortType::Rps => true, + ProcessSortType::Wps => true, + ProcessSortType::TotalRead => true, + ProcessSortType::TotalWrite => true, + ProcessSortType::User => false, + ProcessSortType::State => false, + } + } +} + +/// A thin wrapper around a [`SortableColumn`] to help keep track of +/// how to sort given a chosen column. +pub struct ProcessSortColumn { + /// The underlying column. + sortable_column: SimpleSortableColumn, + + /// The *type* of column. Useful for determining how to sort. + sort_type: ProcessSortType, +} + +impl ProcessSortColumn { + pub fn new(sort_type: ProcessSortType) -> Self { + let sortable_column = { + let name = sort_type.to_str().into(); + let shortcut = sort_type.shortcut(); + let default_descending = sort_type.default_descending(); + + match sort_type.column_type() { + FlexColumn::Flex(max_percentage) => SimpleSortableColumn::new_flex( + name, + shortcut, + default_descending, + max_percentage, + ), + FlexColumn::Hard(hard_length) => { + SimpleSortableColumn::new_hard(name, shortcut, default_descending, hard_length) + } + } + }; + + Self { + sortable_column, + sort_type, + } + } + + pub fn sort_process(&self) { + match &self.sort_type { + ProcessSortType::Pid => {} + ProcessSortType::Count => {} + ProcessSortType::Name => {} + ProcessSortType::Command => {} + ProcessSortType::Cpu => {} + ProcessSortType::Mem => {} + ProcessSortType::MemPercent => {} + ProcessSortType::Rps => {} + ProcessSortType::Wps => {} + ProcessSortType::TotalRead => {} + ProcessSortType::TotalWrite => {} + ProcessSortType::User => {} + ProcessSortType::State => {} + } + } +} + +impl SortableColumn for ProcessSortColumn { + fn shortcut(&self) -> &Option<(KeyEvent, String)> { + self.sortable_column.shortcut() + } + + fn default_descending(&self) -> bool { + self.sortable_column.default_descending() + } + + fn sorting_status(&self) -> super::sort_text_table::SortStatus { + self.sortable_column.sorting_status() + } + + fn set_sorting_status(&mut self, sorting_status: super::sort_text_table::SortStatus) { + self.sortable_column.set_sorting_status(sorting_status) + } + + fn display_name(&self) -> std::borrow::Cow<'static, str> { + self.sortable_column.display_name() + } + + fn get_desired_width(&self) -> &super::text_table::DesiredColumnWidth { + self.sortable_column.get_desired_width() + } + + fn get_x_bounds(&self) -> Option<(u16, u16)> { + self.sortable_column.get_x_bounds() + } + + fn set_x_bounds(&mut self, x_bounds: Option<(u16, u16)>) { + self.sortable_column.set_x_bounds(x_bounds) + } +} + /// A searchable, sortable table to manage processes. pub struct ProcessManager { bounds: Rect, - process_table: SortableTextTable, + process_table: SortableTextTable, sort_table: TextTable, search_input: TextInput, @@ -661,12 +851,26 @@ pub struct ProcessManager { impl ProcessManager { /// Creates a new [`ProcessManager`]. pub fn new(process_defaults: &ProcessDefaults) -> Self { - let process_table_columns = vec![]; + let process_table_columns = vec![ + ProcessSortColumn::new(ProcessSortType::Pid), + ProcessSortColumn::new(ProcessSortType::Name), + ProcessSortColumn::new(ProcessSortType::Cpu), + ProcessSortColumn::new(ProcessSortType::MemPercent), + ProcessSortColumn::new(ProcessSortType::Rps), + ProcessSortColumn::new(ProcessSortType::Wps), + ProcessSortColumn::new(ProcessSortType::TotalRead), + ProcessSortColumn::new(ProcessSortType::TotalWrite), + #[cfg(target_family = "unix")] + ProcessSortColumn::new(ProcessSortType::User), + ProcessSortColumn::new(ProcessSortType::State), + ]; + + let process_table = SortableTextTable::new(process_table_columns).default_sort_index(2); let mut manager = Self { bounds: Rect::default(), - process_table: SortableTextTable::new(process_table_columns), // TODO: Do this - sort_table: TextTable::new(vec![]), // TODO: Do this too + process_table, + sort_table: TextTable::new(vec![]), // TODO: Do this too search_input: TextInput::new(), dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static... selected: ProcessManagerSelection::Processes, @@ -870,14 +1074,8 @@ impl Widget for ProcessManager { }) .borders(Borders::ALL); - self.process_table.table.draw_tui_table( - painter, - f, - &self.display_data, // TODO: Fix this - block, - area, - selected, - ); + self.process_table + .draw_tui_table(painter, f, &self.display_data, block, area, selected); } fn update_data(&mut self, data_collection: &DataCollection) {} diff --git a/src/app/widgets/temp.rs b/src/app/widgets/temp.rs index 3b7fa3e1..5da31ba1 100644 --- a/src/app/widgets/temp.rs +++ b/src/app/widgets/temp.rs @@ -11,7 +11,7 @@ use tui::{ use crate::{ app::{ data_farmer::DataCollection, data_harvester::temperature::TemperatureType, - event::EventResult, sort_text_table::SortableColumn, + event::EventResult, sort_text_table::SimpleSortableColumn, }, canvas::Painter, data_conversion::convert_temp_row, @@ -66,8 +66,8 @@ pub struct TempTable { impl Default for TempTable { fn default() -> Self { let table = SortableTextTable::new(vec![ - SortableColumn::new_flex("Sensor".into(), None, false, 0.8), - SortableColumn::new_hard("Temp".into(), None, false, Some(5)), + SimpleSortableColumn::new_flex("Sensor".into(), None, false, 0.8), + SimpleSortableColumn::new_hard("Temp".into(), None, false, Some(5)), ]) .default_ltr(false);