refactor: add back grouping and command

This commit is contained in:
ClementTsang 2021-08-31 16:53:43 -04:00
parent b1889b0934
commit 204b4dc351
7 changed files with 230 additions and 34 deletions

View File

@ -1374,7 +1374,7 @@ impl AppState {
} }
pub fn start_killing_process(&mut self) { pub fn start_killing_process(&mut self) {
self.reset_multi_tap_keys(); todo!()
// if let Some(proc_widget_state) = self // if let Some(proc_widget_state) = self
// .proc_state // .proc_state

View File

@ -56,7 +56,8 @@ pub struct DataCollection {
pub cpu_harvest: cpu::CpuHarvest, pub cpu_harvest: cpu::CpuHarvest,
pub load_avg_harvest: cpu::LoadAvgHarvest, pub load_avg_harvest: cpu::LoadAvgHarvest,
pub process_harvest: Vec<processes::ProcessHarvest>, pub process_harvest: Vec<processes::ProcessHarvest>,
pub process_count_mapping: HashMap<String, Pid>, pub process_name_pid_map: HashMap<String, Vec<Pid>>,
pub process_cmd_pid_map: HashMap<String, Vec<Pid>>,
pub disk_harvest: Vec<disks::DiskHarvest>, pub disk_harvest: Vec<disks::DiskHarvest>,
pub io_harvest: disks::IoHarvest, pub io_harvest: disks::IoHarvest,
pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>, pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>,
@ -77,7 +78,8 @@ impl Default for DataCollection {
cpu_harvest: cpu::CpuHarvest::default(), cpu_harvest: cpu::CpuHarvest::default(),
load_avg_harvest: cpu::LoadAvgHarvest::default(), load_avg_harvest: cpu::LoadAvgHarvest::default(),
process_harvest: Vec::default(), process_harvest: Vec::default(),
process_count_mapping: HashMap::default(), process_name_pid_map: HashMap::default(),
process_cmd_pid_map: HashMap::default(),
disk_harvest: Vec::default(), disk_harvest: Vec::default(),
io_harvest: disks::IoHarvest::default(), io_harvest: disks::IoHarvest::default(),
io_labels_and_prev: Vec::default(), io_labels_and_prev: Vec::default(),
@ -312,6 +314,27 @@ impl DataCollection {
} }
fn eat_proc(&mut self, list_of_processes: Vec<processes::ProcessHarvest>) { fn eat_proc(&mut self, list_of_processes: Vec<processes::ProcessHarvest>) {
// TODO: Probably more efficient to do this in the data collection step, but it's fine for now.
self.process_name_pid_map.clear();
self.process_cmd_pid_map.clear();
list_of_processes.iter().for_each(|process_harvest| {
if let Some(entry) = self.process_name_pid_map.get_mut(&process_harvest.name) {
entry.push(process_harvest.pid);
} else {
self.process_name_pid_map
.insert(process_harvest.name.to_string(), vec![process_harvest.pid]);
}
if let Some(entry) = self.process_cmd_pid_map.get_mut(&process_harvest.command) {
entry.push(process_harvest.pid);
} else {
self.process_cmd_pid_map.insert(
process_harvest.command.to_string(),
vec![process_harvest.pid],
);
}
});
self.process_harvest = list_of_processes; self.process_harvest = list_of_processes;
} }

View File

@ -301,8 +301,28 @@ where
&self.table.columns &self.table.columns
} }
pub fn set_column(&mut self, column: S, index: usize) { pub fn set_column(&mut self, mut column: S, index: usize) {
self.table.set_column(index, column) if let Some(old_column) = self.table.columns().get(index) {
column.set_sorting_status(old_column.sorting_status());
}
self.table.set_column(index, column);
}
pub fn add_column(&mut self, column: S, index: usize) {
self.table.add_column(index, column);
}
pub fn remove_column(&mut self, index: usize, new_sort_index: Option<usize>) {
self.table.remove_column(index);
// Reset the sort index either a supplied one or a new one if needed.
if index == self.sort_index {
if let Some(new_sort_index) = new_sort_index {
self.set_sort_index(new_sort_index);
} else {
self.set_sort_index(0);
}
}
} }
pub fn set_sort_index(&mut self, new_index: usize) { pub fn set_sort_index(&mut self, new_index: usize) {
@ -341,6 +361,10 @@ where
} }
} }
pub fn invalidate_cached_columns(&mut self) {
self.table.invalidate_cached_columns();
}
/// Draws a [`tui::widgets::Table`] on screen. /// Draws a [`tui::widgets::Table`] on screen.
/// ///
/// Note if the number of columns don't match in the [`SortableTextTable`] and data, /// Note if the number of columns don't match in the [`SortableTextTable`] and data,

View File

@ -44,7 +44,7 @@ pub struct TextInput {
} }
impl Default for TextInput { impl Default for TextInput {
fn default() -> Self { fn default() -> Self {
Self { Self {
text: Default::default(), text: Default::default(),
bounds: Default::default(), bounds: Default::default(),
@ -100,15 +100,17 @@ impl TextInput {
fn move_word_forward(&mut self) -> WidgetEventResult { fn move_word_forward(&mut self) -> WidgetEventResult {
let current_index = self.cursor.cur_cursor(); let current_index = self.cursor.cur_cursor();
for (index, _word) in self.text[current_index..].unicode_word_indices() { if current_index < self.text.len() {
if index > current_index { for (index, _word) in self.text[current_index..].unicode_word_indices() {
self.cursor.set_cursor(index); if index > 0 {
self.cursor_direction = CursorDirection::Right; self.cursor.set_cursor(index + current_index);
return WidgetEventResult::Redraw; self.cursor_direction = CursorDirection::Right;
return WidgetEventResult::Redraw;
}
} }
self.cursor.set_cursor(self.text.len());
} }
self.cursor.set_cursor(self.text.len());
WidgetEventResult::Redraw WidgetEventResult::Redraw
} }

View File

@ -195,6 +195,18 @@ where
} }
} }
pub fn add_column(&mut self, index: usize, column: C) {
if self.columns.len() >= index {
self.columns.insert(index, column);
}
}
pub fn remove_column(&mut self, index: usize) {
if self.columns.len() > index {
self.columns.remove(index);
}
}
pub fn current_scroll_index(&self) -> usize { pub fn current_scroll_index(&self) -> usize {
self.scrollable.current_index() self.scrollable.current_index()
} }
@ -360,6 +372,10 @@ where
} }
} }
pub fn invalidate_cached_columns(&mut self) {
self.cached_column_widths = CachedColumnWidths::Uncached;
}
/// Draws a [`Table`] on screen corresponding to the [`TextTable`]. /// Draws a [`Table`] on screen corresponding to the [`TextTable`].
pub fn draw_tui_table<B: Backend>( pub fn draw_tui_table<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef, &mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef,

View File

@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use float_ord::FloatOrd; use float_ord::FloatOrd;
use itertools::Itertools; use itertools::{Either, Itertools};
use unicode_segmentation::GraphemeCursor; use unicode_segmentation::GraphemeCursor;
use tui::{ use tui::{
@ -928,6 +928,64 @@ impl ProcessManager {
pub fn is_case_sensitive(&self) -> bool { pub fn is_case_sensitive(&self) -> bool {
self.search_modifiers.enable_case_sensitive self.search_modifiers.enable_case_sensitive
} }
fn is_using_command(&self) -> bool {
matches!(
self.process_table.columns()[1].sort_type,
ProcessSortType::Command
)
}
fn toggle_command(&mut self) -> WidgetEventResult {
if self.is_using_command() {
self.process_table
.set_column(ProcessSortColumn::new(ProcessSortType::Name), 1);
} else {
self.process_table
.set_column(ProcessSortColumn::new(ProcessSortType::Command), 1);
}
// Invalidate row cache.
self.process_table.invalidate_cached_columns();
WidgetEventResult::Signal(ReturnSignal::Update)
}
fn is_grouped(&self) -> bool {
matches!(
self.process_table.columns()[0].sort_type,
ProcessSortType::Count
)
}
fn toggle_grouped(&mut self) -> WidgetEventResult {
if self.is_grouped() {
self.process_table
.set_column(ProcessSortColumn::new(ProcessSortType::Pid), 0);
self.process_table
.add_column(ProcessSortColumn::new(ProcessSortType::State), 8);
#[cfg(target_family = "unix")]
{
self.process_table
.add_column(ProcessSortColumn::new(ProcessSortType::User), 8);
}
} else {
self.process_table
.set_column(ProcessSortColumn::new(ProcessSortType::Count), 0);
#[cfg(target_family = "unix")]
{
self.process_table.remove_column(9, Some(2));
}
self.process_table.remove_column(8, Some(2));
}
// Invalidate row cache.
self.process_table.invalidate_cached_columns();
WidgetEventResult::Signal(ReturnSignal::Update)
}
} }
impl Component for ProcessManager { impl Component for ProcessManager {
@ -964,9 +1022,11 @@ impl Component for ProcessManager {
match event.code { match event.code {
KeyCode::Tab => { KeyCode::Tab => {
// Handle grouping/ungrouping // Handle grouping/ungrouping
return self.toggle_grouped();
} }
KeyCode::Char('P') => { KeyCode::Char('P') => {
// Show full command/process name // Show full command/process name
return self.toggle_command();
} }
KeyCode::Char('d') => { KeyCode::Char('d') => {
match self.dd_multi.input('d') { match self.dd_multi.input('d') {
@ -1009,6 +1069,7 @@ impl Component for ProcessManager {
} else if let KeyModifiers::SHIFT = event.modifiers { } else if let KeyModifiers::SHIFT = event.modifiers {
if let KeyCode::Char('P') = event.code { if let KeyCode::Char('P') = event.code {
// Show full command/process name // Show full command/process name
return self.toggle_command();
} }
} }
@ -1229,29 +1290,74 @@ impl Widget for ProcessManager {
} }
fn update_data(&mut self, data_collection: &DataCollection) { fn update_data(&mut self, data_collection: &DataCollection) {
let filtered_sorted_iterator = data_collection let mut id_pid_map: HashMap<String, ProcessHarvest>;
.process_harvest
.iter() let filtered_iter = data_collection.process_harvest.iter().filter(|process| {
.filter(|process| { if let Some(Ok(query)) = &self.process_filter {
if let Some(Ok(query)) = &self.process_filter { query.check(process, self.is_using_command())
query.check( } else {
process, true
matches!( }
self.process_table.columns()[1].sort_type, });
ProcessSortType::Command
), let filtered_grouped_iter = if self.is_grouped() {
) id_pid_map = HashMap::new();
filtered_iter.for_each(|process_harvest| {
let id = if self.is_using_command() {
&process_harvest.command
} else { } else {
true &process_harvest.name
};
if let Some(grouped_process_harvest) = id_pid_map.get_mut(id) {
grouped_process_harvest.cpu_usage_percent += process_harvest.cpu_usage_percent;
grouped_process_harvest.mem_usage_bytes += process_harvest.mem_usage_bytes;
grouped_process_harvest.mem_usage_percent += process_harvest.mem_usage_percent;
grouped_process_harvest.read_bytes_per_sec +=
process_harvest.read_bytes_per_sec;
grouped_process_harvest.write_bytes_per_sec +=
process_harvest.write_bytes_per_sec;
grouped_process_harvest.total_read_bytes += process_harvest.total_read_bytes;
grouped_process_harvest.total_write_bytes += process_harvest.total_write_bytes;
} else {
id_pid_map.insert(id.clone(), process_harvest.clone());
} }
}) });
.sorted_by(
Either::Left(id_pid_map.values())
} else {
Either::Right(filtered_iter)
};
let filtered_sorted_iter = if let ProcessSortType::Count =
self.process_table.current_sorting_column().sort_type
{
let mut v = filtered_grouped_iter.collect::<Vec<_>>();
v.sort_by_cached_key(|k| {
if self.is_using_command() {
data_collection
.process_cmd_pid_map
.get(&k.command)
.map(|v| v.len())
.unwrap_or(0)
} else {
data_collection
.process_name_pid_map
.get(&k.name)
.map(|v| v.len())
.unwrap_or(0)
}
});
Either::Left(v.into_iter())
} else {
Either::Right(filtered_grouped_iter.sorted_by(
match self.process_table.current_sorting_column().sort_type { match self.process_table.current_sorting_column().sort_type {
ProcessSortType::Pid => { ProcessSortType::Pid => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.pid.cmp(&b.pid) |a: &&ProcessHarvest, b: &&ProcessHarvest| a.pid.cmp(&b.pid)
} }
ProcessSortType::Count => { ProcessSortType::Count => {
todo!() // This case should be impossible by the above check.
unreachable!()
} }
ProcessSortType::Name => { ProcessSortType::Name => {
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.name.cmp(&b.name) |a: &&ProcessHarvest, b: &&ProcessHarvest| a.name.cmp(&b.name)
@ -1287,14 +1393,15 @@ impl Widget for ProcessManager {
} }
#[cfg(not(target_family = "unix"))] #[cfg(not(target_family = "unix"))]
{ {
|_a: &&ProcessHarvest, _b: &&ProcessHarvest| Ord::Eq |_a: &&ProcessHarvest, _b: &&ProcessHarvest| std::cmp::Ordering::Equal
} }
} }
ProcessSortType::State => |a: &&ProcessHarvest, b: &&ProcessHarvest| { ProcessSortType::State => |a: &&ProcessHarvest, b: &&ProcessHarvest| {
a.process_state.cmp(&b.process_state) a.process_state.cmp(&b.process_state)
}, },
}, },
); ))
};
self.display_data = if let SortStatus::SortDescending = self self.display_data = if let SortStatus::SortDescending = self
.process_table .process_table
@ -1302,9 +1409,9 @@ impl Widget for ProcessManager {
.sortable_column .sortable_column
.sorting_status() .sorting_status()
{ {
itertools::Either::Left(filtered_sorted_iterator.rev()) Either::Left(filtered_sorted_iter.rev())
} else { } else {
itertools::Either::Right(filtered_sorted_iterator) Either::Right(filtered_sorted_iter)
} }
.map(|process| { .map(|process| {
self.process_table self.process_table
@ -1312,7 +1419,27 @@ impl Widget for ProcessManager {
.iter() .iter()
.map(|column| match &column.sort_type { .map(|column| match &column.sort_type {
ProcessSortType::Pid => (process.pid.to_string().into(), None, None), ProcessSortType::Pid => (process.pid.to_string().into(), None, None),
ProcessSortType::Count => ("".into(), None, None), ProcessSortType::Count => (
if self.is_using_command() {
data_collection
.process_cmd_pid_map
.get(&process.command)
.map(|v| v.len())
.unwrap_or(0)
.to_string()
.into()
} else {
data_collection
.process_name_pid_map
.get(&process.name)
.map(|v| v.len())
.unwrap_or(0)
.to_string()
.into()
},
None,
None,
),
ProcessSortType::Name => (process.name.clone().into(), None, None), ProcessSortType::Name => (process.name.clone().into(), None, None),
ProcessSortType::Command => (process.command.clone().into(), None, None), ProcessSortType::Command => (process.command.clone().into(), None, None),
ProcessSortType::Cpu => ( ProcessSortType::Cpu => (

View File

@ -435,6 +435,10 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r##"
default=true default=true
"##; "##;
pub const DEFAULT_BASIC_LAYOUT: &str = r##"
[[row]]
"##;
// Config and flags // Config and flags
pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml"; pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml";