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()
}
#[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<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)]
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<KeyEvent>, 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<KeyEvent>, default_descending: bool,
hard_length: Option<u16>,
) -> 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<KeyEvent>, 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<S = SimpleSortableColumn>
where
S: SortableColumn,
{
/// Which index we're sorting by.
sort_index: usize,
/// The underlying [`TextTable`].
pub table: TextTable<SortableColumn>,
pub table: TextTable<S>,
}
impl SortableTextTable {
pub fn new(columns: Vec<SortableColumn>) -> Self {
impl<S> SortableTextTable<S>
where
S: SortableColumn,
{
pub fn new(columns: Vec<S>) -> 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<S> Component for SortableTextTable<S>
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;

View File

@ -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

View File

@ -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 {

View File

@ -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,

View File

@ -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<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.
pub struct ProcessManager {
bounds: Rect,
process_table: SortableTextTable,
process_table: SortableTextTable<ProcessSortColumn>,
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) {}

View File

@ -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);