mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-26 23:24:20 +02:00
refactor: begin migration of process widget
This commit is contained in:
parent
69ec526dc6
commit
7ee6f6a737
@ -40,11 +40,12 @@ nvidia = ["nvml-wrapper"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.57"
|
anyhow = "1.0.57"
|
||||||
backtrace = "0.3.65"
|
backtrace = "0.3.65"
|
||||||
|
cfg-if = "1.0.0"
|
||||||
crossterm = "0.18.2"
|
crossterm = "0.18.2"
|
||||||
ctrlc = { version = "3.1.9", features = ["termination"] }
|
ctrlc = { version = "3.1.9", features = ["termination"] }
|
||||||
clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
|
clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
|
||||||
cfg-if = "1.0.0"
|
|
||||||
concat-string = "1.0.1"
|
concat-string = "1.0.1"
|
||||||
|
# const_format = "0.2.23"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
futures-timer = "3.0.2"
|
futures-timer = "3.0.2"
|
||||||
|
704
src/app.rs
704
src/app.rs
File diff suppressed because it is too large
Load Diff
@ -75,23 +75,48 @@ impl Default for ProcessSorting {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ProcessHarvest {
|
pub struct ProcessHarvest {
|
||||||
|
/// The pid of the process.
|
||||||
pub pid: Pid,
|
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,
|
pub cpu_usage_percent: f64,
|
||||||
|
|
||||||
|
/// Memory usage as a percentage.
|
||||||
pub mem_usage_percent: f64,
|
pub mem_usage_percent: f64,
|
||||||
|
|
||||||
|
/// Memory usage as bytes.
|
||||||
pub mem_usage_bytes: u64,
|
pub mem_usage_bytes: u64,
|
||||||
// pub rss_kb: u64,
|
|
||||||
// pub virt_kb: u64,
|
/// The name of the process.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
/// The exact command for the process.
|
||||||
pub command: String,
|
pub command: String,
|
||||||
|
|
||||||
|
/// Bytes read per second.
|
||||||
pub read_bytes_per_sec: u64,
|
pub read_bytes_per_sec: u64,
|
||||||
|
|
||||||
|
/// Bytes written per second.
|
||||||
pub write_bytes_per_sec: u64,
|
pub write_bytes_per_sec: u64,
|
||||||
|
|
||||||
|
/// The total number of bytes read by the process.
|
||||||
pub total_read_bytes: u64,
|
pub total_read_bytes: u64,
|
||||||
|
|
||||||
|
/// The total number of bytes written by the process.
|
||||||
pub total_write_bytes: u64,
|
pub total_write_bytes: u64,
|
||||||
|
|
||||||
|
/// The current state of the process (e.g. zombie, asleep)
|
||||||
pub process_state: String,
|
pub process_state: String,
|
||||||
|
|
||||||
|
/// The process state represented by a character. TODO: Merge with above as a single struct.
|
||||||
pub process_state_char: char,
|
pub process_state_char: char,
|
||||||
|
|
||||||
/// This is the *effective* user ID.
|
/// This is the *effective* user ID of the process.
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub uid: Option<libc::uid_t>,
|
pub uid: Option<libc::uid_t>,
|
||||||
|
// pub rss_kb: u64,
|
||||||
|
// pub virt_kb: u64,
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
use super::ProcWidgetState;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data_conversion::ConvertedProcessData,
|
data_conversion::ConvertedProcessData,
|
||||||
utils::error::{
|
utils::error::{
|
||||||
@ -9,6 +8,8 @@ use crate::{
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::{borrow::Cow, collections::VecDeque};
|
use std::{borrow::Cow, collections::VecDeque};
|
||||||
|
|
||||||
|
use super::widgets::ProcWidget;
|
||||||
|
|
||||||
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
|
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
|
||||||
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
|
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
|
||||||
const OR_LIST: [&str; 2] = ["or", "||"];
|
const OR_LIST: [&str; 2] = ["or", "||"];
|
||||||
@ -39,7 +40,7 @@ pub trait ProcessQuery {
|
|||||||
fn parse_query(&self) -> Result<Query>;
|
fn parse_query(&self) -> Result<Query>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcessQuery for ProcWidgetState {
|
impl ProcessQuery for ProcWidget {
|
||||||
fn parse_query(&self) -> Result<Query> {
|
fn parse_query(&self) -> Result<Query> {
|
||||||
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
|
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
|
||||||
let lhs = process_or(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)?;
|
let mut process_filter = process_string_to_filter(&mut split_query)?;
|
||||||
process_filter.process_regexes(
|
process_filter.process_regexes(
|
||||||
self.process_search_state.is_searching_whole_word,
|
self.search_state.is_searching_whole_word,
|
||||||
self.process_search_state.is_ignoring_case,
|
self.search_state.is_ignoring_case,
|
||||||
self.process_search_state.is_searching_with_regex,
|
self.search_state.is_searching_with_regex,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(process_filter)
|
Ok(process_filter)
|
||||||
|
@ -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 unicode_segmentation::GraphemeCursor;
|
||||||
|
|
||||||
use tui::widgets::TableState;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{layout_manager::BottomWidgetType, query::*},
|
app::{layout_manager::BottomWidgetType, query::*},
|
||||||
constants,
|
constants,
|
||||||
data_conversion::CellContent,
|
data_harvester::processes::ProcessSorting,
|
||||||
data_harvester::processes::{self, ProcessSorting},
|
|
||||||
};
|
};
|
||||||
use ProcessSorting::*;
|
|
||||||
|
pub mod table_state;
|
||||||
|
pub use table_state::*;
|
||||||
|
|
||||||
|
use super::widgets::ProcWidget;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ScrollDirection {
|
pub enum ScrollDirection {
|
||||||
@ -39,218 +40,6 @@ pub struct CanvasTableWidthState {
|
|||||||
pub calculated_column_widths: Vec<u16>,
|
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)]
|
#[derive(PartialEq)]
|
||||||
pub enum KillSignal {
|
pub enum KillSignal {
|
||||||
Cancel,
|
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 struct ProcState {
|
||||||
pub widget_states: HashMap<u64, ProcWidgetState>,
|
pub widget_states: HashMap<u64, ProcWidget>,
|
||||||
pub force_update: Option<u64>,
|
|
||||||
pub force_update_all: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcState {
|
impl ProcState {
|
||||||
pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self {
|
pub fn init(widget_states: HashMap<u64, ProcWidget>) -> Self {
|
||||||
ProcState {
|
ProcState { widget_states }
|
||||||
widget_states,
|
|
||||||
force_update: None,
|
|
||||||
force_update_all: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
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)
|
self.widget_states.get(&widget_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -941,8 +195,7 @@ pub struct CpuWidgetState {
|
|||||||
|
|
||||||
impl CpuWidgetState {
|
impl CpuWidgetState {
|
||||||
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
|
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
|
||||||
const CPU_LEGEND_HEADER: [(Cow<'static, str>, Option<Cow<'static, str>>); 2] =
|
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
|
||||||
[(Cow::Borrowed("CPU"), None), (Cow::Borrowed("Use%"), None)];
|
|
||||||
const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [
|
const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [
|
||||||
WidthBounds::soft_from_str("CPU", Some(0.5)),
|
WidthBounds::soft_from_str("CPU", Some(0.5)),
|
||||||
WidthBounds::soft_from_str("Use%", Some(0.5)),
|
WidthBounds::soft_from_str("Use%", Some(0.5)),
|
||||||
@ -952,7 +205,7 @@ impl CpuWidgetState {
|
|||||||
CPU_LEGEND_HEADER
|
CPU_LEGEND_HEADER
|
||||||
.iter()
|
.iter()
|
||||||
.zip(WIDTHS)
|
.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(),
|
.collect(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1040,7 +293,9 @@ impl Default for TempWidgetState {
|
|||||||
TEMP_HEADERS
|
TEMP_HEADERS
|
||||||
.iter()
|
.iter()
|
||||||
.zip(WIDTHS)
|
.zip(WIDTHS)
|
||||||
.map(|(header, width)| TableComponentColumn::new(*header, None, width))
|
.map(|(header, width)| {
|
||||||
|
TableComponentColumn::new(CellContent::new(*header, None), width)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -1087,7 +342,9 @@ impl Default for DiskWidgetState {
|
|||||||
DISK_HEADERS
|
DISK_HEADERS
|
||||||
.iter()
|
.iter()
|
||||||
.zip(WIDTHS)
|
.zip(WIDTHS)
|
||||||
.map(|(header, width)| TableComponentColumn::new(*header, None, width))
|
.map(|(header, width)| {
|
||||||
|
TableComponentColumn::new(CellContent::new(*header, None), width)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -1169,61 +426,3 @@ pub struct ConfigCategory {
|
|||||||
pub struct ConfigOption {
|
pub struct ConfigOption {
|
||||||
pub set_function: Box<dyn Fn() -> anyhow::Result<()>>,
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
455
src/app/states/table_state.rs
Normal file
455
src/app/states/table_state.rs
Normal 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
2
src/app/widgets.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod process;
|
||||||
|
pub use process::*;
|
320
src/app/widgets/process.rs
Normal file
320
src/app/widgets/process.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -341,8 +341,9 @@ impl Painter {
|
|||||||
|
|
||||||
// Reset column headers for sorting in process widget...
|
// Reset column headers for sorting in process widget...
|
||||||
for proc_widget in app_state.proc_state.widget_states.values_mut() {
|
for proc_widget in app_state.proc_state.widget_states.values_mut() {
|
||||||
proc_widget.columns.column_header_y_loc = None;
|
// FIXME: [Proc] Handle this?
|
||||||
proc_widget.columns.column_header_x_locs = None;
|
// proc_widget.columns.column_header_y_loc = None;
|
||||||
|
// proc_widget.columns.column_header_x_locs = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -506,7 +507,7 @@ impl Painter {
|
|||||||
_ => 0,
|
_ => 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(
|
Battery => self.draw_battery_display(
|
||||||
f,
|
f,
|
||||||
@ -585,7 +586,7 @@ impl Painter {
|
|||||||
ProcSort => 2,
|
ProcSort => 2,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
self.draw_process_features(
|
self.draw_process_widget(
|
||||||
f,
|
f,
|
||||||
app_state,
|
app_state,
|
||||||
vertical_chunks[3],
|
vertical_chunks[3],
|
||||||
@ -736,7 +737,7 @@ impl Painter {
|
|||||||
Disk => {
|
Disk => {
|
||||||
self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id)
|
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,
|
f,
|
||||||
app_state,
|
app_state,
|
||||||
*widget_draw_loc,
|
*widget_draw_loc,
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use std::{borrow::Cow, cmp::min};
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
cmp::{max, min},
|
||||||
|
};
|
||||||
|
|
||||||
use concat_string::concat_string;
|
use concat_string::concat_string;
|
||||||
use tui::{
|
use tui::{
|
||||||
@ -12,9 +15,12 @@ use tui::{
|
|||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{self, TableComponentState},
|
app::{
|
||||||
|
self, CellContent, SortState, TableComponentColumn, TableComponentHeader,
|
||||||
|
TableComponentState,
|
||||||
|
},
|
||||||
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
|
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
|
||||||
data_conversion::{CellContent, TableData, TableRow},
|
data_conversion::{TableData, TableRow},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct TextTableTitle<'a> {
|
pub struct TextTableTitle<'a> {
|
||||||
@ -101,8 +107,8 @@ impl<'a> TextTable<'a> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pub fn draw_text_table<B: Backend>(
|
pub fn draw_text_table<B: Backend, H: TableComponentHeader>(
|
||||||
&self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState,
|
&self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState<H>,
|
||||||
table_data: &TableData,
|
table_data: &TableData,
|
||||||
) {
|
) {
|
||||||
// TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything.
|
// 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,
|
desired,
|
||||||
max_percentage: _,
|
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) => {}
|
app::WidthBounds::Hard(_width) => {}
|
||||||
});
|
});
|
||||||
@ -188,13 +194,7 @@ impl<'a> TextTable<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let columns = &state.columns;
|
let columns = &state.columns;
|
||||||
let header = Row::new(columns.iter().filter_map(|c| {
|
let header = build_header(columns, &state.sort_state)
|
||||||
if c.calculated_width == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(truncate_text(&c.name, c.calculated_width.into(), None))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.style(self.header_style)
|
.style(self.header_style)
|
||||||
.bottom_margin(table_gap);
|
.bottom_margin(table_gap);
|
||||||
let table_rows = sliced_vec.iter().map(|row| {
|
let table_rows = sliced_vec.iter().map(|row| {
|
||||||
@ -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.
|
/// 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<'_> {
|
fn truncate_text<'a>(content: &'a CellContent, width: usize, row_style: Option<Style>) -> Text<'a> {
|
||||||
let (text, opt) = match content {
|
let (main_text, alt_text) = match content {
|
||||||
CellContent::Simple(s) => (s, None),
|
CellContent::Simple(s) => (s, None),
|
||||||
CellContent::HasAlt {
|
CellContent::HasAlt {
|
||||||
alt: short,
|
alt: short,
|
||||||
@ -255,9 +306,11 @@ fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>)
|
|||||||
} => (long, Some(short)),
|
} => (long, Some(short)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true).collect::<Vec<&str>>();
|
let mut text = {
|
||||||
let mut text = if graphemes.len() > width && width > 0 {
|
let graphemes: Vec<&str> =
|
||||||
if let Some(s) = opt {
|
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.
|
// If an alternative exists, use that.
|
||||||
Text::raw(s.as_ref())
|
Text::raw(s.as_ref())
|
||||||
} else {
|
} else {
|
||||||
@ -266,7 +319,44 @@ fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>)
|
|||||||
Text::raw(concat_string!(first_n, "…"))
|
Text::raw(concat_string!(first_n, "…"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text::raw(text.as_ref())
|
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)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(row_style) = row_style {
|
if let Some(row_style) = row_style {
|
||||||
@ -315,4 +405,64 @@ pub fn get_start_position(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,7 +9,7 @@ use tui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{App, KillSignal},
|
app::{App, KillSignal, widgets::ProcWidgetMode},
|
||||||
canvas::Painter,
|
canvas::Painter,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,7 +29,13 @@ impl Painter {
|
|||||||
if let Some(first_pid) = to_kill_processes.1.first() {
|
if let Some(first_pid) = to_kill_processes.1.first() {
|
||||||
return Some(Text::from(vec![
|
return Some(Text::from(vec![
|
||||||
Spans::from(""),
|
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 {
|
if to_kill_processes.1.len() != 1 {
|
||||||
Spans::from(format!(
|
Spans::from(format!(
|
||||||
"Kill {} processes with the name \"{}\"? Press ENTER to confirm.",
|
"Kill {} processes with the name \"{}\"? Press ENTER to confirm.",
|
||||||
|
@ -6,117 +6,6 @@ use std::{
|
|||||||
time::Instant,
|
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(
|
pub fn get_search_start_position(
|
||||||
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
|
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
|
||||||
current_cursor_position: usize, is_force_redraw: bool,
|
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.
|
/// 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 {
|
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
|
||||||
std::cmp::min(
|
std::cmp::min(
|
||||||
@ -226,62 +76,6 @@ mod test {
|
|||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_calculate_basic_use_bars() {
|
fn test_calculate_basic_use_bars() {
|
||||||
// Testing various breakpoints and edge cases.
|
// Testing various breakpoints and edge cases.
|
||||||
@ -329,54 +123,4 @@ mod test {
|
|||||||
));
|
));
|
||||||
assert!(over_timer.is_none());
|
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],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
use std::{borrow::Cow, iter};
|
use std::{borrow::Cow, iter};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{layout_manager::WidgetDirection, App, CpuWidgetState},
|
app::{layout_manager::WidgetDirection, App, CellContent, CpuWidgetState},
|
||||||
canvas::{
|
canvas::{
|
||||||
components::{GraphData, TextTable, TimeGraph},
|
components::{GraphData, TextTable, TimeGraph},
|
||||||
drawing_utils::should_hide_x_label,
|
drawing_utils::should_hide_x_label,
|
||||||
Painter,
|
Painter,
|
||||||
},
|
},
|
||||||
data_conversion::{CellContent, ConvertedCpuData, TableData, TableRow},
|
data_conversion::{ConvertedCpuData, TableData, TableRow},
|
||||||
};
|
};
|
||||||
|
|
||||||
use concat_string::concat_string;
|
use concat_string::concat_string;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::App,
|
||||||
canvas::{
|
canvas::{
|
||||||
drawing_utils::{get_column_widths, get_search_start_position, get_start_position},
|
components::{TextTable, TextTableTitle},
|
||||||
|
drawing_utils::get_search_start_position,
|
||||||
Painter,
|
Painter,
|
||||||
},
|
},
|
||||||
constants::*,
|
constants::*,
|
||||||
@ -11,8 +12,8 @@ use tui::{
|
|||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
terminal::Frame,
|
terminal::Frame,
|
||||||
text::{Span, Spans, Text},
|
text::{Span, Spans},
|
||||||
widgets::{Block, Borders, Paragraph, Row, Table},
|
widgets::{Block, Borders, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
|
use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
|
||||||
@ -90,14 +91,14 @@ const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: &[Option<f64>] = &[
|
|||||||
impl Painter {
|
impl Painter {
|
||||||
/// Draws and handles all process-related drawing. Use this.
|
/// Draws and handles all process-related drawing. Use this.
|
||||||
/// - `widget_id` here represents the widget ID of the process widget itself!
|
/// - `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,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
) {
|
) {
|
||||||
if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
|
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 search_height = if draw_border { 5 } else { 3 };
|
||||||
let is_sort_open = process_widget_state.is_sort_open;
|
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;
|
let mut proc_draw_loc = draw_loc;
|
||||||
if process_widget_state.is_search_enabled() {
|
if process_widget_state.is_search_enabled() {
|
||||||
@ -119,17 +120,11 @@ impl Painter {
|
|||||||
if is_sort_open {
|
if is_sort_open {
|
||||||
let processes_chunk = Layout::default()
|
let processes_chunk = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.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);
|
.split(proc_draw_loc);
|
||||||
proc_draw_loc = processes_chunk[1];
|
proc_draw_loc = processes_chunk[1];
|
||||||
|
|
||||||
self.draw_process_sort(
|
self.draw_sort_table(f, app_state, processes_chunk[0], draw_border, widget_id + 2);
|
||||||
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);
|
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) {
|
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
|
||||||
let recalculate_column_widths =
|
let recalculate_column_widths =
|
||||||
should_get_widget_bounds || proc_widget_state.requires_redraw;
|
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 {
|
if proc_widget_state.requires_redraw {
|
||||||
proc_widget_state.requires_redraw = false;
|
proc_widget_state.requires_redraw = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||||
let margined_draw_loc = Layout::default()
|
let (border_style, highlighted_text_style) = if is_on_widget {
|
||||||
.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 {
|
|
||||||
(
|
(
|
||||||
self.colours.highlighted_border_style,
|
self.colours.highlighted_border_style,
|
||||||
self.colours.currently_selected_text_style,
|
self.colours.currently_selected_text_style,
|
||||||
@ -168,355 +160,69 @@ impl Painter {
|
|||||||
(self.colours.border_style, self.colours.text_style)
|
(self.colours.border_style, self.colours.text_style)
|
||||||
};
|
};
|
||||||
|
|
||||||
let title_base = if app_state.app_config_fields.show_table_scroll_position {
|
TextTable {
|
||||||
if let Some(finalized_process_data) = app_state
|
table_gap: app_state.app_config_fields.table_gap,
|
||||||
.canvas_data
|
is_force_redraw: app_state.is_force_redraw,
|
||||||
.finalized_process_data_map
|
recalculate_column_widths,
|
||||||
.get(&widget_id)
|
header_style: self.colours.table_header_style,
|
||||||
{
|
|
||||||
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,
|
border_style,
|
||||||
),
|
highlighted_text_style,
|
||||||
])
|
title: Some(TextTableTitle {
|
||||||
} else {
|
title: " Processes ".into(),
|
||||||
Spans::from(Span::styled(title_base, self.colours.widget_title_style))
|
is_expanded: app_state.is_expanded,
|
||||||
};
|
}),
|
||||||
|
is_on_widget,
|
||||||
let process_block = if draw_border {
|
draw_border,
|
||||||
Block::default()
|
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
|
||||||
.title(title)
|
title_style: self.colours.widget_title_style,
|
||||||
.borders(Borders::ALL)
|
text_style: self.colours.text_style,
|
||||||
.border_style(border_style)
|
left_to_right: false,
|
||||||
} 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;
|
|
||||||
}
|
}
|
||||||
}
|
.draw_text_table(f, draw_loc, &mut proc_widget_state.table_state, todo!());
|
||||||
}
|
|
||||||
}
|
|
||||||
column_widths
|
|
||||||
};
|
|
||||||
|
|
||||||
proc_widget_state.table_width_state.desired_column_widths = proc_widget_state
|
// FIXME: [Proc] Handle this, and the above TODO
|
||||||
.table_width_state
|
// // Check if we need to update columnar bounds...
|
||||||
.desired_column_widths
|
// if recalculate_column_widths
|
||||||
.iter()
|
// || proc_widget_state.columns.column_header_x_locs.is_none()
|
||||||
.zip(hard_widths)
|
// || proc_widget_state.columns.column_header_y_loc.is_none()
|
||||||
.map(|(current, hard)| {
|
// {
|
||||||
if let Some(hard) = hard {
|
// // y location is just the y location of the widget + border size (1 normally, 0 in basic)
|
||||||
if *hard > *current {
|
// proc_widget_state.columns.column_header_y_loc =
|
||||||
*hard
|
// Some(draw_loc.y + if draw_border { 1 } else { 0 });
|
||||||
} else {
|
|
||||||
*current
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*current
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let soft_widths_max = if proc_widget_state.is_grouped {
|
// // x location is determined using the x locations of the widget; just offset from the left bound
|
||||||
// Note grouped trees are not a thing.
|
// // as appropriate, and use the right bound as limiter.
|
||||||
|
|
||||||
if proc_widget_state.is_using_command {
|
// let mut current_x_left = draw_loc.x + 1;
|
||||||
PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND
|
// let max_x_right = draw_loc.x + draw_loc.width - 1;
|
||||||
} 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 =
|
// let mut x_locs = vec![];
|
||||||
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!(
|
// for width in proc_widget_state
|
||||||
// "DCW: {:?}",
|
// .table_width_state
|
||||||
// proc_widget_state.table_width_state.desired_column_widths
|
// .calculated_column_widths
|
||||||
// );
|
// .iter()
|
||||||
// debug!(
|
// {
|
||||||
// "CCW: {:?}",
|
// let right_bound = current_x_left + width;
|
||||||
// proc_widget_state.table_width_state.calculated_column_widths
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
|
|
||||||
let dcw = &proc_widget_state.table_width_state.desired_column_widths;
|
// if right_bound < max_x_right {
|
||||||
let ccw = &proc_widget_state.table_width_state.calculated_column_widths;
|
// 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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
let process_rows = sliced_vec.iter().map(|(data, disabled)| {
|
// proc_widget_state.columns.column_header_x_locs = Some(x_locs);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
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![];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proc_widget_state.columns.column_header_x_locs = Some(x_locs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if app_state.should_get_widget_bounds() {
|
if app_state.should_get_widget_bounds() {
|
||||||
// Update draw loc in widget map
|
// Update draw loc in widget map
|
||||||
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
|
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.top_left_corner = Some((draw_loc.x, draw_loc.y));
|
||||||
widget.bottom_right_corner = Some((
|
widget.bottom_right_corner =
|
||||||
margined_draw_loc.x + margined_draw_loc.width,
|
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
|
||||||
margined_draw_loc.y + margined_draw_loc.height,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -583,14 +289,8 @@ impl Painter {
|
|||||||
|
|
||||||
let start_position: usize = get_search_start_position(
|
let start_position: usize = get_search_start_position(
|
||||||
num_columns - num_chars_for_text - 5,
|
num_columns - num_chars_for_text - 5,
|
||||||
&proc_widget_state
|
&proc_widget_state.search_state.search_state.cursor_direction,
|
||||||
.process_search_state
|
&mut proc_widget_state.search_state.search_state.cursor_bar,
|
||||||
.search_state
|
|
||||||
.cursor_direction,
|
|
||||||
&mut proc_widget_state
|
|
||||||
.process_search_state
|
|
||||||
.search_state
|
|
||||||
.cursor_bar,
|
|
||||||
current_cursor_position,
|
current_cursor_position,
|
||||||
app_state.is_force_redraw,
|
app_state.is_force_redraw,
|
||||||
);
|
);
|
||||||
@ -625,25 +325,19 @@ impl Painter {
|
|||||||
})];
|
})];
|
||||||
|
|
||||||
// Text options shamelessly stolen from VS Code.
|
// 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
|
self.colours.currently_selected_text_style
|
||||||
} else {
|
} else {
|
||||||
self.colours.text_style
|
self.colours.text_style
|
||||||
};
|
};
|
||||||
|
|
||||||
let whole_word_style = if proc_widget_state
|
let whole_word_style = if proc_widget_state.search_state.is_searching_whole_word {
|
||||||
.process_search_state
|
|
||||||
.is_searching_whole_word
|
|
||||||
{
|
|
||||||
self.colours.currently_selected_text_style
|
self.colours.currently_selected_text_style
|
||||||
} else {
|
} else {
|
||||||
self.colours.text_style
|
self.colours.text_style
|
||||||
};
|
};
|
||||||
|
|
||||||
let regex_style = if proc_widget_state
|
let regex_style = if proc_widget_state.search_state.is_searching_with_regex {
|
||||||
.process_search_state
|
|
||||||
.is_searching_with_regex
|
|
||||||
{
|
|
||||||
self.colours.currently_selected_text_style
|
self.colours.currently_selected_text_style
|
||||||
} else {
|
} else {
|
||||||
self.colours.text_style
|
self.colours.text_style
|
||||||
@ -669,11 +363,7 @@ impl Painter {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
search_text.push(Spans::from(Span::styled(
|
search_text.push(Spans::from(Span::styled(
|
||||||
if let Some(err) = &proc_widget_state
|
if let Some(err) = &proc_widget_state.search_state.search_state.error_message {
|
||||||
.process_search_state
|
|
||||||
.search_state
|
|
||||||
.error_message
|
|
||||||
{
|
|
||||||
err.as_str()
|
err.as_str()
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
@ -683,7 +373,7 @@ impl Painter {
|
|||||||
search_text.push(option_text);
|
search_text.push(option_text);
|
||||||
|
|
||||||
let current_border_style = if proc_widget_state
|
let current_border_style = if proc_widget_state
|
||||||
.process_search_state
|
.search_state
|
||||||
.search_state
|
.search_state
|
||||||
.is_invalid_search
|
.is_invalid_search
|
||||||
{
|
{
|
||||||
@ -751,127 +441,128 @@ impl Painter {
|
|||||||
/// state that is stored.
|
/// state that is stored.
|
||||||
///
|
///
|
||||||
/// This should not be directly called.
|
/// 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,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
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) =
|
// if let Some(proc_widget_state) =
|
||||||
app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
|
// app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
|
||||||
{
|
// {
|
||||||
let current_scroll_position = proc_widget_state.columns.current_scroll_position;
|
// let current_scroll_position = proc_widget_state.columns.current_scroll_position;
|
||||||
let sort_string = proc_widget_state
|
// let sort_string = proc_widget_state
|
||||||
.columns
|
// .columns
|
||||||
.ordered_columns
|
// .ordered_columns
|
||||||
.iter()
|
// .iter()
|
||||||
.filter(|column_type| {
|
// .filter(|column_type| {
|
||||||
proc_widget_state
|
// proc_widget_state
|
||||||
.columns
|
// .columns
|
||||||
.column_mapping
|
// .column_mapping
|
||||||
.get(column_type)
|
// .get(column_type)
|
||||||
.unwrap()
|
// .unwrap()
|
||||||
.enabled
|
// .enabled
|
||||||
})
|
// })
|
||||||
.map(|column_type| column_type.to_string())
|
// .map(|column_type| column_type.to_string())
|
||||||
.collect::<Vec<_>>();
|
// .collect::<Vec<_>>();
|
||||||
|
|
||||||
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
// let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||||
0
|
// 0
|
||||||
} else {
|
// } else {
|
||||||
app_state.app_config_fields.table_gap
|
// app_state.app_config_fields.table_gap
|
||||||
};
|
// };
|
||||||
let position = get_start_position(
|
// let position = get_start_position(
|
||||||
usize::from(
|
// usize::from(
|
||||||
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
|
// (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
|
||||||
),
|
// ),
|
||||||
&proc_widget_state.columns.scroll_direction,
|
// &proc_widget_state.columns.scroll_direction,
|
||||||
&mut proc_widget_state.columns.previous_scroll_position,
|
// &mut proc_widget_state.columns.previous_scroll_position,
|
||||||
current_scroll_position,
|
// current_scroll_position,
|
||||||
app_state.is_force_redraw,
|
// app_state.is_force_redraw,
|
||||||
);
|
// );
|
||||||
|
|
||||||
// Sanity check
|
// // Sanity check
|
||||||
let start_position = if position >= sort_string.len() {
|
// let start_position = if position >= sort_string.len() {
|
||||||
sort_string.len().saturating_sub(1)
|
// sort_string.len().saturating_sub(1)
|
||||||
} else {
|
// } else {
|
||||||
position
|
// position
|
||||||
};
|
// };
|
||||||
|
|
||||||
let sliced_vec = &sort_string[start_position..];
|
// let sliced_vec = &sort_string[start_position..];
|
||||||
|
|
||||||
let sort_options = sliced_vec
|
// let sort_options = sliced_vec
|
||||||
.iter()
|
// .iter()
|
||||||
.map(|column| Row::new(vec![column.as_str()]));
|
// .map(|column| Row::new(vec![column.as_str()]));
|
||||||
|
|
||||||
let column_state = &mut proc_widget_state.columns.column_state;
|
// let column_state = &mut proc_widget_state.columns.column_state;
|
||||||
column_state.select(Some(
|
// column_state.select(Some(
|
||||||
proc_widget_state
|
// proc_widget_state
|
||||||
.columns
|
// .columns
|
||||||
.current_scroll_position
|
// .current_scroll_position
|
||||||
.saturating_sub(start_position),
|
// .saturating_sub(start_position),
|
||||||
));
|
// ));
|
||||||
let current_border_style = if proc_widget_state
|
// let current_border_style = if proc_widget_state
|
||||||
.process_search_state
|
// .search_state
|
||||||
.search_state
|
// .search_state
|
||||||
.is_invalid_search
|
// .is_invalid_search
|
||||||
{
|
// {
|
||||||
self.colours.invalid_query_style
|
// self.colours.invalid_query_style
|
||||||
} else if is_on_widget {
|
// } else if is_on_widget {
|
||||||
self.colours.highlighted_border_style
|
// self.colours.highlighted_border_style
|
||||||
} else {
|
// } else {
|
||||||
self.colours.border_style
|
// self.colours.border_style
|
||||||
};
|
// };
|
||||||
|
|
||||||
let process_sort_block = if draw_border {
|
// let process_sort_block = if draw_border {
|
||||||
Block::default()
|
// Block::default()
|
||||||
.borders(Borders::ALL)
|
// .borders(Borders::ALL)
|
||||||
.border_style(current_border_style)
|
// .border_style(current_border_style)
|
||||||
} else if is_on_widget {
|
// } else if is_on_widget {
|
||||||
Block::default()
|
// Block::default()
|
||||||
.borders(SIDE_BORDERS)
|
// .borders(SIDE_BORDERS)
|
||||||
.border_style(current_border_style)
|
// .border_style(current_border_style)
|
||||||
} else {
|
// } else {
|
||||||
Block::default().borders(Borders::NONE)
|
// Block::default().borders(Borders::NONE)
|
||||||
};
|
// };
|
||||||
|
|
||||||
let highlight_style = if is_on_widget {
|
// let highlight_style = if is_on_widget {
|
||||||
self.colours.currently_selected_text_style
|
// self.colours.currently_selected_text_style
|
||||||
} else {
|
// } else {
|
||||||
self.colours.text_style
|
// self.colours.text_style
|
||||||
};
|
// };
|
||||||
|
|
||||||
let margined_draw_loc = Layout::default()
|
// let margined_draw_loc = Layout::default()
|
||||||
.constraints([Constraint::Percentage(100)])
|
// .constraints([Constraint::Percentage(100)])
|
||||||
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
// .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
||||||
.direction(Direction::Horizontal)
|
// .direction(Direction::Horizontal)
|
||||||
.split(draw_loc)[0];
|
// .split(draw_loc)[0];
|
||||||
|
|
||||||
f.render_stateful_widget(
|
// f.render_stateful_widget(
|
||||||
Table::new(sort_options)
|
// Table::new(sort_options)
|
||||||
.header(
|
// .header(
|
||||||
Row::new(vec!["Sort By"])
|
// Row::new(vec!["Sort By"])
|
||||||
.style(self.colours.table_header_style)
|
// .style(self.colours.table_header_style)
|
||||||
.bottom_margin(table_gap),
|
// .bottom_margin(table_gap),
|
||||||
)
|
// )
|
||||||
.block(process_sort_block)
|
// .block(process_sort_block)
|
||||||
.highlight_style(highlight_style)
|
// .highlight_style(highlight_style)
|
||||||
.style(self.colours.text_style)
|
// .style(self.colours.text_style)
|
||||||
.widths(&[Constraint::Percentage(100)]),
|
// .widths(&[Constraint::Percentage(100)]),
|
||||||
margined_draw_loc,
|
// margined_draw_loc,
|
||||||
column_state,
|
// column_state,
|
||||||
);
|
// );
|
||||||
|
|
||||||
if app_state.should_get_widget_bounds() {
|
// if app_state.should_get_widget_bounds() {
|
||||||
// Update draw loc in widget map
|
// // Update draw loc in widget map
|
||||||
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
|
// 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.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
|
||||||
widget.bottom_right_corner = Some((
|
// widget.bottom_right_corner = Some((
|
||||||
margined_draw_loc.x + margined_draw_loc.width,
|
// margined_draw_loc.x + margined_draw_loc.width,
|
||||||
margined_draw_loc.y + margined_draw_loc.height,
|
// margined_draw_loc.y + margined_draw_loc.height,
|
||||||
));
|
// ));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
//! This mainly concerns converting collected data into things that the canvas
|
//! This mainly concerns converting collected data into things that the canvas
|
||||||
//! can actually handle.
|
//! can actually handle.
|
||||||
|
use crate::app::widgets::{ProcWidget, ProcWidgetMode};
|
||||||
|
use crate::app::CellContent;
|
||||||
use crate::canvas::Point;
|
use crate::canvas::Point;
|
||||||
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
|
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{data_farmer, data_harvester, App, ProcWidgetState},
|
app::{data_farmer, data_harvester, App},
|
||||||
utils::{self, gen_util::*},
|
utils::{self, gen_util::*},
|
||||||
};
|
};
|
||||||
use concat_string::concat_string;
|
use concat_string::concat_string;
|
||||||
use data_harvester::processes::ProcessSorting;
|
use data_harvester::processes::ProcessSorting;
|
||||||
use fxhash::FxBuildHasher;
|
use fxhash::FxBuildHasher;
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
@ -23,29 +24,6 @@ pub struct ConvertedBatteryData {
|
|||||||
pub health: String,
|
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)]
|
#[derive(Default)]
|
||||||
pub struct TableData {
|
pub struct TableData {
|
||||||
pub data: Vec<TableRow>,
|
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!
|
// FIXME: [OPT] This is an easy target for optimization, too many to_strings!
|
||||||
pub fn stringify_process_data(
|
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)> {
|
) -> 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_using_command = proc_widget_state.is_using_command;
|
||||||
let is_tree = proc_widget_state.is_tree_mode;
|
let is_tree = matches!(proc_widget_state.mode, ProcWidgetMode::Tree);
|
||||||
let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem);
|
// FIXME: [Proc] Handle this, it shouldn't always be true lol.
|
||||||
|
let mem_enabled = true;
|
||||||
|
|
||||||
finalized_process_data
|
finalized_process_data
|
||||||
.iter()
|
.iter()
|
||||||
|
72
src/lib.rs
72
src/lib.rs
@ -29,6 +29,7 @@ use crossterm::{
|
|||||||
use app::{
|
use app::{
|
||||||
data_harvester::{self, processes::ProcessSorting},
|
data_harvester::{self, processes::ProcessSorting},
|
||||||
layout_manager::{UsedWidgets, WidgetDirection},
|
layout_manager::{UsedWidgets, WidgetDirection},
|
||||||
|
widgets::{ProcWidget, ProcWidgetMode},
|
||||||
App,
|
App,
|
||||||
};
|
};
|
||||||
use constants::*;
|
use constants::*;
|
||||||
@ -305,13 +306,8 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
|
|||||||
pub fn handle_force_redraws(app: &mut App) {
|
pub fn handle_force_redraws(app: &mut App) {
|
||||||
// Currently we use an Option... because we might want to future-proof this
|
// Currently we use an Option... because we might want to future-proof this
|
||||||
// if we eventually get widget-specific redrawing!
|
// if we eventually get widget-specific redrawing!
|
||||||
if app.proc_state.force_update_all {
|
|
||||||
update_all_process_lists(app);
|
// FIXME: [PROC] handle updating processes if force redraw!
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.cpu_state.force_update.is_some() {
|
if app.cpu_state.force_update.is_some() {
|
||||||
convert_cpu_data_points(&app.data_collection, &mut app.canvas_data.cpu_data);
|
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| {
|
.map(|process_state| {
|
||||||
(
|
(
|
||||||
process_state
|
process_state
|
||||||
.process_search_state
|
.search_state
|
||||||
.search_state
|
.search_state
|
||||||
.is_invalid_or_blank_search(),
|
.is_invalid_or_blank_search(),
|
||||||
process_state.is_using_command,
|
process_state.is_using_command,
|
||||||
process_state.is_grouped,
|
process_state.mode,
|
||||||
process_state.is_tree_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 {
|
if !app.is_frozen {
|
||||||
convert_process_data(
|
convert_process_data(
|
||||||
&app.data_collection,
|
&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 process_filter = app.get_process_filter(widget_id);
|
||||||
let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
|
let filtered_process_data: Vec<ConvertedProcessData> = match mode {
|
||||||
app.canvas_data
|
ProcWidgetMode::Tree => app
|
||||||
|
.canvas_data
|
||||||
.single_process_data
|
.single_process_data
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_pid, process)| {
|
.map(|(_pid, process)| {
|
||||||
@ -398,9 +394,9 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
|
|||||||
}
|
}
|
||||||
process_clone
|
process_clone
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>(),
|
||||||
} else {
|
ProcWidgetMode::Grouped | ProcWidgetMode::Normal => app
|
||||||
app.canvas_data
|
.canvas_data
|
||||||
.single_process_data
|
.single_process_data
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_pid, process)| {
|
.filter_map(|(_pid, process)| {
|
||||||
@ -419,35 +415,35 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
|
if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
|
||||||
let mut finalized_process_data = if is_tree {
|
let mut finalized_process_data = match proc_widget_state.mode {
|
||||||
tree_process_data(
|
ProcWidgetMode::Tree => tree_process_data(
|
||||||
&filtered_process_data,
|
&filtered_process_data,
|
||||||
is_using_command,
|
is_using_command,
|
||||||
&proc_widget_state.process_sorting_type,
|
&proc_widget_state.process_sorting_type,
|
||||||
proc_widget_state.is_process_sort_descending,
|
proc_widget_state.is_process_sort_descending,
|
||||||
)
|
),
|
||||||
} else if is_grouped {
|
ProcWidgetMode::Grouped => {
|
||||||
group_process_data(&filtered_process_data, is_using_command)
|
let mut data = group_process_data(&filtered_process_data, is_using_command);
|
||||||
} else {
|
sort_process_data(&mut data, proc_widget_state);
|
||||||
filtered_process_data
|
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 proc_widget_state.table_state.current_scroll_position >= finalized_process_data.len()
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
proc_widget_state.scroll_state.current_scroll_position =
|
proc_widget_state.table_state.current_scroll_position =
|
||||||
finalized_process_data.len().saturating_sub(1);
|
finalized_process_data.len().saturating_sub(1);
|
||||||
proc_widget_state.scroll_state.scroll_bar = 0;
|
proc_widget_state.table_state.scroll_bar = 0;
|
||||||
proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down;
|
proc_widget_state.table_state.scroll_direction = app::ScrollDirection::Down;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.canvas_data.stringified_process_data_map.insert(
|
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(
|
fn sort_process_data(to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &ProcWidget) {
|
||||||
to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &app::ProcWidgetState,
|
|
||||||
) {
|
|
||||||
to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());
|
to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());
|
||||||
|
|
||||||
match &proc_widget_state.process_sorting_type {
|
match &proc_widget_state.process_sorting_type {
|
||||||
@ -510,7 +504,7 @@ fn sort_process_data(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProcessSorting::Pid => {
|
ProcessSorting::Pid => {
|
||||||
if !proc_widget_state.is_grouped {
|
if !matches!(proc_widget_state.mode, ProcWidgetMode::Grouped) {
|
||||||
to_sort_vec.sort_by(|a, b| {
|
to_sort_vec.sort_by(|a, b| {
|
||||||
utils::gen_util::get_ordering(
|
utils::gen_util::get_ordering(
|
||||||
a.pid,
|
a.pid,
|
||||||
@ -573,7 +567,7 @@ fn sort_process_data(
|
|||||||
(None, None) => std::cmp::Ordering::Less,
|
(None, None) => std::cmp::Ordering::Less,
|
||||||
}),
|
}),
|
||||||
ProcessSorting::Count => {
|
ProcessSorting::Count => {
|
||||||
if proc_widget_state.is_grouped {
|
if matches!(proc_widget_state.mode, ProcWidgetMode::Grouped) {
|
||||||
to_sort_vec.sort_by(|a, b| {
|
to_sort_vec.sort_by(|a, b| {
|
||||||
utils::gen_util::get_ordering(
|
utils::gen_util::get_ordering(
|
||||||
a.group_pids.len(),
|
a.group_pids.len(),
|
||||||
|
@ -10,7 +10,11 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{layout_manager::*, *},
|
app::{
|
||||||
|
layout_manager::*,
|
||||||
|
widgets::{ProcWidget, ProcWidgetMode},
|
||||||
|
*,
|
||||||
|
},
|
||||||
canvas::ColourScheme,
|
canvas::ColourScheme,
|
||||||
constants::*,
|
constants::*,
|
||||||
units::data_units::DataUnit,
|
units::data_units::DataUnit,
|
||||||
@ -265,7 +269,7 @@ pub fn build_app(
|
|||||||
let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new();
|
let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new();
|
||||||
let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new();
|
let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new();
|
||||||
let mut net_state_map: HashMap<u64, NetWidgetState> = 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 temp_state_map: HashMap<u64, TempWidgetState> = HashMap::new();
|
||||||
let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new();
|
let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new();
|
||||||
let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new();
|
let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new();
|
||||||
@ -353,15 +357,22 @@ pub fn build_app(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Proc => {
|
Proc => {
|
||||||
|
let mode = if is_grouped {
|
||||||
|
ProcWidgetMode::Grouped
|
||||||
|
} else if is_default_tree {
|
||||||
|
ProcWidgetMode::Tree
|
||||||
|
} else {
|
||||||
|
ProcWidgetMode::Normal
|
||||||
|
};
|
||||||
|
|
||||||
proc_state_map.insert(
|
proc_state_map.insert(
|
||||||
widget.widget_id,
|
widget.widget_id,
|
||||||
ProcWidgetState::init(
|
ProcWidget::init(
|
||||||
|
mode,
|
||||||
is_case_sensitive,
|
is_case_sensitive,
|
||||||
is_match_whole_word,
|
is_match_whole_word,
|
||||||
is_use_regex,
|
is_use_regex,
|
||||||
is_grouped,
|
|
||||||
show_memory_as_values,
|
show_memory_as_values,
|
||||||
is_default_tree,
|
|
||||||
is_default_command,
|
is_default_command,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -466,7 +477,7 @@ pub fn build_app(
|
|||||||
let mapping = HashMap::new();
|
let mapping = HashMap::new();
|
||||||
for widget in search_case_enabled_widgets {
|
for widget in search_case_enabled_widgets {
|
||||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
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);
|
flags.search_case_enabled_widgets_map = Some(mapping);
|
||||||
@ -480,7 +491,7 @@ pub fn build_app(
|
|||||||
let mapping = HashMap::new();
|
let mapping = HashMap::new();
|
||||||
for widget in search_whole_word_enabled_widgets {
|
for widget in search_whole_word_enabled_widgets {
|
||||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
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);
|
flags.search_whole_word_enabled_widgets_map = Some(mapping);
|
||||||
@ -492,7 +503,7 @@ pub fn build_app(
|
|||||||
let mapping = HashMap::new();
|
let mapping = HashMap::new();
|
||||||
for widget in search_regex_enabled_widgets {
|
for widget in search_regex_enabled_widgets {
|
||||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
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);
|
flags.search_regex_enabled_widgets_map = Some(mapping);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user