refactor: begin migration of process widget

This commit is contained in:
ClementTsang 2022-05-07 03:38:55 -04:00
parent 69ec526dc6
commit 7ee6f6a737
17 changed files with 1589 additions and 2164 deletions

View file

@ -40,11 +40,12 @@ nvidia = ["nvml-wrapper"]
[dependencies]
anyhow = "1.0.57"
backtrace = "0.3.65"
cfg-if = "1.0.0"
crossterm = "0.18.2"
ctrlc = { version = "3.1.9", features = ["termination"] }
clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
cfg-if = "1.0.0"
concat-string = "1.0.1"
# const_format = "0.2.23"
dirs = "4.0.0"
futures = "0.3.21"
futures-timer = "3.0.2"

File diff suppressed because it is too large Load diff

View file

@ -75,23 +75,48 @@ impl Default for ProcessSorting {
#[derive(Debug, Clone, Default)]
pub struct ProcessHarvest {
/// The pid of the process.
pub pid: Pid,
pub parent_pid: Option<Pid>, // Remember, parent_pid 0 is root...
/// The parent PID of the process. Remember, parent_pid 0 is root.
pub parent_pid: Option<Pid>,
/// CPU usage as a percentage.
pub cpu_usage_percent: f64,
/// Memory usage as a percentage.
pub mem_usage_percent: f64,
/// Memory usage as bytes.
pub mem_usage_bytes: u64,
// pub rss_kb: u64,
// pub virt_kb: u64,
/// The name of the process.
pub name: String,
/// The exact command for the process.
pub command: String,
/// Bytes read per second.
pub read_bytes_per_sec: u64,
/// Bytes written per second.
pub write_bytes_per_sec: u64,
/// The total number of bytes read by the process.
pub total_read_bytes: u64,
/// The total number of bytes written by the process.
pub total_write_bytes: u64,
/// The current state of the process (e.g. zombie, asleep)
pub process_state: String,
/// The process state represented by a character. TODO: Merge with above as a single struct.
pub process_state_char: char,
/// This is the *effective* user ID.
/// This is the *effective* user ID of the process.
#[cfg(target_family = "unix")]
pub uid: Option<libc::uid_t>,
// pub rss_kb: u64,
// pub virt_kb: u64,
}

View file

@ -1,4 +1,3 @@
use super::ProcWidgetState;
use crate::{
data_conversion::ConvertedProcessData,
utils::error::{
@ -9,6 +8,8 @@ use crate::{
use std::fmt::Debug;
use std::{borrow::Cow, collections::VecDeque};
use super::widgets::ProcWidget;
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
const OR_LIST: [&str; 2] = ["or", "||"];
@ -39,7 +40,7 @@ pub trait ProcessQuery {
fn parse_query(&self) -> Result<Query>;
}
impl ProcessQuery for ProcWidgetState {
impl ProcessQuery for ProcWidget {
fn parse_query(&self) -> Result<Query> {
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
let lhs = process_or(query)?;
@ -437,9 +438,9 @@ impl ProcessQuery for ProcWidgetState {
let mut process_filter = process_string_to_filter(&mut split_query)?;
process_filter.process_regexes(
self.process_search_state.is_searching_whole_word,
self.process_search_state.is_ignoring_case,
self.process_search_state.is_searching_with_regex,
self.search_state.is_searching_whole_word,
self.search_state.is_ignoring_case,
self.search_state.is_searching_with_regex,
)?;
Ok(process_filter)

View file

@ -1,16 +1,17 @@
use std::{borrow::Cow, collections::HashMap, convert::TryInto, time::Instant};
use std::{collections::HashMap, time::Instant};
use unicode_segmentation::GraphemeCursor;
use tui::widgets::TableState;
use crate::{
app::{layout_manager::BottomWidgetType, query::*},
constants,
data_conversion::CellContent,
data_harvester::processes::{self, ProcessSorting},
data_harvester::processes::ProcessSorting,
};
use ProcessSorting::*;
pub mod table_state;
pub use table_state::*;
use super::widgets::ProcWidget;
#[derive(Debug)]
pub enum ScrollDirection {
@ -39,218 +40,6 @@ pub struct CanvasTableWidthState {
pub calculated_column_widths: Vec<u16>,
}
/// A bound on the width of a column.
#[derive(Clone, Copy, Debug)]
pub enum WidthBounds {
/// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point.
Soft {
/// The minimum amount before giving up and hiding.
min_width: u16,
/// The desired, calculated width. Take this if possible as the base starting width.
desired: u16,
/// The max width, as a percentage of the total width available. If [`None`],
/// then it can grow as desired.
max_percentage: Option<f32>,
},
/// A width of this type is either as long as specified, or does not appear at all.
Hard(u16),
}
impl WidthBounds {
pub const fn soft_from_str(name: &'static str, max_percentage: Option<f32>) -> WidthBounds {
let len = name.len() as u16;
WidthBounds::Soft {
min_width: len,
desired: len,
max_percentage,
}
}
pub const fn soft_from_str_with_alt(
name: &'static str, alt: &'static str, max_percentage: Option<f32>,
) -> WidthBounds {
WidthBounds::Soft {
min_width: alt.len() as u16,
desired: name.len() as u16,
max_percentage,
}
}
}
pub struct TableComponentColumn {
/// The name of the column. Displayed if possible as the header.
pub name: CellContent,
/// A restriction on this column's width, if desired.
pub width_bounds: WidthBounds,
/// The calculated width of the column.
pub calculated_width: u16,
}
impl TableComponentColumn {
pub fn new<I>(name: I, alt: Option<I>, width_bounds: WidthBounds) -> Self
where
I: Into<Cow<'static, str>>,
{
Self {
name: if let Some(alt) = alt {
CellContent::HasAlt {
alt: alt.into(),
main: name.into(),
}
} else {
CellContent::Simple(name.into())
},
width_bounds,
calculated_width: 0,
}
}
pub fn should_skip(&self) -> bool {
self.calculated_width == 0
}
}
/// [`TableComponentState`] deals with fields for a scrollable's current state.
#[derive(Default)]
pub struct TableComponentState {
pub current_scroll_position: usize,
pub scroll_bar: usize,
pub scroll_direction: ScrollDirection,
pub table_state: TableState,
pub columns: Vec<TableComponentColumn>,
}
impl TableComponentState {
pub fn new(columns: Vec<TableComponentColumn>) -> Self {
Self {
current_scroll_position: 0,
scroll_bar: 0,
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
columns,
}
}
/// Calculates widths for the columns for this table.
///
/// * `total_width` is the, well, total width available.
/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
/// false.
///
/// **NOTE:** Trailing 0's may break tui-rs, remember to filter them out later!
pub fn calculate_column_widths(&mut self, total_width: u16, left_to_right: bool) {
use itertools::Either;
use std::cmp::{max, min};
let mut total_width_left = total_width;
for column in self.columns.iter_mut() {
column.calculated_width = 0;
}
let columns = if left_to_right {
Either::Left(self.columns.iter_mut())
} else {
Either::Right(self.columns.iter_mut().rev())
};
let mut num_columns = 0;
for column in columns {
match &column.width_bounds {
WidthBounds::Soft {
min_width,
desired,
max_percentage,
} => {
let soft_limit = max(
if let Some(max_percentage) = max_percentage {
// Rust doesn't have an `into()` or `try_into()` for floats to integers???
((*max_percentage * f32::from(total_width)).ceil()) as u16
} else {
*desired
},
*min_width,
);
let space_taken = min(min(soft_limit, *desired), total_width_left);
if *min_width > space_taken {
break;
} else if space_taken > 0 {
total_width_left = total_width_left.saturating_sub(space_taken + 1);
column.calculated_width = space_taken;
num_columns += 1;
}
}
WidthBounds::Hard(width) => {
let space_taken = min(*width, total_width_left);
if *width > space_taken {
break;
} else if space_taken > 0 {
total_width_left = total_width_left.saturating_sub(space_taken + 1);
column.calculated_width = space_taken;
num_columns += 1;
}
}
}
}
if num_columns > 0 {
// Redistribute remaining.
let mut num_dist = num_columns;
let amount_per_slot = total_width_left / num_dist;
total_width_left %= num_dist;
for column in self.columns.iter_mut() {
if num_dist == 0 {
break;
}
if column.calculated_width > 0 {
if total_width_left > 0 {
column.calculated_width += amount_per_slot + 1;
total_width_left -= 1;
} else {
column.calculated_width += amount_per_slot;
}
num_dist -= 1;
}
}
}
}
/// Updates the position if possible, and if there is a valid change, returns the new position.
pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option<usize> {
if change == 0 {
return None;
}
let csp: Result<i64, _> = self.current_scroll_position.try_into();
if let Ok(csp) = csp {
let proposed: Result<usize, _> = (csp + change).try_into();
if let Ok(proposed) = proposed {
if proposed < num_entries {
self.current_scroll_position = proposed;
if change < 0 {
self.scroll_direction = ScrollDirection::Up;
} else {
self.scroll_direction = ScrollDirection::Down;
}
return Some(self.current_scroll_position);
}
}
}
None
}
}
#[derive(PartialEq)]
pub enum KillSignal {
Cancel,
@ -342,555 +131,20 @@ impl AppSearchState {
}
}
/// 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, if 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,
command_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: TableComponentState,
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);
}
if is_using_command {
columns.toggle(&ProcessSorting::ProcessName);
columns.toggle(&ProcessSorting::Command);
}
ProcWidgetState {
process_search_state,
is_grouped,
scroll_state: TableComponentState::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.scroll_bar = 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();
}
}
pub struct ProcState {
pub widget_states: HashMap<u64, ProcWidgetState>,
pub force_update: Option<u64>,
pub force_update_all: bool,
pub widget_states: HashMap<u64, ProcWidget>,
}
impl ProcState {
pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self {
ProcState {
widget_states,
force_update: None,
force_update_all: false,
}
pub fn init(widget_states: HashMap<u64, ProcWidget>) -> Self {
ProcState { widget_states }
}
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> {
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidget> {
self.widget_states.get_mut(&widget_id)
}
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> {
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidget> {
self.widget_states.get(&widget_id)
}
}
@ -941,8 +195,7 @@ pub struct CpuWidgetState {
impl CpuWidgetState {
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
const CPU_LEGEND_HEADER: [(Cow<'static, str>, Option<Cow<'static, str>>); 2] =
[(Cow::Borrowed("CPU"), None), (Cow::Borrowed("Use%"), None)];
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [
WidthBounds::soft_from_str("CPU", Some(0.5)),
WidthBounds::soft_from_str("Use%", Some(0.5)),
@ -952,7 +205,7 @@ impl CpuWidgetState {
CPU_LEGEND_HEADER
.iter()
.zip(WIDTHS)
.map(|(c, width)| TableComponentColumn::new(c.0.clone(), c.1.clone(), width))
.map(|(c, width)| TableComponentColumn::new(CellContent::new(*c, None), width))
.collect(),
);
@ -1040,7 +293,9 @@ impl Default for TempWidgetState {
TEMP_HEADERS
.iter()
.zip(WIDTHS)
.map(|(header, width)| TableComponentColumn::new(*header, None, width))
.map(|(header, width)| {
TableComponentColumn::new(CellContent::new(*header, None), width)
})
.collect(),
),
}
@ -1087,7 +342,9 @@ impl Default for DiskWidgetState {
DISK_HEADERS
.iter()
.zip(WIDTHS)
.map(|(header, width)| TableComponentColumn::new(*header, None, width))
.map(|(header, width)| {
TableComponentColumn::new(CellContent::new(*header, None), width)
})
.collect(),
),
}
@ -1169,61 +426,3 @@ pub struct ConfigCategory {
pub struct ConfigOption {
pub set_function: Box<dyn Fn() -> anyhow::Result<()>>,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_scroll_update_position() {
fn check_scroll_update(
scroll: &mut TableComponentState, change: i64, max: usize, ret: Option<usize>,
new_position: usize,
) {
assert_eq!(scroll.update_position(change, max), ret);
assert_eq!(scroll.current_scroll_position, new_position);
}
let mut scroll = TableComponentState {
current_scroll_position: 5,
scroll_bar: 0,
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
columns: vec![],
};
let s = &mut scroll;
// Update by 0. Should not change.
check_scroll_update(s, 0, 15, None, 5);
// Update by 5. Should increment to index 10.
check_scroll_update(s, 5, 15, Some(10), 10);
// Update by 5. Should not change.
check_scroll_update(s, 5, 15, None, 10);
// Update by 4. Should increment to index 14 (supposed max).
check_scroll_update(s, 4, 15, Some(14), 14);
// Update by 1. Should do nothing.
check_scroll_update(s, 1, 15, None, 14);
// Update by -15. Should do nothing.
check_scroll_update(s, -15, 15, None, 14);
// Update by -14. Should land on position 0.
check_scroll_update(s, -14, 15, Some(0), 0);
// Update by -1. Should do nothing.
check_scroll_update(s, -15, 15, None, 0);
// Update by 0. Should do nothing.
check_scroll_update(s, 0, 15, None, 0);
// Update by 15. Should do nothing.
check_scroll_update(s, 15, 15, None, 0);
// Update by 15 but with a larger bound. Should increment to 15.
check_scroll_update(s, 15, 16, Some(15), 15);
}
}

View file

@ -0,0 +1,455 @@
use std::{borrow::Cow, convert::TryInto};
use tui::widgets::TableState;
use super::ScrollDirection;
/// A bound on the width of a column.
#[derive(Clone, Copy, Debug)]
pub enum WidthBounds {
/// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point.
Soft {
/// The minimum amount before giving up and hiding.
min_width: u16,
/// The desired, calculated width. Take this if possible as the base starting width.
desired: u16,
/// The max width, as a percentage of the total width available. If [`None`],
/// then it can grow as desired.
max_percentage: Option<f32>,
},
/// A width of this type is either as long as specified, or does not appear at all.
Hard(u16),
}
impl WidthBounds {
pub const fn soft_from_str(name: &'static str, max_percentage: Option<f32>) -> WidthBounds {
let len = name.len() as u16;
WidthBounds::Soft {
min_width: len,
desired: len,
max_percentage,
}
}
pub const fn soft_from_str_with_alt(
name: &'static str, alt: &'static str, max_percentage: Option<f32>,
) -> WidthBounds {
WidthBounds::Soft {
min_width: alt.len() as u16,
desired: name.len() as u16,
max_percentage,
}
}
}
/// A [`CellContent`] contains text information for display in a table.
#[derive(Clone)]
pub enum CellContent {
Simple(Cow<'static, str>),
HasAlt {
alt: Cow<'static, str>,
main: Cow<'static, str>,
},
}
impl CellContent {
/// Creates a new [`CellContent`].
pub fn new<I>(name: I, alt: Option<I>) -> Self
where
I: Into<Cow<'static, str>>,
{
if let Some(alt) = alt {
CellContent::HasAlt {
alt: alt.into(),
main: name.into(),
}
} else {
CellContent::Simple(name.into())
}
}
/// Returns the length of the [`CellContent`]. Note that for a [`CellContent::HasAlt`], it will return
/// the length of the "main" field.
pub fn len(&self) -> usize {
match self {
CellContent::Simple(s) => s.len(),
CellContent::HasAlt { alt: _, main: long } => long.len(),
}
}
/// Whether the [`CellContent`]'s text is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub trait TableComponentHeader {
fn header_text(&self) -> &CellContent;
}
impl TableComponentHeader for CellContent {
fn header_text(&self) -> &CellContent {
self
}
}
impl From<&'static str> for CellContent {
fn from(s: &'static str) -> Self {
CellContent::Simple(s.into())
}
}
pub struct TableComponentColumn<H: TableComponentHeader> {
/// The header of the column.
pub header: H,
/// A restriction on this column's width, if desired.
pub width_bounds: WidthBounds,
/// The calculated width of the column.
pub calculated_width: u16,
/// Marks that this column is currently "hidden", and should *always* be skipped.
pub is_hidden: bool,
}
impl<H: TableComponentHeader> TableComponentColumn<H> {
pub fn new(header: H, width_bounds: WidthBounds) -> Self {
Self {
header,
width_bounds,
calculated_width: 0,
is_hidden: false,
}
}
pub fn default_hard(header: H) -> Self {
let width = header.header_text().len() as u16;
Self {
header,
width_bounds: WidthBounds::Hard(width),
calculated_width: 0,
is_hidden: false,
}
}
pub fn default_soft(header: H, max_percentage: Option<f32>) -> Self {
let min_width = header.header_text().len() as u16;
Self {
header,
width_bounds: WidthBounds::Soft {
min_width,
desired: min_width,
max_percentage,
},
calculated_width: 0,
is_hidden: false,
}
}
pub fn is_zero_width(&self) -> bool {
self.calculated_width == 0
}
pub fn is_skipped(&self) -> bool {
self.is_zero_width() || self.is_hidden
}
}
pub enum SortOrder {
Ascending,
Descending,
}
/// Represents the current table's sorting state.
pub enum SortState {
Unsortable,
Sortable { index: usize, order: SortOrder },
}
impl Default for SortState {
fn default() -> Self {
SortState::Unsortable
}
}
/// [`TableComponentState`] deals with fields for a scrollable's current state.
pub struct TableComponentState<H: TableComponentHeader = CellContent> {
pub current_scroll_position: usize,
pub scroll_bar: usize,
pub scroll_direction: ScrollDirection,
pub table_state: TableState,
pub columns: Vec<TableComponentColumn<H>>,
pub sort_state: SortState,
}
impl<H: TableComponentHeader> TableComponentState<H> {
pub fn new(columns: Vec<TableComponentColumn<H>>) -> Self {
Self {
current_scroll_position: 0,
scroll_bar: 0,
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
columns,
sort_state: Default::default(),
}
}
/// Calculates widths for the columns for this table.
///
/// * `total_width` is the, well, total width available.
/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
/// false.
///
/// **NOTE:** Trailing 0's may break tui-rs, remember to filter them out later!
pub fn calculate_column_widths(&mut self, total_width: u16, left_to_right: bool) {
use itertools::Either;
use std::cmp::{max, min};
let mut total_width_left = total_width;
for column in self.columns.iter_mut() {
column.calculated_width = 0;
}
let columns = if left_to_right {
Either::Left(self.columns.iter_mut())
} else {
Either::Right(self.columns.iter_mut().rev())
};
let arrow_offset = match self.sort_state {
SortState::Unsortable => 0,
SortState::Sortable { index: _, order: _ } => 1,
};
let mut num_columns = 0;
for column in columns {
if column.is_hidden {
continue;
}
match &column.width_bounds {
WidthBounds::Soft {
min_width,
desired,
max_percentage,
} => {
let offset_min = *min_width + arrow_offset;
let soft_limit = max(
if let Some(max_percentage) = max_percentage {
// Rust doesn't have an `into()` or `try_into()` for floats to integers???
((*max_percentage * f32::from(total_width)).ceil()) as u16
} else {
*desired
},
offset_min,
);
let space_taken = min(min(soft_limit, *desired), total_width_left);
if offset_min > space_taken {
break;
} else if space_taken > 0 {
total_width_left = total_width_left.saturating_sub(space_taken + 1);
column.calculated_width = space_taken;
num_columns += 1;
}
}
WidthBounds::Hard(width) => {
let min_width = *width + arrow_offset;
let space_taken = min(min_width, total_width_left);
if min_width > space_taken {
break;
} else if space_taken > 0 {
total_width_left = total_width_left.saturating_sub(space_taken + 1);
column.calculated_width = space_taken;
num_columns += 1;
}
}
}
}
if num_columns > 0 {
// Redistribute remaining.
let mut num_dist = num_columns;
let amount_per_slot = total_width_left / num_dist;
total_width_left %= num_dist;
for column in self.columns.iter_mut() {
if num_dist == 0 {
break;
}
if column.calculated_width > 0 {
if total_width_left > 0 {
column.calculated_width += amount_per_slot + 1;
total_width_left -= 1;
} else {
column.calculated_width += amount_per_slot;
}
num_dist -= 1;
}
}
}
}
/// Updates the position if possible, and if there is a valid change, returns the new position.
pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option<usize> {
if change == 0 {
return None;
}
let csp: Result<i64, _> = self.current_scroll_position.try_into();
if let Ok(csp) = csp {
let proposed: Result<usize, _> = (csp + change).try_into();
if let Ok(proposed) = proposed {
if proposed < num_entries {
self.current_scroll_position = proposed;
if change < 0 {
self.scroll_direction = ScrollDirection::Up;
} else {
self.scroll_direction = ScrollDirection::Down;
}
return Some(self.current_scroll_position);
}
}
}
None
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_scroll_update_position() {
#[track_caller]
fn check_scroll_update(
scroll: &mut TableComponentState, change: i64, max: usize, ret: Option<usize>,
new_position: usize,
) {
assert_eq!(scroll.update_position(change, max), ret);
assert_eq!(scroll.current_scroll_position, new_position);
}
let mut scroll = TableComponentState {
current_scroll_position: 5,
scroll_bar: 0,
scroll_direction: ScrollDirection::Down,
table_state: Default::default(),
columns: vec![],
sort_state: Default::default(),
};
let s = &mut scroll;
// Update by 0. Should not change.
check_scroll_update(s, 0, 15, None, 5);
// Update by 5. Should increment to index 10.
check_scroll_update(s, 5, 15, Some(10), 10);
// Update by 5. Should not change.
check_scroll_update(s, 5, 15, None, 10);
// Update by 4. Should increment to index 14 (supposed max).
check_scroll_update(s, 4, 15, Some(14), 14);
// Update by 1. Should do nothing.
check_scroll_update(s, 1, 15, None, 14);
// Update by -15. Should do nothing.
check_scroll_update(s, -15, 15, None, 14);
// Update by -14. Should land on position 0.
check_scroll_update(s, -14, 15, Some(0), 0);
// Update by -1. Should do nothing.
check_scroll_update(s, -15, 15, None, 0);
// Update by 0. Should do nothing.
check_scroll_update(s, 0, 15, None, 0);
// Update by 15. Should do nothing.
check_scroll_update(s, 15, 15, None, 0);
// Update by 15 but with a larger bound. Should increment to 15.
check_scroll_update(s, 15, 16, Some(15), 15);
}
#[test]
fn test_table_width_calculation() {
#[track_caller]
fn test_calculation(state: &mut TableComponentState, width: u16, expected: Vec<u16>) {
state.calculate_column_widths(width, true);
assert_eq!(
state
.columns
.iter()
.filter_map(|c| if c.calculated_width == 0 {
None
} else {
Some(c.calculated_width)
})
.collect::<Vec<_>>(),
expected
)
}
let mut state = TableComponentState::new(vec![
TableComponentColumn::default_hard(CellContent::from("a")),
TableComponentColumn::new(
"a".into(),
WidthBounds::Soft {
min_width: 1,
desired: 10,
max_percentage: Some(0.125),
},
),
TableComponentColumn::new(
"a".into(),
WidthBounds::Soft {
min_width: 2,
desired: 10,
max_percentage: Some(0.5),
},
),
]);
test_calculation(&mut state, 0, vec![]);
test_calculation(&mut state, 1, vec![1]);
test_calculation(&mut state, 2, vec![1]);
test_calculation(&mut state, 3, vec![1, 1]);
test_calculation(&mut state, 4, vec![1, 1]);
test_calculation(&mut state, 5, vec![2, 1]);
test_calculation(&mut state, 6, vec![1, 1, 2]);
test_calculation(&mut state, 7, vec![1, 1, 3]);
test_calculation(&mut state, 8, vec![1, 1, 4]);
test_calculation(&mut state, 14, vec![2, 2, 7]);
test_calculation(&mut state, 20, vec![2, 4, 11]);
test_calculation(&mut state, 100, vec![27, 35, 35]);
state.sort_state = SortState::Sortable {
index: 1,
order: SortOrder::Ascending,
};
test_calculation(&mut state, 0, vec![]);
test_calculation(&mut state, 1, vec![]);
test_calculation(&mut state, 2, vec![2]);
test_calculation(&mut state, 3, vec![2]);
test_calculation(&mut state, 4, vec![3]);
test_calculation(&mut state, 5, vec![2, 2]);
test_calculation(&mut state, 6, vec![2, 2]);
test_calculation(&mut state, 7, vec![3, 2]);
test_calculation(&mut state, 8, vec![3, 3]);
test_calculation(&mut state, 14, vec![2, 2, 7]);
test_calculation(&mut state, 20, vec![3, 4, 10]);
test_calculation(&mut state, 100, vec![27, 35, 35]);
}
}

2
src/app/widgets.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod process;
pub use process::*;

320
src/app/widgets/process.rs Normal file
View file

@ -0,0 +1,320 @@
use std::borrow::Cow;
use crate::{
app::{
query::*, AppSearchState, CanvasTableWidthState, CellContent, TableComponentColumn,
TableComponentHeader, TableComponentState, WidthBounds,
},
data_harvester::processes,
};
/// 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;
}
}
#[derive(Copy, Clone, Debug)]
pub enum ProcWidgetMode {
Tree,
Grouped,
Normal,
}
pub enum ProcWidgetColumn {
CpuPercent,
Memory { show_percentage: bool },
PidOrCount { is_count: bool },
ProcNameOrCommand { is_command: bool },
ReadPerSecond,
WritePerSecond,
TotalRead,
TotalWrite,
State,
User,
}
impl ProcWidgetColumn {
const CPU_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("CPU%"));
const MEM_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("Mem%"));
const MEM: CellContent = CellContent::Simple(Cow::Borrowed("Mem"));
const READS_PER_SECOND: CellContent = CellContent::Simple(Cow::Borrowed("R/s"));
const WRITES_PER_SECOND: CellContent = CellContent::Simple(Cow::Borrowed("W/s"));
const TOTAL_READ: CellContent = CellContent::Simple(Cow::Borrowed("T.Read"));
const TOTAL_WRITE: CellContent = CellContent::Simple(Cow::Borrowed("T.Write"));
const STATE: CellContent = CellContent::Simple(Cow::Borrowed("State"));
const PROCESS_NAME: CellContent = CellContent::Simple(Cow::Borrowed("Name"));
const COMMAND: CellContent = CellContent::Simple(Cow::Borrowed("Command"));
const PID: CellContent = CellContent::Simple(Cow::Borrowed("PID"));
const COUNT: CellContent = CellContent::Simple(Cow::Borrowed("Count"));
const USER: CellContent = CellContent::Simple(Cow::Borrowed("User"));
const SHORTCUT_CPU_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("CPU%(c)"));
const SHORTCUT_MEM_PERCENT: CellContent = CellContent::Simple(Cow::Borrowed("Mem%(m)"));
const SHORTCUT_MEM: CellContent = CellContent::Simple(Cow::Borrowed("Mem(m)"));
const SHORTCUT_PROCESS_NAME: CellContent = CellContent::Simple(Cow::Borrowed("Name(n)"));
const SHORTCUT_COMMAND: CellContent = CellContent::Simple(Cow::Borrowed("Command(n)"));
const SHORTCUT_PID: CellContent = CellContent::Simple(Cow::Borrowed("PID(p)"));
fn text(&self) -> &CellContent {
match self {
ProcWidgetColumn::CpuPercent => &Self::CPU_PERCENT,
ProcWidgetColumn::Memory { show_percentage } => {
if *show_percentage {
&Self::MEM_PERCENT
} else {
&Self::MEM
}
}
ProcWidgetColumn::PidOrCount { is_count } => {
if *is_count {
&Self::COUNT
} else {
&Self::PID
}
}
ProcWidgetColumn::ProcNameOrCommand { is_command } => {
if *is_command {
&Self::COMMAND
} else {
&Self::PROCESS_NAME
}
}
ProcWidgetColumn::ReadPerSecond => &Self::READS_PER_SECOND,
ProcWidgetColumn::WritePerSecond => &Self::WRITES_PER_SECOND,
ProcWidgetColumn::TotalRead => &Self::TOTAL_READ,
ProcWidgetColumn::TotalWrite => &Self::TOTAL_WRITE,
ProcWidgetColumn::State => &Self::STATE,
ProcWidgetColumn::User => &Self::USER,
}
}
}
impl TableComponentHeader for ProcWidgetColumn {
fn header_text(&self) -> &CellContent {
match self {
ProcWidgetColumn::CpuPercent => &Self::SHORTCUT_CPU_PERCENT,
ProcWidgetColumn::Memory { show_percentage } => {
if *show_percentage {
&Self::SHORTCUT_MEM_PERCENT
} else {
&Self::SHORTCUT_MEM
}
}
ProcWidgetColumn::PidOrCount { is_count } => {
if *is_count {
&Self::COUNT
} else {
&Self::SHORTCUT_PID
}
}
ProcWidgetColumn::ProcNameOrCommand { is_command } => {
if *is_command {
&Self::SHORTCUT_COMMAND
} else {
&Self::SHORTCUT_PROCESS_NAME
}
}
ProcWidgetColumn::ReadPerSecond => &Self::READS_PER_SECOND,
ProcWidgetColumn::WritePerSecond => &Self::WRITES_PER_SECOND,
ProcWidgetColumn::TotalRead => &Self::TOTAL_READ,
ProcWidgetColumn::TotalWrite => &Self::TOTAL_WRITE,
ProcWidgetColumn::State => &Self::STATE,
ProcWidgetColumn::User => &Self::USER,
}
}
}
pub struct ProcWidget {
pub mode: ProcWidgetMode,
pub requires_redraw: bool,
pub search_state: ProcessSearchState,
pub table_state: TableComponentState<ProcWidgetColumn>,
pub sort_table_state: TableComponentState<CellContent>,
pub is_sort_open: bool,
pub force_update: bool,
// Hmm...
pub is_process_sort_descending: bool,
pub process_sorting_type: processes::ProcessSorting,
// TO REMOVE
pub is_using_command: bool,
pub table_width_state: CanvasTableWidthState,
}
impl ProcWidget {
pub fn init(
mode: ProcWidgetMode, is_case_sensitive: bool, is_match_whole_word: bool,
is_use_regex: bool, show_memory_as_values: 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 matches!(mode, ProcWidgetMode::Tree) {
(processes::ProcessSorting::Pid, false)
} else {
(processes::ProcessSorting::CpuPercent, true)
};
let is_count = matches!(mode, ProcWidgetMode::Grouped);
let sort_table_state = TableComponentState::new(vec![TableComponentColumn::new(
CellContent::Simple("Sort By".into()),
WidthBounds::Hard(7),
)]);
let table_state = TableComponentState::new(vec![
TableComponentColumn::default_hard(ProcWidgetColumn::PidOrCount { is_count }),
TableComponentColumn::default_soft(
ProcWidgetColumn::ProcNameOrCommand {
is_command: is_using_command,
},
Some(0.7),
),
TableComponentColumn::default_hard(ProcWidgetColumn::CpuPercent),
TableComponentColumn::default_hard(ProcWidgetColumn::Memory {
show_percentage: !show_memory_as_values,
}),
TableComponentColumn::default_hard(ProcWidgetColumn::ReadPerSecond),
TableComponentColumn::default_hard(ProcWidgetColumn::WritePerSecond),
TableComponentColumn::default_hard(ProcWidgetColumn::TotalRead),
TableComponentColumn::default_hard(ProcWidgetColumn::TotalWrite),
TableComponentColumn::default_hard(ProcWidgetColumn::User),
TableComponentColumn::default_hard(ProcWidgetColumn::State),
]);
ProcWidget {
search_state: process_search_state,
table_state,
sort_table_state,
process_sorting_type,
is_process_sort_descending,
is_using_command,
is_sort_open: false,
table_width_state: CanvasTableWidthState::default(),
requires_redraw: false,
mode,
force_update: false,
}
}
pub fn get_search_cursor_position(&self) -> usize {
self.search_state.search_state.grapheme_cursor.cur_cursor()
}
pub fn get_char_cursor_position(&self) -> usize {
self.search_state.search_state.char_cursor_position
}
pub fn is_search_enabled(&self) -> bool {
self.search_state.search_state.is_enabled
}
pub fn get_current_search_query(&self) -> &String {
&self.search_state.search_state.current_search_query
}
pub fn update_query(&mut self) {
if self
.search_state
.search_state
.current_search_query
.is_empty()
{
self.search_state.search_state.is_blank_search = true;
self.search_state.search_state.is_invalid_search = false;
self.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.search_state.search_state.query = Some(parsed_query);
self.search_state.search_state.is_blank_search = false;
self.search_state.search_state.is_invalid_search = false;
self.search_state.search_state.error_message = None;
} else if let Err(err) = parsed_query {
self.search_state.search_state.is_blank_search = false;
self.search_state.search_state.is_invalid_search = true;
self.search_state.search_state.error_message = Some(err.to_string());
}
}
self.table_state.scroll_bar = 0;
self.table_state.current_scroll_position = 0;
}
pub fn clear_search(&mut self) {
self.search_state.search_state.reset();
}
pub fn search_walk_forward(&mut self, start_position: usize) {
self.search_state
.search_state
.grapheme_cursor
.next_boundary(
&self.search_state.search_state.current_search_query[start_position..],
start_position,
)
.unwrap();
}
pub fn search_walk_back(&mut self, start_position: usize) {
self.search_state
.search_state
.grapheme_cursor
.prev_boundary(
&self.search_state.search_state.current_search_query[..start_position],
0,
)
.unwrap();
}
pub fn num_enabled_columns(&self) -> usize {
self.table_state
.columns
.iter()
.filter(|c| !c.is_skipped())
.count()
}
}

View file

@ -341,8 +341,9 @@ impl Painter {
// Reset column headers for sorting in process widget...
for proc_widget in app_state.proc_state.widget_states.values_mut() {
proc_widget.columns.column_header_y_loc = None;
proc_widget.columns.column_header_x_locs = None;
// FIXME: [Proc] Handle this?
// proc_widget.columns.column_header_y_loc = None;
// proc_widget.columns.column_header_x_locs = None;
}
}
@ -506,7 +507,7 @@ impl Painter {
_ => 0,
};
self.draw_process_features(f, app_state, rect[0], true, widget_id);
self.draw_process_widget(f, app_state, rect[0], true, widget_id);
}
Battery => self.draw_battery_display(
f,
@ -585,7 +586,7 @@ impl Painter {
ProcSort => 2,
_ => 0,
};
self.draw_process_features(
self.draw_process_widget(
f,
app_state,
vertical_chunks[3],
@ -736,7 +737,7 @@ impl Painter {
Disk => {
self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id)
}
Proc => self.draw_process_features(
Proc => self.draw_process_widget(
f,
app_state,
*widget_draw_loc,

View file

@ -1,4 +1,7 @@
use std::{borrow::Cow, cmp::min};
use std::{
borrow::Cow,
cmp::{max, min},
};
use concat_string::concat_string;
use tui::{
@ -12,9 +15,12 @@ use tui::{
use unicode_segmentation::UnicodeSegmentation;
use crate::{
app::{self, TableComponentState},
app::{
self, CellContent, SortState, TableComponentColumn, TableComponentHeader,
TableComponentState,
},
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
data_conversion::{CellContent, TableData, TableRow},
data_conversion::{TableData, TableRow},
};
pub struct TextTableTitle<'a> {
@ -101,8 +107,8 @@ impl<'a> TextTable<'a> {
}
})
}
pub fn draw_text_table<B: Backend>(
&self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState,
pub fn draw_text_table<B: Backend, H: TableComponentHeader>(
&self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState<H>,
table_data: &TableData,
) {
// TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything.
@ -179,7 +185,7 @@ impl<'a> TextTable<'a> {
desired,
max_percentage: _,
} => {
*desired = std::cmp::max(column.name.len(), *data_width) as u16;
*desired = max(column.header.header_text().len(), *data_width) as u16;
}
app::WidthBounds::Hard(_width) => {}
});
@ -188,15 +194,9 @@ impl<'a> TextTable<'a> {
}
let columns = &state.columns;
let header = Row::new(columns.iter().filter_map(|c| {
if c.calculated_width == 0 {
None
} else {
Some(truncate_text(&c.name, c.calculated_width.into(), None))
}
}))
.style(self.header_style)
.bottom_margin(table_gap);
let header = build_header(columns, &state.sort_state)
.style(self.header_style)
.bottom_margin(table_gap);
let table_rows = sliced_vec.iter().map(|row| {
let (row, style) = match row {
TableRow::Raw(row) => (row, None),
@ -245,9 +245,60 @@ impl<'a> TextTable<'a> {
}
}
/// Constructs the table header.
fn build_header<'a, H: TableComponentHeader>(
columns: &'a [TableComponentColumn<H>], sort_state: &SortState,
) -> Row<'a> {
use itertools::Either;
const UP_ARROW: &str = "";
const DOWN_ARROW: &str = "";
let iter = match sort_state {
SortState::Unsortable => Either::Left(columns.iter().filter_map(|c| {
if c.calculated_width == 0 {
None
} else {
Some(truncate_text(
c.header.header_text(),
c.calculated_width.into(),
None,
))
}
})),
SortState::Sortable { index, order } => {
let arrow = match order {
app::SortOrder::Ascending => UP_ARROW,
app::SortOrder::Descending => DOWN_ARROW,
};
Either::Right(columns.iter().enumerate().filter_map(move |(itx, c)| {
if c.calculated_width == 0 {
None
} else if itx == *index {
Some(truncate_suffixed_text(
c.header.header_text(),
arrow,
c.calculated_width.into(),
None,
))
} else {
Some(truncate_text(
c.header.header_text(),
c.calculated_width.into(),
None,
))
}
}))
}
};
Row::new(iter)
}
/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>) -> Text<'_> {
let (text, opt) = match content {
fn truncate_text<'a>(content: &'a CellContent, width: usize, row_style: Option<Style>) -> Text<'a> {
let (main_text, alt_text) = match content {
CellContent::Simple(s) => (s, None),
CellContent::HasAlt {
alt: short,
@ -255,18 +306,57 @@ fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>)
} => (long, Some(short)),
};
let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true).collect::<Vec<&str>>();
let mut text = if graphemes.len() > width && width > 0 {
if let Some(s) = opt {
// If an alternative exists, use that.
Text::raw(s.as_ref())
let mut text = {
let graphemes: Vec<&str> =
UnicodeSegmentation::graphemes(main_text.as_ref(), true).collect();
if graphemes.len() > width && width > 0 {
if let Some(s) = alt_text {
// If an alternative exists, use that.
Text::raw(s.as_ref())
} else {
// Truncate with ellipsis
let first_n = graphemes[..(width - 1)].concat();
Text::raw(concat_string!(first_n, ""))
}
} else {
// Truncate with ellipsis
let first_n = graphemes[..(width - 1)].concat();
Text::raw(concat_string!(first_n, ""))
Text::raw(main_text.as_ref())
}
};
if let Some(row_style) = row_style {
text.patch_style(row_style);
}
text
}
fn truncate_suffixed_text<'a>(
content: &'a CellContent, suffix: &str, width: usize, row_style: Option<Style>,
) -> Text<'a> {
let (main_text, alt_text) = match content {
CellContent::Simple(s) => (s, None),
CellContent::HasAlt {
alt: short,
main: long,
} => (long, Some(short)),
};
let mut text = {
let suffixed = concat_string!(main_text, suffix);
let graphemes: Vec<&str> =
UnicodeSegmentation::graphemes(suffixed.as_str(), true).collect();
if graphemes.len() > width && width > 1 {
if let Some(alt) = alt_text {
// If an alternative exists, use that + arrow.
Text::raw(concat_string!(alt, suffix))
} else {
// Truncate with ellipsis + arrow.
let first_n = graphemes[..(width - 2)].concat();
Text::raw(concat_string!(first_n, "", suffix))
}
} else {
Text::raw(suffixed)
}
} else {
Text::raw(text.as_ref())
};
if let Some(row_style) = row_style {
@ -315,4 +405,64 @@ pub fn get_start_position(
}
#[cfg(test)]
mod test {}
mod test {
use super::*;
#[test]
fn test_get_start_position() {
use crate::app::ScrollDirection::{self, Down, Up};
#[track_caller]
fn test_get(
bar: usize, num: usize, direction: ScrollDirection, selected: usize, force: bool,
expected_posn: usize, expected_bar: usize,
) {
let mut bar = bar;
assert_eq!(
get_start_position(num, &direction, &mut bar, selected, force),
expected_posn
);
assert_eq!(bar, expected_bar);
}
// Scrolling down from start
test_get(0, 10, Down, 0, false, 0, 0);
// Simple scrolling down
test_get(0, 10, Down, 1, false, 0, 0);
// Scrolling down from the middle high up
test_get(0, 10, Down, 5, false, 0, 0);
// Scrolling down into boundary
test_get(0, 10, Down, 11, false, 1, 1);
// Scrolling down from the with non-zero bar
test_get(5, 10, Down, 15, false, 5, 5);
// Force redraw scrolling down (e.g. resize)
test_get(5, 15, Down, 15, true, 0, 0);
// Test jumping down
test_get(1, 10, Down, 20, true, 10, 10);
// Scrolling up from bottom
test_get(10, 10, Up, 20, false, 10, 10);
// Simple scrolling up
test_get(10, 10, Up, 19, false, 10, 10);
// Scrolling up from the middle
test_get(10, 10, Up, 10, false, 10, 10);
// Scrolling up into boundary
test_get(10, 10, Up, 9, false, 9, 9);
// Force redraw scrolling up (e.g. resize)
test_get(5, 10, Up, 15, true, 5, 5);
// Test jumping up
test_get(10, 10, Up, 0, false, 0, 0);
}
}

View file

@ -9,7 +9,7 @@ use tui::{
};
use crate::{
app::{App, KillSignal},
app::{App, KillSignal, widgets::ProcWidgetMode},
canvas::Painter,
};
@ -29,7 +29,13 @@ impl Painter {
if let Some(first_pid) = to_kill_processes.1.first() {
return Some(Text::from(vec![
Spans::from(""),
if app_state.is_grouped(app_state.current_widget.widget_id) {
if app_state
.proc_state
.widget_states
.get(&app_state.current_widget.widget_id)
.map(|p| matches!(p.mode, ProcWidgetMode::Grouped))
.unwrap_or(false)
{
if to_kill_processes.1.len() != 1 {
Spans::from(format!(
"Kill {} processes with the name \"{}\"? Press ENTER to confirm.",

View file

@ -6,117 +6,6 @@ use std::{
time::Instant,
};
/// Return a (hard)-width vector for column widths.
///
/// * `total_width` is the, well, total width available. **NOTE:** This function automatically
/// takes away 2 from the width as part of the left/right
/// bounds.
/// * `hard_widths` is inflexible column widths. Use a `None` to represent a soft width.
/// * `soft_widths_min` is the lower limit for a soft width. Use `None` if a hard width goes there.
/// * `soft_widths_max` is the upper limit for a soft width, in percentage of the total width. Use
/// `None` if a hard width goes there.
/// * `soft_widths_desired` is the desired soft width. Use `None` if a hard width goes there.
/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
/// false.
///
/// **NOTE:** This function ASSUMES THAT ALL PASSED SLICES ARE OF THE SAME SIZE.
///
/// **NOTE:** The returned vector may not be the same size as the slices, this is because including
/// 0-constraints breaks tui-rs.
pub fn get_column_widths(
total_width: u16, hard_widths: &[Option<u16>], soft_widths_min: &[Option<u16>],
soft_widths_max: &[Option<f64>], soft_widths_desired: &[Option<u16>], left_to_right: bool,
) -> Vec<u16> {
debug_assert!(
hard_widths.len() == soft_widths_min.len(),
"hard width length != soft width min length!"
);
debug_assert!(
soft_widths_min.len() == soft_widths_max.len(),
"soft width min length != soft width max length!"
);
debug_assert!(
soft_widths_max.len() == soft_widths_desired.len(),
"soft width max length != soft width desired length!"
);
if total_width > 2 {
let initial_width = total_width - 2;
let mut total_width_left = initial_width;
let mut column_widths: Vec<u16> = vec![0; hard_widths.len()];
let range: Vec<usize> = if left_to_right {
(0..hard_widths.len()).collect()
} else {
(0..hard_widths.len()).rev().collect()
};
for itx in &range {
if let Some(Some(hard_width)) = hard_widths.get(*itx) {
// Hard width...
let space_taken = min(*hard_width, total_width_left);
// TODO [COLUMN MOVEMENT]: Remove this
if *hard_width > space_taken {
break;
}
column_widths[*itx] = space_taken;
total_width_left -= space_taken;
total_width_left = total_width_left.saturating_sub(1);
} else if let (
Some(Some(soft_width_max)),
Some(Some(soft_width_min)),
Some(Some(soft_width_desired)),
) = (
soft_widths_max.get(*itx),
soft_widths_min.get(*itx),
soft_widths_desired.get(*itx),
) {
// Soft width...
let soft_limit = max(
if soft_width_max.is_sign_negative() {
*soft_width_desired
} else {
(*soft_width_max * initial_width as f64).ceil() as u16
},
*soft_width_min,
);
let space_taken = min(min(soft_limit, *soft_width_desired), total_width_left);
// TODO [COLUMN MOVEMENT]: Remove this
if *soft_width_min > space_taken {
break;
}
column_widths[*itx] = space_taken;
total_width_left -= space_taken;
total_width_left = total_width_left.saturating_sub(1);
}
}
while let Some(0) = column_widths.last() {
column_widths.pop();
}
if !column_widths.is_empty() {
// Redistribute remaining.
let amount_per_slot = total_width_left / column_widths.len() as u16;
total_width_left %= column_widths.len() as u16;
for (index, width) in column_widths.iter_mut().enumerate() {
if index < total_width_left.into() {
*width += amount_per_slot + 1;
} else {
*width += amount_per_slot;
}
}
}
column_widths
} else {
vec![]
}
}
pub fn get_search_start_position(
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
current_cursor_position: usize, is_force_redraw: bool,
@ -154,45 +43,6 @@ pub fn get_search_start_position(
}
}
pub fn get_start_position(
num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize,
currently_selected_position: usize, is_force_redraw: bool,
) -> usize {
if is_force_redraw {
*scroll_position_bar = 0;
}
// FIXME: Note that num_rows is WRONG here! It assumes the number of rows - 1... oops.
match scroll_direction {
app::ScrollDirection::Down => {
if currently_selected_position < *scroll_position_bar + num_rows {
// If, using previous_scrolled_position, we can see the element
// (so within that and + num_rows) just reuse the current previously scrolled position
*scroll_position_bar
} else if currently_selected_position >= num_rows {
// Else if the current position past the last element visible in the list, omit
// until we can see that element
*scroll_position_bar = currently_selected_position - num_rows;
*scroll_position_bar
} else {
// Else, if it is not past the last element visible, do not omit anything
0
}
}
app::ScrollDirection::Up => {
if currently_selected_position <= *scroll_position_bar {
// If it's past the first element, then show from that element downwards
*scroll_position_bar = currently_selected_position;
} else if currently_selected_position >= *scroll_position_bar + num_rows {
*scroll_position_bar = currently_selected_position - num_rows;
}
// Else, don't change what our start position is from whatever it is set to!
*scroll_position_bar
}
}
}
/// Calculate how many bars are to be drawn within basic mode's components.
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
std::cmp::min(
@ -226,62 +76,6 @@ mod test {
use super::*;
#[test]
fn test_get_start_position() {
use crate::app::ScrollDirection::{self, Down, Up};
fn test(
bar: usize, num: usize, direction: ScrollDirection, selected: usize, force: bool,
expected_posn: usize, expected_bar: usize,
) {
let mut bar = bar;
assert_eq!(
get_start_position(num, &direction, &mut bar, selected, force),
expected_posn
);
assert_eq!(bar, expected_bar);
}
// Scrolling down from start
test(0, 10, Down, 0, false, 0, 0);
// Simple scrolling down
test(0, 10, Down, 1, false, 0, 0);
// Scrolling down from the middle high up
test(0, 10, Down, 5, false, 0, 0);
// Scrolling down into boundary
test(0, 10, Down, 11, false, 1, 1);
// Scrolling down from the with non-zero bar
test(5, 10, Down, 15, false, 5, 5);
// Force redraw scrolling down (e.g. resize)
test(5, 15, Down, 15, true, 0, 0);
// Test jumping down
test(1, 10, Down, 20, true, 10, 10);
// Scrolling up from bottom
test(10, 10, Up, 20, false, 10, 10);
// Simple scrolling up
test(10, 10, Up, 19, false, 10, 10);
// Scrolling up from the middle
test(10, 10, Up, 10, false, 10, 10);
// Scrolling up into boundary
test(10, 10, Up, 9, false, 9, 9);
// Force redraw scrolling up (e.g. resize)
test(5, 10, Up, 15, true, 5, 5);
// Test jumping up
test(10, 10, Up, 0, false, 0, 0);
}
#[test]
fn test_calculate_basic_use_bars() {
// Testing various breakpoints and edge cases.
@ -329,54 +123,4 @@ mod test {
));
assert!(over_timer.is_none());
}
#[test]
fn test_width_calculation() {
// TODO: Implement width calculation test; can reuse old ones as basis
}
#[test]
fn test_zero_width() {
assert_eq!(
get_column_widths(
0,
&[Some(1), None, None],
&[None, Some(1), Some(2)],
&[None, Some(0.125), Some(0.5)],
&[None, Some(10), Some(10)],
true
),
vec![],
);
}
#[test]
fn test_two_width() {
assert_eq!(
get_column_widths(
2,
&[Some(1), None, None],
&[None, Some(1), Some(2)],
&[None, Some(0.125), Some(0.5)],
&[None, Some(10), Some(10)],
true
),
vec![],
);
}
#[test]
fn test_non_zero_width() {
assert_eq!(
get_column_widths(
16,
&[Some(1), None, None],
&[None, Some(1), Some(2)],
&[None, Some(0.125), Some(0.5)],
&[None, Some(10), Some(10)],
true
),
vec![2, 2, 7],
);
}
}

View file

@ -1,13 +1,13 @@
use std::{borrow::Cow, iter};
use crate::{
app::{layout_manager::WidgetDirection, App, CpuWidgetState},
app::{layout_manager::WidgetDirection, App, CellContent, CpuWidgetState},
canvas::{
components::{GraphData, TextTable, TimeGraph},
drawing_utils::should_hide_x_label,
Painter,
},
data_conversion::{CellContent, ConvertedCpuData, TableData, TableRow},
data_conversion::{ConvertedCpuData, TableData, TableRow},
};
use concat_string::concat_string;

View file

@ -1,7 +1,8 @@
use crate::{
app::App,
canvas::{
drawing_utils::{get_column_widths, get_search_start_position, get_start_position},
components::{TextTable, TextTableTitle},
drawing_utils::get_search_start_position,
Painter,
},
constants::*,
@ -11,8 +12,8 @@ use tui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
terminal::Frame,
text::{Span, Spans, Text},
widgets::{Block, Borders, Paragraph, Row, Table},
text::{Span, Spans},
widgets::{Block, Borders, Paragraph},
};
use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
@ -90,14 +91,14 @@ const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: &[Option<f64>] = &[
impl Painter {
/// Draws and handles all process-related drawing. Use this.
/// - `widget_id` here represents the widget ID of the process widget itself!
pub fn draw_process_features<B: Backend>(
pub fn draw_process_widget<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
) {
if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
let search_height = if draw_border { 5 } else { 3 };
let is_sort_open = process_widget_state.is_sort_open;
let header_len = process_widget_state.columns.longest_header_len;
const SORT_MENU_WIDTH: u16 = 8;
let mut proc_draw_loc = draw_loc;
if process_widget_state.is_search_enabled() {
@ -119,17 +120,11 @@ impl Painter {
if is_sort_open {
let processes_chunk = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(header_len + 4), Constraint::Min(0)])
.constraints([Constraint::Length(SORT_MENU_WIDTH + 4), Constraint::Min(0)])
.split(proc_draw_loc);
proc_draw_loc = processes_chunk[1];
self.draw_process_sort(
f,
app_state,
processes_chunk[0],
draw_border,
widget_id + 2,
);
self.draw_sort_table(f, app_state, processes_chunk[0], draw_border, widget_id + 2);
}
self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id);
@ -148,18 +143,15 @@ impl Painter {
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
let recalculate_column_widths =
should_get_widget_bounds || proc_widget_state.requires_redraw;
// Reset redraw marker.
// TODO: this should ideally be handled generically in the future.
if proc_widget_state.requires_redraw {
proc_widget_state.requires_redraw = false;
}
let is_on_widget = widget_id == app_state.current_widget.widget_id;
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc)[0];
let (border_style, highlight_style) = if is_on_widget {
let (border_style, highlighted_text_style) = if is_on_widget {
(
self.colours.highlighted_border_style,
self.colours.currently_selected_text_style,
@ -168,355 +160,69 @@ impl Painter {
(self.colours.border_style, self.colours.text_style)
};
let title_base = if app_state.app_config_fields.show_table_scroll_position {
if let Some(finalized_process_data) = app_state
.canvas_data
.finalized_process_data_map
.get(&widget_id)
{
let title = format!(
" Processes ({} of {}) ",
proc_widget_state
.scroll_state
.current_scroll_position
.saturating_add(1),
finalized_process_data.len()
);
if title.len() <= draw_loc.width.into() {
title
} else {
" Processes ".to_string()
}
} else {
" Processes ".to_string()
}
} else {
" Processes ".to_string()
};
let title = if app_state.is_expanded
&& !proc_widget_state
.process_search_state
.search_state
.is_enabled
&& !proc_widget_state.is_sort_open
{
const ESCAPE_ENDING: &str = "── Esc to go back ";
let (chosen_title_base, expanded_title_base) = {
let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING);
if temp_title_base.len() > draw_loc.width.into() {
(
" Processes ".to_string(),
format!("{}{}", " Processes ", ESCAPE_ENDING),
)
} else {
(title_base, temp_title_base)
}
};
Spans::from(vec![
Span::styled(chosen_title_base, self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to go back ",
"".repeat(
usize::from(draw_loc.width).saturating_sub(
UnicodeSegmentation::graphemes(
expanded_title_base.as_str(),
true
)
.count()
+ 2
)
)
),
border_style,
),
])
} else {
Spans::from(Span::styled(title_base, self.colours.widget_title_style))
};
let process_block = if draw_border {
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(border_style)
} else if is_on_widget {
Block::default()
.borders(SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style)
} else {
Block::default().borders(Borders::NONE)
};
if let Some(process_data) = &app_state
.canvas_data
.stringified_process_data_map
.get(&widget_id)
{
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap))
.saturating_sub(self.table_height_offset),
),
&proc_widget_state.scroll_state.scroll_direction,
&mut proc_widget_state.scroll_state.scroll_bar,
proc_widget_state.scroll_state.current_scroll_position,
app_state.is_force_redraw,
);
// Sanity check
let start_position = if position >= process_data.len() {
process_data.len().saturating_sub(1)
} else {
position
};
let sliced_vec = &process_data[start_position..];
let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| {
(
data.iter()
.map(|(entry, _alternative)| entry)
.collect::<Vec<_>>(),
disabled,
)
});
let proc_table_state = &mut proc_widget_state.scroll_state.table_state;
proc_table_state.select(Some(
proc_widget_state
.scroll_state
.current_scroll_position
.saturating_sub(start_position),
));
// Draw!
let process_headers = proc_widget_state.columns.get_column_headers(
&proc_widget_state.process_sorting_type,
proc_widget_state.is_process_sort_descending,
);
// Calculate widths
// FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths
let hard_widths = if proc_widget_state.is_grouped {
PROCESS_HEADERS_HARD_WIDTH_GROUPED
} else {
PROCESS_HEADERS_HARD_WIDTH_NO_GROUP
};
if recalculate_column_widths {
let mut column_widths = process_headers
.iter()
.map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16)
.collect::<Vec<_>>();
let soft_widths_min = column_widths
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>();
proc_widget_state.table_width_state.desired_column_widths = {
for (row, _disabled) in processed_sliced_vec.clone() {
for (col, entry) in row.iter().enumerate() {
if let Some(col_width) = column_widths.get_mut(col) {
let grapheme_len = UnicodeWidthStr::width(entry.as_str());
if grapheme_len as u16 > *col_width {
*col_width = grapheme_len as u16;
}
}
}
}
column_widths
};
proc_widget_state.table_width_state.desired_column_widths = proc_widget_state
.table_width_state
.desired_column_widths
.iter()
.zip(hard_widths)
.map(|(current, hard)| {
if let Some(hard) = hard {
if *hard > *current {
*hard
} else {
*current
}
} else {
*current
}
})
.collect::<Vec<_>>();
let soft_widths_max = if proc_widget_state.is_grouped {
// Note grouped trees are not a thing.
if proc_widget_state.is_using_command {
PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND
} else {
PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE
}
} else if proc_widget_state.is_using_command {
PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND
} else if proc_widget_state.is_tree_mode {
PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE
} else {
PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE
};
proc_widget_state.table_width_state.calculated_column_widths =
get_column_widths(
draw_loc.width,
hard_widths,
&soft_widths_min,
soft_widths_max,
&(proc_widget_state
.table_width_state
.desired_column_widths
.iter()
.map(|width| Some(*width))
.collect::<Vec<_>>()),
true,
);
// debug!(
// "DCW: {:?}",
// proc_widget_state.table_width_state.desired_column_widths
// );
// debug!(
// "CCW: {:?}",
// proc_widget_state.table_width_state.calculated_column_widths
// );
}
let dcw = &proc_widget_state.table_width_state.desired_column_widths;
let ccw = &proc_widget_state.table_width_state.calculated_column_widths;
let process_rows = sliced_vec.iter().map(|(data, disabled)| {
let truncated_data = data.iter().zip(hard_widths).enumerate().map(
|(itx, ((entry, alternative), width))| {
if let (Some(desired_col_width), Some(calculated_col_width)) =
(dcw.get(itx), ccw.get(itx))
{
if width.is_none() {
if *desired_col_width > *calculated_col_width
&& *calculated_col_width > 0
{
let calculated_col_width: usize =
(*calculated_col_width).into();
let graphemes =
UnicodeSegmentation::graphemes(entry.as_str(), true)
.collect::<Vec<&str>>();
if let Some(alternative) = alternative {
Text::raw(alternative)
} else if graphemes.len() > calculated_col_width
&& calculated_col_width > 1
{
// Truncate with ellipsis
let first_n =
graphemes[..(calculated_col_width - 1)].concat();
Text::raw(format!("{}", first_n))
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
} else {
Text::raw(entry)
}
},
);
if *disabled {
Row::new(truncated_data).style(self.colours.disabled_text_style)
} else {
Row::new(truncated_data)
}
});
f.render_stateful_widget(
Table::new(process_rows)
.header(
Row::new(process_headers)
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.block(process_block)
.highlight_style(highlight_style)
.style(self.colours.text_style)
.widths(
&(proc_widget_state
.table_width_state
.calculated_column_widths
.iter()
.map(|calculated_width| Constraint::Length(*calculated_width))
.collect::<Vec<_>>()),
),
margined_draw_loc,
proc_table_state,
);
} else {
f.render_widget(process_block, margined_draw_loc);
TextTable {
table_gap: app_state.app_config_fields.table_gap,
is_force_redraw: app_state.is_force_redraw,
recalculate_column_widths,
header_style: self.colours.table_header_style,
border_style,
highlighted_text_style,
title: Some(TextTableTitle {
title: " Processes ".into(),
is_expanded: app_state.is_expanded,
}),
is_on_widget,
draw_border,
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
title_style: self.colours.widget_title_style,
text_style: self.colours.text_style,
left_to_right: false,
}
.draw_text_table(f, draw_loc, &mut proc_widget_state.table_state, todo!());
// Check if we need to update columnar bounds...
if recalculate_column_widths
|| proc_widget_state.columns.column_header_x_locs.is_none()
|| proc_widget_state.columns.column_header_y_loc.is_none()
{
// y location is just the y location of the widget + border size (1 normally, 0 in basic)
proc_widget_state.columns.column_header_y_loc =
Some(draw_loc.y + if draw_border { 1 } else { 0 });
// FIXME: [Proc] Handle this, and the above TODO
// // Check if we need to update columnar bounds...
// if recalculate_column_widths
// || proc_widget_state.columns.column_header_x_locs.is_none()
// || proc_widget_state.columns.column_header_y_loc.is_none()
// {
// // y location is just the y location of the widget + border size (1 normally, 0 in basic)
// proc_widget_state.columns.column_header_y_loc =
// Some(draw_loc.y + if draw_border { 1 } else { 0 });
// x location is determined using the x locations of the widget; just offset from the left bound
// as appropriate, and use the right bound as limiter.
// // x location is determined using the x locations of the widget; just offset from the left bound
// // as appropriate, and use the right bound as limiter.
let mut current_x_left = draw_loc.x + 1;
let max_x_right = draw_loc.x + draw_loc.width - 1;
// let mut current_x_left = draw_loc.x + 1;
// let max_x_right = draw_loc.x + draw_loc.width - 1;
let mut x_locs = vec![];
// let mut x_locs = vec![];
for width in proc_widget_state
.table_width_state
.calculated_column_widths
.iter()
{
let right_bound = current_x_left + width;
// for width in proc_widget_state
// .table_width_state
// .calculated_column_widths
// .iter()
// {
// let right_bound = current_x_left + width;
if right_bound < max_x_right {
x_locs.push((current_x_left, right_bound));
current_x_left = right_bound + 1;
} else {
x_locs.push((current_x_left, max_x_right));
break;
}
}
// if right_bound < max_x_right {
// x_locs.push((current_x_left, right_bound));
// current_x_left = right_bound + 1;
// } else {
// x_locs.push((current_x_left, max_x_right));
// break;
// }
// }
proc_widget_state.columns.column_header_x_locs = Some(x_locs);
}
// proc_widget_state.columns.column_header_x_locs = Some(x_locs);
// }
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
widget.bottom_right_corner =
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
}
}
}
@ -583,14 +289,8 @@ impl Painter {
let start_position: usize = get_search_start_position(
num_columns - num_chars_for_text - 5,
&proc_widget_state
.process_search_state
.search_state
.cursor_direction,
&mut proc_widget_state
.process_search_state
.search_state
.cursor_bar,
&proc_widget_state.search_state.search_state.cursor_direction,
&mut proc_widget_state.search_state.search_state.cursor_bar,
current_cursor_position,
app_state.is_force_redraw,
);
@ -625,25 +325,19 @@ impl Painter {
})];
// Text options shamelessly stolen from VS Code.
let case_style = if !proc_widget_state.process_search_state.is_ignoring_case {
let case_style = if !proc_widget_state.search_state.is_ignoring_case {
self.colours.currently_selected_text_style
} else {
self.colours.text_style
};
let whole_word_style = if proc_widget_state
.process_search_state
.is_searching_whole_word
{
let whole_word_style = if proc_widget_state.search_state.is_searching_whole_word {
self.colours.currently_selected_text_style
} else {
self.colours.text_style
};
let regex_style = if proc_widget_state
.process_search_state
.is_searching_with_regex
{
let regex_style = if proc_widget_state.search_state.is_searching_with_regex {
self.colours.currently_selected_text_style
} else {
self.colours.text_style
@ -669,11 +363,7 @@ impl Painter {
]);
search_text.push(Spans::from(Span::styled(
if let Some(err) = &proc_widget_state
.process_search_state
.search_state
.error_message
{
if let Some(err) = &proc_widget_state.search_state.search_state.error_message {
err.as_str()
} else {
""
@ -683,7 +373,7 @@ impl Painter {
search_text.push(option_text);
let current_border_style = if proc_widget_state
.process_search_state
.search_state
.search_state
.is_invalid_search
{
@ -751,127 +441,128 @@ impl Painter {
/// state that is stored.
///
/// This should not be directly called.
fn draw_process_sort<B: Backend>(
fn draw_sort_table<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
) {
let is_on_widget = widget_id == app_state.current_widget.widget_id;
// FIXME: [Proc] Redo drawing sort table!
// let is_on_widget = widget_id == app_state.current_widget.widget_id;
if let Some(proc_widget_state) =
app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
{
let current_scroll_position = proc_widget_state.columns.current_scroll_position;
let sort_string = proc_widget_state
.columns
.ordered_columns
.iter()
.filter(|column_type| {
proc_widget_state
.columns
.column_mapping
.get(column_type)
.unwrap()
.enabled
})
.map(|column_type| column_type.to_string())
.collect::<Vec<_>>();
// if let Some(proc_widget_state) =
// app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
// {
// let current_scroll_position = proc_widget_state.columns.current_scroll_position;
// let sort_string = proc_widget_state
// .columns
// .ordered_columns
// .iter()
// .filter(|column_type| {
// proc_widget_state
// .columns
// .column_mapping
// .get(column_type)
// .unwrap()
// .enabled
// })
// .map(|column_type| column_type.to_string())
// .collect::<Vec<_>>();
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
0
} else {
app_state.app_config_fields.table_gap
};
let position = get_start_position(
usize::from(
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
),
&proc_widget_state.columns.scroll_direction,
&mut proc_widget_state.columns.previous_scroll_position,
current_scroll_position,
app_state.is_force_redraw,
);
// let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
// 0
// } else {
// app_state.app_config_fields.table_gap
// };
// let position = get_start_position(
// usize::from(
// (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
// ),
// &proc_widget_state.columns.scroll_direction,
// &mut proc_widget_state.columns.previous_scroll_position,
// current_scroll_position,
// app_state.is_force_redraw,
// );
// Sanity check
let start_position = if position >= sort_string.len() {
sort_string.len().saturating_sub(1)
} else {
position
};
// // Sanity check
// let start_position = if position >= sort_string.len() {
// sort_string.len().saturating_sub(1)
// } else {
// position
// };
let sliced_vec = &sort_string[start_position..];
// let sliced_vec = &sort_string[start_position..];
let sort_options = sliced_vec
.iter()
.map(|column| Row::new(vec![column.as_str()]));
// let sort_options = sliced_vec
// .iter()
// .map(|column| Row::new(vec![column.as_str()]));
let column_state = &mut proc_widget_state.columns.column_state;
column_state.select(Some(
proc_widget_state
.columns
.current_scroll_position
.saturating_sub(start_position),
));
let current_border_style = if proc_widget_state
.process_search_state
.search_state
.is_invalid_search
{
self.colours.invalid_query_style
} else if is_on_widget {
self.colours.highlighted_border_style
} else {
self.colours.border_style
};
// let column_state = &mut proc_widget_state.columns.column_state;
// column_state.select(Some(
// proc_widget_state
// .columns
// .current_scroll_position
// .saturating_sub(start_position),
// ));
// let current_border_style = if proc_widget_state
// .search_state
// .search_state
// .is_invalid_search
// {
// self.colours.invalid_query_style
// } else if is_on_widget {
// self.colours.highlighted_border_style
// } else {
// self.colours.border_style
// };
let process_sort_block = if draw_border {
Block::default()
.borders(Borders::ALL)
.border_style(current_border_style)
} else if is_on_widget {
Block::default()
.borders(SIDE_BORDERS)
.border_style(current_border_style)
} else {
Block::default().borders(Borders::NONE)
};
// let process_sort_block = if draw_border {
// Block::default()
// .borders(Borders::ALL)
// .border_style(current_border_style)
// } else if is_on_widget {
// Block::default()
// .borders(SIDE_BORDERS)
// .border_style(current_border_style)
// } else {
// Block::default().borders(Borders::NONE)
// };
let highlight_style = if is_on_widget {
self.colours.currently_selected_text_style
} else {
self.colours.text_style
};
// let highlight_style = if is_on_widget {
// self.colours.currently_selected_text_style
// } else {
// self.colours.text_style
// };
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)])
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
.direction(Direction::Horizontal)
.split(draw_loc)[0];
// let margined_draw_loc = Layout::default()
// .constraints([Constraint::Percentage(100)])
// .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
// .direction(Direction::Horizontal)
// .split(draw_loc)[0];
f.render_stateful_widget(
Table::new(sort_options)
.header(
Row::new(vec!["Sort By"])
.style(self.colours.table_header_style)
.bottom_margin(table_gap),
)
.block(process_sort_block)
.highlight_style(highlight_style)
.style(self.colours.text_style)
.widths(&[Constraint::Percentage(100)]),
margined_draw_loc,
column_state,
);
// f.render_stateful_widget(
// Table::new(sort_options)
// .header(
// Row::new(vec!["Sort By"])
// .style(self.colours.table_header_style)
// .bottom_margin(table_gap),
// )
// .block(process_sort_block)
// .highlight_style(highlight_style)
// .style(self.colours.text_style)
// .widths(&[Constraint::Percentage(100)]),
// margined_draw_loc,
// column_state,
// );
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
widget.bottom_right_corner = Some((
margined_draw_loc.x + margined_draw_loc.width,
margined_draw_loc.y + margined_draw_loc.height,
));
}
}
}
// if app_state.should_get_widget_bounds() {
// // Update draw loc in widget map
// if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
// widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
// widget.bottom_right_corner = Some((
// margined_draw_loc.x + margined_draw_loc.width,
// margined_draw_loc.y + margined_draw_loc.height,
// ));
// }
// }
// }
}
}

View file

@ -1,16 +1,17 @@
//! This mainly concerns converting collected data into things that the canvas
//! can actually handle.
use crate::app::widgets::{ProcWidget, ProcWidgetMode};
use crate::app::CellContent;
use crate::canvas::Point;
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
use crate::{
app::{data_farmer, data_harvester, App, ProcWidgetState},
app::{data_farmer, data_harvester, App},
utils::{self, gen_util::*},
};
use concat_string::concat_string;
use data_harvester::processes::ProcessSorting;
use fxhash::FxBuildHasher;
use indexmap::IndexSet;
use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
#[derive(Default, Debug)]
@ -23,29 +24,6 @@ pub struct ConvertedBatteryData {
pub health: String,
}
pub enum CellContent {
Simple(Cow<'static, str>),
HasAlt {
alt: Cow<'static, str>,
main: Cow<'static, str>,
},
}
impl CellContent {
/// Returns the length of the [`CellContent`]. Note that for a [`CellContent::HasAlt`], it will return
/// the length of the "main" field.
pub fn len(&self) -> usize {
match self {
CellContent::Simple(s) => s.len(),
CellContent::HasAlt { alt: _, main: long } => long.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Default)]
pub struct TableData {
pub data: Vec<TableRow>,
@ -1247,12 +1225,13 @@ pub fn tree_process_data(
// FIXME: [OPT] This is an easy target for optimization, too many to_strings!
pub fn stringify_process_data(
proc_widget_state: &ProcWidgetState, finalized_process_data: &[ConvertedProcessData],
proc_widget_state: &ProcWidget, finalized_process_data: &[ConvertedProcessData],
) -> Vec<(Vec<(String, Option<String>)>, bool)> {
let is_proc_widget_grouped = proc_widget_state.is_grouped;
let is_proc_widget_grouped = matches!(proc_widget_state.mode, ProcWidgetMode::Grouped);
let is_using_command = proc_widget_state.is_using_command;
let is_tree = proc_widget_state.is_tree_mode;
let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem);
let is_tree = matches!(proc_widget_state.mode, ProcWidgetMode::Tree);
// FIXME: [Proc] Handle this, it shouldn't always be true lol.
let mem_enabled = true;
finalized_process_data
.iter()

View file

@ -29,6 +29,7 @@ use crossterm::{
use app::{
data_harvester::{self, processes::ProcessSorting},
layout_manager::{UsedWidgets, WidgetDirection},
widgets::{ProcWidget, ProcWidgetMode},
App,
};
use constants::*;
@ -305,13 +306,8 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
pub fn handle_force_redraws(app: &mut App) {
// Currently we use an Option... because we might want to future-proof this
// if we eventually get widget-specific redrawing!
if app.proc_state.force_update_all {
update_all_process_lists(app);
app.proc_state.force_update_all = false;
} else if let Some(widget_id) = app.proc_state.force_update {
update_final_process_list(app, widget_id);
app.proc_state.force_update = None;
}
// FIXME: [PROC] handle updating processes if force redraw!
if app.cpu_state.force_update.is_some() {
convert_cpu_data_points(&app.data_collection, &mut app.canvas_data.cpu_data);
@ -365,16 +361,15 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
.map(|process_state| {
(
process_state
.process_search_state
.search_state
.search_state
.is_invalid_or_blank_search(),
process_state.is_using_command,
process_state.is_grouped,
process_state.is_tree_mode,
process_state.mode,
)
});
if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
if let Some((is_invalid_or_blank, is_using_command, mode)) = process_states {
if !app.is_frozen {
convert_process_data(
&app.data_collection,
@ -384,8 +379,9 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
);
}
let process_filter = app.get_process_filter(widget_id);
let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
app.canvas_data
let filtered_process_data: Vec<ConvertedProcessData> = match mode {
ProcWidgetMode::Tree => app
.canvas_data
.single_process_data
.iter()
.map(|(_pid, process)| {
@ -398,9 +394,9 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
}
process_clone
})
.collect::<Vec<_>>()
} else {
app.canvas_data
.collect::<Vec<_>>(),
ProcWidgetMode::Grouped | ProcWidgetMode::Normal => app
.canvas_data
.single_process_data
.iter()
.filter_map(|(_pid, process)| {
@ -419,35 +415,35 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
}
})
.cloned()
.collect::<Vec<_>>()
.collect::<Vec<_>>(),
};
if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
let mut finalized_process_data = if is_tree {
tree_process_data(
let mut finalized_process_data = match proc_widget_state.mode {
ProcWidgetMode::Tree => tree_process_data(
&filtered_process_data,
is_using_command,
&proc_widget_state.process_sorting_type,
proc_widget_state.is_process_sort_descending,
)
} else if is_grouped {
group_process_data(&filtered_process_data, is_using_command)
} else {
filtered_process_data
),
ProcWidgetMode::Grouped => {
let mut data = group_process_data(&filtered_process_data, is_using_command);
sort_process_data(&mut data, proc_widget_state);
data
}
ProcWidgetMode::Normal => {
let mut data = filtered_process_data;
sort_process_data(&mut data, proc_widget_state);
data
}
};
// Note tree mode is sorted well before this, as it's special.
if !is_tree {
sort_process_data(&mut finalized_process_data, proc_widget_state);
}
if proc_widget_state.scroll_state.current_scroll_position
>= finalized_process_data.len()
if proc_widget_state.table_state.current_scroll_position >= finalized_process_data.len()
{
proc_widget_state.scroll_state.current_scroll_position =
proc_widget_state.table_state.current_scroll_position =
finalized_process_data.len().saturating_sub(1);
proc_widget_state.scroll_state.scroll_bar = 0;
proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down;
proc_widget_state.table_state.scroll_bar = 0;
proc_widget_state.table_state.scroll_direction = app::ScrollDirection::Down;
}
app.canvas_data.stringified_process_data_map.insert(
@ -461,9 +457,7 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
}
}
fn sort_process_data(
to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &app::ProcWidgetState,
) {
fn sort_process_data(to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &ProcWidget) {
to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());
match &proc_widget_state.process_sorting_type {
@ -510,7 +504,7 @@ fn sort_process_data(
}
}
ProcessSorting::Pid => {
if !proc_widget_state.is_grouped {
if !matches!(proc_widget_state.mode, ProcWidgetMode::Grouped) {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.pid,
@ -573,7 +567,7 @@ fn sort_process_data(
(None, None) => std::cmp::Ordering::Less,
}),
ProcessSorting::Count => {
if proc_widget_state.is_grouped {
if matches!(proc_widget_state.mode, ProcWidgetMode::Grouped) {
to_sort_vec.sort_by(|a, b| {
utils::gen_util::get_ordering(
a.group_pids.len(),

View file

@ -10,7 +10,11 @@ use std::{
};
use crate::{
app::{layout_manager::*, *},
app::{
layout_manager::*,
widgets::{ProcWidget, ProcWidgetMode},
*,
},
canvas::ColourScheme,
constants::*,
units::data_units::DataUnit,
@ -265,7 +269,7 @@ pub fn build_app(
let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new();
let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new();
let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::new();
let mut proc_state_map: HashMap<u64, ProcWidgetState> = HashMap::new();
let mut proc_state_map: HashMap<u64, ProcWidget> = HashMap::new();
let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::new();
let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new();
let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new();
@ -353,15 +357,22 @@ pub fn build_app(
);
}
Proc => {
let mode = if is_grouped {
ProcWidgetMode::Grouped
} else if is_default_tree {
ProcWidgetMode::Tree
} else {
ProcWidgetMode::Normal
};
proc_state_map.insert(
widget.widget_id,
ProcWidgetState::init(
ProcWidget::init(
mode,
is_case_sensitive,
is_match_whole_word,
is_use_regex,
is_grouped,
show_memory_as_values,
is_default_tree,
is_default_command,
),
);
@ -466,7 +477,7 @@ pub fn build_app(
let mapping = HashMap::new();
for widget in search_case_enabled_widgets {
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
proc_widget.process_search_state.is_ignoring_case = !widget.enabled;
proc_widget.search_state.is_ignoring_case = !widget.enabled;
}
}
flags.search_case_enabled_widgets_map = Some(mapping);
@ -480,7 +491,7 @@ pub fn build_app(
let mapping = HashMap::new();
for widget in search_whole_word_enabled_widgets {
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
proc_widget.process_search_state.is_searching_whole_word = widget.enabled;
proc_widget.search_state.is_searching_whole_word = widget.enabled;
}
}
flags.search_whole_word_enabled_widgets_map = Some(mapping);
@ -492,7 +503,7 @@ pub fn build_app(
let mapping = HashMap::new();
for widget in search_regex_enabled_widgets {
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
proc_widget.process_search_state.is_searching_with_regex = widget.enabled;
proc_widget.search_state.is_searching_with_regex = widget.enabled;
}
}
flags.search_regex_enabled_widgets_map = Some(mapping);