refactor: begin migration of process widget
This commit is contained in:
parent
69ec526dc6
commit
7ee6f6a737
|
@ -40,11 +40,12 @@ nvidia = ["nvml-wrapper"]
|
|||
[dependencies]
|
||||
anyhow = "1.0.57"
|
||||
backtrace = "0.3.65"
|
||||
cfg-if = "1.0.0"
|
||||
crossterm = "0.18.2"
|
||||
ctrlc = { version = "3.1.9", features = ["termination"] }
|
||||
clap = { version = "3.1.12", features = ["default", "cargo", "wrap_help"] }
|
||||
cfg-if = "1.0.0"
|
||||
concat-string = "1.0.1"
|
||||
# const_format = "0.2.23"
|
||||
dirs = "4.0.0"
|
||||
futures = "0.3.21"
|
||||
futures-timer = "3.0.2"
|
||||
|
|
798
src/app.rs
798
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)]
|
||||
pub struct ProcessHarvest {
|
||||
/// The pid of the process.
|
||||
pub pid: Pid,
|
||||
pub parent_pid: Option<Pid>, // Remember, parent_pid 0 is root...
|
||||
|
||||
/// The parent PID of the process. Remember, parent_pid 0 is root.
|
||||
pub parent_pid: Option<Pid>,
|
||||
|
||||
/// CPU usage as a percentage.
|
||||
pub cpu_usage_percent: f64,
|
||||
|
||||
/// Memory usage as a percentage.
|
||||
pub mem_usage_percent: f64,
|
||||
|
||||
/// Memory usage as bytes.
|
||||
pub mem_usage_bytes: u64,
|
||||
// pub rss_kb: u64,
|
||||
// pub virt_kb: u64,
|
||||
|
||||
/// The name of the process.
|
||||
pub name: String,
|
||||
|
||||
/// The exact command for the process.
|
||||
pub command: String,
|
||||
|
||||
/// Bytes read per second.
|
||||
pub read_bytes_per_sec: u64,
|
||||
|
||||
/// Bytes written per second.
|
||||
pub write_bytes_per_sec: u64,
|
||||
|
||||
/// The total number of bytes read by the process.
|
||||
pub total_read_bytes: u64,
|
||||
|
||||
/// The total number of bytes written by the process.
|
||||
pub total_write_bytes: u64,
|
||||
|
||||
/// The current state of the process (e.g. zombie, asleep)
|
||||
pub process_state: String,
|
||||
|
||||
/// The process state represented by a character. TODO: Merge with above as a single struct.
|
||||
pub process_state_char: char,
|
||||
|
||||
/// This is the *effective* user ID.
|
||||
/// This is the *effective* user ID of the process.
|
||||
#[cfg(target_family = "unix")]
|
||||
pub uid: Option<libc::uid_t>,
|
||||
// pub rss_kb: u64,
|
||||
// pub virt_kb: u64,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use super::ProcWidgetState;
|
||||
use crate::{
|
||||
data_conversion::ConvertedProcessData,
|
||||
utils::error::{
|
||||
|
@ -9,6 +8,8 @@ use crate::{
|
|||
use std::fmt::Debug;
|
||||
use std::{borrow::Cow, collections::VecDeque};
|
||||
|
||||
use super::widgets::ProcWidget;
|
||||
|
||||
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
|
||||
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
|
||||
const OR_LIST: [&str; 2] = ["or", "||"];
|
||||
|
@ -39,7 +40,7 @@ pub trait ProcessQuery {
|
|||
fn parse_query(&self) -> Result<Query>;
|
||||
}
|
||||
|
||||
impl ProcessQuery for ProcWidgetState {
|
||||
impl ProcessQuery for ProcWidget {
|
||||
fn parse_query(&self) -> Result<Query> {
|
||||
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
|
||||
let lhs = process_or(query)?;
|
||||
|
@ -437,9 +438,9 @@ impl ProcessQuery for ProcWidgetState {
|
|||
|
||||
let mut process_filter = process_string_to_filter(&mut split_query)?;
|
||||
process_filter.process_regexes(
|
||||
self.process_search_state.is_searching_whole_word,
|
||||
self.process_search_state.is_ignoring_case,
|
||||
self.process_search_state.is_searching_with_regex,
|
||||
self.search_state.is_searching_whole_word,
|
||||
self.search_state.is_ignoring_case,
|
||||
self.search_state.is_searching_with_regex,
|
||||
)?;
|
||||
|
||||
Ok(process_filter)
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
use std::{borrow::Cow, collections::HashMap, convert::TryInto, time::Instant};
|
||||
use std::{collections::HashMap, time::Instant};
|
||||
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
|
||||
use tui::widgets::TableState;
|
||||
|
||||
use crate::{
|
||||
app::{layout_manager::BottomWidgetType, query::*},
|
||||
constants,
|
||||
data_conversion::CellContent,
|
||||
data_harvester::processes::{self, ProcessSorting},
|
||||
data_harvester::processes::ProcessSorting,
|
||||
};
|
||||
use ProcessSorting::*;
|
||||
|
||||
pub mod table_state;
|
||||
pub use table_state::*;
|
||||
|
||||
use super::widgets::ProcWidget;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScrollDirection {
|
||||
|
@ -39,218 +40,6 @@ pub struct CanvasTableWidthState {
|
|||
pub calculated_column_widths: Vec<u16>,
|
||||
}
|
||||
|
||||
/// A bound on the width of a column.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum WidthBounds {
|
||||
/// A width of this type is either as long as `min`, but can otherwise shrink and grow up to a point.
|
||||
Soft {
|
||||
/// The minimum amount before giving up and hiding.
|
||||
min_width: u16,
|
||||
|
||||
/// The desired, calculated width. Take this if possible as the base starting width.
|
||||
desired: u16,
|
||||
|
||||
/// The max width, as a percentage of the total width available. If [`None`],
|
||||
/// then it can grow as desired.
|
||||
max_percentage: Option<f32>,
|
||||
},
|
||||
|
||||
/// A width of this type is either as long as specified, or does not appear at all.
|
||||
Hard(u16),
|
||||
}
|
||||
|
||||
impl WidthBounds {
|
||||
pub const fn soft_from_str(name: &'static str, max_percentage: Option<f32>) -> WidthBounds {
|
||||
let len = name.len() as u16;
|
||||
WidthBounds::Soft {
|
||||
min_width: len,
|
||||
desired: len,
|
||||
max_percentage,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn soft_from_str_with_alt(
|
||||
name: &'static str, alt: &'static str, max_percentage: Option<f32>,
|
||||
) -> WidthBounds {
|
||||
WidthBounds::Soft {
|
||||
min_width: alt.len() as u16,
|
||||
desired: name.len() as u16,
|
||||
max_percentage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TableComponentColumn {
|
||||
/// The name of the column. Displayed if possible as the header.
|
||||
pub name: CellContent,
|
||||
|
||||
/// A restriction on this column's width, if desired.
|
||||
pub width_bounds: WidthBounds,
|
||||
|
||||
/// The calculated width of the column.
|
||||
pub calculated_width: u16,
|
||||
}
|
||||
|
||||
impl TableComponentColumn {
|
||||
pub fn new<I>(name: I, alt: Option<I>, width_bounds: WidthBounds) -> Self
|
||||
where
|
||||
I: Into<Cow<'static, str>>,
|
||||
{
|
||||
Self {
|
||||
name: if let Some(alt) = alt {
|
||||
CellContent::HasAlt {
|
||||
alt: alt.into(),
|
||||
main: name.into(),
|
||||
}
|
||||
} else {
|
||||
CellContent::Simple(name.into())
|
||||
},
|
||||
width_bounds,
|
||||
calculated_width: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_skip(&self) -> bool {
|
||||
self.calculated_width == 0
|
||||
}
|
||||
}
|
||||
|
||||
/// [`TableComponentState`] deals with fields for a scrollable's current state.
|
||||
#[derive(Default)]
|
||||
pub struct TableComponentState {
|
||||
pub current_scroll_position: usize,
|
||||
pub scroll_bar: usize,
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub table_state: TableState,
|
||||
pub columns: Vec<TableComponentColumn>,
|
||||
}
|
||||
|
||||
impl TableComponentState {
|
||||
pub fn new(columns: Vec<TableComponentColumn>) -> Self {
|
||||
Self {
|
||||
current_scroll_position: 0,
|
||||
scroll_bar: 0,
|
||||
scroll_direction: ScrollDirection::Down,
|
||||
table_state: Default::default(),
|
||||
columns,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates widths for the columns for this table.
|
||||
///
|
||||
/// * `total_width` is the, well, total width available.
|
||||
/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
|
||||
/// false.
|
||||
///
|
||||
/// **NOTE:** Trailing 0's may break tui-rs, remember to filter them out later!
|
||||
pub fn calculate_column_widths(&mut self, total_width: u16, left_to_right: bool) {
|
||||
use itertools::Either;
|
||||
use std::cmp::{max, min};
|
||||
|
||||
let mut total_width_left = total_width;
|
||||
|
||||
for column in self.columns.iter_mut() {
|
||||
column.calculated_width = 0;
|
||||
}
|
||||
|
||||
let columns = if left_to_right {
|
||||
Either::Left(self.columns.iter_mut())
|
||||
} else {
|
||||
Either::Right(self.columns.iter_mut().rev())
|
||||
};
|
||||
|
||||
let mut num_columns = 0;
|
||||
for column in columns {
|
||||
match &column.width_bounds {
|
||||
WidthBounds::Soft {
|
||||
min_width,
|
||||
desired,
|
||||
max_percentage,
|
||||
} => {
|
||||
let soft_limit = max(
|
||||
if let Some(max_percentage) = max_percentage {
|
||||
// Rust doesn't have an `into()` or `try_into()` for floats to integers???
|
||||
((*max_percentage * f32::from(total_width)).ceil()) as u16
|
||||
} else {
|
||||
*desired
|
||||
},
|
||||
*min_width,
|
||||
);
|
||||
let space_taken = min(min(soft_limit, *desired), total_width_left);
|
||||
|
||||
if *min_width > space_taken {
|
||||
break;
|
||||
} else if space_taken > 0 {
|
||||
total_width_left = total_width_left.saturating_sub(space_taken + 1);
|
||||
column.calculated_width = space_taken;
|
||||
num_columns += 1;
|
||||
}
|
||||
}
|
||||
WidthBounds::Hard(width) => {
|
||||
let space_taken = min(*width, total_width_left);
|
||||
|
||||
if *width > space_taken {
|
||||
break;
|
||||
} else if space_taken > 0 {
|
||||
total_width_left = total_width_left.saturating_sub(space_taken + 1);
|
||||
column.calculated_width = space_taken;
|
||||
num_columns += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if num_columns > 0 {
|
||||
// Redistribute remaining.
|
||||
let mut num_dist = num_columns;
|
||||
let amount_per_slot = total_width_left / num_dist;
|
||||
total_width_left %= num_dist;
|
||||
for column in self.columns.iter_mut() {
|
||||
if num_dist == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if column.calculated_width > 0 {
|
||||
if total_width_left > 0 {
|
||||
column.calculated_width += amount_per_slot + 1;
|
||||
total_width_left -= 1;
|
||||
} else {
|
||||
column.calculated_width += amount_per_slot;
|
||||
}
|
||||
|
||||
num_dist -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the position if possible, and if there is a valid change, returns the new position.
|
||||
pub fn update_position(&mut self, change: i64, num_entries: usize) -> Option<usize> {
|
||||
if change == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let csp: Result<i64, _> = self.current_scroll_position.try_into();
|
||||
if let Ok(csp) = csp {
|
||||
let proposed: Result<usize, _> = (csp + change).try_into();
|
||||
if let Ok(proposed) = proposed {
|
||||
if proposed < num_entries {
|
||||
self.current_scroll_position = proposed;
|
||||
if change < 0 {
|
||||
self.scroll_direction = ScrollDirection::Up;
|
||||
} else {
|
||||
self.scroll_direction = ScrollDirection::Down;
|
||||
}
|
||||
|
||||
return Some(self.current_scroll_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum KillSignal {
|
||||
Cancel,
|
||||
|
@ -342,555 +131,20 @@ impl AppSearchState {
|
|||
}
|
||||
}
|
||||
|
||||
/// ProcessSearchState only deals with process' search's current settings and state.
|
||||
pub struct ProcessSearchState {
|
||||
pub search_state: AppSearchState,
|
||||
pub is_ignoring_case: bool,
|
||||
pub is_searching_whole_word: bool,
|
||||
pub is_searching_with_regex: bool,
|
||||
}
|
||||
|
||||
impl Default for ProcessSearchState {
|
||||
fn default() -> Self {
|
||||
ProcessSearchState {
|
||||
search_state: AppSearchState::default(),
|
||||
is_ignoring_case: true,
|
||||
is_searching_whole_word: false,
|
||||
is_searching_with_regex: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessSearchState {
|
||||
pub fn search_toggle_ignore_case(&mut self) {
|
||||
self.is_ignoring_case = !self.is_ignoring_case;
|
||||
}
|
||||
|
||||
pub fn search_toggle_whole_word(&mut self) {
|
||||
self.is_searching_whole_word = !self.is_searching_whole_word;
|
||||
}
|
||||
|
||||
pub fn search_toggle_regex(&mut self) {
|
||||
self.is_searching_with_regex = !self.is_searching_with_regex;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ColumnInfo {
|
||||
pub enabled: bool,
|
||||
pub shortcut: Option<&'static str>,
|
||||
// FIXME: Move column width logic here!
|
||||
// pub hard_width: Option<u16>,
|
||||
// pub max_soft_width: Option<f64>,
|
||||
}
|
||||
|
||||
pub struct ProcColumn {
|
||||
pub ordered_columns: Vec<ProcessSorting>,
|
||||
/// The y location of headers. Since they're all aligned, it's just one value.
|
||||
pub column_header_y_loc: Option<u16>,
|
||||
/// The x start and end bounds for each header.
|
||||
pub column_header_x_locs: Option<Vec<(u16, u16)>>,
|
||||
pub column_mapping: HashMap<ProcessSorting, ColumnInfo>,
|
||||
pub longest_header_len: u16,
|
||||
pub column_state: TableState,
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub current_scroll_position: usize,
|
||||
pub previous_scroll_position: usize,
|
||||
pub backup_prev_scroll_position: usize,
|
||||
}
|
||||
|
||||
impl Default for ProcColumn {
|
||||
fn default() -> Self {
|
||||
let ordered_columns = vec![
|
||||
Count,
|
||||
Pid,
|
||||
ProcessName,
|
||||
Command,
|
||||
CpuPercent,
|
||||
Mem,
|
||||
MemPercent,
|
||||
ReadPerSecond,
|
||||
WritePerSecond,
|
||||
TotalRead,
|
||||
TotalWrite,
|
||||
User,
|
||||
State,
|
||||
];
|
||||
|
||||
let mut column_mapping = HashMap::new();
|
||||
let mut longest_header_len = 0;
|
||||
for column in ordered_columns.clone() {
|
||||
longest_header_len = std::cmp::max(longest_header_len, column.to_string().len());
|
||||
match column {
|
||||
CpuPercent => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("c"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
MemPercent => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("m"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Mem => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: Some("m"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
ProcessName => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("n"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Command => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: Some("n"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Pid => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: Some("p"),
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Count => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: false,
|
||||
shortcut: None,
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
User => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: cfg!(target_family = "unix"),
|
||||
shortcut: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
column_mapping.insert(
|
||||
column,
|
||||
ColumnInfo {
|
||||
enabled: true,
|
||||
shortcut: None,
|
||||
// hard_width: None,
|
||||
// max_soft_width: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let longest_header_len = longest_header_len as u16;
|
||||
|
||||
ProcColumn {
|
||||
ordered_columns,
|
||||
column_mapping,
|
||||
longest_header_len,
|
||||
column_state: TableState::default(),
|
||||
scroll_direction: ScrollDirection::default(),
|
||||
current_scroll_position: 0,
|
||||
previous_scroll_position: 0,
|
||||
backup_prev_scroll_position: 0,
|
||||
column_header_y_loc: None,
|
||||
column_header_x_locs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcColumn {
|
||||
/// Returns its new status.
|
||||
pub fn toggle(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = !(mapping.enabled);
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_set(&mut self, column: &ProcessSorting, setting: bool) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = setting;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_enable(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = true;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_disable(&mut self, column: &ProcessSorting) -> Option<bool> {
|
||||
if let Some(mapping) = self.column_mapping.get_mut(column) {
|
||||
mapping.enabled = false;
|
||||
Some(mapping.enabled)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self, column: &ProcessSorting) -> bool {
|
||||
if let Some(mapping) = self.column_mapping.get(column) {
|
||||
mapping.enabled
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_enabled_columns_len(&self) -> usize {
|
||||
self.ordered_columns
|
||||
.iter()
|
||||
.filter_map(|column_type| {
|
||||
if let Some(col_map) = self.column_mapping.get(column_type) {
|
||||
if col_map.enabled {
|
||||
Some(1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// NOTE: ALWAYS call this when opening the sorted window.
|
||||
pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) {
|
||||
// TODO [Custom Columns]: If we add custom columns, this may be needed!
|
||||
// Since column indices will change, this runs the risk of OOB. So, if you change columns, CALL THIS AND ADAPT!
|
||||
let mut true_index = 0;
|
||||
for column in &self.ordered_columns {
|
||||
if *column == *proc_sorting_type {
|
||||
break;
|
||||
}
|
||||
if self.column_mapping.get(column).unwrap().enabled {
|
||||
true_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
self.current_scroll_position = true_index;
|
||||
self.backup_prev_scroll_position = self.previous_scroll_position;
|
||||
}
|
||||
|
||||
/// This function sets the scroll position based on the index.
|
||||
pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) {
|
||||
self.current_scroll_position = visual_index;
|
||||
self.backup_prev_scroll_position = self.previous_scroll_position;
|
||||
}
|
||||
|
||||
pub fn get_column_headers(
|
||||
&self, proc_sorting_type: &ProcessSorting, sort_reverse: bool,
|
||||
) -> Vec<String> {
|
||||
const DOWN_ARROW: char = '▼';
|
||||
const UP_ARROW: char = '▲';
|
||||
|
||||
// TODO: Gonna have to figure out how to do left/right GUI notation if we add it.
|
||||
self.ordered_columns
|
||||
.iter()
|
||||
.filter_map(|column_type| {
|
||||
let mapping = self.column_mapping.get(column_type).unwrap();
|
||||
let mut command_str = String::default();
|
||||
if let Some(command) = mapping.shortcut {
|
||||
command_str = format!("({})", command);
|
||||
}
|
||||
|
||||
if mapping.enabled {
|
||||
Some(format!(
|
||||
"{}{}{}",
|
||||
column_type,
|
||||
command_str,
|
||||
if proc_sorting_type == column_type {
|
||||
if sort_reverse {
|
||||
DOWN_ARROW
|
||||
} else {
|
||||
UP_ARROW
|
||||
}
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcWidgetState {
|
||||
pub process_search_state: ProcessSearchState,
|
||||
pub is_grouped: bool,
|
||||
pub scroll_state: TableComponentState,
|
||||
pub process_sorting_type: processes::ProcessSorting,
|
||||
pub is_process_sort_descending: bool,
|
||||
pub is_using_command: bool,
|
||||
pub current_column_index: usize,
|
||||
pub is_sort_open: bool,
|
||||
pub columns: ProcColumn,
|
||||
pub is_tree_mode: bool,
|
||||
pub table_width_state: CanvasTableWidthState,
|
||||
pub requires_redraw: bool,
|
||||
}
|
||||
|
||||
impl ProcWidgetState {
|
||||
pub fn init(
|
||||
is_case_sensitive: bool, is_match_whole_word: bool, is_use_regex: bool, is_grouped: bool,
|
||||
show_memory_as_values: bool, is_tree_mode: bool, is_using_command: bool,
|
||||
) -> Self {
|
||||
let mut process_search_state = ProcessSearchState::default();
|
||||
|
||||
if is_case_sensitive {
|
||||
// By default it's off
|
||||
process_search_state.search_toggle_ignore_case();
|
||||
}
|
||||
if is_match_whole_word {
|
||||
process_search_state.search_toggle_whole_word();
|
||||
}
|
||||
if is_use_regex {
|
||||
process_search_state.search_toggle_regex();
|
||||
}
|
||||
|
||||
let (process_sorting_type, is_process_sort_descending) = if is_tree_mode {
|
||||
(processes::ProcessSorting::Pid, false)
|
||||
} else {
|
||||
(processes::ProcessSorting::CpuPercent, true)
|
||||
};
|
||||
|
||||
// TODO: If we add customizable columns, this should pull from config
|
||||
let mut columns = ProcColumn::default();
|
||||
columns.set_to_sorted_index_from_type(&process_sorting_type);
|
||||
if is_grouped {
|
||||
// Normally defaults to showing by PID, toggle count on instead.
|
||||
columns.toggle(&ProcessSorting::Count);
|
||||
columns.toggle(&ProcessSorting::Pid);
|
||||
}
|
||||
if show_memory_as_values {
|
||||
// Normally defaults to showing by percent, toggle value on instead.
|
||||
columns.toggle(&ProcessSorting::Mem);
|
||||
columns.toggle(&ProcessSorting::MemPercent);
|
||||
}
|
||||
if is_using_command {
|
||||
columns.toggle(&ProcessSorting::ProcessName);
|
||||
columns.toggle(&ProcessSorting::Command);
|
||||
}
|
||||
|
||||
ProcWidgetState {
|
||||
process_search_state,
|
||||
is_grouped,
|
||||
scroll_state: TableComponentState::default(),
|
||||
process_sorting_type,
|
||||
is_process_sort_descending,
|
||||
is_using_command,
|
||||
current_column_index: 0,
|
||||
is_sort_open: false,
|
||||
columns,
|
||||
is_tree_mode,
|
||||
table_width_state: CanvasTableWidthState::default(),
|
||||
requires_redraw: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates sorting when using the column list.
|
||||
/// ...this really should be part of the ProcColumn struct (along with the sorting fields),
|
||||
/// but I'm too lazy.
|
||||
///
|
||||
/// Sorry, future me, you're gonna have to refactor this later. Too busy getting
|
||||
/// the feature to work in the first place! :)
|
||||
pub fn update_sorting_with_columns(&mut self) {
|
||||
let mut true_index = 0;
|
||||
let mut enabled_index = 0;
|
||||
let target_itx = self.columns.current_scroll_position;
|
||||
for column in &self.columns.ordered_columns {
|
||||
let enabled = self.columns.column_mapping.get(column).unwrap().enabled;
|
||||
if enabled_index == target_itx && enabled {
|
||||
break;
|
||||
}
|
||||
if enabled {
|
||||
enabled_index += 1;
|
||||
}
|
||||
true_index += 1;
|
||||
}
|
||||
|
||||
if let Some(new_sort_type) = self.columns.ordered_columns.get(true_index) {
|
||||
if *new_sort_type == self.process_sorting_type {
|
||||
// Just reverse the search if we're reselecting!
|
||||
self.is_process_sort_descending = !(self.is_process_sort_descending);
|
||||
} else {
|
||||
self.process_sorting_type = new_sort_type.clone();
|
||||
match self.process_sorting_type {
|
||||
ProcessSorting::State
|
||||
| ProcessSorting::Pid
|
||||
| ProcessSorting::ProcessName
|
||||
| ProcessSorting::Command => {
|
||||
// Also invert anything that uses alphabetical sorting by default.
|
||||
self.is_process_sort_descending = false;
|
||||
}
|
||||
_ => {
|
||||
self.is_process_sort_descending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_command_and_name(&mut self, is_using_command: bool) {
|
||||
if let Some(pn) = self
|
||||
.columns
|
||||
.column_mapping
|
||||
.get_mut(&ProcessSorting::ProcessName)
|
||||
{
|
||||
pn.enabled = !is_using_command;
|
||||
}
|
||||
if let Some(c) = self
|
||||
.columns
|
||||
.column_mapping
|
||||
.get_mut(&ProcessSorting::Command)
|
||||
{
|
||||
c.enabled = is_using_command;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_search_cursor_position(&self) -> usize {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.cur_cursor()
|
||||
}
|
||||
|
||||
pub fn get_char_cursor_position(&self) -> usize {
|
||||
self.process_search_state.search_state.char_cursor_position
|
||||
}
|
||||
|
||||
pub fn is_search_enabled(&self) -> bool {
|
||||
self.process_search_state.search_state.is_enabled
|
||||
}
|
||||
|
||||
pub fn get_current_search_query(&self) -> &String {
|
||||
&self.process_search_state.search_state.current_search_query
|
||||
}
|
||||
|
||||
pub fn update_query(&mut self) {
|
||||
if self
|
||||
.process_search_state
|
||||
.search_state
|
||||
.current_search_query
|
||||
.is_empty()
|
||||
{
|
||||
self.process_search_state.search_state.is_blank_search = true;
|
||||
self.process_search_state.search_state.is_invalid_search = false;
|
||||
self.process_search_state.search_state.error_message = None;
|
||||
} else {
|
||||
let parsed_query = self.parse_query();
|
||||
// debug!("Parsed query: {:#?}", parsed_query);
|
||||
|
||||
if let Ok(parsed_query) = parsed_query {
|
||||
self.process_search_state.search_state.query = Some(parsed_query);
|
||||
self.process_search_state.search_state.is_blank_search = false;
|
||||
self.process_search_state.search_state.is_invalid_search = false;
|
||||
self.process_search_state.search_state.error_message = None;
|
||||
} else if let Err(err) = parsed_query {
|
||||
self.process_search_state.search_state.is_blank_search = false;
|
||||
self.process_search_state.search_state.is_invalid_search = true;
|
||||
self.process_search_state.search_state.error_message = Some(err.to_string());
|
||||
}
|
||||
}
|
||||
self.scroll_state.scroll_bar = 0;
|
||||
self.scroll_state.current_scroll_position = 0;
|
||||
}
|
||||
|
||||
pub fn clear_search(&mut self) {
|
||||
self.process_search_state.search_state.reset();
|
||||
}
|
||||
|
||||
pub fn search_walk_forward(&mut self, start_position: usize) {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.next_boundary(
|
||||
&self.process_search_state.search_state.current_search_query[start_position..],
|
||||
start_position,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn search_walk_back(&mut self, start_position: usize) {
|
||||
self.process_search_state
|
||||
.search_state
|
||||
.grapheme_cursor
|
||||
.prev_boundary(
|
||||
&self.process_search_state.search_state.current_search_query[..start_position],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProcState {
|
||||
pub widget_states: HashMap<u64, ProcWidgetState>,
|
||||
pub force_update: Option<u64>,
|
||||
pub force_update_all: bool,
|
||||
pub widget_states: HashMap<u64, ProcWidget>,
|
||||
}
|
||||
|
||||
impl ProcState {
|
||||
pub fn init(widget_states: HashMap<u64, ProcWidgetState>) -> Self {
|
||||
ProcState {
|
||||
widget_states,
|
||||
force_update: None,
|
||||
force_update_all: false,
|
||||
}
|
||||
pub fn init(widget_states: HashMap<u64, ProcWidget>) -> Self {
|
||||
ProcState { widget_states }
|
||||
}
|
||||
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidgetState> {
|
||||
pub fn get_mut_widget_state(&mut self, widget_id: u64) -> Option<&mut ProcWidget> {
|
||||
self.widget_states.get_mut(&widget_id)
|
||||
}
|
||||
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidgetState> {
|
||||
pub fn get_widget_state(&self, widget_id: u64) -> Option<&ProcWidget> {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
@ -941,8 +195,7 @@ pub struct CpuWidgetState {
|
|||
|
||||
impl CpuWidgetState {
|
||||
pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self {
|
||||
const CPU_LEGEND_HEADER: [(Cow<'static, str>, Option<Cow<'static, str>>); 2] =
|
||||
[(Cow::Borrowed("CPU"), None), (Cow::Borrowed("Use%"), None)];
|
||||
const CPU_LEGEND_HEADER: [&str; 2] = ["CPU", "Use%"];
|
||||
const WIDTHS: [WidthBounds; CPU_LEGEND_HEADER.len()] = [
|
||||
WidthBounds::soft_from_str("CPU", Some(0.5)),
|
||||
WidthBounds::soft_from_str("Use%", Some(0.5)),
|
||||
|
@ -952,7 +205,7 @@ impl CpuWidgetState {
|
|||
CPU_LEGEND_HEADER
|
||||
.iter()
|
||||
.zip(WIDTHS)
|
||||
.map(|(c, width)| TableComponentColumn::new(c.0.clone(), c.1.clone(), width))
|
||||
.map(|(c, width)| TableComponentColumn::new(CellContent::new(*c, None), width))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
|
@ -1040,7 +293,9 @@ impl Default for TempWidgetState {
|
|||
TEMP_HEADERS
|
||||
.iter()
|
||||
.zip(WIDTHS)
|
||||
.map(|(header, width)| TableComponentColumn::new(*header, None, width))
|
||||
.map(|(header, width)| {
|
||||
TableComponentColumn::new(CellContent::new(*header, None), width)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
|
@ -1087,7 +342,9 @@ impl Default for DiskWidgetState {
|
|||
DISK_HEADERS
|
||||
.iter()
|
||||
.zip(WIDTHS)
|
||||
.map(|(header, width)| TableComponentColumn::new(*header, None, width))
|
||||
.map(|(header, width)| {
|
||||
TableComponentColumn::new(CellContent::new(*header, None), width)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
|
@ -1169,61 +426,3 @@ pub struct ConfigCategory {
|
|||
pub struct ConfigOption {
|
||||
pub set_function: Box<dyn Fn() -> anyhow::Result<()>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_scroll_update_position() {
|
||||
fn check_scroll_update(
|
||||
scroll: &mut TableComponentState, change: i64, max: usize, ret: Option<usize>,
|
||||
new_position: usize,
|
||||
) {
|
||||
assert_eq!(scroll.update_position(change, max), ret);
|
||||
assert_eq!(scroll.current_scroll_position, new_position);
|
||||
}
|
||||
|
||||
let mut scroll = TableComponentState {
|
||||
current_scroll_position: 5,
|
||||
scroll_bar: 0,
|
||||
scroll_direction: ScrollDirection::Down,
|
||||
table_state: Default::default(),
|
||||
columns: vec![],
|
||||
};
|
||||
let s = &mut scroll;
|
||||
|
||||
// Update by 0. Should not change.
|
||||
check_scroll_update(s, 0, 15, None, 5);
|
||||
|
||||
// Update by 5. Should increment to index 10.
|
||||
check_scroll_update(s, 5, 15, Some(10), 10);
|
||||
|
||||
// Update by 5. Should not change.
|
||||
check_scroll_update(s, 5, 15, None, 10);
|
||||
|
||||
// Update by 4. Should increment to index 14 (supposed max).
|
||||
check_scroll_update(s, 4, 15, Some(14), 14);
|
||||
|
||||
// Update by 1. Should do nothing.
|
||||
check_scroll_update(s, 1, 15, None, 14);
|
||||
|
||||
// Update by -15. Should do nothing.
|
||||
check_scroll_update(s, -15, 15, None, 14);
|
||||
|
||||
// Update by -14. Should land on position 0.
|
||||
check_scroll_update(s, -14, 15, Some(0), 0);
|
||||
|
||||
// Update by -1. Should do nothing.
|
||||
check_scroll_update(s, -15, 15, None, 0);
|
||||
|
||||
// Update by 0. Should do nothing.
|
||||
check_scroll_update(s, 0, 15, None, 0);
|
||||
|
||||
// Update by 15. Should do nothing.
|
||||
check_scroll_update(s, 15, 15, None, 0);
|
||||
|
||||
// Update by 15 but with a larger bound. Should increment to 15.
|
||||
check_scroll_update(s, 15, 16, Some(15), 15);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod process;
|
||||
pub use process::*;
|
|
@ -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...
|
||||
for proc_widget in app_state.proc_state.widget_states.values_mut() {
|
||||
proc_widget.columns.column_header_y_loc = None;
|
||||
proc_widget.columns.column_header_x_locs = None;
|
||||
// FIXME: [Proc] Handle this?
|
||||
// proc_widget.columns.column_header_y_loc = None;
|
||||
// proc_widget.columns.column_header_x_locs = None;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -506,7 +507,7 @@ impl Painter {
|
|||
_ => 0,
|
||||
};
|
||||
|
||||
self.draw_process_features(f, app_state, rect[0], true, widget_id);
|
||||
self.draw_process_widget(f, app_state, rect[0], true, widget_id);
|
||||
}
|
||||
Battery => self.draw_battery_display(
|
||||
f,
|
||||
|
@ -585,7 +586,7 @@ impl Painter {
|
|||
ProcSort => 2,
|
||||
_ => 0,
|
||||
};
|
||||
self.draw_process_features(
|
||||
self.draw_process_widget(
|
||||
f,
|
||||
app_state,
|
||||
vertical_chunks[3],
|
||||
|
@ -736,7 +737,7 @@ impl Painter {
|
|||
Disk => {
|
||||
self.draw_disk_table(f, app_state, *widget_draw_loc, true, widget.widget_id)
|
||||
}
|
||||
Proc => self.draw_process_features(
|
||||
Proc => self.draw_process_widget(
|
||||
f,
|
||||
app_state,
|
||||
*widget_draw_loc,
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use std::{borrow::Cow, cmp::min};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{max, min},
|
||||
};
|
||||
|
||||
use concat_string::concat_string;
|
||||
use tui::{
|
||||
|
@ -12,9 +15,12 @@ use tui::{
|
|||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{
|
||||
app::{self, TableComponentState},
|
||||
app::{
|
||||
self, CellContent, SortState, TableComponentColumn, TableComponentHeader,
|
||||
TableComponentState,
|
||||
},
|
||||
constants::{SIDE_BORDERS, TABLE_GAP_HEIGHT_LIMIT},
|
||||
data_conversion::{CellContent, TableData, TableRow},
|
||||
data_conversion::{TableData, TableRow},
|
||||
};
|
||||
|
||||
pub struct TextTableTitle<'a> {
|
||||
|
@ -101,8 +107,8 @@ impl<'a> TextTable<'a> {
|
|||
}
|
||||
})
|
||||
}
|
||||
pub fn draw_text_table<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState,
|
||||
pub fn draw_text_table<B: Backend, H: TableComponentHeader>(
|
||||
&self, f: &mut Frame<'_, B>, draw_loc: Rect, state: &mut TableComponentState<H>,
|
||||
table_data: &TableData,
|
||||
) {
|
||||
// TODO: This is a *really* ugly hack to get basic mode to hide the border when not selected, without shifting everything.
|
||||
|
@ -179,7 +185,7 @@ impl<'a> TextTable<'a> {
|
|||
desired,
|
||||
max_percentage: _,
|
||||
} => {
|
||||
*desired = std::cmp::max(column.name.len(), *data_width) as u16;
|
||||
*desired = max(column.header.header_text().len(), *data_width) as u16;
|
||||
}
|
||||
app::WidthBounds::Hard(_width) => {}
|
||||
});
|
||||
|
@ -188,15 +194,9 @@ impl<'a> TextTable<'a> {
|
|||
}
|
||||
|
||||
let columns = &state.columns;
|
||||
let header = Row::new(columns.iter().filter_map(|c| {
|
||||
if c.calculated_width == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(truncate_text(&c.name, c.calculated_width.into(), None))
|
||||
}
|
||||
}))
|
||||
.style(self.header_style)
|
||||
.bottom_margin(table_gap);
|
||||
let header = build_header(columns, &state.sort_state)
|
||||
.style(self.header_style)
|
||||
.bottom_margin(table_gap);
|
||||
let table_rows = sliced_vec.iter().map(|row| {
|
||||
let (row, style) = match row {
|
||||
TableRow::Raw(row) => (row, None),
|
||||
|
@ -245,9 +245,60 @@ impl<'a> TextTable<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Constructs the table header.
|
||||
fn build_header<'a, H: TableComponentHeader>(
|
||||
columns: &'a [TableComponentColumn<H>], sort_state: &SortState,
|
||||
) -> Row<'a> {
|
||||
use itertools::Either;
|
||||
|
||||
const UP_ARROW: &str = "▲";
|
||||
const DOWN_ARROW: &str = "▼";
|
||||
|
||||
let iter = match sort_state {
|
||||
SortState::Unsortable => Either::Left(columns.iter().filter_map(|c| {
|
||||
if c.calculated_width == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(truncate_text(
|
||||
c.header.header_text(),
|
||||
c.calculated_width.into(),
|
||||
None,
|
||||
))
|
||||
}
|
||||
})),
|
||||
SortState::Sortable { index, order } => {
|
||||
let arrow = match order {
|
||||
app::SortOrder::Ascending => UP_ARROW,
|
||||
app::SortOrder::Descending => DOWN_ARROW,
|
||||
};
|
||||
|
||||
Either::Right(columns.iter().enumerate().filter_map(move |(itx, c)| {
|
||||
if c.calculated_width == 0 {
|
||||
None
|
||||
} else if itx == *index {
|
||||
Some(truncate_suffixed_text(
|
||||
c.header.header_text(),
|
||||
arrow,
|
||||
c.calculated_width.into(),
|
||||
None,
|
||||
))
|
||||
} else {
|
||||
Some(truncate_text(
|
||||
c.header.header_text(),
|
||||
c.calculated_width.into(),
|
||||
None,
|
||||
))
|
||||
}
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
Row::new(iter)
|
||||
}
|
||||
|
||||
/// Truncates text if it is too long, and adds an ellipsis at the end if needed.
|
||||
fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>) -> Text<'_> {
|
||||
let (text, opt) = match content {
|
||||
fn truncate_text<'a>(content: &'a CellContent, width: usize, row_style: Option<Style>) -> Text<'a> {
|
||||
let (main_text, alt_text) = match content {
|
||||
CellContent::Simple(s) => (s, None),
|
||||
CellContent::HasAlt {
|
||||
alt: short,
|
||||
|
@ -255,18 +306,57 @@ fn truncate_text(content: &CellContent, width: usize, row_style: Option<Style>)
|
|||
} => (long, Some(short)),
|
||||
};
|
||||
|
||||
let graphemes = UnicodeSegmentation::graphemes(text.as_ref(), true).collect::<Vec<&str>>();
|
||||
let mut text = if graphemes.len() > width && width > 0 {
|
||||
if let Some(s) = opt {
|
||||
// If an alternative exists, use that.
|
||||
Text::raw(s.as_ref())
|
||||
let mut text = {
|
||||
let graphemes: Vec<&str> =
|
||||
UnicodeSegmentation::graphemes(main_text.as_ref(), true).collect();
|
||||
if graphemes.len() > width && width > 0 {
|
||||
if let Some(s) = alt_text {
|
||||
// If an alternative exists, use that.
|
||||
Text::raw(s.as_ref())
|
||||
} else {
|
||||
// Truncate with ellipsis
|
||||
let first_n = graphemes[..(width - 1)].concat();
|
||||
Text::raw(concat_string!(first_n, "…"))
|
||||
}
|
||||
} else {
|
||||
// Truncate with ellipsis
|
||||
let first_n = graphemes[..(width - 1)].concat();
|
||||
Text::raw(concat_string!(first_n, "…"))
|
||||
Text::raw(main_text.as_ref())
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(row_style) = row_style {
|
||||
text.patch_style(row_style);
|
||||
}
|
||||
|
||||
text
|
||||
}
|
||||
|
||||
fn truncate_suffixed_text<'a>(
|
||||
content: &'a CellContent, suffix: &str, width: usize, row_style: Option<Style>,
|
||||
) -> Text<'a> {
|
||||
let (main_text, alt_text) = match content {
|
||||
CellContent::Simple(s) => (s, None),
|
||||
CellContent::HasAlt {
|
||||
alt: short,
|
||||
main: long,
|
||||
} => (long, Some(short)),
|
||||
};
|
||||
|
||||
let mut text = {
|
||||
let suffixed = concat_string!(main_text, suffix);
|
||||
let graphemes: Vec<&str> =
|
||||
UnicodeSegmentation::graphemes(suffixed.as_str(), true).collect();
|
||||
if graphemes.len() > width && width > 1 {
|
||||
if let Some(alt) = alt_text {
|
||||
// If an alternative exists, use that + arrow.
|
||||
Text::raw(concat_string!(alt, suffix))
|
||||
} else {
|
||||
// Truncate with ellipsis + arrow.
|
||||
let first_n = graphemes[..(width - 2)].concat();
|
||||
Text::raw(concat_string!(first_n, "…", suffix))
|
||||
}
|
||||
} else {
|
||||
Text::raw(suffixed)
|
||||
}
|
||||
} else {
|
||||
Text::raw(text.as_ref())
|
||||
};
|
||||
|
||||
if let Some(row_style) = row_style {
|
||||
|
@ -315,4 +405,64 @@ pub fn get_start_position(
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {}
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_start_position() {
|
||||
use crate::app::ScrollDirection::{self, Down, Up};
|
||||
|
||||
#[track_caller]
|
||||
|
||||
fn test_get(
|
||||
bar: usize, num: usize, direction: ScrollDirection, selected: usize, force: bool,
|
||||
expected_posn: usize, expected_bar: usize,
|
||||
) {
|
||||
let mut bar = bar;
|
||||
assert_eq!(
|
||||
get_start_position(num, &direction, &mut bar, selected, force),
|
||||
expected_posn
|
||||
);
|
||||
assert_eq!(bar, expected_bar);
|
||||
}
|
||||
|
||||
// Scrolling down from start
|
||||
test_get(0, 10, Down, 0, false, 0, 0);
|
||||
|
||||
// Simple scrolling down
|
||||
test_get(0, 10, Down, 1, false, 0, 0);
|
||||
|
||||
// Scrolling down from the middle high up
|
||||
test_get(0, 10, Down, 5, false, 0, 0);
|
||||
|
||||
// Scrolling down into boundary
|
||||
test_get(0, 10, Down, 11, false, 1, 1);
|
||||
|
||||
// Scrolling down from the with non-zero bar
|
||||
test_get(5, 10, Down, 15, false, 5, 5);
|
||||
|
||||
// Force redraw scrolling down (e.g. resize)
|
||||
test_get(5, 15, Down, 15, true, 0, 0);
|
||||
|
||||
// Test jumping down
|
||||
test_get(1, 10, Down, 20, true, 10, 10);
|
||||
|
||||
// Scrolling up from bottom
|
||||
test_get(10, 10, Up, 20, false, 10, 10);
|
||||
|
||||
// Simple scrolling up
|
||||
test_get(10, 10, Up, 19, false, 10, 10);
|
||||
|
||||
// Scrolling up from the middle
|
||||
test_get(10, 10, Up, 10, false, 10, 10);
|
||||
|
||||
// Scrolling up into boundary
|
||||
test_get(10, 10, Up, 9, false, 9, 9);
|
||||
|
||||
// Force redraw scrolling up (e.g. resize)
|
||||
test_get(5, 10, Up, 15, true, 5, 5);
|
||||
|
||||
// Test jumping up
|
||||
test_get(10, 10, Up, 0, false, 0, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use tui::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
app::{App, KillSignal},
|
||||
app::{App, KillSignal, widgets::ProcWidgetMode},
|
||||
canvas::Painter,
|
||||
};
|
||||
|
||||
|
@ -29,7 +29,13 @@ impl Painter {
|
|||
if let Some(first_pid) = to_kill_processes.1.first() {
|
||||
return Some(Text::from(vec![
|
||||
Spans::from(""),
|
||||
if app_state.is_grouped(app_state.current_widget.widget_id) {
|
||||
if app_state
|
||||
.proc_state
|
||||
.widget_states
|
||||
.get(&app_state.current_widget.widget_id)
|
||||
.map(|p| matches!(p.mode, ProcWidgetMode::Grouped))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if to_kill_processes.1.len() != 1 {
|
||||
Spans::from(format!(
|
||||
"Kill {} processes with the name \"{}\"? Press ENTER to confirm.",
|
||||
|
|
|
@ -6,117 +6,6 @@ use std::{
|
|||
time::Instant,
|
||||
};
|
||||
|
||||
/// Return a (hard)-width vector for column widths.
|
||||
///
|
||||
/// * `total_width` is the, well, total width available. **NOTE:** This function automatically
|
||||
/// takes away 2 from the width as part of the left/right
|
||||
/// bounds.
|
||||
/// * `hard_widths` is inflexible column widths. Use a `None` to represent a soft width.
|
||||
/// * `soft_widths_min` is the lower limit for a soft width. Use `None` if a hard width goes there.
|
||||
/// * `soft_widths_max` is the upper limit for a soft width, in percentage of the total width. Use
|
||||
/// `None` if a hard width goes there.
|
||||
/// * `soft_widths_desired` is the desired soft width. Use `None` if a hard width goes there.
|
||||
/// * `left_to_right` is a boolean whether to go from left to right if true, or right to left if
|
||||
/// false.
|
||||
///
|
||||
/// **NOTE:** This function ASSUMES THAT ALL PASSED SLICES ARE OF THE SAME SIZE.
|
||||
///
|
||||
/// **NOTE:** The returned vector may not be the same size as the slices, this is because including
|
||||
/// 0-constraints breaks tui-rs.
|
||||
pub fn get_column_widths(
|
||||
total_width: u16, hard_widths: &[Option<u16>], soft_widths_min: &[Option<u16>],
|
||||
soft_widths_max: &[Option<f64>], soft_widths_desired: &[Option<u16>], left_to_right: bool,
|
||||
) -> Vec<u16> {
|
||||
debug_assert!(
|
||||
hard_widths.len() == soft_widths_min.len(),
|
||||
"hard width length != soft width min length!"
|
||||
);
|
||||
debug_assert!(
|
||||
soft_widths_min.len() == soft_widths_max.len(),
|
||||
"soft width min length != soft width max length!"
|
||||
);
|
||||
debug_assert!(
|
||||
soft_widths_max.len() == soft_widths_desired.len(),
|
||||
"soft width max length != soft width desired length!"
|
||||
);
|
||||
|
||||
if total_width > 2 {
|
||||
let initial_width = total_width - 2;
|
||||
let mut total_width_left = initial_width;
|
||||
let mut column_widths: Vec<u16> = vec![0; hard_widths.len()];
|
||||
let range: Vec<usize> = if left_to_right {
|
||||
(0..hard_widths.len()).collect()
|
||||
} else {
|
||||
(0..hard_widths.len()).rev().collect()
|
||||
};
|
||||
|
||||
for itx in &range {
|
||||
if let Some(Some(hard_width)) = hard_widths.get(*itx) {
|
||||
// Hard width...
|
||||
let space_taken = min(*hard_width, total_width_left);
|
||||
|
||||
// TODO [COLUMN MOVEMENT]: Remove this
|
||||
if *hard_width > space_taken {
|
||||
break;
|
||||
}
|
||||
|
||||
column_widths[*itx] = space_taken;
|
||||
total_width_left -= space_taken;
|
||||
total_width_left = total_width_left.saturating_sub(1);
|
||||
} else if let (
|
||||
Some(Some(soft_width_max)),
|
||||
Some(Some(soft_width_min)),
|
||||
Some(Some(soft_width_desired)),
|
||||
) = (
|
||||
soft_widths_max.get(*itx),
|
||||
soft_widths_min.get(*itx),
|
||||
soft_widths_desired.get(*itx),
|
||||
) {
|
||||
// Soft width...
|
||||
let soft_limit = max(
|
||||
if soft_width_max.is_sign_negative() {
|
||||
*soft_width_desired
|
||||
} else {
|
||||
(*soft_width_max * initial_width as f64).ceil() as u16
|
||||
},
|
||||
*soft_width_min,
|
||||
);
|
||||
let space_taken = min(min(soft_limit, *soft_width_desired), total_width_left);
|
||||
|
||||
// TODO [COLUMN MOVEMENT]: Remove this
|
||||
if *soft_width_min > space_taken {
|
||||
break;
|
||||
}
|
||||
|
||||
column_widths[*itx] = space_taken;
|
||||
total_width_left -= space_taken;
|
||||
total_width_left = total_width_left.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(0) = column_widths.last() {
|
||||
column_widths.pop();
|
||||
}
|
||||
|
||||
if !column_widths.is_empty() {
|
||||
// Redistribute remaining.
|
||||
let amount_per_slot = total_width_left / column_widths.len() as u16;
|
||||
total_width_left %= column_widths.len() as u16;
|
||||
for (index, width) in column_widths.iter_mut().enumerate() {
|
||||
if index < total_width_left.into() {
|
||||
*width += amount_per_slot + 1;
|
||||
} else {
|
||||
*width += amount_per_slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
column_widths
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_search_start_position(
|
||||
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
|
||||
current_cursor_position: usize, is_force_redraw: bool,
|
||||
|
@ -154,45 +43,6 @@ pub fn get_search_start_position(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_start_position(
|
||||
num_rows: usize, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut usize,
|
||||
currently_selected_position: usize, is_force_redraw: bool,
|
||||
) -> usize {
|
||||
if is_force_redraw {
|
||||
*scroll_position_bar = 0;
|
||||
}
|
||||
|
||||
// FIXME: Note that num_rows is WRONG here! It assumes the number of rows - 1... oops.
|
||||
|
||||
match scroll_direction {
|
||||
app::ScrollDirection::Down => {
|
||||
if currently_selected_position < *scroll_position_bar + num_rows {
|
||||
// If, using previous_scrolled_position, we can see the element
|
||||
// (so within that and + num_rows) just reuse the current previously scrolled position
|
||||
*scroll_position_bar
|
||||
} else if currently_selected_position >= num_rows {
|
||||
// Else if the current position past the last element visible in the list, omit
|
||||
// until we can see that element
|
||||
*scroll_position_bar = currently_selected_position - num_rows;
|
||||
*scroll_position_bar
|
||||
} else {
|
||||
// Else, if it is not past the last element visible, do not omit anything
|
||||
0
|
||||
}
|
||||
}
|
||||
app::ScrollDirection::Up => {
|
||||
if currently_selected_position <= *scroll_position_bar {
|
||||
// If it's past the first element, then show from that element downwards
|
||||
*scroll_position_bar = currently_selected_position;
|
||||
} else if currently_selected_position >= *scroll_position_bar + num_rows {
|
||||
*scroll_position_bar = currently_selected_position - num_rows;
|
||||
}
|
||||
// Else, don't change what our start position is from whatever it is set to!
|
||||
*scroll_position_bar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate how many bars are to be drawn within basic mode's components.
|
||||
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
|
||||
std::cmp::min(
|
||||
|
@ -226,62 +76,6 @@ mod test {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_start_position() {
|
||||
use crate::app::ScrollDirection::{self, Down, Up};
|
||||
|
||||
fn test(
|
||||
bar: usize, num: usize, direction: ScrollDirection, selected: usize, force: bool,
|
||||
expected_posn: usize, expected_bar: usize,
|
||||
) {
|
||||
let mut bar = bar;
|
||||
assert_eq!(
|
||||
get_start_position(num, &direction, &mut bar, selected, force),
|
||||
expected_posn
|
||||
);
|
||||
assert_eq!(bar, expected_bar);
|
||||
}
|
||||
|
||||
// Scrolling down from start
|
||||
test(0, 10, Down, 0, false, 0, 0);
|
||||
|
||||
// Simple scrolling down
|
||||
test(0, 10, Down, 1, false, 0, 0);
|
||||
|
||||
// Scrolling down from the middle high up
|
||||
test(0, 10, Down, 5, false, 0, 0);
|
||||
|
||||
// Scrolling down into boundary
|
||||
test(0, 10, Down, 11, false, 1, 1);
|
||||
|
||||
// Scrolling down from the with non-zero bar
|
||||
test(5, 10, Down, 15, false, 5, 5);
|
||||
|
||||
// Force redraw scrolling down (e.g. resize)
|
||||
test(5, 15, Down, 15, true, 0, 0);
|
||||
|
||||
// Test jumping down
|
||||
test(1, 10, Down, 20, true, 10, 10);
|
||||
|
||||
// Scrolling up from bottom
|
||||
test(10, 10, Up, 20, false, 10, 10);
|
||||
|
||||
// Simple scrolling up
|
||||
test(10, 10, Up, 19, false, 10, 10);
|
||||
|
||||
// Scrolling up from the middle
|
||||
test(10, 10, Up, 10, false, 10, 10);
|
||||
|
||||
// Scrolling up into boundary
|
||||
test(10, 10, Up, 9, false, 9, 9);
|
||||
|
||||
// Force redraw scrolling up (e.g. resize)
|
||||
test(5, 10, Up, 15, true, 5, 5);
|
||||
|
||||
// Test jumping up
|
||||
test(10, 10, Up, 0, false, 0, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_basic_use_bars() {
|
||||
// Testing various breakpoints and edge cases.
|
||||
|
@ -329,54 +123,4 @@ mod test {
|
|||
));
|
||||
assert!(over_timer.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_width_calculation() {
|
||||
// TODO: Implement width calculation test; can reuse old ones as basis
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero_width() {
|
||||
assert_eq!(
|
||||
get_column_widths(
|
||||
0,
|
||||
&[Some(1), None, None],
|
||||
&[None, Some(1), Some(2)],
|
||||
&[None, Some(0.125), Some(0.5)],
|
||||
&[None, Some(10), Some(10)],
|
||||
true
|
||||
),
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_width() {
|
||||
assert_eq!(
|
||||
get_column_widths(
|
||||
2,
|
||||
&[Some(1), None, None],
|
||||
&[None, Some(1), Some(2)],
|
||||
&[None, Some(0.125), Some(0.5)],
|
||||
&[None, Some(10), Some(10)],
|
||||
true
|
||||
),
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_zero_width() {
|
||||
assert_eq!(
|
||||
get_column_widths(
|
||||
16,
|
||||
&[Some(1), None, None],
|
||||
&[None, Some(1), Some(2)],
|
||||
&[None, Some(0.125), Some(0.5)],
|
||||
&[None, Some(10), Some(10)],
|
||||
true
|
||||
),
|
||||
vec![2, 2, 7],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use std::{borrow::Cow, iter};
|
||||
|
||||
use crate::{
|
||||
app::{layout_manager::WidgetDirection, App, CpuWidgetState},
|
||||
app::{layout_manager::WidgetDirection, App, CellContent, CpuWidgetState},
|
||||
canvas::{
|
||||
components::{GraphData, TextTable, TimeGraph},
|
||||
drawing_utils::should_hide_x_label,
|
||||
Painter,
|
||||
},
|
||||
data_conversion::{CellContent, ConvertedCpuData, TableData, TableRow},
|
||||
data_conversion::{ConvertedCpuData, TableData, TableRow},
|
||||
};
|
||||
|
||||
use concat_string::concat_string;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::{
|
||||
app::App,
|
||||
canvas::{
|
||||
drawing_utils::{get_column_widths, get_search_start_position, get_start_position},
|
||||
components::{TextTable, TextTableTitle},
|
||||
drawing_utils::get_search_start_position,
|
||||
Painter,
|
||||
},
|
||||
constants::*,
|
||||
|
@ -11,8 +12,8 @@ use tui::{
|
|||
backend::Backend,
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
terminal::Frame,
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Paragraph, Row, Table},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
|
||||
use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
|
||||
|
@ -90,14 +91,14 @@ const PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: &[Option<f64>] = &[
|
|||
impl Painter {
|
||||
/// Draws and handles all process-related drawing. Use this.
|
||||
/// - `widget_id` here represents the widget ID of the process widget itself!
|
||||
pub fn draw_process_features<B: Backend>(
|
||||
pub fn draw_process_widget<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
) {
|
||||
if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) {
|
||||
let search_height = if draw_border { 5 } else { 3 };
|
||||
let is_sort_open = process_widget_state.is_sort_open;
|
||||
let header_len = process_widget_state.columns.longest_header_len;
|
||||
const SORT_MENU_WIDTH: u16 = 8;
|
||||
|
||||
let mut proc_draw_loc = draw_loc;
|
||||
if process_widget_state.is_search_enabled() {
|
||||
|
@ -119,17 +120,11 @@ impl Painter {
|
|||
if is_sort_open {
|
||||
let processes_chunk = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(header_len + 4), Constraint::Min(0)])
|
||||
.constraints([Constraint::Length(SORT_MENU_WIDTH + 4), Constraint::Min(0)])
|
||||
.split(proc_draw_loc);
|
||||
proc_draw_loc = processes_chunk[1];
|
||||
|
||||
self.draw_process_sort(
|
||||
f,
|
||||
app_state,
|
||||
processes_chunk[0],
|
||||
draw_border,
|
||||
widget_id + 2,
|
||||
);
|
||||
self.draw_sort_table(f, app_state, processes_chunk[0], draw_border, widget_id + 2);
|
||||
}
|
||||
|
||||
self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id);
|
||||
|
@ -148,18 +143,15 @@ impl Painter {
|
|||
if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) {
|
||||
let recalculate_column_widths =
|
||||
should_get_widget_bounds || proc_widget_state.requires_redraw;
|
||||
|
||||
// Reset redraw marker.
|
||||
// TODO: this should ideally be handled generically in the future.
|
||||
if proc_widget_state.requires_redraw {
|
||||
proc_widget_state.requires_redraw = false;
|
||||
}
|
||||
|
||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||
let margined_draw_loc = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
||||
.direction(Direction::Horizontal)
|
||||
.split(draw_loc)[0];
|
||||
|
||||
let (border_style, highlight_style) = if is_on_widget {
|
||||
let (border_style, highlighted_text_style) = if is_on_widget {
|
||||
(
|
||||
self.colours.highlighted_border_style,
|
||||
self.colours.currently_selected_text_style,
|
||||
|
@ -168,355 +160,69 @@ impl Painter {
|
|||
(self.colours.border_style, self.colours.text_style)
|
||||
};
|
||||
|
||||
let title_base = if app_state.app_config_fields.show_table_scroll_position {
|
||||
if let Some(finalized_process_data) = app_state
|
||||
.canvas_data
|
||||
.finalized_process_data_map
|
||||
.get(&widget_id)
|
||||
{
|
||||
let title = format!(
|
||||
" Processes ({} of {}) ",
|
||||
proc_widget_state
|
||||
.scroll_state
|
||||
.current_scroll_position
|
||||
.saturating_add(1),
|
||||
finalized_process_data.len()
|
||||
);
|
||||
|
||||
if title.len() <= draw_loc.width.into() {
|
||||
title
|
||||
} else {
|
||||
" Processes ".to_string()
|
||||
}
|
||||
} else {
|
||||
" Processes ".to_string()
|
||||
}
|
||||
} else {
|
||||
" Processes ".to_string()
|
||||
};
|
||||
|
||||
let title = if app_state.is_expanded
|
||||
&& !proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.is_enabled
|
||||
&& !proc_widget_state.is_sort_open
|
||||
{
|
||||
const ESCAPE_ENDING: &str = "── Esc to go back ";
|
||||
|
||||
let (chosen_title_base, expanded_title_base) = {
|
||||
let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING);
|
||||
|
||||
if temp_title_base.len() > draw_loc.width.into() {
|
||||
(
|
||||
" Processes ".to_string(),
|
||||
format!("{}{}", " Processes ", ESCAPE_ENDING),
|
||||
)
|
||||
} else {
|
||||
(title_base, temp_title_base)
|
||||
}
|
||||
};
|
||||
|
||||
Spans::from(vec![
|
||||
Span::styled(chosen_title_base, self.colours.widget_title_style),
|
||||
Span::styled(
|
||||
format!(
|
||||
"─{}─ Esc to go back ",
|
||||
"─".repeat(
|
||||
usize::from(draw_loc.width).saturating_sub(
|
||||
UnicodeSegmentation::graphemes(
|
||||
expanded_title_base.as_str(),
|
||||
true
|
||||
)
|
||||
.count()
|
||||
+ 2
|
||||
)
|
||||
)
|
||||
),
|
||||
border_style,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
Spans::from(Span::styled(title_base, self.colours.widget_title_style))
|
||||
};
|
||||
|
||||
let process_block = if draw_border {
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
} else if is_on_widget {
|
||||
Block::default()
|
||||
.borders(SIDE_BORDERS)
|
||||
.border_style(self.colours.highlighted_border_style)
|
||||
} else {
|
||||
Block::default().borders(Borders::NONE)
|
||||
};
|
||||
|
||||
if let Some(process_data) = &app_state
|
||||
.canvas_data
|
||||
.stringified_process_data_map
|
||||
.get(&widget_id)
|
||||
{
|
||||
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||
0
|
||||
} else {
|
||||
app_state.app_config_fields.table_gap
|
||||
};
|
||||
let position = get_start_position(
|
||||
usize::from(
|
||||
(draw_loc.height + (1 - table_gap))
|
||||
.saturating_sub(self.table_height_offset),
|
||||
),
|
||||
&proc_widget_state.scroll_state.scroll_direction,
|
||||
&mut proc_widget_state.scroll_state.scroll_bar,
|
||||
proc_widget_state.scroll_state.current_scroll_position,
|
||||
app_state.is_force_redraw,
|
||||
);
|
||||
|
||||
// Sanity check
|
||||
let start_position = if position >= process_data.len() {
|
||||
process_data.len().saturating_sub(1)
|
||||
} else {
|
||||
position
|
||||
};
|
||||
|
||||
let sliced_vec = &process_data[start_position..];
|
||||
let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| {
|
||||
(
|
||||
data.iter()
|
||||
.map(|(entry, _alternative)| entry)
|
||||
.collect::<Vec<_>>(),
|
||||
disabled,
|
||||
)
|
||||
});
|
||||
|
||||
let proc_table_state = &mut proc_widget_state.scroll_state.table_state;
|
||||
proc_table_state.select(Some(
|
||||
proc_widget_state
|
||||
.scroll_state
|
||||
.current_scroll_position
|
||||
.saturating_sub(start_position),
|
||||
));
|
||||
|
||||
// Draw!
|
||||
let process_headers = proc_widget_state.columns.get_column_headers(
|
||||
&proc_widget_state.process_sorting_type,
|
||||
proc_widget_state.is_process_sort_descending,
|
||||
);
|
||||
|
||||
// Calculate widths
|
||||
// FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths
|
||||
let hard_widths = if proc_widget_state.is_grouped {
|
||||
PROCESS_HEADERS_HARD_WIDTH_GROUPED
|
||||
} else {
|
||||
PROCESS_HEADERS_HARD_WIDTH_NO_GROUP
|
||||
};
|
||||
|
||||
if recalculate_column_widths {
|
||||
let mut column_widths = process_headers
|
||||
.iter()
|
||||
.map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let soft_widths_min = column_widths
|
||||
.iter()
|
||||
.map(|width| Some(*width))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
proc_widget_state.table_width_state.desired_column_widths = {
|
||||
for (row, _disabled) in processed_sliced_vec.clone() {
|
||||
for (col, entry) in row.iter().enumerate() {
|
||||
if let Some(col_width) = column_widths.get_mut(col) {
|
||||
let grapheme_len = UnicodeWidthStr::width(entry.as_str());
|
||||
if grapheme_len as u16 > *col_width {
|
||||
*col_width = grapheme_len as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
column_widths
|
||||
};
|
||||
|
||||
proc_widget_state.table_width_state.desired_column_widths = proc_widget_state
|
||||
.table_width_state
|
||||
.desired_column_widths
|
||||
.iter()
|
||||
.zip(hard_widths)
|
||||
.map(|(current, hard)| {
|
||||
if let Some(hard) = hard {
|
||||
if *hard > *current {
|
||||
*hard
|
||||
} else {
|
||||
*current
|
||||
}
|
||||
} else {
|
||||
*current
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let soft_widths_max = if proc_widget_state.is_grouped {
|
||||
// Note grouped trees are not a thing.
|
||||
|
||||
if proc_widget_state.is_using_command {
|
||||
PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND
|
||||
} else {
|
||||
PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE
|
||||
}
|
||||
} else if proc_widget_state.is_using_command {
|
||||
PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND
|
||||
} else if proc_widget_state.is_tree_mode {
|
||||
PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE
|
||||
} else {
|
||||
PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE
|
||||
};
|
||||
|
||||
proc_widget_state.table_width_state.calculated_column_widths =
|
||||
get_column_widths(
|
||||
draw_loc.width,
|
||||
hard_widths,
|
||||
&soft_widths_min,
|
||||
soft_widths_max,
|
||||
&(proc_widget_state
|
||||
.table_width_state
|
||||
.desired_column_widths
|
||||
.iter()
|
||||
.map(|width| Some(*width))
|
||||
.collect::<Vec<_>>()),
|
||||
true,
|
||||
);
|
||||
|
||||
// debug!(
|
||||
// "DCW: {:?}",
|
||||
// proc_widget_state.table_width_state.desired_column_widths
|
||||
// );
|
||||
// debug!(
|
||||
// "CCW: {:?}",
|
||||
// proc_widget_state.table_width_state.calculated_column_widths
|
||||
// );
|
||||
}
|
||||
|
||||
let dcw = &proc_widget_state.table_width_state.desired_column_widths;
|
||||
let ccw = &proc_widget_state.table_width_state.calculated_column_widths;
|
||||
|
||||
let process_rows = sliced_vec.iter().map(|(data, disabled)| {
|
||||
let truncated_data = data.iter().zip(hard_widths).enumerate().map(
|
||||
|(itx, ((entry, alternative), width))| {
|
||||
if let (Some(desired_col_width), Some(calculated_col_width)) =
|
||||
(dcw.get(itx), ccw.get(itx))
|
||||
{
|
||||
if width.is_none() {
|
||||
if *desired_col_width > *calculated_col_width
|
||||
&& *calculated_col_width > 0
|
||||
{
|
||||
let calculated_col_width: usize =
|
||||
(*calculated_col_width).into();
|
||||
|
||||
let graphemes =
|
||||
UnicodeSegmentation::graphemes(entry.as_str(), true)
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
if let Some(alternative) = alternative {
|
||||
Text::raw(alternative)
|
||||
} else if graphemes.len() > calculated_col_width
|
||||
&& calculated_col_width > 1
|
||||
{
|
||||
// Truncate with ellipsis
|
||||
let first_n =
|
||||
graphemes[..(calculated_col_width - 1)].concat();
|
||||
Text::raw(format!("{}…", first_n))
|
||||
} else {
|
||||
Text::raw(entry)
|
||||
}
|
||||
} else {
|
||||
Text::raw(entry)
|
||||
}
|
||||
} else {
|
||||
Text::raw(entry)
|
||||
}
|
||||
} else {
|
||||
Text::raw(entry)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if *disabled {
|
||||
Row::new(truncated_data).style(self.colours.disabled_text_style)
|
||||
} else {
|
||||
Row::new(truncated_data)
|
||||
}
|
||||
});
|
||||
|
||||
f.render_stateful_widget(
|
||||
Table::new(process_rows)
|
||||
.header(
|
||||
Row::new(process_headers)
|
||||
.style(self.colours.table_header_style)
|
||||
.bottom_margin(table_gap),
|
||||
)
|
||||
.block(process_block)
|
||||
.highlight_style(highlight_style)
|
||||
.style(self.colours.text_style)
|
||||
.widths(
|
||||
&(proc_widget_state
|
||||
.table_width_state
|
||||
.calculated_column_widths
|
||||
.iter()
|
||||
.map(|calculated_width| Constraint::Length(*calculated_width))
|
||||
.collect::<Vec<_>>()),
|
||||
),
|
||||
margined_draw_loc,
|
||||
proc_table_state,
|
||||
);
|
||||
} else {
|
||||
f.render_widget(process_block, margined_draw_loc);
|
||||
TextTable {
|
||||
table_gap: app_state.app_config_fields.table_gap,
|
||||
is_force_redraw: app_state.is_force_redraw,
|
||||
recalculate_column_widths,
|
||||
header_style: self.colours.table_header_style,
|
||||
border_style,
|
||||
highlighted_text_style,
|
||||
title: Some(TextTableTitle {
|
||||
title: " Processes ".into(),
|
||||
is_expanded: app_state.is_expanded,
|
||||
}),
|
||||
is_on_widget,
|
||||
draw_border,
|
||||
show_table_scroll_position: app_state.app_config_fields.show_table_scroll_position,
|
||||
title_style: self.colours.widget_title_style,
|
||||
text_style: self.colours.text_style,
|
||||
left_to_right: false,
|
||||
}
|
||||
.draw_text_table(f, draw_loc, &mut proc_widget_state.table_state, todo!());
|
||||
|
||||
// Check if we need to update columnar bounds...
|
||||
if recalculate_column_widths
|
||||
|| proc_widget_state.columns.column_header_x_locs.is_none()
|
||||
|| proc_widget_state.columns.column_header_y_loc.is_none()
|
||||
{
|
||||
// y location is just the y location of the widget + border size (1 normally, 0 in basic)
|
||||
proc_widget_state.columns.column_header_y_loc =
|
||||
Some(draw_loc.y + if draw_border { 1 } else { 0 });
|
||||
// FIXME: [Proc] Handle this, and the above TODO
|
||||
// // Check if we need to update columnar bounds...
|
||||
// if recalculate_column_widths
|
||||
// || proc_widget_state.columns.column_header_x_locs.is_none()
|
||||
// || proc_widget_state.columns.column_header_y_loc.is_none()
|
||||
// {
|
||||
// // y location is just the y location of the widget + border size (1 normally, 0 in basic)
|
||||
// proc_widget_state.columns.column_header_y_loc =
|
||||
// Some(draw_loc.y + if draw_border { 1 } else { 0 });
|
||||
|
||||
// x location is determined using the x locations of the widget; just offset from the left bound
|
||||
// as appropriate, and use the right bound as limiter.
|
||||
// // x location is determined using the x locations of the widget; just offset from the left bound
|
||||
// // as appropriate, and use the right bound as limiter.
|
||||
|
||||
let mut current_x_left = draw_loc.x + 1;
|
||||
let max_x_right = draw_loc.x + draw_loc.width - 1;
|
||||
// let mut current_x_left = draw_loc.x + 1;
|
||||
// let max_x_right = draw_loc.x + draw_loc.width - 1;
|
||||
|
||||
let mut x_locs = vec![];
|
||||
// let mut x_locs = vec![];
|
||||
|
||||
for width in proc_widget_state
|
||||
.table_width_state
|
||||
.calculated_column_widths
|
||||
.iter()
|
||||
{
|
||||
let right_bound = current_x_left + width;
|
||||
// for width in proc_widget_state
|
||||
// .table_width_state
|
||||
// .calculated_column_widths
|
||||
// .iter()
|
||||
// {
|
||||
// let right_bound = current_x_left + width;
|
||||
|
||||
if right_bound < max_x_right {
|
||||
x_locs.push((current_x_left, right_bound));
|
||||
current_x_left = right_bound + 1;
|
||||
} else {
|
||||
x_locs.push((current_x_left, max_x_right));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// if right_bound < max_x_right {
|
||||
// x_locs.push((current_x_left, right_bound));
|
||||
// current_x_left = right_bound + 1;
|
||||
// } else {
|
||||
// x_locs.push((current_x_left, max_x_right));
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
proc_widget_state.columns.column_header_x_locs = Some(x_locs);
|
||||
}
|
||||
// proc_widget_state.columns.column_header_x_locs = Some(x_locs);
|
||||
// }
|
||||
|
||||
if app_state.should_get_widget_bounds() {
|
||||
// Update draw loc in widget map
|
||||
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
|
||||
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
|
||||
widget.bottom_right_corner = Some((
|
||||
margined_draw_loc.x + margined_draw_loc.width,
|
||||
margined_draw_loc.y + margined_draw_loc.height,
|
||||
));
|
||||
widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
|
||||
widget.bottom_right_corner =
|
||||
Some((draw_loc.x + draw_loc.width, draw_loc.y + draw_loc.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -583,14 +289,8 @@ impl Painter {
|
|||
|
||||
let start_position: usize = get_search_start_position(
|
||||
num_columns - num_chars_for_text - 5,
|
||||
&proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.cursor_direction,
|
||||
&mut proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.cursor_bar,
|
||||
&proc_widget_state.search_state.search_state.cursor_direction,
|
||||
&mut proc_widget_state.search_state.search_state.cursor_bar,
|
||||
current_cursor_position,
|
||||
app_state.is_force_redraw,
|
||||
);
|
||||
|
@ -625,25 +325,19 @@ impl Painter {
|
|||
})];
|
||||
|
||||
// Text options shamelessly stolen from VS Code.
|
||||
let case_style = if !proc_widget_state.process_search_state.is_ignoring_case {
|
||||
let case_style = if !proc_widget_state.search_state.is_ignoring_case {
|
||||
self.colours.currently_selected_text_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
};
|
||||
|
||||
let whole_word_style = if proc_widget_state
|
||||
.process_search_state
|
||||
.is_searching_whole_word
|
||||
{
|
||||
let whole_word_style = if proc_widget_state.search_state.is_searching_whole_word {
|
||||
self.colours.currently_selected_text_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
};
|
||||
|
||||
let regex_style = if proc_widget_state
|
||||
.process_search_state
|
||||
.is_searching_with_regex
|
||||
{
|
||||
let regex_style = if proc_widget_state.search_state.is_searching_with_regex {
|
||||
self.colours.currently_selected_text_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
|
@ -669,11 +363,7 @@ impl Painter {
|
|||
]);
|
||||
|
||||
search_text.push(Spans::from(Span::styled(
|
||||
if let Some(err) = &proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.error_message
|
||||
{
|
||||
if let Some(err) = &proc_widget_state.search_state.search_state.error_message {
|
||||
err.as_str()
|
||||
} else {
|
||||
""
|
||||
|
@ -683,7 +373,7 @@ impl Painter {
|
|||
search_text.push(option_text);
|
||||
|
||||
let current_border_style = if proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.search_state
|
||||
.is_invalid_search
|
||||
{
|
||||
|
@ -751,127 +441,128 @@ impl Painter {
|
|||
/// state that is stored.
|
||||
///
|
||||
/// This should not be directly called.
|
||||
fn draw_process_sort<B: Backend>(
|
||||
fn draw_sort_table<B: Backend>(
|
||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||
widget_id: u64,
|
||||
) {
|
||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||
// FIXME: [Proc] Redo drawing sort table!
|
||||
// let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||
|
||||
if let Some(proc_widget_state) =
|
||||
app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
|
||||
{
|
||||
let current_scroll_position = proc_widget_state.columns.current_scroll_position;
|
||||
let sort_string = proc_widget_state
|
||||
.columns
|
||||
.ordered_columns
|
||||
.iter()
|
||||
.filter(|column_type| {
|
||||
proc_widget_state
|
||||
.columns
|
||||
.column_mapping
|
||||
.get(column_type)
|
||||
.unwrap()
|
||||
.enabled
|
||||
})
|
||||
.map(|column_type| column_type.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
// if let Some(proc_widget_state) =
|
||||
// app_state.proc_state.widget_states.get_mut(&(widget_id - 2))
|
||||
// {
|
||||
// let current_scroll_position = proc_widget_state.columns.current_scroll_position;
|
||||
// let sort_string = proc_widget_state
|
||||
// .columns
|
||||
// .ordered_columns
|
||||
// .iter()
|
||||
// .filter(|column_type| {
|
||||
// proc_widget_state
|
||||
// .columns
|
||||
// .column_mapping
|
||||
// .get(column_type)
|
||||
// .unwrap()
|
||||
// .enabled
|
||||
// })
|
||||
// .map(|column_type| column_type.to_string())
|
||||
// .collect::<Vec<_>>();
|
||||
|
||||
let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||
0
|
||||
} else {
|
||||
app_state.app_config_fields.table_gap
|
||||
};
|
||||
let position = get_start_position(
|
||||
usize::from(
|
||||
(draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
|
||||
),
|
||||
&proc_widget_state.columns.scroll_direction,
|
||||
&mut proc_widget_state.columns.previous_scroll_position,
|
||||
current_scroll_position,
|
||||
app_state.is_force_redraw,
|
||||
);
|
||||
// let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT {
|
||||
// 0
|
||||
// } else {
|
||||
// app_state.app_config_fields.table_gap
|
||||
// };
|
||||
// let position = get_start_position(
|
||||
// usize::from(
|
||||
// (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset),
|
||||
// ),
|
||||
// &proc_widget_state.columns.scroll_direction,
|
||||
// &mut proc_widget_state.columns.previous_scroll_position,
|
||||
// current_scroll_position,
|
||||
// app_state.is_force_redraw,
|
||||
// );
|
||||
|
||||
// Sanity check
|
||||
let start_position = if position >= sort_string.len() {
|
||||
sort_string.len().saturating_sub(1)
|
||||
} else {
|
||||
position
|
||||
};
|
||||
// // Sanity check
|
||||
// let start_position = if position >= sort_string.len() {
|
||||
// sort_string.len().saturating_sub(1)
|
||||
// } else {
|
||||
// position
|
||||
// };
|
||||
|
||||
let sliced_vec = &sort_string[start_position..];
|
||||
// let sliced_vec = &sort_string[start_position..];
|
||||
|
||||
let sort_options = sliced_vec
|
||||
.iter()
|
||||
.map(|column| Row::new(vec![column.as_str()]));
|
||||
// let sort_options = sliced_vec
|
||||
// .iter()
|
||||
// .map(|column| Row::new(vec![column.as_str()]));
|
||||
|
||||
let column_state = &mut proc_widget_state.columns.column_state;
|
||||
column_state.select(Some(
|
||||
proc_widget_state
|
||||
.columns
|
||||
.current_scroll_position
|
||||
.saturating_sub(start_position),
|
||||
));
|
||||
let current_border_style = if proc_widget_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.is_invalid_search
|
||||
{
|
||||
self.colours.invalid_query_style
|
||||
} else if is_on_widget {
|
||||
self.colours.highlighted_border_style
|
||||
} else {
|
||||
self.colours.border_style
|
||||
};
|
||||
// let column_state = &mut proc_widget_state.columns.column_state;
|
||||
// column_state.select(Some(
|
||||
// proc_widget_state
|
||||
// .columns
|
||||
// .current_scroll_position
|
||||
// .saturating_sub(start_position),
|
||||
// ));
|
||||
// let current_border_style = if proc_widget_state
|
||||
// .search_state
|
||||
// .search_state
|
||||
// .is_invalid_search
|
||||
// {
|
||||
// self.colours.invalid_query_style
|
||||
// } else if is_on_widget {
|
||||
// self.colours.highlighted_border_style
|
||||
// } else {
|
||||
// self.colours.border_style
|
||||
// };
|
||||
|
||||
let process_sort_block = if draw_border {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(current_border_style)
|
||||
} else if is_on_widget {
|
||||
Block::default()
|
||||
.borders(SIDE_BORDERS)
|
||||
.border_style(current_border_style)
|
||||
} else {
|
||||
Block::default().borders(Borders::NONE)
|
||||
};
|
||||
// let process_sort_block = if draw_border {
|
||||
// Block::default()
|
||||
// .borders(Borders::ALL)
|
||||
// .border_style(current_border_style)
|
||||
// } else if is_on_widget {
|
||||
// Block::default()
|
||||
// .borders(SIDE_BORDERS)
|
||||
// .border_style(current_border_style)
|
||||
// } else {
|
||||
// Block::default().borders(Borders::NONE)
|
||||
// };
|
||||
|
||||
let highlight_style = if is_on_widget {
|
||||
self.colours.currently_selected_text_style
|
||||
} else {
|
||||
self.colours.text_style
|
||||
};
|
||||
// let highlight_style = if is_on_widget {
|
||||
// self.colours.currently_selected_text_style
|
||||
// } else {
|
||||
// self.colours.text_style
|
||||
// };
|
||||
|
||||
let margined_draw_loc = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)])
|
||||
.horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
||||
.direction(Direction::Horizontal)
|
||||
.split(draw_loc)[0];
|
||||
// let margined_draw_loc = Layout::default()
|
||||
// .constraints([Constraint::Percentage(100)])
|
||||
// .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 })
|
||||
// .direction(Direction::Horizontal)
|
||||
// .split(draw_loc)[0];
|
||||
|
||||
f.render_stateful_widget(
|
||||
Table::new(sort_options)
|
||||
.header(
|
||||
Row::new(vec!["Sort By"])
|
||||
.style(self.colours.table_header_style)
|
||||
.bottom_margin(table_gap),
|
||||
)
|
||||
.block(process_sort_block)
|
||||
.highlight_style(highlight_style)
|
||||
.style(self.colours.text_style)
|
||||
.widths(&[Constraint::Percentage(100)]),
|
||||
margined_draw_loc,
|
||||
column_state,
|
||||
);
|
||||
// f.render_stateful_widget(
|
||||
// Table::new(sort_options)
|
||||
// .header(
|
||||
// Row::new(vec!["Sort By"])
|
||||
// .style(self.colours.table_header_style)
|
||||
// .bottom_margin(table_gap),
|
||||
// )
|
||||
// .block(process_sort_block)
|
||||
// .highlight_style(highlight_style)
|
||||
// .style(self.colours.text_style)
|
||||
// .widths(&[Constraint::Percentage(100)]),
|
||||
// margined_draw_loc,
|
||||
// column_state,
|
||||
// );
|
||||
|
||||
if app_state.should_get_widget_bounds() {
|
||||
// Update draw loc in widget map
|
||||
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
|
||||
widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
|
||||
widget.bottom_right_corner = Some((
|
||||
margined_draw_loc.x + margined_draw_loc.width,
|
||||
margined_draw_loc.y + margined_draw_loc.height,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// if app_state.should_get_widget_bounds() {
|
||||
// // Update draw loc in widget map
|
||||
// if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
|
||||
// widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
|
||||
// widget.bottom_right_corner = Some((
|
||||
// margined_draw_loc.x + margined_draw_loc.width,
|
||||
// margined_draw_loc.y + margined_draw_loc.height,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
//! This mainly concerns converting collected data into things that the canvas
|
||||
//! can actually handle.
|
||||
use crate::app::widgets::{ProcWidget, ProcWidgetMode};
|
||||
use crate::app::CellContent;
|
||||
use crate::canvas::Point;
|
||||
use crate::{app::AxisScaling, units::data_units::DataUnit, Pid};
|
||||
use crate::{
|
||||
app::{data_farmer, data_harvester, App, ProcWidgetState},
|
||||
app::{data_farmer, data_harvester, App},
|
||||
utils::{self, gen_util::*},
|
||||
};
|
||||
use concat_string::concat_string;
|
||||
use data_harvester::processes::ProcessSorting;
|
||||
use fxhash::FxBuildHasher;
|
||||
use indexmap::IndexSet;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
|
@ -23,29 +24,6 @@ pub struct ConvertedBatteryData {
|
|||
pub health: String,
|
||||
}
|
||||
|
||||
pub enum CellContent {
|
||||
Simple(Cow<'static, str>),
|
||||
HasAlt {
|
||||
alt: Cow<'static, str>,
|
||||
main: Cow<'static, str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl CellContent {
|
||||
/// Returns the length of the [`CellContent`]. Note that for a [`CellContent::HasAlt`], it will return
|
||||
/// the length of the "main" field.
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
CellContent::Simple(s) => s.len(),
|
||||
CellContent::HasAlt { alt: _, main: long } => long.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TableData {
|
||||
pub data: Vec<TableRow>,
|
||||
|
@ -1247,12 +1225,13 @@ pub fn tree_process_data(
|
|||
|
||||
// FIXME: [OPT] This is an easy target for optimization, too many to_strings!
|
||||
pub fn stringify_process_data(
|
||||
proc_widget_state: &ProcWidgetState, finalized_process_data: &[ConvertedProcessData],
|
||||
proc_widget_state: &ProcWidget, finalized_process_data: &[ConvertedProcessData],
|
||||
) -> Vec<(Vec<(String, Option<String>)>, bool)> {
|
||||
let is_proc_widget_grouped = proc_widget_state.is_grouped;
|
||||
let is_proc_widget_grouped = matches!(proc_widget_state.mode, ProcWidgetMode::Grouped);
|
||||
let is_using_command = proc_widget_state.is_using_command;
|
||||
let is_tree = proc_widget_state.is_tree_mode;
|
||||
let mem_enabled = proc_widget_state.columns.is_enabled(&ProcessSorting::Mem);
|
||||
let is_tree = matches!(proc_widget_state.mode, ProcWidgetMode::Tree);
|
||||
// FIXME: [Proc] Handle this, it shouldn't always be true lol.
|
||||
let mem_enabled = true;
|
||||
|
||||
finalized_process_data
|
||||
.iter()
|
||||
|
|
72
src/lib.rs
72
src/lib.rs
|
@ -29,6 +29,7 @@ use crossterm::{
|
|||
use app::{
|
||||
data_harvester::{self, processes::ProcessSorting},
|
||||
layout_manager::{UsedWidgets, WidgetDirection},
|
||||
widgets::{ProcWidget, ProcWidgetMode},
|
||||
App,
|
||||
};
|
||||
use constants::*;
|
||||
|
@ -305,13 +306,8 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
|
|||
pub fn handle_force_redraws(app: &mut App) {
|
||||
// Currently we use an Option... because we might want to future-proof this
|
||||
// if we eventually get widget-specific redrawing!
|
||||
if app.proc_state.force_update_all {
|
||||
update_all_process_lists(app);
|
||||
app.proc_state.force_update_all = false;
|
||||
} else if let Some(widget_id) = app.proc_state.force_update {
|
||||
update_final_process_list(app, widget_id);
|
||||
app.proc_state.force_update = None;
|
||||
}
|
||||
|
||||
// FIXME: [PROC] handle updating processes if force redraw!
|
||||
|
||||
if app.cpu_state.force_update.is_some() {
|
||||
convert_cpu_data_points(&app.data_collection, &mut app.canvas_data.cpu_data);
|
||||
|
@ -365,16 +361,15 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
|
|||
.map(|process_state| {
|
||||
(
|
||||
process_state
|
||||
.process_search_state
|
||||
.search_state
|
||||
.search_state
|
||||
.is_invalid_or_blank_search(),
|
||||
process_state.is_using_command,
|
||||
process_state.is_grouped,
|
||||
process_state.is_tree_mode,
|
||||
process_state.mode,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states {
|
||||
if let Some((is_invalid_or_blank, is_using_command, mode)) = process_states {
|
||||
if !app.is_frozen {
|
||||
convert_process_data(
|
||||
&app.data_collection,
|
||||
|
@ -384,8 +379,9 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
|
|||
);
|
||||
}
|
||||
let process_filter = app.get_process_filter(widget_id);
|
||||
let filtered_process_data: Vec<ConvertedProcessData> = if is_tree {
|
||||
app.canvas_data
|
||||
let filtered_process_data: Vec<ConvertedProcessData> = match mode {
|
||||
ProcWidgetMode::Tree => app
|
||||
.canvas_data
|
||||
.single_process_data
|
||||
.iter()
|
||||
.map(|(_pid, process)| {
|
||||
|
@ -398,9 +394,9 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
|
|||
}
|
||||
process_clone
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
app.canvas_data
|
||||
.collect::<Vec<_>>(),
|
||||
ProcWidgetMode::Grouped | ProcWidgetMode::Normal => app
|
||||
.canvas_data
|
||||
.single_process_data
|
||||
.iter()
|
||||
.filter_map(|(_pid, process)| {
|
||||
|
@ -419,35 +415,35 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
|
|||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<_>>(),
|
||||
};
|
||||
|
||||
if let Some(proc_widget_state) = app.proc_state.get_mut_widget_state(widget_id) {
|
||||
let mut finalized_process_data = if is_tree {
|
||||
tree_process_data(
|
||||
let mut finalized_process_data = match proc_widget_state.mode {
|
||||
ProcWidgetMode::Tree => tree_process_data(
|
||||
&filtered_process_data,
|
||||
is_using_command,
|
||||
&proc_widget_state.process_sorting_type,
|
||||
proc_widget_state.is_process_sort_descending,
|
||||
)
|
||||
} else if is_grouped {
|
||||
group_process_data(&filtered_process_data, is_using_command)
|
||||
} else {
|
||||
filtered_process_data
|
||||
),
|
||||
ProcWidgetMode::Grouped => {
|
||||
let mut data = group_process_data(&filtered_process_data, is_using_command);
|
||||
sort_process_data(&mut data, proc_widget_state);
|
||||
data
|
||||
}
|
||||
ProcWidgetMode::Normal => {
|
||||
let mut data = filtered_process_data;
|
||||
sort_process_data(&mut data, proc_widget_state);
|
||||
data
|
||||
}
|
||||
};
|
||||
|
||||
// Note tree mode is sorted well before this, as it's special.
|
||||
if !is_tree {
|
||||
sort_process_data(&mut finalized_process_data, proc_widget_state);
|
||||
}
|
||||
|
||||
if proc_widget_state.scroll_state.current_scroll_position
|
||||
>= finalized_process_data.len()
|
||||
if proc_widget_state.table_state.current_scroll_position >= finalized_process_data.len()
|
||||
{
|
||||
proc_widget_state.scroll_state.current_scroll_position =
|
||||
proc_widget_state.table_state.current_scroll_position =
|
||||
finalized_process_data.len().saturating_sub(1);
|
||||
proc_widget_state.scroll_state.scroll_bar = 0;
|
||||
proc_widget_state.scroll_state.scroll_direction = app::ScrollDirection::Down;
|
||||
proc_widget_state.table_state.scroll_bar = 0;
|
||||
proc_widget_state.table_state.scroll_direction = app::ScrollDirection::Down;
|
||||
}
|
||||
|
||||
app.canvas_data.stringified_process_data_map.insert(
|
||||
|
@ -461,9 +457,7 @@ fn update_final_process_list(app: &mut App, widget_id: u64) {
|
|||
}
|
||||
}
|
||||
|
||||
fn sort_process_data(
|
||||
to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &app::ProcWidgetState,
|
||||
) {
|
||||
fn sort_process_data(to_sort_vec: &mut [ConvertedProcessData], proc_widget_state: &ProcWidget) {
|
||||
to_sort_vec.sort_by_cached_key(|c| c.name.to_lowercase());
|
||||
|
||||
match &proc_widget_state.process_sorting_type {
|
||||
|
@ -510,7 +504,7 @@ fn sort_process_data(
|
|||
}
|
||||
}
|
||||
ProcessSorting::Pid => {
|
||||
if !proc_widget_state.is_grouped {
|
||||
if !matches!(proc_widget_state.mode, ProcWidgetMode::Grouped) {
|
||||
to_sort_vec.sort_by(|a, b| {
|
||||
utils::gen_util::get_ordering(
|
||||
a.pid,
|
||||
|
@ -573,7 +567,7 @@ fn sort_process_data(
|
|||
(None, None) => std::cmp::Ordering::Less,
|
||||
}),
|
||||
ProcessSorting::Count => {
|
||||
if proc_widget_state.is_grouped {
|
||||
if matches!(proc_widget_state.mode, ProcWidgetMode::Grouped) {
|
||||
to_sort_vec.sort_by(|a, b| {
|
||||
utils::gen_util::get_ordering(
|
||||
a.group_pids.len(),
|
||||
|
|
|
@ -10,7 +10,11 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
app::{layout_manager::*, *},
|
||||
app::{
|
||||
layout_manager::*,
|
||||
widgets::{ProcWidget, ProcWidgetMode},
|
||||
*,
|
||||
},
|
||||
canvas::ColourScheme,
|
||||
constants::*,
|
||||
units::data_units::DataUnit,
|
||||
|
@ -265,7 +269,7 @@ pub fn build_app(
|
|||
let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new();
|
||||
let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new();
|
||||
let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::new();
|
||||
let mut proc_state_map: HashMap<u64, ProcWidgetState> = HashMap::new();
|
||||
let mut proc_state_map: HashMap<u64, ProcWidget> = HashMap::new();
|
||||
let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::new();
|
||||
let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new();
|
||||
let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new();
|
||||
|
@ -353,15 +357,22 @@ pub fn build_app(
|
|||
);
|
||||
}
|
||||
Proc => {
|
||||
let mode = if is_grouped {
|
||||
ProcWidgetMode::Grouped
|
||||
} else if is_default_tree {
|
||||
ProcWidgetMode::Tree
|
||||
} else {
|
||||
ProcWidgetMode::Normal
|
||||
};
|
||||
|
||||
proc_state_map.insert(
|
||||
widget.widget_id,
|
||||
ProcWidgetState::init(
|
||||
ProcWidget::init(
|
||||
mode,
|
||||
is_case_sensitive,
|
||||
is_match_whole_word,
|
||||
is_use_regex,
|
||||
is_grouped,
|
||||
show_memory_as_values,
|
||||
is_default_tree,
|
||||
is_default_command,
|
||||
),
|
||||
);
|
||||
|
@ -466,7 +477,7 @@ pub fn build_app(
|
|||
let mapping = HashMap::new();
|
||||
for widget in search_case_enabled_widgets {
|
||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
||||
proc_widget.process_search_state.is_ignoring_case = !widget.enabled;
|
||||
proc_widget.search_state.is_ignoring_case = !widget.enabled;
|
||||
}
|
||||
}
|
||||
flags.search_case_enabled_widgets_map = Some(mapping);
|
||||
|
@ -480,7 +491,7 @@ pub fn build_app(
|
|||
let mapping = HashMap::new();
|
||||
for widget in search_whole_word_enabled_widgets {
|
||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
||||
proc_widget.process_search_state.is_searching_whole_word = widget.enabled;
|
||||
proc_widget.search_state.is_searching_whole_word = widget.enabled;
|
||||
}
|
||||
}
|
||||
flags.search_whole_word_enabled_widgets_map = Some(mapping);
|
||||
|
@ -492,7 +503,7 @@ pub fn build_app(
|
|||
let mapping = HashMap::new();
|
||||
for widget in search_regex_enabled_widgets {
|
||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
||||
proc_widget.process_search_state.is_searching_with_regex = widget.enabled;
|
||||
proc_widget.search_state.is_searching_with_regex = widget.enabled;
|
||||
}
|
||||
}
|
||||
flags.search_regex_enabled_widgets_map = Some(mapping);
|
||||
|
|
Loading…
Reference in New Issue