refactor: Add sort capabilities to processes

This commit is contained in:
ClementTsang 2021-08-30 01:30:21 -04:00
parent 3fa50605b3
commit 27736b7fc0
9 changed files with 335 additions and 125 deletions

View File

@ -17,3 +17,6 @@ pub use text_input::TextInput;
pub mod carousel;
pub use carousel::Carousel;
pub mod sort_menu;
pub use sort_menu::SortMenu;

View File

@ -2,7 +2,7 @@ use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEve
use tui::{layout::Rect, widgets::TableState};
use crate::app::{
event::{WidgetEventResult, MultiKey, MultiKeyResult},
event::{MultiKey, MultiKeyResult, WidgetEventResult},
Component,
};
@ -110,7 +110,7 @@ impl Scrollable {
}
/// Update the index with this! This will automatically update the scroll direction as well!
fn update_index(&mut self, new_index: usize) {
pub fn set_index(&mut self, new_index: usize) {
use std::cmp::Ordering;
match new_index.cmp(&self.current_index) {
@ -130,7 +130,7 @@ impl Scrollable {
fn skip_to_first(&mut self) -> WidgetEventResult {
if self.current_index != 0 {
self.update_index(0);
self.set_index(0);
WidgetEventResult::Redraw
} else {
@ -141,7 +141,7 @@ impl Scrollable {
fn skip_to_last(&mut self) -> WidgetEventResult {
let last_index = self.num_items - 1;
if self.current_index != last_index {
self.update_index(last_index);
self.set_index(last_index);
WidgetEventResult::Redraw
} else {
@ -161,7 +161,7 @@ impl Scrollable {
} else if self.current_index == new_index {
WidgetEventResult::NoRedraw
} else {
self.update_index(new_index);
self.set_index(new_index);
WidgetEventResult::Redraw
}
}
@ -176,12 +176,12 @@ impl Scrollable {
if self.current_index == new_index {
WidgetEventResult::NoRedraw
} else {
self.update_index(new_index);
self.set_index(new_index);
WidgetEventResult::Redraw
}
}
pub fn update_num_items(&mut self, num_items: usize) {
pub fn set_num_items(&mut self, num_items: usize) {
self.num_items = num_items;
if num_items <= self.current_index {

View File

@ -0,0 +1,76 @@
use crossterm::event::{KeyEvent, MouseEvent};
use tui::{backend::Backend, layout::Rect, widgets::Block, Frame};
use crate::{
app::{event::WidgetEventResult, text_table::SimpleColumn, Component, TextTable},
canvas::Painter,
};
use super::sort_text_table::SortableColumn;
/// A sortable, scrollable table with columns.
pub struct SortMenu {
/// The underlying table.
table: TextTable,
/// The bounds.
bounds: Rect,
}
impl SortMenu {
/// Creates a new [`SortMenu`].
pub fn new(num_columns: usize) -> Self {
let sort_menu_columns = vec![SimpleColumn::new_hard("Sort By".into(), None)];
let mut sort_menu = TextTable::new(sort_menu_columns);
sort_menu.set_num_items(num_columns);
Self {
table: sort_menu,
bounds: Default::default(),
}
}
/// Updates the index of the [`SortMenu`].
pub fn set_index(&mut self, index: usize) {
self.table.scrollable.set_index(index);
}
/// Returns the current index of the [`SortMenu`].
pub fn current_index(&mut self) -> usize {
self.table.scrollable.current_index()
}
/// Draws a [`tui::widgets::Table`] on screen corresponding to the sort columns of this [`SortableTextTable`].
pub fn draw_sort_menu<B: Backend, C: SortableColumn>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, columns: &[C], block: Block<'_>,
block_area: Rect,
) {
self.set_bounds(block_area);
let data = columns
.iter()
.map(|c| vec![(c.original_name().clone().into(), None, None)])
.collect::<Vec<_>>();
self.table
.draw_tui_table(painter, f, &data, block, block_area, true);
}
}
impl Component for SortMenu {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
self.table.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
self.table.handle_mouse_event(event)
}
}

View File

@ -257,9 +257,6 @@ where
/// The underlying [`TextTable`].
pub table: TextTable<S>,
/// A corresponding "sort" menu.
pub sort_menu: TextTable,
}
impl<S> SortableTextTable<S>
@ -268,15 +265,9 @@ where
{
/// Creates a new [`SortableTextTable`]. Note that `columns` cannot be empty.
pub fn new(columns: Vec<S>) -> Self {
let sort_menu_columns = columns
.iter()
.map(|column| SimpleColumn::new_hard(column.original_name().clone(), None))
.collect::<Vec<_>>();
let mut st = Self {
sort_index: 0,
table: TextTable::new(columns),
sort_menu: TextTable::new(sort_menu_columns),
};
st.set_sort_index(0);
st
@ -296,11 +287,16 @@ where
self.table.current_scroll_index()
}
/// Returns the current column.
pub fn current_column(&self) -> &S {
/// Returns the current column the table is sorting by.
pub fn current_sorting_column(&self) -> &S {
&self.table.columns[self.sort_index]
}
/// Returns the current column index the table is sorting by.
pub fn current_sorting_column_index(&self) -> usize {
self.sort_index
}
pub fn columns(&self) -> &[S] {
&self.table.columns
}
@ -309,7 +305,7 @@ where
self.table.set_column(index, column)
}
fn set_sort_index(&mut self, new_index: usize) {
pub 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_status() {
@ -356,12 +352,6 @@ where
self.table
.draw_tui_table(painter, f, data, block, block_area, show_selected_entry);
}
/// Draws a [`tui::widgets::Table`] on screen corresponding to the sort columns of this [`SortableTextTable`].
pub fn draw_sort_table<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, block: Block<'_>, block_area: Rect,
) {
}
}
impl<S> Component for SortableTextTable<S>

View File

@ -173,6 +173,10 @@ where
self
}
pub fn columns(&self) -> &[C] {
&self.columns
}
fn displayed_column_names(&self) -> Vec<Cow<'static, str>> {
self.columns
.iter()
@ -181,7 +185,7 @@ where
}
pub fn set_num_items(&mut self, num_items: usize) {
self.scrollable.update_num_items(num_items);
self.scrollable.set_num_items(num_items);
}
pub fn set_column(&mut self, index: usize, column: C) {

View File

@ -174,16 +174,12 @@ impl Widget for CpuGraph {
}
};
// debug!("Area: {:?}", area);
let split_area = Layout::default()
.margin(0)
.direction(Direction::Horizontal)
.constraints(constraints)
.split(area);
// debug!("Split area: {:?}", split_area);
const Y_BOUNDS: [f64; 2] = [0.0, 100.5];
let y_bound_labels: [Cow<'static, str>; 2] = ["0%".into(), "100%".into()];

View File

@ -7,7 +7,7 @@ use unicode_segmentation::GraphemeCursor;
use tui::{
backend::Backend,
layout::Rect,
layout::{Constraint, Direction, Layout, Rect},
widgets::{Block, Borders, TableState},
Frame,
};
@ -15,7 +15,7 @@ use tui::{
use crate::{
app::{
data_harvester::processes::ProcessHarvest,
event::{MultiKey, MultiKeyResult, WidgetEventResult},
event::{MultiKey, MultiKeyResult, ReturnSignal, WidgetEventResult},
query::*,
DataCollection,
},
@ -30,7 +30,7 @@ use super::{
sort_text_table::{SimpleSortableColumn, SortStatus, SortableColumn},
text_table::TextTableData,
AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection,
SortableTextTable, TextInput, TextTable, Widget,
SortMenu, SortableTextTable, TextInput, Widget,
};
/// AppSearchState deals with generic searching (I might do this in the future).
@ -836,11 +836,17 @@ impl SortableColumn for ProcessSortColumn {
}
}
enum ProcessSortState {
Shown,
Hidden,
}
/// A searchable, sortable table to manage processes.
pub struct ProcessManager {
bounds: Rect,
process_table: SortableTextTable<ProcessSortColumn>,
sort_table: TextTable,
sort_menu: SortMenu,
search_input: TextInput,
dd_multi: MultiKey,
@ -848,7 +854,7 @@ pub struct ProcessManager {
selected: ProcessManagerSelection,
in_tree_mode: bool,
show_sort: bool, // TODO: Add this for temp and disk???
sort_status: ProcessSortState,
show_search: bool,
search_modifiers: SearchModifiers,
@ -873,17 +879,15 @@ impl ProcessManager {
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,
sort_table: TextTable::new(vec![]), // TODO: Do this too
sort_menu: SortMenu::new(process_table_columns.len()),
process_table: SortableTextTable::new(process_table_columns).default_sort_index(2),
search_input: TextInput::new(),
dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static...
selected: ProcessManagerSelection::Processes,
in_tree_mode: false,
show_sort: false,
sort_status: ProcessSortState::Hidden,
show_search: false,
search_modifiers: SearchModifiers::default(),
display_data: Default::default(),
@ -911,7 +915,9 @@ impl ProcessManager {
if let ProcessManagerSelection::Sort = self.selected {
WidgetEventResult::NoRedraw
} else {
self.show_sort = true;
self.sort_menu
.set_index(self.process_table.current_sorting_column_index());
self.sort_status = ProcessSortState::Shown;
self.selected = ProcessManagerSelection::Sort;
WidgetEventResult::Redraw
}
@ -945,6 +951,20 @@ impl Component for ProcessManager {
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
// "Global" handling:
match event.code {
KeyCode::Esc => {
if let ProcessSortState::Shown = self.sort_status {
self.sort_status = ProcessSortState::Hidden;
if let ProcessManagerSelection::Sort = self.selected {
self.selected = ProcessManagerSelection::Processes;
}
return WidgetEventResult::Redraw;
}
}
_ => {}
}
match self.selected {
ProcessManagerSelection::Processes => {
// Try to catch some stuff first...
@ -982,7 +1002,7 @@ impl Component for ProcessManager {
self.in_tree_mode = !self.in_tree_mode;
return WidgetEventResult::Redraw;
}
KeyCode::F(6) => {
KeyCode::Char('s') | KeyCode::F(6) => {
return self.open_sort();
}
KeyCode::F(9) => {
@ -1003,6 +1023,18 @@ impl Component for ProcessManager {
self.process_table.handle_key_event(event)
}
ProcessManagerSelection::Sort => {
match event.code {
KeyCode::Enter if event.modifiers.is_empty() => {
self.process_table
.set_sort_index(self.sort_menu.current_index());
return WidgetEventResult::Signal(ReturnSignal::Update);
}
_ => {}
}
self.sort_menu.handle_key_event(event)
}
ProcessManagerSelection::Search => {
if event.modifiers.is_empty() {
match event.code {
KeyCode::F(1) => {}
@ -1019,9 +1051,8 @@ impl Component for ProcessManager {
}
}
self.sort_table.handle_key_event(event)
self.search_input.handle_key_event(event)
}
ProcessManagerSelection::Search => self.search_input.handle_key_event(event),
}
}
@ -1041,12 +1072,12 @@ impl Component for ProcessManager {
WidgetEventResult::Signal(s) => WidgetEventResult::Signal(s),
}
}
} else if self.sort_table.does_border_intersect_mouse(&event) {
} else if self.sort_menu.does_border_intersect_mouse(&event) {
if let ProcessManagerSelection::Sort = self.selected {
self.sort_table.handle_mouse_event(event)
self.sort_menu.handle_mouse_event(event)
} else {
self.selected = ProcessManagerSelection::Sort;
self.sort_table.handle_mouse_event(event);
self.sort_menu.handle_mouse_event(event);
WidgetEventResult::Redraw
}
} else if self.search_input.does_border_intersect_mouse(&event) {
@ -1063,7 +1094,7 @@ impl Component for ProcessManager {
}
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => match self.selected {
ProcessManagerSelection::Processes => self.process_table.handle_mouse_event(event),
ProcessManagerSelection::Sort => self.sort_table.handle_mouse_event(event),
ProcessManagerSelection::Sort => self.sort_menu.handle_mouse_event(event),
ProcessManagerSelection::Search => self.search_input.handle_mouse_event(event),
},
_ => WidgetEventResult::NoRedraw,
@ -1079,16 +1110,76 @@ impl Widget for ProcessManager {
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
let block = Block::default()
.border_style(if selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
match self.sort_status {
ProcessSortState::Shown => {
const SORT_CONSTRAINTS: [Constraint; 2] =
[Constraint::Length(10), Constraint::Min(0)];
self.process_table
.draw_tui_table(painter, f, &self.display_data, block, area, selected);
let split_area = Layout::default()
.margin(0)
.direction(Direction::Horizontal)
.constraints(SORT_CONSTRAINTS)
.split(area);
let sort_block = Block::default()
.border_style(if selected {
if let ProcessManagerSelection::Sort = self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
}
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
self.sort_menu.draw_sort_menu(
painter,
f,
self.process_table.columns(),
sort_block,
split_area[0],
);
let process_block = Block::default()
.border_style(if selected {
if let ProcessManagerSelection::Processes = self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
}
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
self.process_table.draw_tui_table(
painter,
f,
&self.display_data,
process_block,
split_area[1],
selected,
);
}
ProcessSortState::Hidden => {
let block = Block::default()
.border_style(if selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
self.process_table.draw_tui_table(
painter,
f,
&self.display_data,
block,
area,
selected,
);
}
}
}
fn update_data(&mut self, data_collection: &DataCollection) {
@ -1099,58 +1190,60 @@ impl Widget for ProcessManager {
// TODO: Filtering
true
})
.sorted_by(match self.process_table.current_column().sort_type {
ProcessSortType::Pid => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.pid.cmp(&b.pid)
}
ProcessSortType::Count => {
todo!()
}
ProcessSortType::Name => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.name.cmp(&b.name)
}
ProcessSortType::Command => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.command.cmp(&b.command)
}
ProcessSortType::Cpu => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
FloatOrd(a.cpu_usage_percent).cmp(&FloatOrd(b.cpu_usage_percent))
},
ProcessSortType::Mem => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.mem_usage_bytes.cmp(&b.mem_usage_bytes)
},
ProcessSortType::MemPercent => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
FloatOrd(a.mem_usage_percent).cmp(&FloatOrd(b.mem_usage_percent))
},
ProcessSortType::Rps => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.read_bytes_per_sec.cmp(&b.read_bytes_per_sec)
},
ProcessSortType::Wps => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.write_bytes_per_sec.cmp(&b.write_bytes_per_sec)
},
ProcessSortType::TotalRead => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.total_read_bytes.cmp(&b.total_read_bytes)
},
ProcessSortType::TotalWrite => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.total_write_bytes.cmp(&b.total_write_bytes)
},
ProcessSortType::User => {
#[cfg(target_family = "unix")]
{
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.user.cmp(&b.user)
.sorted_by(
match self.process_table.current_sorting_column().sort_type {
ProcessSortType::Pid => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.pid.cmp(&b.pid)
}
#[cfg(not(target_family = "unix"))]
{
|_a: &&ProcessHarvest, _b: &&ProcessHarvest| Ord::Eq
ProcessSortType::Count => {
todo!()
}
}
ProcessSortType::State => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.process_state.cmp(&b.process_state)
}
});
ProcessSortType::Name => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.name.cmp(&b.name)
}
ProcessSortType::Command => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.command.cmp(&b.command)
}
ProcessSortType::Cpu => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
FloatOrd(a.cpu_usage_percent).cmp(&FloatOrd(b.cpu_usage_percent))
},
ProcessSortType::Mem => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.mem_usage_bytes.cmp(&b.mem_usage_bytes)
},
ProcessSortType::MemPercent => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
FloatOrd(a.mem_usage_percent).cmp(&FloatOrd(b.mem_usage_percent))
},
ProcessSortType::Rps => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.read_bytes_per_sec.cmp(&b.read_bytes_per_sec)
},
ProcessSortType::Wps => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.write_bytes_per_sec.cmp(&b.write_bytes_per_sec)
},
ProcessSortType::TotalRead => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.total_read_bytes.cmp(&b.total_read_bytes)
},
ProcessSortType::TotalWrite => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.total_write_bytes.cmp(&b.total_write_bytes)
},
ProcessSortType::User => {
#[cfg(target_family = "unix")]
{
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.user.cmp(&b.user)
}
#[cfg(not(target_family = "unix"))]
{
|_a: &&ProcessHarvest, _b: &&ProcessHarvest| Ord::Eq
}
}
ProcessSortType::State => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.process_state.cmp(&b.process_state)
},
},
);
self.display_data = if let SortStatus::SortDescending = self
.process_table
.current_column()
.current_sorting_column()
.sortable_column
.sorting_status()
{

View File

@ -4,7 +4,7 @@
#[macro_use]
extern crate log;
use bottom::{app::event::EventResult, canvas, constants::*, options::*, *};
use bottom::{app::event::EventResult, canvas, options::*, *};
use std::{
boxed::Box,

View File

@ -359,16 +359,41 @@ impl Painter {
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
match layout_node {
LayoutNode::Row(row) => {
let split_area = Layout::default()
.margin(0)
.direction(Direction::Horizontal)
.constraints(row.constraints.clone())
.split(area);
let split_area = {
let initial_split = Layout::default()
.margin(0)
.direction(Direction::Horizontal)
.constraints(row.constraints.clone())
.split(area);
// debug!(
// "Row - constraints: {:#?}, split_area: {:#?}",
// row.constraints, split_area
// );
if initial_split.is_empty() {
vec![]
} else {
let mut checked_split =
Vec::with_capacity(initial_split.len());
let mut right_value = initial_split[0].right();
checked_split.push(initial_split[0]);
for rect in initial_split[1..].iter() {
if right_value == rect.left() {
right_value = rect.right();
checked_split.push(*rect);
} else {
let diff = rect.x.saturating_sub(right_value);
let new_rect = Rect::new(
right_value,
rect.y,
rect.width + diff,
rect.height,
);
right_value = new_rect.right();
checked_split.push(new_rect);
}
}
checked_split
}
};
for (child, child_area) in node.children(arena).zip(split_area) {
traverse_and_draw_tree(
@ -384,16 +409,41 @@ impl Painter {
}
}
LayoutNode::Col(col) => {
let split_area = Layout::default()
.margin(0)
.direction(Direction::Vertical)
.constraints(col.constraints.clone())
.split(area);
let split_area = {
let initial_split = Layout::default()
.margin(0)
.direction(Direction::Vertical)
.constraints(col.constraints.clone())
.split(area);
// debug!(
// "Col - constraints: {:#?}, split_area: {:#?}",
// col.constraints, split_area
// );
if initial_split.is_empty() {
vec![]
} else {
let mut checked_split =
Vec::with_capacity(initial_split.len());
let mut bottom_value = initial_split[0].bottom();
checked_split.push(initial_split[0]);
for rect in initial_split[1..].iter() {
if bottom_value == rect.top() {
bottom_value = rect.bottom();
checked_split.push(*rect);
} else {
let diff = rect.y.saturating_sub(bottom_value);
let new_rect = Rect::new(
rect.x,
bottom_value,
rect.width,
rect.height + diff,
);
bottom_value = new_rect.bottom();
checked_split.push(new_rect);
}
}
checked_split
}
};
for (child, child_area) in node.children(arena).zip(split_area) {
traverse_and_draw_tree(
@ -409,8 +459,6 @@ impl Painter {
}
}
LayoutNode::Widget => {
// debug!("Widget - area: {:#?}", area);
if let Some(widget) = lookup_map.get_mut(&node) {
widget.set_bounds(area);
widget.draw(painter, f, area, selected_id == node);