refactor: another pass on sorting and columns

This commit is contained in:
ClementTsang 2021-08-29 01:19:34 -04:00
parent 64d47d54d3
commit 74293aa243
6 changed files with 350 additions and 75 deletions

View File

@ -52,34 +52,80 @@ fn get_shortcut_name(e: &KeyEvent) -> String {
format!("({}{})", modifier, key).into() format!("({}{})", modifier, key).into()
} }
#[derive(Debug)] #[derive(Copy, Clone, Debug)]
enum SortStatus { pub enum SortStatus {
NotSorting, NotSorting,
SortAscending, SortAscending,
SortDescending, 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<T> 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)] #[derive(Debug)]
pub struct SortableColumn { pub struct SimpleSortableColumn {
pub shortcut: Option<(KeyEvent, String)>, pub shortcut: Option<(KeyEvent, String)>,
pub default_descending: bool, pub default_descending: bool,
pub internal: SimpleColumn, pub internal: SimpleColumn,
sorting: SortStatus,
/// Whether this column is currently selected for sorting, and which direction.
sorting_status: SortStatus,
} }
impl SortableColumn { impl SimpleSortableColumn {
/// Creates a new [`SortableColumn`]. /// Creates a new [`SimpleSortableColumn`].
fn new( fn new(
shortcut_name: Cow<'static, str>, shortcut: Option<KeyEvent>, default_descending: bool, full_name: Cow<'static, str>, shortcut: Option<(KeyEvent, String)>,
desired_width: DesiredColumnWidth, default_descending: bool, desired_width: DesiredColumnWidth,
) -> Self { ) -> Self {
let shortcut = shortcut.map(|e| (e, get_shortcut_name(&e)));
Self { Self {
shortcut, shortcut,
default_descending, default_descending,
internal: SimpleColumn::new(shortcut_name, desired_width), internal: SimpleColumn::new(full_name, desired_width),
sorting: SortStatus::NotSorting, sorting_status: SortStatus::NotSorting,
} }
} }
@ -89,18 +135,22 @@ impl SortableColumn {
name: Cow<'static, str>, shortcut: Option<KeyEvent>, default_descending: bool, name: Cow<'static, str>, shortcut: Option<KeyEvent>, default_descending: bool,
hard_length: Option<u16>, hard_length: Option<u16>,
) -> Self { ) -> Self {
let shortcut_name = if let Some(shortcut) = shortcut { let (full_name, shortcut) = if let Some(shortcut) = shortcut {
get_shortcut_name(&shortcut).into() let shortcut_name = get_shortcut_name(&shortcut);
(
format!("{}{}", name, shortcut_name).into(),
Some((shortcut, shortcut_name)),
)
} else { } else {
name (name, None)
}; };
let shortcut_name_len = shortcut_name.len(); let full_name_len = full_name.len();
SortableColumn::new( SimpleSortableColumn::new(
shortcut_name, full_name,
shortcut, shortcut,
default_descending, 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<KeyEvent>, default_descending: bool, name: Cow<'static, str>, shortcut: Option<KeyEvent>, default_descending: bool,
max_percentage: f64, max_percentage: f64,
) -> Self { ) -> Self {
let shortcut_name = if let Some(shortcut) = shortcut { let (full_name, shortcut) = if let Some(shortcut) = shortcut {
get_shortcut_name(&shortcut).into() let shortcut_name = get_shortcut_name(&shortcut);
(
format!("{}{}", name, shortcut_name).into(),
Some((shortcut, shortcut_name)),
)
} else { } else {
name (name, None)
}; };
let shortcut_name_len = shortcut_name.len(); let full_name_len = full_name.len();
SortableColumn::new( SimpleSortableColumn::new(
shortcut_name, full_name,
shortcut, shortcut,
default_descending, default_descending,
DesiredColumnWidth::Flex { DesiredColumnWidth::Flex {
desired: shortcut_name_len as u16, desired: full_name_len as u16,
max_percentage, 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> { fn display_name(&self) -> Cow<'static, str> {
const UP_ARROW: &str = ""; const UP_ARROW: &str = "";
const DOWN_ARROW: &str = ""; const DOWN_ARROW: &str = "";
format!( format!(
"{}{}", "{}{}",
self.internal.display_name(), self.internal.display_name(),
match &self.sorting { match &self.sorting_status {
SortStatus::NotSorting => "", SortStatus::NotSorting => "",
SortStatus::SortAscending => UP_ARROW, SortStatus::SortAscending => UP_ARROW,
SortStatus::SortDescending => DOWN_ARROW, SortStatus::SortDescending => DOWN_ARROW,
@ -158,16 +228,22 @@ impl TableColumn for SortableColumn {
} }
/// A sortable, scrollable table with columns. /// A sortable, scrollable table with columns.
pub struct SortableTextTable { pub struct SortableTextTable<S = SimpleSortableColumn>
where
S: SortableColumn,
{
/// Which index we're sorting by. /// Which index we're sorting by.
sort_index: usize, sort_index: usize,
/// The underlying [`TextTable`]. /// The underlying [`TextTable`].
pub table: TextTable<SortableColumn>, pub table: TextTable<S>,
} }
impl SortableTextTable { impl<S> SortableTextTable<S>
pub fn new(columns: Vec<SortableColumn>) -> Self { where
S: SortableColumn,
{
pub fn new(columns: Vec<S>) -> Self {
let mut st = Self { let mut st = Self {
sort_index: 0, sort_index: 0,
table: TextTable::new(columns), table: TextTable::new(columns),
@ -193,32 +269,32 @@ impl SortableTextTable {
fn set_sort_index(&mut self, new_index: usize) { fn set_sort_index(&mut self, new_index: usize) {
if new_index == self.sort_index { if new_index == self.sort_index {
if let Some(column) = self.table.columns.get_mut(self.sort_index) { if let Some(column) = self.table.columns.get_mut(self.sort_index) {
match column.sorting { match column.sorting_status() {
SortStatus::NotSorting => { SortStatus::NotSorting => {
if column.default_descending { if column.default_descending() {
column.sorting = SortStatus::SortDescending; column.set_sorting_status(SortStatus::SortDescending);
} else { } else {
column.sorting = SortStatus::SortAscending; column.set_sorting_status(SortStatus::SortAscending);
} }
} }
SortStatus::SortAscending => { SortStatus::SortAscending => {
column.sorting = SortStatus::SortDescending; column.set_sorting_status(SortStatus::SortDescending);
} }
SortStatus::SortDescending => { SortStatus::SortDescending => {
column.sorting = SortStatus::SortAscending; column.set_sorting_status(SortStatus::SortAscending);
} }
} }
} }
} else { } else {
if let Some(column) = self.table.columns.get_mut(self.sort_index) { 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 let Some(column) = self.table.columns.get_mut(new_index) {
if column.default_descending { if column.default_descending() {
column.sorting = SortStatus::SortDescending; column.set_sorting_status(SortStatus::SortDescending);
} else { } else {
column.sorting = SortStatus::SortAscending; column.set_sorting_status(SortStatus::SortAscending);
} }
} }
@ -244,10 +320,13 @@ impl SortableTextTable {
} }
} }
impl Component for SortableTextTable { impl<S> Component for SortableTextTable<S>
where
S: SortableColumn,
{
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
for (index, column) in self.table.columns.iter().enumerate() { for (index, column) in self.table.columns.iter().enumerate() {
if let Some((shortcut, _)) = column.shortcut { if let &Some((shortcut, _)) = column.shortcut() {
if shortcut == event { if shortcut == event {
self.set_sort_index(index); self.set_sort_index(index);
return EventResult::Redraw; return EventResult::Redraw;
@ -270,7 +349,7 @@ impl Component for SortableTextTable {
if y == 0 { if y == 0 {
for (index, column) in self.table.columns.iter().enumerate() { 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 { if x >= start && x <= end {
self.set_sort_index(index); self.set_sort_index(index);
return EventResult::Redraw; return EventResult::Redraw;

View File

@ -9,7 +9,7 @@ use tui::{
use crate::{ use crate::{
app::{ app::{
event::EventResult, sort_text_table::SortableColumn, time_graph::TimeGraphData, event::EventResult, sort_text_table::SimpleSortableColumn, time_graph::TimeGraphData,
AppConfigFields, DataCollection, AppConfigFields, DataCollection,
}, },
canvas::Painter, canvas::Painter,
@ -96,8 +96,8 @@ impl CpuGraph {
pub fn from_config(app_config_fields: &AppConfigFields) -> Self { pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
let graph = TimeGraph::from_config(app_config_fields); let graph = TimeGraph::from_config(app_config_fields);
let legend = SortableTextTable::new(vec![ let legend = SortableTextTable::new(vec![
SortableColumn::new_flex("CPU".into(), None, false, 0.5), SimpleSortableColumn::new_flex("CPU".into(), None, false, 0.5),
SortableColumn::new_flex("Use%".into(), None, false, 0.5), SimpleSortableColumn::new_flex("Use%".into(), None, false, 0.5),
]); ]);
let legend_position = if app_config_fields.left_legend { let legend_position = if app_config_fields.left_legend {
CpuGraphLegendPosition::Left CpuGraphLegendPosition::Left

View File

@ -9,7 +9,7 @@ use tui::{
}; };
use crate::{ use crate::{
app::{data_farmer::DataCollection, event::EventResult, sort_text_table::SortableColumn}, app::{data_farmer::DataCollection, event::EventResult, sort_text_table::SimpleSortableColumn},
canvas::Painter, canvas::Painter,
data_conversion::convert_disk_row, data_conversion::convert_disk_row,
}; };
@ -63,13 +63,13 @@ pub struct DiskTable {
impl Default for DiskTable { impl Default for DiskTable {
fn default() -> Self { fn default() -> Self {
let table = SortableTextTable::new(vec![ let table = SortableTextTable::new(vec![
SortableColumn::new_flex("Disk".into(), None, false, 0.2), SimpleSortableColumn::new_flex("Disk".into(), None, false, 0.2),
SortableColumn::new_flex("Mount".into(), None, false, 0.2), SimpleSortableColumn::new_flex("Mount".into(), None, false, 0.2),
SortableColumn::new_hard("Used".into(), None, false, Some(5)), SimpleSortableColumn::new_hard("Used".into(), None, false, Some(5)),
SortableColumn::new_hard("Free".into(), None, false, Some(6)), SimpleSortableColumn::new_hard("Free".into(), None, false, Some(6)),
SortableColumn::new_hard("Total".into(), None, false, Some(6)), SimpleSortableColumn::new_hard("Total".into(), None, false, Some(6)),
SortableColumn::new_hard("R/s".into(), None, false, Some(7)), SimpleSortableColumn::new_hard("R/s".into(), None, false, Some(7)),
SortableColumn::new_hard("W/s".into(), None, false, Some(7)), SimpleSortableColumn::new_hard("W/s".into(), None, false, Some(7)),
]); ]);
Self { Self {

View File

@ -476,8 +476,6 @@ impl NetGraph {
match &mut self.draw_cache { match &mut self.draw_cache {
NetGraphCacheState::Uncached => { NetGraphCacheState::Uncached => {
debug!("No cache!");
let (cached_upper_bound, labels) = adjust_network_data_point( let (cached_upper_bound, labels) = adjust_network_data_point(
current_max_value, current_max_value,
&self.scale_type, &self.scale_type,

View File

@ -23,8 +23,10 @@ use crate::{
use ProcessSorting::*; use ProcessSorting::*;
use super::{ use super::{
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component, sort_text_table::{SimpleSortableColumn, SortableColumn},
CursorDirection, ScrollDirection, SortableTextTable, TextInput, TextTable, Widget, text_table::TextTableData,
AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection,
SortableTextTable, TextInput, TextTable, Widget,
}; };
/// AppSearchState deals with generic searching (I might do this in the future). /// AppSearchState deals with generic searching (I might do this in the future).
@ -638,10 +640,198 @@ struct SearchModifiers {
enable_regex: bool, enable_regex: bool,
} }
enum FlexColumn {
Flex(f64),
Hard(Option<u16>),
}
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<KeyEvent> {
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. /// A searchable, sortable table to manage processes.
pub struct ProcessManager { pub struct ProcessManager {
bounds: Rect, bounds: Rect,
process_table: SortableTextTable, process_table: SortableTextTable<ProcessSortColumn>,
sort_table: TextTable, sort_table: TextTable,
search_input: TextInput, search_input: TextInput,
@ -661,12 +851,26 @@ pub struct ProcessManager {
impl ProcessManager { impl ProcessManager {
/// Creates a new [`ProcessManager`]. /// Creates a new [`ProcessManager`].
pub fn new(process_defaults: &ProcessDefaults) -> Self { 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 { let mut manager = Self {
bounds: Rect::default(), bounds: Rect::default(),
process_table: SortableTextTable::new(process_table_columns), // TODO: Do this process_table,
sort_table: TextTable::new(vec![]), // TODO: Do this too sort_table: TextTable::new(vec![]), // TODO: Do this too
search_input: TextInput::new(), search_input: TextInput::new(),
dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static... dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static...
selected: ProcessManagerSelection::Processes, selected: ProcessManagerSelection::Processes,
@ -870,14 +1074,8 @@ impl Widget for ProcessManager {
}) })
.borders(Borders::ALL); .borders(Borders::ALL);
self.process_table.table.draw_tui_table( self.process_table
painter, .draw_tui_table(painter, f, &self.display_data, block, area, selected);
f,
&self.display_data, // TODO: Fix this
block,
area,
selected,
);
} }
fn update_data(&mut self, data_collection: &DataCollection) {} fn update_data(&mut self, data_collection: &DataCollection) {}

View File

@ -11,7 +11,7 @@ use tui::{
use crate::{ use crate::{
app::{ app::{
data_farmer::DataCollection, data_harvester::temperature::TemperatureType, data_farmer::DataCollection, data_harvester::temperature::TemperatureType,
event::EventResult, sort_text_table::SortableColumn, event::EventResult, sort_text_table::SimpleSortableColumn,
}, },
canvas::Painter, canvas::Painter,
data_conversion::convert_temp_row, data_conversion::convert_temp_row,
@ -66,8 +66,8 @@ pub struct TempTable {
impl Default for TempTable { impl Default for TempTable {
fn default() -> Self { fn default() -> Self {
let table = SortableTextTable::new(vec![ let table = SortableTextTable::new(vec![
SortableColumn::new_flex("Sensor".into(), None, false, 0.8), SimpleSortableColumn::new_flex("Sensor".into(), None, false, 0.8),
SortableColumn::new_hard("Temp".into(), None, false, Some(5)), SimpleSortableColumn::new_hard("Temp".into(), None, false, Some(5)),
]) ])
.default_ltr(false); .default_ltr(false);