diff --git a/src/app/widgets/base.rs b/src/app/widgets/base.rs index d32ea72e..c5bf80cf 100644 --- a/src/app/widgets/base.rs +++ b/src/app/widgets/base.rs @@ -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; diff --git a/src/app/widgets/base/scrollable.rs b/src/app/widgets/base/scrollable.rs index 54000a06..dc55a717 100644 --- a/src/app/widgets/base/scrollable.rs +++ b/src/app/widgets/base/scrollable.rs @@ -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 { diff --git a/src/app/widgets/base/sort_menu.rs b/src/app/widgets/base/sort_menu.rs new file mode 100644 index 00000000..fc85d0bb --- /dev/null +++ b/src/app/widgets/base/sort_menu.rs @@ -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( + &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::>(); + + 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) + } +} diff --git a/src/app/widgets/base/sort_text_table.rs b/src/app/widgets/base/sort_text_table.rs index cc95f2c0..e8f0ff18 100644 --- a/src/app/widgets/base/sort_text_table.rs +++ b/src/app/widgets/base/sort_text_table.rs @@ -257,9 +257,6 @@ where /// The underlying [`TextTable`]. pub table: TextTable, - - /// A corresponding "sort" menu. - pub sort_menu: TextTable, } impl SortableTextTable @@ -268,15 +265,9 @@ where { /// Creates a new [`SortableTextTable`]. Note that `columns` cannot be empty. pub fn new(columns: Vec) -> Self { - let sort_menu_columns = columns - .iter() - .map(|column| SimpleColumn::new_hard(column.original_name().clone(), None)) - .collect::>(); - 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( - &mut self, painter: &Painter, f: &mut Frame<'_, B>, block: Block<'_>, block_area: Rect, - ) { - } } impl Component for SortableTextTable diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index 87f3f69d..3ce56886 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -173,6 +173,10 @@ where self } + pub fn columns(&self) -> &[C] { + &self.columns + } + fn displayed_column_names(&self) -> Vec> { 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) { diff --git a/src/app/widgets/cpu.rs b/src/app/widgets/cpu.rs index 12647a47..e680aa00 100644 --- a/src/app/widgets/cpu.rs +++ b/src/app/widgets/cpu.rs @@ -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()]; diff --git a/src/app/widgets/process.rs b/src/app/widgets/process.rs index 735616e1..c1b26c66 100644 --- a/src/app/widgets/process.rs +++ b/src/app/widgets/process.rs @@ -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, - 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( &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() { diff --git a/src/bin/main.rs b/src/bin/main.rs index 9b1edfbd..c77d6811 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -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, diff --git a/src/canvas.rs b/src/canvas.rs index 1725d82d..e333e3b9 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -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);