mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-26 23:24:20 +02:00
refactor: add back grouping and command
This commit is contained in:
parent
b1889b0934
commit
204b4dc351
@ -1374,7 +1374,7 @@ impl AppState {
|
||||
}
|
||||
|
||||
pub fn start_killing_process(&mut self) {
|
||||
self.reset_multi_tap_keys();
|
||||
todo!()
|
||||
|
||||
// if let Some(proc_widget_state) = self
|
||||
// .proc_state
|
||||
|
@ -56,7 +56,8 @@ pub struct DataCollection {
|
||||
pub cpu_harvest: cpu::CpuHarvest,
|
||||
pub load_avg_harvest: cpu::LoadAvgHarvest,
|
||||
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 io_harvest: disks::IoHarvest,
|
||||
pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>,
|
||||
@ -77,7 +78,8 @@ impl Default for DataCollection {
|
||||
cpu_harvest: cpu::CpuHarvest::default(),
|
||||
load_avg_harvest: cpu::LoadAvgHarvest::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(),
|
||||
io_harvest: disks::IoHarvest::default(),
|
||||
io_labels_and_prev: Vec::default(),
|
||||
@ -312,6 +314,27 @@ impl DataCollection {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -301,8 +301,28 @@ where
|
||||
&self.table.columns
|
||||
}
|
||||
|
||||
pub fn set_column(&mut self, column: S, index: usize) {
|
||||
self.table.set_column(index, column)
|
||||
pub fn set_column(&mut self, mut column: S, index: usize) {
|
||||
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) {
|
||||
@ -341,6 +361,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_cached_columns(&mut self) {
|
||||
self.table.invalidate_cached_columns();
|
||||
}
|
||||
|
||||
/// Draws a [`tui::widgets::Table`] on screen.
|
||||
///
|
||||
/// Note if the number of columns don't match in the [`SortableTextTable`] and data,
|
||||
|
@ -44,7 +44,7 @@ pub struct TextInput {
|
||||
}
|
||||
|
||||
impl Default for TextInput {
|
||||
fn default() -> Self {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: Default::default(),
|
||||
bounds: Default::default(),
|
||||
@ -100,15 +100,17 @@ impl TextInput {
|
||||
fn move_word_forward(&mut self) -> WidgetEventResult {
|
||||
let current_index = self.cursor.cur_cursor();
|
||||
|
||||
for (index, _word) in self.text[current_index..].unicode_word_indices() {
|
||||
if index > current_index {
|
||||
self.cursor.set_cursor(index);
|
||||
self.cursor_direction = CursorDirection::Right;
|
||||
return WidgetEventResult::Redraw;
|
||||
if current_index < self.text.len() {
|
||||
for (index, _word) in self.text[current_index..].unicode_word_indices() {
|
||||
if index > 0 {
|
||||
self.cursor.set_cursor(index + current_index);
|
||||
self.cursor_direction = CursorDirection::Right;
|
||||
return WidgetEventResult::Redraw;
|
||||
}
|
||||
}
|
||||
self.cursor.set_cursor(self.text.len());
|
||||
}
|
||||
|
||||
self.cursor.set_cursor(self.text.len());
|
||||
WidgetEventResult::Redraw
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
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`].
|
||||
pub fn draw_tui_table<B: Backend>(
|
||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef,
|
||||
|
@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
|
||||
use float_ord::FloatOrd;
|
||||
use itertools::Itertools;
|
||||
use itertools::{Either, Itertools};
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
|
||||
use tui::{
|
||||
@ -928,6 +928,64 @@ impl ProcessManager {
|
||||
pub fn is_case_sensitive(&self) -> bool {
|
||||
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 {
|
||||
@ -964,9 +1022,11 @@ impl Component for ProcessManager {
|
||||
match event.code {
|
||||
KeyCode::Tab => {
|
||||
// Handle grouping/ungrouping
|
||||
return self.toggle_grouped();
|
||||
}
|
||||
KeyCode::Char('P') => {
|
||||
// Show full command/process name
|
||||
return self.toggle_command();
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
match self.dd_multi.input('d') {
|
||||
@ -1009,6 +1069,7 @@ impl Component for ProcessManager {
|
||||
} else if let KeyModifiers::SHIFT = event.modifiers {
|
||||
if let KeyCode::Char('P') = event.code {
|
||||
// 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) {
|
||||
let filtered_sorted_iterator = data_collection
|
||||
.process_harvest
|
||||
.iter()
|
||||
.filter(|process| {
|
||||
if let Some(Ok(query)) = &self.process_filter {
|
||||
query.check(
|
||||
process,
|
||||
matches!(
|
||||
self.process_table.columns()[1].sort_type,
|
||||
ProcessSortType::Command
|
||||
),
|
||||
)
|
||||
let mut id_pid_map: HashMap<String, ProcessHarvest>;
|
||||
|
||||
let filtered_iter = data_collection.process_harvest.iter().filter(|process| {
|
||||
if let Some(Ok(query)) = &self.process_filter {
|
||||
query.check(process, self.is_using_command())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
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 {
|
||||
ProcessSortType::Pid => {
|
||||
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.pid.cmp(&b.pid)
|
||||
}
|
||||
ProcessSortType::Count => {
|
||||
todo!()
|
||||
// This case should be impossible by the above check.
|
||||
unreachable!()
|
||||
}
|
||||
ProcessSortType::Name => {
|
||||
|a: &&ProcessHarvest, b: &&ProcessHarvest| a.name.cmp(&b.name)
|
||||
@ -1287,14 +1393,15 @@ impl Widget for ProcessManager {
|
||||
}
|
||||
#[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| {
|
||||
a.process_state.cmp(&b.process_state)
|
||||
},
|
||||
},
|
||||
);
|
||||
))
|
||||
};
|
||||
|
||||
self.display_data = if let SortStatus::SortDescending = self
|
||||
.process_table
|
||||
@ -1302,9 +1409,9 @@ impl Widget for ProcessManager {
|
||||
.sortable_column
|
||||
.sorting_status()
|
||||
{
|
||||
itertools::Either::Left(filtered_sorted_iterator.rev())
|
||||
Either::Left(filtered_sorted_iter.rev())
|
||||
} else {
|
||||
itertools::Either::Right(filtered_sorted_iterator)
|
||||
Either::Right(filtered_sorted_iter)
|
||||
}
|
||||
.map(|process| {
|
||||
self.process_table
|
||||
@ -1312,7 +1419,27 @@ impl Widget for ProcessManager {
|
||||
.iter()
|
||||
.map(|column| match &column.sort_type {
|
||||
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::Command => (process.command.clone().into(), None, None),
|
||||
ProcessSortType::Cpu => (
|
||||
|
@ -435,6 +435,10 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r##"
|
||||
default=true
|
||||
"##;
|
||||
|
||||
pub const DEFAULT_BASIC_LAYOUT: &str = r##"
|
||||
[[row]]
|
||||
"##;
|
||||
|
||||
// Config and flags
|
||||
pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml";
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user