Files
bottom/src/app/widgets/process.rs
T
ClementTsang 48c572dbaf refactor: change up event handling logistics
Slightly move around the ideas of EventResult, ReturnSignalResult,
and how they all work.

The gist of it is that we now have widgets returning EventResults (and
renamed to WidgetEventResult), and the main app event handler returns
ReturnSignalResult (now named EventResult).

Also add a new signal to handle re-updating data inputs! This is needed
for the process, and any sortable/configurable widget.
2021-08-29 19:43:27 -04:00

1213 lines
42 KiB
Rust

use std::collections::HashMap;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use float_ord::FloatOrd;
use itertools::Itertools;
use unicode_segmentation::GraphemeCursor;
use tui::{
backend::Backend,
layout::Rect,
widgets::{Block, Borders, TableState},
Frame,
};
use crate::{
app::{
data_harvester::processes::ProcessHarvest,
event::{MultiKey, MultiKeyResult, WidgetEventResult},
query::*,
DataCollection,
},
canvas::Painter,
data_conversion::get_string_with_bytes,
data_harvester::processes::{self, ProcessSorting},
options::ProcessDefaults,
};
use ProcessSorting::*;
use super::{
sort_text_table::{SimpleSortableColumn, SortStatus, 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).
pub struct AppSearchState {
pub is_enabled: bool,
pub current_search_query: String,
pub is_blank_search: bool,
pub is_invalid_search: bool,
pub grapheme_cursor: GraphemeCursor,
pub cursor_direction: CursorDirection,
pub cursor_bar: usize,
/// This represents the position in terms of CHARACTERS, not graphemes
pub char_cursor_position: usize,
/// The query
pub query: Option<Query>,
pub error_message: Option<String>,
}
impl Default for AppSearchState {
fn default() -> Self {
AppSearchState {
is_enabled: false,
current_search_query: String::default(),
is_invalid_search: false,
is_blank_search: true,
grapheme_cursor: GraphemeCursor::new(0, 0, true),
cursor_direction: CursorDirection::Right,
cursor_bar: 0,
char_cursor_position: 0,
query: None,
error_message: None,
}
}
}
impl AppSearchState {
/// Returns a reset but still enabled app search state
pub fn reset(&mut self) {
*self = AppSearchState {
is_enabled: self.is_enabled,
..AppSearchState::default()
}
}
pub fn is_invalid_or_blank_search(&self) -> bool {
self.is_blank_search || self.is_invalid_search
}
}
/// ProcessSearchState only deals with process' search's current settings and state.
pub struct ProcessSearchState {
pub search_state: AppSearchState,
pub is_ignoring_case: bool,
pub is_searching_whole_word: bool,
pub is_searching_with_regex: bool,
}
impl Default for ProcessSearchState {
fn default() -> Self {
ProcessSearchState {
search_state: AppSearchState::default(),
is_ignoring_case: true,
is_searching_whole_word: false,
is_searching_with_regex: false,
}
}
}
impl ProcessSearchState {
pub fn search_toggle_ignore_case(&mut self) {
self.is_ignoring_case = !self.is_ignoring_case;
}
pub fn search_toggle_whole_word(&mut self) {
self.is_searching_whole_word = !self.is_searching_whole_word;
}
pub fn search_toggle_regex(&mut self) {
self.is_searching_with_regex = !self.is_searching_with_regex;
}
}
pub struct ColumnInfo {
pub enabled: bool,
pub shortcut: Option<&'static str>,
// FIXME: Move column width logic here!
// pub hard_width: Option<u16>,
// pub max_soft_width: Option<f64>,
}
pub struct ProcColumn {
pub ordered_columns: Vec<ProcessSorting>,
/// The y location of headers. Since they're all aligned, it's just one value.
pub column_header_y_loc: Option<u16>,
/// The x start and end bounds for each header.
pub column_header_x_locs: Option<Vec<(u16, u16)>>,
pub column_mapping: HashMap<ProcessSorting, ColumnInfo>,
pub longest_header_len: u16,
pub column_state: TableState,
pub scroll_direction: ScrollDirection,
pub current_scroll_position: usize,
pub previous_scroll_position: usize,
pub backup_prev_scroll_position: usize,
}
impl Default for ProcColumn {
fn default() -> Self {
let ordered_columns = vec![
Count,
Pid,
ProcessName,
Command,
CpuPercent,
Mem,
MemPercent,
ReadPerSecond,
WritePerSecond,
TotalRead,
TotalWrite,
User,
State,
];
let mut column_mapping = HashMap::new();
let mut longest_header_len = 0;
for column in ordered_columns.clone() {
longest_header_len = std::cmp::max(longest_header_len, column.to_string().len());
match column {
CpuPercent => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: Some("c"),
// hard_width: None,
// max_soft_width: None,
},
);
}
MemPercent => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: Some("m"),
// hard_width: None,
// max_soft_width: None,
},
);
}
Mem => {
column_mapping.insert(
column,
ColumnInfo {
enabled: false,
shortcut: Some("m"),
// hard_width: None,
// max_soft_width: None,
},
);
}
ProcessName => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: Some("n"),
// hard_width: None,
// max_soft_width: None,
},
);
}
Command => {
column_mapping.insert(
column,
ColumnInfo {
enabled: false,
shortcut: Some("n"),
// hard_width: None,
// max_soft_width: None,
},
);
}
Pid => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: Some("p"),
// hard_width: None,
// max_soft_width: None,
},
);
}
Count => {
column_mapping.insert(
column,
ColumnInfo {
enabled: false,
shortcut: None,
// hard_width: None,
// max_soft_width: None,
},
);
}
User => {
column_mapping.insert(
column,
ColumnInfo {
enabled: cfg!(target_family = "unix"),
shortcut: None,
},
);
}
_ => {
column_mapping.insert(
column,
ColumnInfo {
enabled: true,
shortcut: None,
// hard_width: None,
// max_soft_width: None,
},
);
}
}
}
let longest_header_len = longest_header_len as u16;
ProcColumn {
ordered_columns,
column_mapping,
longest_header_len,
column_state: TableState::default(),
scroll_direction: ScrollDirection::default(),
current_scroll_position: 0,
previous_scroll_position: 0,
backup_prev_scroll_position: 0,
column_header_y_loc: None,
column_header_x_locs: None,
}
}
}
impl ProcColumn {
/// Returns its new status.
pub fn toggle(&mut self, column: &ProcessSorting) -> Option<bool> {
if let Some(mapping) = self.column_mapping.get_mut(column) {
mapping.enabled = !(mapping.enabled);
Some(mapping.enabled)
} else {
None
}
}
pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option<bool> {
if let Some(mapping) = self.column_mapping.get_mut(column) {
mapping.enabled = setting;
Some(mapping.enabled)
} else {
None
}
}
pub fn try_enable(&mut self, column: &ProcessSorting) -> Option<bool> {
if let Some(mapping) = self.column_mapping.get_mut(column) {
mapping.enabled = true;
Some(mapping.enabled)
} else {
None
}
}
pub fn try_disable(&mut self, column: &ProcessSorting) -> Option<bool> {
if let Some(mapping) = self.column_mapping.get_mut(column) {
mapping.enabled = false;
Some(mapping.enabled)
} else {
None
}
}
pub fn is_enabled(&self, column: &ProcessSorting) -> bool {
if let Some(mapping) = self.column_mapping.get(column) {
mapping.enabled
} else {
false
}
}
pub fn get_enabled_columns_len(&self) -> usize {
self.ordered_columns
.iter()
.filter_map(|column_type| {
if let Some(col_map) = self.column_mapping.get(column_type) {
if col_map.enabled {
Some(1)
} else {
None
}
} else {
None
}
})
.sum()
}
/// NOTE: ALWAYS call this when opening the sorted window.
pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) {
// TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT!
let mut true_index = 0;
for column in &self.ordered_columns {
if *column == *proc_sorting_type {
break;
}
if self.column_mapping.get(column).unwrap().enabled {
true_index += 1;
}
}
self.current_scroll_position = true_index;
self.backup_prev_scroll_position = self.previous_scroll_position;
}
/// This function sets the scroll position based on the index.
pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) {
self.current_scroll_position = visual_index;
self.backup_prev_scroll_position = self.previous_scroll_position;
}
pub fn get_column_headers(
&self, proc_sorting_type: &ProcessSorting, sort_reverse: bool,
) -> Vec<String> {
const DOWN_ARROW: char = '▼';
const UP_ARROW: char = '▲';
// TODO: Gonna have to figure out how to do left/right GUI notation if we add it.
self.ordered_columns
.iter()
.filter_map(|column_type| {
let mapping = self.column_mapping.get(column_type).unwrap();
let mut command_str = String::default();
if let Some(command) = mapping.shortcut {
command_str = format!("({})", command);
}
if mapping.enabled {
Some(format!(
"{}{}{}",
column_type.to_string(),
command_str.as_str(),
if proc_sorting_type == column_type {
if sort_reverse {
DOWN_ARROW
} else {
UP_ARROW
}
} else {
' '
}
))
} else {
None
}
})
.collect()
}
}
pub struct ProcWidgetState {
pub process_search_state: ProcessSearchState,
pub is_grouped: bool,
pub scroll_state: AppScrollWidgetState,
pub process_sorting_type: processes::ProcessSorting,
pub is_process_sort_descending: bool,
pub is_using_command: bool,
pub current_column_index: usize,
pub is_sort_open: bool,
pub columns: ProcColumn,
pub is_tree_mode: bool,
pub table_width_state: CanvasTableWidthState,
pub requires_redraw: bool,
}
impl ProcWidgetState {
pub fn init(
is_case_sensitive: bool, is_match_whole_word: bool, is_use_regex: bool, is_grouped: bool,
show_memory_as_values: bool, is_tree_mode: bool, is_using_command: bool,
) -> Self {
let mut process_search_state = ProcessSearchState::default();
if is_case_sensitive {
// By default it's off
process_search_state.search_toggle_ignore_case();
}
if is_match_whole_word {
process_search_state.search_toggle_whole_word();
}
if is_use_regex {
process_search_state.search_toggle_regex();
}
let (process_sorting_type, is_process_sort_descending) = if is_tree_mode {
(processes::ProcessSorting::Pid, false)
} else {
(processes::ProcessSorting::CpuPercent, true)
};
// TODO: If we add customizable columns, this should pull from config
let mut columns = ProcColumn::default();
columns.set_to_sorted_index_from_type(&process_sorting_type);
if is_grouped {
// Normally defaults to showing by PID, toggle count on instead.
columns.toggle(&ProcessSorting::Count);
columns.toggle(&ProcessSorting::Pid);
}
if show_memory_as_values {
// Normally defaults to showing by percent, toggle value on instead.
columns.toggle(&ProcessSorting::Mem);
columns.toggle(&ProcessSorting::MemPercent);
}
ProcWidgetState {
process_search_state,
is_grouped,
scroll_state: AppScrollWidgetState::default(),
process_sorting_type,
is_process_sort_descending,
is_using_command,
current_column_index: 0,
is_sort_open: false,
columns,
is_tree_mode,
table_width_state: CanvasTableWidthState::default(),
requires_redraw: false,
}
}
/// Updates sorting when using the column list.
/// ...this really should be part of the ProcColumn struct (along with the sorting fields),
/// but I'm too lazy.
///
/// Sorry, future me, you're gonna have to refactor this later. Too busy getting
/// the feature to work in the first place! :)
pub fn update_sorting_with_columns(&mut self) {
let mut true_index = 0;
let mut enabled_index = 0;
let target_itx = self.columns.current_scroll_position;
for column in &self.columns.ordered_columns {
let enabled = self.columns.column_mapping.get(column).unwrap().enabled;
if enabled_index == target_itx && enabled {
break;
}
if enabled {
enabled_index += 1;
}
true_index += 1;
}
if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) {
if *new_sort_type == self.process_sorting_type {
// Just reverse the search if we're reselecting!
self.is_process_sort_descending = !(self.is_process_sort_descending);
} else {
self.process_sorting_type = new_sort_type.clone();
match self.process_sorting_type {
ProcessSorting::State
| ProcessSorting::Pid
| ProcessSorting::ProcessName
| ProcessSorting::Command => {
// Also invert anything that uses alphabetical sorting by default.
self.is_process_sort_descending = false;
}
_ => {
self.is_process_sort_descending = true;
}
}
}
}
}
pub fn toggle_command_and_name(&mut self, is_using_command: bool) {
if let Some(pn) = self
.columns
.column_mapping
.get_mut(&ProcessSorting::ProcessName)
{
pn.enabled = !is_using_command;
}
if let Some(c) = self
.columns
.column_mapping
.get_mut(&ProcessSorting::Command)
{
c.enabled = is_using_command;
}
}
pub fn get_search_cursor_position(&self) -> usize {
self.process_search_state
.search_state
.grapheme_cursor
.cur_cursor()
}
pub fn get_char_cursor_position(&self) -> usize {
self.process_search_state.search_state.char_cursor_position
}
pub fn is_search_enabled(&self) -> bool {
self.process_search_state.search_state.is_enabled
}
pub fn get_current_search_query(&self) -> &String {
&self.process_search_state.search_state.current_search_query
}
pub fn update_query(&mut self) {
if self
.process_search_state
.search_state
.current_search_query
.is_empty()
{
self.process_search_state.search_state.is_blank_search = true;
self.process_search_state.search_state.is_invalid_search = false;
self.process_search_state.search_state.error_message = None;
} else {
let parsed_query = self.parse_query();
// debug!("Parsed query: {:#?}", parsed_query);
if let Ok(parsed_query) = parsed_query {
self.process_search_state.search_state.query = Some(parsed_query);
self.process_search_state.search_state.is_blank_search = false;
self.process_search_state.search_state.is_invalid_search = false;
self.process_search_state.search_state.error_message = None;
} else if let Err(err) = parsed_query {
self.process_search_state.search_state.is_blank_search = false;
self.process_search_state.search_state.is_invalid_search = true;
self.process_search_state.search_state.error_message = Some(err.to_string());
}
}
self.scroll_state.previous_scroll_position = 0;
self.scroll_state.current_scroll_position = 0;
}
pub fn clear_search(&mut self) {
self.process_search_state.search_state.reset();
}
pub fn search_walk_forward(&mut self, start_position: usize) {
self.process_search_state
.search_state
.grapheme_cursor
.next_boundary(
&self.process_search_state.search_state.current_search_query[start_position..],
start_position,
)
.unwrap();
}
pub fn search_walk_back(&mut self, start_position: usize) {
self.process_search_state
.search_state
.grapheme_cursor
.prev_boundary(
&self.process_search_state.search_state.current_search_query[..start_position],
0,
)
.unwrap();
}
}
#[derive(Default)]
pub struct ProcState {
pub widget_states: HashMap<u64, ProcWidgetState>,
pub force_update: Option<u64>,
pub force_update_all: bool,
}
impl ProcState {
pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self {
ProcState {
widget_states,
force_update: None,
force_update_all: false,
}
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> {
self.widget_states.get(&widget_id)
}
}
/// The currently selected part of a [`ProcessManager`]
enum ProcessManagerSelection {
Processes,
Sort,
Search,
}
#[derive(Default)]
/// The state of the search modifiers.
struct SearchModifiers {
enable_case_sensitive: bool,
enable_whole_word: bool,
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.35),
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.08),
ProcessSortType::State => Hard(Some(8)),
}
}
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<ProcessSortColumn>,
sort_table: TextTable,
search_input: TextInput,
dd_multi: MultiKey,
selected: ProcessManagerSelection,
in_tree_mode: bool,
show_sort: bool, // TODO: Add this for temp and disk???
show_search: bool,
search_modifiers: SearchModifiers,
display_data: TextTableData,
}
impl ProcessManager {
/// Creates a new [`ProcessManager`].
pub fn new(process_defaults: &ProcessDefaults) -> Self {
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,
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,
in_tree_mode: false,
show_sort: false,
show_search: false,
search_modifiers: SearchModifiers::default(),
display_data: Default::default(),
};
manager.set_tree_mode(process_defaults.is_tree);
manager
}
fn set_tree_mode(&mut self, in_tree_mode: bool) {
self.in_tree_mode = in_tree_mode;
}
fn open_search(&mut self) -> WidgetEventResult {
if let ProcessManagerSelection::Search = self.selected {
WidgetEventResult::NoRedraw
} else {
self.show_search = true;
self.selected = ProcessManagerSelection::Search;
WidgetEventResult::Redraw
}
}
fn open_sort(&mut self) -> WidgetEventResult {
if let ProcessManagerSelection::Sort = self.selected {
WidgetEventResult::NoRedraw
} else {
self.show_sort = true;
self.selected = ProcessManagerSelection::Sort;
WidgetEventResult::Redraw
}
}
/// Returns whether the process manager is searching the current term with the restriction that it must
/// match entire word.
pub fn is_searching_whole_word(&self) -> bool {
self.search_modifiers.enable_whole_word
}
/// Returns whether the process manager is searching the current term using regex.
pub fn is_searching_with_regex(&self) -> bool {
self.search_modifiers.enable_regex
}
/// Returns whether the process manager is searching the current term with the restriction that case-sensitivity
/// matters.
pub fn is_case_sensitive(&self) -> bool {
self.search_modifiers.enable_case_sensitive
}
}
impl Component for ProcessManager {
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 {
match self.selected {
ProcessManagerSelection::Processes => {
// Try to catch some stuff first...
if event.modifiers.is_empty() {
match event.code {
KeyCode::Tab => {
// Handle grouping/ungrouping
}
KeyCode::Char('P') => {
// Show full command/process name
}
KeyCode::Char('d') => {
match self.dd_multi.input('d') {
MultiKeyResult::Completed => {
// Kill the selected process(es)
}
MultiKeyResult::Accepted | MultiKeyResult::Rejected => {
return WidgetEventResult::NoRedraw;
}
}
}
KeyCode::Char('/') => {
return self.open_search();
}
KeyCode::Char('%') => {
// Handle switching memory usage type
}
KeyCode::Char('+') => {
// Expand a branch
}
KeyCode::Char('-') => {
// Collapse a branch
}
KeyCode::Char('t') | KeyCode::F(5) => {
self.in_tree_mode = !self.in_tree_mode;
return WidgetEventResult::Redraw;
}
KeyCode::F(6) => {
return self.open_sort();
}
KeyCode::F(9) => {
// Kill the selected process(es)
}
_ => {}
}
} else if let KeyModifiers::CONTROL = event.modifiers {
if let KeyCode::Char('f') = event.code {
return self.open_search();
}
} else if let KeyModifiers::SHIFT = event.modifiers {
if let KeyCode::Char('P') = event.code {
// Show full command/process name
}
}
self.process_table.handle_key_event(event)
}
ProcessManagerSelection::Sort => {
if event.modifiers.is_empty() {
match event.code {
KeyCode::F(1) => {}
KeyCode::F(2) => {}
KeyCode::F(3) => {}
_ => {}
}
} else if let KeyModifiers::ALT = event.modifiers {
match event.code {
KeyCode::Char('c') | KeyCode::Char('C') => {}
KeyCode::Char('w') | KeyCode::Char('W') => {}
KeyCode::Char('r') | KeyCode::Char('R') => {}
_ => {}
}
}
self.sort_table.handle_key_event(event)
}
ProcessManagerSelection::Search => self.search_input.handle_key_event(event),
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
match &event.kind {
MouseEventKind::Down(MouseButton::Left) => {
if self.process_table.does_intersect_mouse(&event) {
if let ProcessManagerSelection::Processes = self.selected {
self.process_table.handle_mouse_event(event)
} else {
self.selected = ProcessManagerSelection::Processes;
match self.process_table.handle_mouse_event(event) {
WidgetEventResult::Quit => WidgetEventResult::Quit,
WidgetEventResult::Redraw | WidgetEventResult::NoRedraw => {
WidgetEventResult::Redraw
}
WidgetEventResult::Signal(s) => WidgetEventResult::Signal(s),
}
}
} else if self.sort_table.does_intersect_mouse(&event) {
if let ProcessManagerSelection::Sort = self.selected {
self.sort_table.handle_mouse_event(event)
} else {
self.selected = ProcessManagerSelection::Sort;
self.sort_table.handle_mouse_event(event);
WidgetEventResult::Redraw
}
} else if self.search_input.does_intersect_mouse(&event) {
if let ProcessManagerSelection::Search = self.selected {
self.search_input.handle_mouse_event(event)
} else {
self.selected = ProcessManagerSelection::Search;
self.search_input.handle_mouse_event(event);
WidgetEventResult::Redraw
}
} else {
WidgetEventResult::NoRedraw
}
}
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::Search => self.search_input.handle_mouse_event(event),
},
_ => WidgetEventResult::NoRedraw,
}
}
}
impl Widget for ProcessManager {
fn get_pretty_name(&self) -> &'static str {
"Processes"
}
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);
self.process_table
.draw_tui_table(painter, f, &self.display_data, block, area, selected);
}
fn update_data(&mut self, data_collection: &DataCollection) {
let filtered_sorted_iterator = data_collection
.process_harvest
.iter()
.filter(|process| {
// 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)
}
#[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()
.sortable_column
.sorting_status()
{
itertools::Either::Left(filtered_sorted_iterator.rev())
} else {
itertools::Either::Right(filtered_sorted_iterator)
}
.map(|process| {
self.process_table
.columns()
.iter()
.map(|column| match &column.sort_type {
ProcessSortType::Pid => (process.pid.to_string().into(), None, None),
ProcessSortType::Count => ("".into(), None, None),
ProcessSortType::Name => (process.name.clone().into(), None, None),
ProcessSortType::Command => (process.command.clone().into(), None, None),
ProcessSortType::Cpu => (
format!("{:.1}%", process.cpu_usage_percent).into(),
None,
None,
),
ProcessSortType::Mem => (
get_string_with_bytes(process.mem_usage_bytes).into(),
None,
None,
),
ProcessSortType::MemPercent => (
format!("{:.1}%", process.mem_usage_percent).into(),
None,
None,
),
ProcessSortType::Rps => (
get_string_with_bytes(process.read_bytes_per_sec).into(),
None,
None,
),
ProcessSortType::Wps => (
get_string_with_bytes(process.write_bytes_per_sec).into(),
None,
None,
),
ProcessSortType::TotalRead => (
get_string_with_bytes(process.total_read_bytes).into(),
None,
None,
),
ProcessSortType::TotalWrite => (
get_string_with_bytes(process.total_write_bytes).into(),
None,
None,
),
ProcessSortType::User => (process.user.clone(), None, None),
ProcessSortType::State => (
process.process_state.clone().into(),
None, // Currently disabled; what happens if you try to sort in the shortened form?
None,
),
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
}
}