refactor: add text input

This commit is contained in:
ClementTsang 2021-08-30 23:48:11 -04:00
parent 27736b7fc0
commit b1889b0934
18 changed files with 907 additions and 972 deletions

4
Cargo.lock generated
View File

@ -1560,9 +1560,9 @@ checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-width"

View File

@ -60,7 +60,7 @@ thiserror = "1.0.24"
toml = "0.5.8"
tui = { version = "0.16.0", features = ["crossterm"], default-features = false }
typed-builder = "0.9.0"
unicode-segmentation = "1.7.1"
unicode-segmentation = "1.8.0"
unicode-width = "0.1"
# For debugging only... disable on release builds with --no-default-target for no? TODO: Redo this.

View File

@ -1,3 +1,3 @@
cognitive-complexity-threshold = 100
type-complexity-threshold = 500
too-many-arguments-threshold = 8
too-many-arguments-threshold = 10

View File

@ -259,9 +259,8 @@ impl AppState {
self.dd_err = None;
}
/// Handles a global [`KeyEvent`], and returns [`Some(EventResult)`] if the global shortcut is consumed by some global
/// shortcut. If not, it returns [`None`].
fn handle_global_shortcut(&mut self, event: KeyEvent) -> Option<EventResult> {
/// Handles a global [`KeyEvent`], and returns an [`EventResult`].
fn handle_global_shortcut(&mut self, event: KeyEvent) -> EventResult {
// TODO: Write this.
if event.modifiers.is_empty() {
@ -269,36 +268,36 @@ impl AppState {
KeyCode::Esc => {
if self.is_expanded {
self.is_expanded = false;
Some(EventResult::Redraw)
EventResult::Redraw
} else if self.help_dialog_state.is_showing_help {
self.help_dialog_state.is_showing_help = false;
self.help_dialog_state.scroll_state.current_scroll_index = 0;
Some(EventResult::Redraw)
EventResult::Redraw
} else if self.delete_dialog_state.is_showing_dd {
self.close_dd();
Some(EventResult::Redraw)
EventResult::Redraw
} else {
None
EventResult::NoRedraw
}
}
KeyCode::Char('q') => Some(EventResult::Quit),
KeyCode::Char('q') => EventResult::Quit,
KeyCode::Char('e') => {
self.is_expanded = !self.is_expanded;
Some(EventResult::Redraw)
EventResult::Redraw
}
KeyCode::Char('?') => {
self.help_dialog_state.is_showing_help = true;
Some(EventResult::Redraw)
EventResult::Redraw
}
_ => None,
_ => EventResult::NoRedraw,
}
} else if let KeyModifiers::CONTROL = event.modifiers {
match event.code {
KeyCode::Char('c') => Some(EventResult::Quit),
_ => None,
KeyCode::Char('c') => EventResult::Quit,
_ => EventResult::NoRedraw,
}
} else {
None
EventResult::NoRedraw
}
}
@ -327,15 +326,15 @@ impl AppState {
pub fn handle_event(&mut self, event: BottomEvent) -> EventResult {
match event {
BottomEvent::KeyInput(event) => {
if let Some(event_result) = self.handle_global_shortcut(event) {
// See if it's caught by a global shortcut first...
event_result
} else if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
// If it isn't, send it to the current widget!
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
let result = widget.handle_key_event(event);
self.convert_widget_event_result(result)
match self.convert_widget_event_result(result) {
EventResult::Quit => EventResult::Quit,
EventResult::Redraw => EventResult::Redraw,
EventResult::NoRedraw => self.handle_global_shortcut(event),
}
} else {
EventResult::NoRedraw
self.handle_global_shortcut(event)
}
}
BottomEvent::MouseInput(event) => {
@ -357,12 +356,11 @@ impl AppState {
let was_id_already_selected = self.selected_widget == *id;
self.selected_widget = *id;
let result = widget.handle_mouse_event(event);
if was_id_already_selected {
let result = widget.handle_mouse_event(event);
return self.convert_widget_event_result(result);
} else {
// If the aren't equal, *force* a redraw.
let result = widget.handle_mouse_event(event);
let _ = self.convert_widget_event_result(result);
return EventResult::Redraw;
}

View File

@ -218,7 +218,7 @@ fn read_proc(
user: user_table
.get_uid_to_username_mapping(uid)
.map(Into::into)
.unwrap_or("N/A".into()),
.unwrap_or_else(|_| "N/A".into()),
},
new_process_times,
))

View File

@ -1,449 +1,434 @@
use super::ProcWidgetState;
use crate::{
data_conversion::ConvertedProcessData,
utils::error::{
BottomError::{self, QueryError},
Result,
},
use crate::utils::error::{
BottomError::{self, QueryError},
Result,
};
use std::fmt::Debug;
use std::{borrow::Cow, collections::VecDeque};
use super::data_harvester::processes::ProcessHarvest;
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
const OR_LIST: [&str; 2] = ["or", "||"];
const AND_LIST: [&str; 2] = ["and", "&&"];
/// I only separated this as otherwise, the states.rs file gets huge... and this should
/// belong in another file anyways, IMO.
pub trait ProcessQuery {
/// In charge of parsing the given query.
/// We are defining the following language for a query (case-insensitive prefixes):
///
/// - Process names: No prefix required, can use regex, match word, or case.
/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
/// rather than a prefix.
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare.
/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare.
/// - STATE: Use prefix `state`, can use regex, match word, or case.
/// - USER: Use prefix `user`, can use regex, match word, or case.
/// - Read/s: Use prefix `r`. Can compare.
/// - Write/s: Use prefix `w`. Can compare.
/// - Total read: Use prefix `read`. Can compare.
/// - Total write: Use prefix `write`. Can compare.
///
/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed
/// or quoted elements after splitting to treat as process names.
/// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
fn parse_query(&self) -> Result<Query>;
}
/// In charge of parsing the given query.
/// We are defining the following language for a query (case-insensitive prefixes):
///
/// - Process names: No prefix required, can use regex, match word, or case.
/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
/// rather than a prefix.
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare.
/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare.
/// - STATE: Use prefix `state`, can use regex, match word, or case.
/// - USER: Use prefix `user`, can use regex, match word, or case.
/// - Read/s: Use prefix `r`. Can compare.
/// - Write/s: Use prefix `w`. Can compare.
/// - Total read: Use prefix `read`. Can compare.
/// - Total write: Use prefix `write`. Can compare.
///
/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed
/// or quoted elements after splitting to treat as process names.
/// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
pub fn parse_query(
search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
) -> Result<Query> {
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
let lhs = process_or(query)?;
let mut list_of_ors = vec![lhs];
impl ProcessQuery for ProcWidgetState {
fn parse_query(&self) -> Result<Query> {
fn process_string_to_filter(query: &mut VecDeque<String>) -> Result<Query> {
let lhs = process_or(query)?;
let mut list_of_ors = vec![lhs];
while query.front().is_some() {
list_of_ors.push(process_or(query)?);
}
Ok(Query { query: list_of_ors })
while query.front().is_some() {
list_of_ors.push(process_or(query)?);
}
fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
let mut lhs = process_and(query)?;
let mut rhs: Option<Box<And>> = None;
Ok(Query { query: list_of_ors })
}
while let Some(queue_top) = query.front() {
// debug!("OR QT: {:?}", queue_top);
if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
query.pop_front();
rhs = Some(Box::new(process_and(query)?));
fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
let mut lhs = process_and(query)?;
let mut rhs: Option<Box<And>> = None;
if let Some(queue_next) = query.front() {
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
// Must merge LHS and RHS
lhs = And {
lhs: Prefix {
or: Some(Box::new(Or { lhs, rhs })),
regex_prefix: None,
compare_prefix: None,
},
rhs: None,
};
rhs = None;
}
} else {
break;
}
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
} else {
break;
}
}
while let Some(queue_top) = query.front() {
// debug!("OR QT: {:?}", queue_top);
if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
query.pop_front();
rhs = Some(Box::new(process_and(query)?));
Ok(Or { lhs, rhs })
}
fn process_and(query: &mut VecDeque<String>) -> Result<And> {
let mut lhs = process_prefix(query, false)?;
let mut rhs: Option<Box<Prefix>> = None;
while let Some(queue_top) = query.front() {
// debug!("AND QT: {:?}", queue_top);
if AND_LIST.contains(&queue_top.to_lowercase().as_str()) {
query.pop_front();
rhs = Some(Box::new(process_prefix(query, false)?));
if let Some(next_queue_top) = query.front() {
if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {
// Must merge LHS and RHS
lhs = Prefix {
or: Some(Box::new(Or {
lhs: And { lhs, rhs },
rhs: None,
})),
regex_prefix: None,
compare_prefix: None,
};
rhs = None;
} else {
break;
}
} else {
break;
}
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
} else {
break;
}
}
Ok(And { lhs, rhs })
}
fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> {
if let Some(queue_top) = query.pop_front() {
if inside_quotation {
if queue_top == "\"" {
// This means we hit something like "". Return an empty prefix, and to deal with
// the close quote checker, add one to the top of the stack. Ugly fix but whatever.
query.push_front("\"".to_string());
return Ok(Prefix {
or: None,
regex_prefix: Some((
PrefixType::Name,
StringQuery::Value(String::default()),
)),
compare_prefix: None,
});
} else {
let mut quoted_string = queue_top;
while let Some(next_str) = query.front() {
if next_str == "\"" {
// Stop!
break;
} else {
quoted_string.push_str(next_str);
query.pop_front();
}
}
return Ok(Prefix {
or: None,
regex_prefix: Some((
PrefixType::Name,
StringQuery::Value(quoted_string),
)),
compare_prefix: None,
});
}
} else if queue_top == "(" {
if query.is_empty() {
return Err(QueryError(Cow::Borrowed("Missing closing parentheses")));
}
let mut list_of_ors = VecDeque::new();
while let Some(in_paren_query_top) = query.front() {
if in_paren_query_top != ")" {
list_of_ors.push_back(process_or(query)?);
} else {
break;
}
}
// Ensure not empty
if list_of_ors.is_empty() {
return Err(QueryError("No values within parentheses group".into()));
}
// Now convert this back to a OR...
let initial_or = Or {
lhs: And {
if let Some(queue_next) = query.front() {
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
// Must merge LHS and RHS
lhs = And {
lhs: Prefix {
or: list_of_ors.pop_front().map(Box::new),
compare_prefix: None,
or: Some(Box::new(Or { lhs, rhs })),
regex_prefix: None,
compare_prefix: None,
},
rhs: None,
},
rhs: None,
};
let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
lhs: And {
lhs: Prefix {
or: Some(Box::new(lhs)),
compare_prefix: None,
regex_prefix: None,
},
rhs: Some(Box::new(Prefix {
or: Some(Box::new(rhs)),
compare_prefix: None,
regex_prefix: None,
})),
},
rhs: None,
});
};
rhs = None;
}
} else {
break;
}
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
} else {
break;
}
}
if let Some(close_paren) = query.pop_front() {
if close_paren == ")" {
return Ok(Prefix {
or: Some(Box::new(returned_or)),
regex_prefix: None,
compare_prefix: None,
});
Ok(Or { lhs, rhs })
}
fn process_and(query: &mut VecDeque<String>) -> Result<And> {
let mut lhs = process_prefix(query, false)?;
let mut rhs: Option<Box<Prefix>> = None;
while let Some(queue_top) = query.front() {
// debug!("AND QT: {:?}", queue_top);
if AND_LIST.contains(&queue_top.to_lowercase().as_str()) {
query.pop_front();
rhs = Some(Box::new(process_prefix(query, false)?));
if let Some(next_queue_top) = query.front() {
if AND_LIST.contains(&next_queue_top.to_lowercase().as_str()) {
// Must merge LHS and RHS
lhs = Prefix {
or: Some(Box::new(Or {
lhs: And { lhs, rhs },
rhs: None,
})),
regex_prefix: None,
compare_prefix: None,
};
rhs = None;
} else {
break;
}
} else {
break;
}
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
} else {
break;
}
}
Ok(And { lhs, rhs })
}
fn process_prefix(query: &mut VecDeque<String>, inside_quotation: bool) -> Result<Prefix> {
if let Some(queue_top) = query.pop_front() {
if inside_quotation {
if queue_top == "\"" {
// This means we hit something like "". Return an empty prefix, and to deal with
// the close quote checker, add one to the top of the stack. Ugly fix but whatever.
query.push_front("\"".to_string());
return Ok(Prefix {
or: None,
regex_prefix: Some((
PrefixType::Name,
StringQuery::Value(String::default()),
)),
compare_prefix: None,
});
} else {
let mut quoted_string = queue_top;
while let Some(next_str) = query.front() {
if next_str == "\"" {
// Stop!
break;
} else {
return Err(QueryError("Missing closing parentheses".into()));
quoted_string.push_str(next_str);
query.pop_front();
}
}
return Ok(Prefix {
or: None,
regex_prefix: Some((PrefixType::Name, StringQuery::Value(quoted_string))),
compare_prefix: None,
});
}
} else if queue_top == "(" {
if query.is_empty() {
return Err(QueryError(Cow::Borrowed("Missing closing parentheses")));
}
let mut list_of_ors = VecDeque::new();
while let Some(in_paren_query_top) = query.front() {
if in_paren_query_top != ")" {
list_of_ors.push_back(process_or(query)?);
} else {
break;
}
}
// Ensure not empty
if list_of_ors.is_empty() {
return Err(QueryError("No values within parentheses group".into()));
}
// Now convert this back to a OR...
let initial_or = Or {
lhs: And {
lhs: Prefix {
or: list_of_ors.pop_front().map(Box::new),
compare_prefix: None,
regex_prefix: None,
},
rhs: None,
},
rhs: None,
};
let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
lhs: And {
lhs: Prefix {
or: Some(Box::new(lhs)),
compare_prefix: None,
regex_prefix: None,
},
rhs: Some(Box::new(Prefix {
or: Some(Box::new(rhs)),
compare_prefix: None,
regex_prefix: None,
})),
},
rhs: None,
});
if let Some(close_paren) = query.pop_front() {
if close_paren == ")" {
return Ok(Prefix {
or: Some(Box::new(returned_or)),
regex_prefix: None,
compare_prefix: None,
});
} else {
return Err(QueryError("Missing closing parentheses".into()));
}
} else if queue_top == ")" {
return Err(QueryError("Missing opening parentheses".into()));
} else if queue_top == "\"" {
// Similar to parentheses, trap and check for missing closing quotes. Note, however, that we
// will DIRECTLY call another process_prefix call...
} else {
return Err(QueryError("Missing closing parentheses".into()));
}
} else if queue_top == ")" {
return Err(QueryError("Missing opening parentheses".into()));
} else if queue_top == "\"" {
// Similar to parentheses, trap and check for missing closing quotes. Note, however, that we
// will DIRECTLY call another process_prefix call...
let prefix = process_prefix(query, true)?;
if let Some(close_paren) = query.pop_front() {
if close_paren == "\"" {
return Ok(prefix);
} else {
return Err(QueryError("Missing closing quotation".into()));
}
let prefix = process_prefix(query, true)?;
if let Some(close_paren) = query.pop_front() {
if close_paren == "\"" {
return Ok(prefix);
} else {
return Err(QueryError("Missing closing quotation".into()));
}
} else {
// Get prefix type...
let prefix_type = queue_top.parse::<PrefixType>()?;
let content = if let PrefixType::Name = prefix_type {
Some(queue_top)
} else {
query.pop_front()
};
return Err(QueryError("Missing closing quotation".into()));
}
} else {
// Get prefix type...
let prefix_type = queue_top.parse::<PrefixType>()?;
let content = if let PrefixType::Name = prefix_type {
Some(queue_top)
} else {
query.pop_front()
};
if let Some(content) = content {
match &prefix_type {
PrefixType::Name => {
return Ok(Prefix {
or: None,
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
compare_prefix: None,
})
}
PrefixType::Pid | PrefixType::State | PrefixType::User => {
// We have to check if someone put an "="...
if content == "=" {
// Check next string if possible
if let Some(queue_next) = query.pop_front() {
// TODO: Need to consider the following cases:
// - (test)
// - (test
// - test)
// These are split into 2 to 3 different strings due to parentheses being
// delimiters in our query system.
//
// Do we want these to be valid? They should, as a string, right?
if let Some(content) = content {
match &prefix_type {
PrefixType::Name => {
return Ok(Prefix {
or: None,
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
compare_prefix: None,
})
}
PrefixType::Pid | PrefixType::State | PrefixType::User => {
// We have to check if someone put an "="...
if content == "=" {
// Check next string if possible
if let Some(queue_next) = query.pop_front() {
// TODO: Need to consider the following cases:
// - (test)
// - (test
// - test)
// These are split into 2 to 3 different strings due to parentheses being
// delimiters in our query system.
//
// Do we want these to be valid? They should, as a string, right?
return Ok(Prefix {
or: None,
regex_prefix: Some((
prefix_type,
StringQuery::Value(queue_next),
)),
compare_prefix: None,
});
}
} else {
return Ok(Prefix {
or: None,
regex_prefix: Some((
prefix_type,
StringQuery::Value(content),
StringQuery::Value(queue_next),
)),
compare_prefix: None,
});
}
} else {
return Ok(Prefix {
or: None,
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
compare_prefix: None,
});
}
_ => {
// Now we gotta parse the content... yay.
}
_ => {
// Now we gotta parse the content... yay.
let mut condition: Option<QueryComparison> = None;
let mut value: Option<f64> = None;
let mut condition: Option<QueryComparison> = None;
let mut value: Option<f64> = None;
if content == "=" {
condition = Some(QueryComparison::Equal);
if let Some(queue_next) = query.pop_front() {
value = queue_next.parse::<f64>().ok();
} else {
return Err(QueryError("Missing value".into()));
}
} else if content == ">" || content == "<" {
// We also have to check if the next string is an "="...
if let Some(queue_next) = query.pop_front() {
if queue_next == "=" {
condition = Some(if content == ">" {
QueryComparison::GreaterOrEqual
} else {
QueryComparison::LessOrEqual
});
if let Some(queue_next_next) = query.pop_front() {
value = queue_next_next.parse::<f64>().ok();
} else {
return Err(QueryError("Missing value".into()));
}
if content == "=" {
condition = Some(QueryComparison::Equal);
if let Some(queue_next) = query.pop_front() {
value = queue_next.parse::<f64>().ok();
} else {
return Err(QueryError("Missing value".into()));
}
} else if content == ">" || content == "<" {
// We also have to check if the next string is an "="...
if let Some(queue_next) = query.pop_front() {
if queue_next == "=" {
condition = Some(if content == ">" {
QueryComparison::GreaterOrEqual
} else {
condition = Some(if content == ">" {
QueryComparison::Greater
} else {
QueryComparison::Less
});
value = queue_next.parse::<f64>().ok();
QueryComparison::LessOrEqual
});
if let Some(queue_next_next) = query.pop_front() {
value = queue_next_next.parse::<f64>().ok();
} else {
return Err(QueryError("Missing value".into()));
}
} else {
return Err(QueryError("Missing value".into()));
condition = Some(if content == ">" {
QueryComparison::Greater
} else {
QueryComparison::Less
});
value = queue_next.parse::<f64>().ok();
}
} else {
return Err(QueryError("Missing value".into()));
}
}
if let Some(condition) = condition {
if let Some(read_value) = value {
// Now we want to check one last thing - is there a unit?
// If no unit, assume base.
// Furthermore, base must be PEEKED at initially, and will
// require (likely) prefix_type specific checks
// Lastly, if it *is* a unit, remember to POP!
if let Some(condition) = condition {
if let Some(read_value) = value {
// Now we want to check one last thing - is there a unit?
// If no unit, assume base.
// Furthermore, base must be PEEKED at initially, and will
// require (likely) prefix_type specific checks
// Lastly, if it *is* a unit, remember to POP!
let mut value = read_value;
let mut value = read_value;
match prefix_type {
PrefixType::MemBytes
| PrefixType::Rps
| PrefixType::Wps
| PrefixType::TRead
| PrefixType::TWrite => {
if let Some(potential_unit) = query.front() {
match potential_unit.to_lowercase().as_str() {
"tb" => {
value *= 1_000_000_000_000.0;
query.pop_front();
}
"tib" => {
value *= 1_099_511_627_776.0;
query.pop_front();
}
"gb" => {
value *= 1_000_000_000.0;
query.pop_front();
}
"gib" => {
value *= 1_073_741_824.0;
query.pop_front();
}
"mb" => {
value *= 1_000_000.0;
query.pop_front();
}
"mib" => {
value *= 1_048_576.0;
query.pop_front();
}
"kb" => {
value *= 1000.0;
query.pop_front();
}
"kib" => {
value *= 1024.0;
query.pop_front();
}
"b" => {
// Just gotta pop.
query.pop_front();
}
_ => {}
match prefix_type {
PrefixType::MemBytes
| PrefixType::Rps
| PrefixType::Wps
| PrefixType::TRead
| PrefixType::TWrite => {
if let Some(potential_unit) = query.front() {
match potential_unit.to_lowercase().as_str() {
"tb" => {
value *= 1_000_000_000_000.0;
query.pop_front();
}
"tib" => {
value *= 1_099_511_627_776.0;
query.pop_front();
}
"gb" => {
value *= 1_000_000_000.0;
query.pop_front();
}
"gib" => {
value *= 1_073_741_824.0;
query.pop_front();
}
"mb" => {
value *= 1_000_000.0;
query.pop_front();
}
"mib" => {
value *= 1_048_576.0;
query.pop_front();
}
"kb" => {
value *= 1000.0;
query.pop_front();
}
"kib" => {
value *= 1024.0;
query.pop_front();
}
"b" => {
// Just gotta pop.
query.pop_front();
}
_ => {}
}
}
_ => {}
}
return Ok(Prefix {
or: None,
regex_prefix: None,
compare_prefix: Some((
prefix_type,
NumericalQuery { condition, value },
)),
});
_ => {}
}
return Ok(Prefix {
or: None,
regex_prefix: None,
compare_prefix: Some((
prefix_type,
NumericalQuery { condition, value },
)),
});
}
}
}
} else {
return Err(QueryError("Missing argument for search prefix".into()));
}
} else {
return Err(QueryError("Missing argument for search prefix".into()));
}
} else if inside_quotation {
// Uh oh, it's empty with quotes!
return Err(QueryError("Missing closing quotation".into()));
}
Err(QueryError("Invalid query".into()))
} else if inside_quotation {
// Uh oh, it's empty with quotes!
return Err(QueryError("Missing closing quotation".into()));
}
let mut split_query = VecDeque::new();
self.get_current_search_query()
.split_whitespace()
.for_each(|s| {
// From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses
let mut last = 0;
for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
if last != index {
split_query.push_back(s[last..index].to_owned());
}
split_query.push_back(matched.to_owned());
last = index + matched.len();
}
if last < s.len() {
split_query.push_back(s[last..].to_owned());
}
});
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,
)?;
Ok(process_filter)
Err(QueryError("Invalid query".into()))
}
let mut split_query = VecDeque::new();
search_query.split_whitespace().for_each(|s| {
// From https://stackoverflow.com/a/56923739 in order to get a split but include the parentheses
let mut last = 0;
for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
if last != index {
split_query.push_back(s[last..index].to_owned());
}
split_query.push_back(matched.to_owned());
last = index + matched.len();
}
if last < s.len() {
split_query.push_back(s[last..].to_owned());
}
});
let mut process_filter = process_string_to_filter(&mut split_query)?;
process_filter.process_regexes(
is_searching_whole_word,
is_ignoring_case,
is_searching_with_regex,
)?;
Ok(process_filter)
}
pub struct Query {
@ -467,7 +452,7 @@ impl Query {
Ok(())
}
pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool {
pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
self.query
.iter()
.all(|ok| ok.check(process, is_using_command))
@ -507,7 +492,7 @@ impl Or {
Ok(())
}
pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool {
pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
if let Some(rhs) = &self.rhs {
self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command)
} else {
@ -552,7 +537,7 @@ impl And {
Ok(())
}
pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool {
pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
if let Some(rhs) = &self.rhs {
self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command)
} else {
@ -662,7 +647,7 @@ impl Prefix {
Ok(())
}
pub fn check(&self, process: &ConvertedProcessData, is_using_command: bool) -> bool {
pub fn check(&self, process: &ProcessHarvest, is_using_command: bool) -> bool {
fn matches_condition(condition: &QueryComparison, lhs: f64, rhs: f64) -> bool {
match condition {
QueryComparison::Equal => (lhs - rhs).abs() < std::f64::EPSILON,
@ -686,9 +671,12 @@ impl Prefix {
PrefixType::Pid => r.is_match(process.pid.to_string().as_str()),
PrefixType::State => r.is_match(process.process_state.as_str()),
PrefixType::User => {
if let Some(user) = &process.user {
r.is_match(user.as_str())
} else {
#[cfg(target_family = "unix")]
{
r.is_match(process.user.as_ref())
}
#[cfg(not(target_family = "unix"))]
{
false
}
}
@ -701,12 +689,12 @@ impl Prefix {
match prefix_type {
PrefixType::PCpu => matches_condition(
&numerical_query.condition,
process.cpu_percent_usage,
process.cpu_usage_percent,
numerical_query.value,
),
PrefixType::PMem => matches_condition(
&numerical_query.condition,
process.mem_percent_usage,
process.mem_usage_percent,
numerical_query.value,
),
PrefixType::MemBytes => matches_condition(
@ -716,22 +704,22 @@ impl Prefix {
),
PrefixType::Rps => matches_condition(
&numerical_query.condition,
process.rps_f64,
process.read_bytes_per_sec as f64,
numerical_query.value,
),
PrefixType::Wps => matches_condition(
&numerical_query.condition,
process.wps_f64,
process.write_bytes_per_sec as f64,
numerical_query.value,
),
PrefixType::TRead => matches_condition(
&numerical_query.condition,
process.tr_f64,
process.total_read_bytes as f64,
numerical_query.value,
),
PrefixType::TWrite => matches_condition(
&numerical_query.condition,
process.tw_f64,
process.total_write_bytes as f64,
numerical_query.value,
),
_ => true,

View File

@ -83,7 +83,7 @@ pub trait Component {
let y = event.row;
let bounds = self.bounds();
x >= bounds.left() && x < bounds.right() && y >= bounds.top() && y < bounds.bottom()
does_bound_intersect_coordinate(x, y, bounds)
}
/// Returns whether a [`MouseEvent`] intersects a [`Component`]'s bounds, including any borders, if there are.
@ -92,10 +92,14 @@ pub trait Component {
let y = event.row;
let bounds = self.border_bounds();
x >= bounds.left() && x < bounds.right() && y >= bounds.top() && y < bounds.bottom()
does_bound_intersect_coordinate(x, y, bounds)
}
}
pub fn does_bound_intersect_coordinate(x: u16, y: u16, bounds: Rect) -> bool {
x >= bounds.left() && x < bounds.right() && y >= bounds.top() && y < bounds.bottom()
}
/// A trait for actual fully-fledged widgets to be displayed in bottom.
#[enum_dispatch]
#[allow(unused_variables)]

View File

@ -1,3 +1,5 @@
use std::cmp::Ordering;
use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use tui::{layout::Rect, widgets::TableState};
@ -83,7 +85,6 @@ impl Scrollable {
} else if self.current_index >= num_visible_rows {
// Else if the current position past the last element visible in the list, omit
// until we can see that element. The +1 is of how indexes start at 0.
self.window_index.index = self.current_index - num_visible_rows + 1;
self.window_index.index
} else {
@ -96,6 +97,8 @@ impl Scrollable {
// If it's past the first element, then show from that element downwards
self.window_index.index = self.current_index;
} else if self.current_index >= self.window_index.index + num_visible_rows {
// Else, if the current index is off screen (sometimes caused by a sudden size change),
// just put it so that the selected index is the last entry,
self.window_index.index = self.current_index - num_visible_rows + 1;
}
// Else, don't change what our start position is from whatever it is set to!
@ -111,8 +114,6 @@ impl Scrollable {
/// Update the index with this! This will automatically update the scroll direction as well!
pub fn set_index(&mut self, new_index: usize) {
use std::cmp::Ordering;
match new_index.cmp(&self.current_index) {
Ordering::Greater => {
self.current_index = new_index;
@ -156,9 +157,7 @@ impl Scrollable {
}
let new_index = self.current_index + change_by;
if new_index >= self.num_items {
WidgetEventResult::NoRedraw
} else if self.current_index == new_index {
if new_index >= self.num_items || self.current_index == new_index {
WidgetEventResult::NoRedraw
} else {
self.set_index(new_index);
@ -234,12 +233,16 @@ impl Component for Scrollable {
let y = usize::from(event.row - self.bounds.top());
if let Some(selected) = self.tui_state.selected() {
if y > selected {
let offset = y - selected;
return self.move_down(offset);
} else if y < selected {
let offset = selected - y;
return self.move_up(offset);
match y.cmp(&selected) {
Ordering::Less => {
let offset = selected - y;
return self.move_up(offset);
}
Ordering::Equal => {}
Ordering::Greater => {
let offset = y - selected;
return self.move_down(offset);
}
}
}
}

View File

@ -49,7 +49,7 @@ impl SortMenu {
let data = columns
.iter()
.map(|c| vec![(c.original_name().clone().into(), None, None)])
.map(|c| vec![(c.original_name().clone(), None, None)])
.collect::<Vec<_>>();
self.table

View File

@ -11,7 +11,7 @@ use crate::{
canvas::Painter,
};
use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn, TextTableData};
use super::text_table::{DesiredColumnWidth, SimpleColumn, TableColumn, TextTableDataRef};
fn get_shortcut_name(e: &KeyEvent) -> String {
let modifier = if e.modifiers.is_empty() {
@ -48,7 +48,7 @@ fn get_shortcut_name(e: &KeyEvent) -> String {
KeyCode::Esc => "Esc".into(),
};
format!("({}{})", modifier, key).into()
format!("({}{})", modifier, key)
}
#[derive(Copy, Clone, Debug)]
@ -346,8 +346,8 @@ where
/// Note if the number of columns don't match in the [`SortableTextTable`] and data,
/// it will only create as many columns as it can grab data from both sources from.
pub fn draw_tui_table<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>,
block_area: Rect, show_selected_entry: bool,
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef,
block: Block<'_>, block_area: Rect, show_selected_entry: bool,
) {
self.table
.draw_tui_table(painter, f, data, block, block_area, show_selected_entry);
@ -360,7 +360,7 @@ where
{
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
for (index, column) in self.table.columns.iter().enumerate() {
if let &Some((shortcut, _)) = column.shortcut() {
if let Some((shortcut, _)) = *column.shortcut() {
if shortcut == event {
self.set_sort_index(index);
return WidgetEventResult::Signal(ReturnSignal::Update);

View File

@ -1,47 +1,87 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
use tui::layout::Rect;
use itertools::Itertools;
use tui::{
backend::Backend,
layout::{Alignment, Rect},
text::{Span, Spans},
widgets::Paragraph,
Frame,
};
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
use unicode_width::UnicodeWidthStr;
use crate::app::{
event::WidgetEventResult::{self},
Component,
use crate::{
app::{
event::{
ReturnSignal,
WidgetEventResult::{self},
},
Component,
},
canvas::Painter,
};
enum CursorDirection {
Left,
Right,
}
/// We save the previous window index for future reference, but we must invalidate if the area changes.
#[derive(Default)]
struct WindowIndex {
start_index: usize,
cached_area: Rect,
}
/// A single-line component for taking text inputs.
pub struct TextInput {
text: String,
cursor_index: usize,
bounds: Rect,
border_bounds: Rect,
cursor: GraphemeCursor,
cursor_direction: CursorDirection,
window_index: WindowIndex,
}
impl Default for TextInput {
fn default() -> Self {
Self {
text: Default::default(),
bounds: Default::default(),
cursor: GraphemeCursor::new(0, 0, true),
cursor_direction: CursorDirection::Right,
window_index: Default::default(),
}
}
}
impl TextInput {
/// Creates a new [`TextInput`].
pub fn new() -> Self {
Self {
..Default::default()
/// Returns a reference to the current query.
pub fn query(&self) -> &str {
&self.text
}
fn move_back(&mut self) -> usize {
let current_position = self.cursor.cur_cursor();
if let Ok(Some(new_position)) = self.cursor.prev_boundary(&self.text[..current_position], 0)
{
self.cursor_direction = CursorDirection::Left;
new_position
} else {
current_position
}
}
fn set_cursor(&mut self, new_cursor_index: usize) -> WidgetEventResult {
if self.cursor_index == new_cursor_index {
WidgetEventResult::NoRedraw
fn move_forward(&mut self) -> usize {
let current_position = self.cursor.cur_cursor();
if let Ok(Some(new_position)) = self
.cursor
.next_boundary(&self.text[current_position..], current_position)
{
self.cursor_direction = CursorDirection::Right;
new_position
} else {
self.cursor_index = new_cursor_index;
WidgetEventResult::Redraw
}
}
fn move_back(&mut self, amount_to_subtract: usize) -> WidgetEventResult {
self.set_cursor(self.cursor_index.saturating_sub(amount_to_subtract))
}
fn move_forward(&mut self, amount_to_add: usize) -> WidgetEventResult {
let new_cursor = self.cursor_index + amount_to_add;
if new_cursor >= self.text.len() {
self.set_cursor(self.text.len() - 1)
} else {
self.set_cursor(new_cursor)
current_position
}
}
@ -50,38 +90,199 @@ impl TextInput {
WidgetEventResult::NoRedraw
} else {
self.text = String::default();
self.cursor_index = 0;
WidgetEventResult::Redraw
self.cursor = GraphemeCursor::new(0, 0, true);
self.window_index = Default::default();
self.cursor_direction = CursorDirection::Left;
WidgetEventResult::Signal(ReturnSignal::Update)
}
}
fn move_word_forward(&mut self) -> WidgetEventResult {
// TODO: Implement this
WidgetEventResult::NoRedraw
let current_index = self.cursor.cur_cursor();
for (index, _word) in self.text[current_index..].unicode_word_indices() {
if index > current_index {
self.cursor.set_cursor(index);
self.cursor_direction = CursorDirection::Right;
return WidgetEventResult::Redraw;
}
}
self.cursor.set_cursor(self.text.len());
WidgetEventResult::Redraw
}
fn move_word_back(&mut self) -> WidgetEventResult {
// TODO: Implement this
let current_index = self.cursor.cur_cursor();
for (index, _word) in self.text[..current_index].unicode_word_indices().rev() {
if index < current_index {
self.cursor.set_cursor(index);
self.cursor_direction = CursorDirection::Left;
return WidgetEventResult::Redraw;
}
}
WidgetEventResult::NoRedraw
}
fn clear_previous_word(&mut self) -> WidgetEventResult {
// TODO: Implement this
WidgetEventResult::NoRedraw
fn clear_word_from_cursor(&mut self) -> WidgetEventResult {
// Fairly simple logic - create the word index iterator, skip the word that intersects with the current
// cursor location, draw the rest, update the string.
let current_index = self.cursor.cur_cursor();
let mut start_delete_index = 0;
let mut saw_non_whitespace = false;
for (index, word) in self.text[..current_index].split_word_bound_indices().rev() {
if word.trim().is_empty() {
if saw_non_whitespace {
// It's whitespace! Stop!
break;
}
} else {
saw_non_whitespace = true;
start_delete_index = index;
}
}
if start_delete_index == current_index {
WidgetEventResult::NoRedraw
} else {
self.text.drain(start_delete_index..current_index);
self.cursor = GraphemeCursor::new(start_delete_index, self.text.len(), true);
self.cursor_direction = CursorDirection::Left;
WidgetEventResult::Signal(ReturnSignal::Update)
}
}
fn clear_previous_grapheme(&mut self) -> WidgetEventResult {
// TODO: Implement this
WidgetEventResult::NoRedraw
let current_index = self.cursor.cur_cursor();
if current_index > 0 {
let new_index = self.move_back();
self.text.drain(new_index..current_index);
self.cursor = GraphemeCursor::new(new_index, self.text.len(), true);
self.cursor_direction = CursorDirection::Left;
WidgetEventResult::Signal(ReturnSignal::Update)
} else {
WidgetEventResult::NoRedraw
}
}
pub fn update(&mut self, new_text: String) {
self.text = new_text;
fn clear_current_grapheme(&mut self) -> WidgetEventResult {
let current_index = self.cursor.cur_cursor();
if self.cursor_index >= self.text.len() {
self.cursor_index = self.text.len() - 1;
if current_index < self.text.len() {
let current_index_bound = self.move_forward();
self.text.drain(current_index..current_index_bound);
self.cursor = GraphemeCursor::new(current_index, self.text.len(), true);
self.cursor_direction = CursorDirection::Left;
WidgetEventResult::Signal(ReturnSignal::Update)
} else {
WidgetEventResult::NoRedraw
}
}
fn insert_character(&mut self, c: char) -> WidgetEventResult {
let current_index = self.cursor.cur_cursor();
self.text.insert(current_index, c);
self.cursor = GraphemeCursor::new(current_index, self.text.len(), true);
self.move_forward();
WidgetEventResult::Signal(ReturnSignal::Update)
}
/// Updates the window indexes and returns the start index.
pub fn update_window_index(&mut self, num_visible_columns: usize) -> usize {
if self.window_index.cached_area != self.bounds {
self.window_index.start_index = 0;
self.window_index.cached_area = self.bounds;
}
let current_index = self.cursor.cur_cursor();
match self.cursor_direction {
CursorDirection::Right => {
if current_index < self.window_index.start_index + num_visible_columns {
self.window_index.start_index
} else if current_index >= num_visible_columns {
self.window_index.start_index = current_index - num_visible_columns + 1;
self.window_index.start_index
} else {
0
}
}
CursorDirection::Left => {
if current_index <= self.window_index.start_index {
self.window_index.start_index = current_index;
} else if current_index >= self.window_index.start_index + num_visible_columns {
self.window_index.start_index = current_index - num_visible_columns + 1;
}
self.window_index.start_index
}
}
}
/// Draws the [`TextInput`] on screen.
pub fn draw_text_input<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
self.set_bounds(area);
const SEARCH_PROMPT: &str = "> ";
let prompt = if area.width > 5 { SEARCH_PROMPT } else { "" };
let num_visible_columns = area.width as usize - prompt.len();
let start_position = self.update_window_index(num_visible_columns);
let cursor_start = self.cursor.cur_cursor();
let mut graphemes = self.text.grapheme_indices(true).peekable();
let mut current_grapheme_posn = 0;
graphemes
.peeking_take_while(|(index, _)| *index < start_position)
.for_each(|(_, s)| {
current_grapheme_posn += UnicodeWidthStr::width(s);
});
let before_cursor = graphemes
.peeking_take_while(|(index, _)| *index < cursor_start)
.map(|(_, grapheme)| grapheme)
.collect::<String>();
let cursor = graphemes
.next()
.map(|(_, grapheme)| grapheme)
.unwrap_or(" ");
let after_cursor = graphemes.map(|(_, grapheme)| grapheme).collect::<String>();
// FIXME: This is NOT done! This is an incomplete (but kinda working) implementation, for now.
let search_text = vec![Spans::from(vec![
Span::styled(
prompt,
if selected {
painter.colours.highlighted_border_style
} else {
painter.colours.text_style
},
),
Span::styled(before_cursor, painter.colours.text_style),
Span::styled(cursor, painter.colours.currently_selected_text_style),
Span::styled(after_cursor, painter.colours.text_style),
])];
f.render_widget(
Paragraph::new(search_text)
.style(painter.colours.text_style)
.alignment(Alignment::Left),
area,
);
}
}
impl Component for TextInput {
@ -93,35 +294,59 @@ impl Component for TextInput {
self.bounds = new_bounds;
}
fn border_bounds(&self) -> Rect {
self.border_bounds
}
fn set_border_bounds(&mut self, new_bounds: Rect) {
self.border_bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
if event.modifiers.is_empty() {
match event.code {
KeyCode::Left => self.move_back(1),
KeyCode::Right => self.move_forward(1),
KeyCode::Left => {
let original_cursor = self.cursor.cur_cursor();
if self.move_back() == original_cursor {
WidgetEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
}
}
KeyCode::Right => {
let original_cursor = self.cursor.cur_cursor();
if self.move_forward() == original_cursor {
WidgetEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
}
}
KeyCode::Backspace => self.clear_previous_grapheme(),
KeyCode::Delete => self.clear_current_grapheme(),
KeyCode::Char(c) => self.insert_character(c),
_ => WidgetEventResult::NoRedraw,
}
} else if let KeyModifiers::CONTROL = event.modifiers {
match event.code {
KeyCode::Char('a') => self.set_cursor(0),
KeyCode::Char('e') => self.set_cursor(self.text.len()),
KeyCode::Char('a') => {
let prev_index = self.cursor.cur_cursor();
self.cursor.set_cursor(0);
if self.cursor.cur_cursor() == prev_index {
WidgetEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
}
}
KeyCode::Char('e') => {
let prev_index = self.cursor.cur_cursor();
self.cursor.set_cursor(self.text.len());
if self.cursor.cur_cursor() == prev_index {
WidgetEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
}
}
KeyCode::Char('u') => self.clear_text(),
KeyCode::Char('w') => self.clear_previous_word(),
KeyCode::Char('w') => self.clear_word_from_cursor(),
KeyCode::Char('h') => self.clear_previous_grapheme(),
_ => WidgetEventResult::NoRedraw,
}
} else if let KeyModifiers::ALT = event.modifiers {
match event.code {
KeyCode::Char('b') => self.move_word_forward(),
KeyCode::Char('f') => self.move_word_back(),
KeyCode::Char('b') => self.move_word_back(),
KeyCode::Char('f') => self.move_word_forward(),
_ => WidgetEventResult::NoRedraw,
}
} else {
@ -133,15 +358,12 @@ impl Component for TextInput {
// We are assuming this is within bounds...
let x = event.column;
let widget_x = self.bounds.x;
let new_cursor_index = usize::from(x.saturating_sub(widget_x));
if new_cursor_index >= self.text.len() {
self.cursor_index = self.text.len() - 1;
let widget_x = self.bounds.x + 2;
if x >= widget_x {
// TODO: do this
WidgetEventResult::Redraw
} else {
self.cursor_index = new_cursor_index;
WidgetEventResult::NoRedraw
}
WidgetEventResult::Redraw
}
}

View File

@ -40,6 +40,7 @@ pub trait TableColumn {
}
pub type TextTableData = Vec<Vec<(Cow<'static, str>, Option<Cow<'static, str>>, Option<Style>)>>;
pub type TextTableDataRef = [Vec<(Cow<'static, str>, Option<Cow<'static, str>>, Option<Style>)>];
/// A [`SimpleColumn`] represents some column in a [`TextTable`].
#[derive(Debug)]
@ -199,7 +200,7 @@ where
}
pub fn get_desired_column_widths(
columns: &[C], data: &TextTableData,
columns: &[C], data: &TextTableDataRef,
) -> Vec<DesiredColumnWidth> {
columns
.iter()
@ -237,7 +238,7 @@ where
.collect::<Vec<_>>()
}
fn get_cache(&mut self, area: Rect, data: &TextTableData) -> Vec<u16> {
fn get_cache(&mut self, area: Rect, data: &TextTableDataRef) -> Vec<u16> {
fn calculate_column_widths(
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
) -> Vec<u16> {
@ -296,9 +297,18 @@ where
column_widths
}
// If empty, do NOT save the cache! We have to get it again when it updates.
// If empty, get the cached values if they exist; if they don't, do not cache!
if data.is_empty() {
vec![0; self.columns.len()]
match &self.cached_column_widths {
CachedColumnWidths::Uncached => {
let desired_widths = TextTable::get_desired_column_widths(&self.columns, data);
calculate_column_widths(self.left_to_right, desired_widths, area.width)
}
CachedColumnWidths::Cached {
cached_area: _,
cached_data,
} => cached_data.clone(),
}
} else {
let was_cached: bool;
let column_widths = match &mut self.cached_column_widths {
@ -351,12 +361,9 @@ where
}
/// Draws a [`Table`] on screen corresponding to the [`TextTable`].
///
/// Note if the number of columns don't match in the [`TextTable`] and data,
/// it will only create as many columns as it can grab data from both sources from.
pub fn draw_tui_table<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>,
block_area: Rect, show_selected_entry: bool,
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef,
block: Block<'_>, block_area: Rect, show_selected_entry: bool,
) {
use tui::widgets::Row;

View File

@ -257,7 +257,7 @@ impl TimeGraph {
.style(painter.colours.graph_style)
.labels(
y_bound_labels
.into_iter()
.iter()
.map(|label| Span::styled(label.clone(), painter.colours.graph_style))
.collect(),
);

View File

@ -1,335 +0,0 @@
#[derive(Debug, Clone)]
pub struct Chart<'a> {
/// A block to display around the widget eventually
block: Option<Block<'a>>,
/// The horizontal axis
x_axis: Axis<'a>,
/// The vertical axis
y_axis: Axis<'a>,
/// A reference to the datasets
datasets: Vec<Dataset<'a>>,
/// The widget base style
style: Style,
/// Constraints used to determine whether the legend should be shown or not
hidden_legend_constraints: (Constraint, Constraint),
}
impl<'a> Chart<'a> {
pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
Chart {
block: None,
x_axis: Axis::default(),
y_axis: Axis::default(),
style: Default::default(),
datasets,
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
}
}
pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> Chart<'a> {
self.style = style;
self
}
pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
self.x_axis = axis;
self
}
pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
self.y_axis = axis;
self
}
/// Set the constraints used to determine whether the legend should be shown or not.
///
/// # Examples
///
/// ```
/// # use tui::widgets::Chart;
/// # use tui::layout::Constraint;
/// let constraints = (
/// Constraint::Ratio(1, 3),
/// Constraint::Ratio(1, 4)
/// );
/// // Hide the legend when either its width is greater than 33% of the total widget width
/// // or if its height is greater than 25% of the total widget height.
/// let _chart: Chart = Chart::new(vec![])
/// .hidden_legend_constraints(constraints);
/// ```
pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
self.hidden_legend_constraints = constraints;
self
}
/// Compute the internal layout of the chart given the area. If the area is too small some
/// elements may be automatically hidden
fn layout(&self, area: Rect) -> ChartLayout {
let mut layout = ChartLayout::default();
if area.height == 0 || area.width == 0 {
return layout;
}
let mut x = area.left();
let mut y = area.bottom() - 1;
if self.x_axis.labels.is_some() && y > area.top() {
layout.label_x = Some(y);
y -= 1;
}
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
x += self.max_width_of_labels_left_of_y_axis(area);
if self.x_axis.labels.is_some() && y > area.top() {
layout.axis_x = Some(y);
y -= 1;
}
if self.y_axis.labels.is_some() && x + 1 < area.right() {
layout.axis_y = Some(x);
x += 1;
}
if x < area.right() && y > 1 {
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
}
if let Some(ref title) = self.x_axis.title {
let w = title.width() as u16;
if w < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_x = Some((x + layout.graph_area.width - w, y));
}
}
if let Some(ref title) = self.y_axis.title {
let w = title.width() as u16;
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_y = Some((x, area.top()));
}
}
if let Some(inner_width) = self.datasets.iter().map(|d| d.name.width() as u16).max() {
let legend_width = inner_width + 2;
let legend_height = self.datasets.len() as u16 + 2;
let max_legend_width = self
.hidden_legend_constraints
.0
.apply(layout.graph_area.width);
let max_legend_height = self
.hidden_legend_constraints
.1
.apply(layout.graph_area.height);
if inner_width > 0
&& legend_width < max_legend_width
&& legend_height < max_legend_height
{
layout.legend_area = Some(Rect::new(
layout.graph_area.right() - legend_width,
layout.graph_area.top(),
legend_width,
legend_height,
));
}
}
layout
}
fn max_width_of_labels_left_of_y_axis(&self, area: Rect) -> u16 {
let mut max_width = self
.y_axis
.labels
.as_ref()
.map(|l| l.iter().map(Span::width).max().unwrap_or_default() as u16)
.unwrap_or_default();
if let Some(ref x_labels) = self.x_axis.labels {
if !x_labels.is_empty() {
max_width = max(max_width, x_labels[0].content.width() as u16);
}
}
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
max_width.min(area.width / 3)
}
fn render_x_labels(
&mut self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,
) {
let y = match layout.label_x {
Some(y) => y,
None => return,
};
let labels = self.x_axis.labels.as_ref().unwrap();
let labels_len = labels.len() as u16;
if labels_len < 2 {
return;
}
let width_between_ticks = graph_area.width / (labels_len - 1);
for (i, label) in labels.iter().enumerate() {
let label_width = label.width() as u16;
let label_width = if i == 0 {
// the first label is put between the left border of the chart and the y axis.
graph_area
.left()
.saturating_sub(chart_area.left())
.min(label_width)
} else {
// other labels are put on the left of each tick on the x axis
width_between_ticks.min(label_width)
};
buf.set_span(
graph_area.left() + i as u16 * width_between_ticks - label_width,
y,
label,
label_width,
);
}
}
fn render_y_labels(
&mut self, buf: &mut Buffer, layout: &ChartLayout, chart_area: Rect, graph_area: Rect,
) {
let x = match layout.label_y {
Some(x) => x,
None => return,
};
let labels = self.y_axis.labels.as_ref().unwrap();
let labels_len = labels.len() as u16;
let label_width = graph_area.left().saturating_sub(chart_area.left());
for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
if dy < graph_area.bottom() {
buf.set_span(x, graph_area.bottom() - 1 - dy, label, label_width as u16);
}
}
}
}
impl<'a> Widget for Chart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
return;
}
buf.set_style(area, self.style);
// Sample the style of the entire widget. This sample will be used to reset the style of
// the cells that are part of the components put on top of the graph area (i.e legend and
// axis names).
let original_style = buf.get(area.left(), area.top()).style();
let chart_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
let layout = self.layout(chart_area);
let graph_area = layout.graph_area;
if graph_area.width < 1 || graph_area.height < 1 {
return;
}
self.render_x_labels(buf, &layout, chart_area, graph_area);
self.render_y_labels(buf, &layout, chart_area, graph_area);
if let Some(y) = layout.axis_x {
for x in graph_area.left()..graph_area.right() {
buf.get_mut(x, y)
.set_symbol(symbols::line::HORIZONTAL)
.set_style(self.x_axis.style);
}
}
if let Some(x) = layout.axis_y {
for y in graph_area.top()..graph_area.bottom() {
buf.get_mut(x, y)
.set_symbol(symbols::line::VERTICAL)
.set_style(self.y_axis.style);
}
}
if let Some(y) = layout.axis_x {
if let Some(x) = layout.axis_y {
buf.get_mut(x, y)
.set_symbol(symbols::line::BOTTOM_LEFT)
.set_style(self.x_axis.style);
}
}
for dataset in &self.datasets {
Canvas::default()
.background_color(self.style.bg.unwrap_or(Color::Reset))
.x_bounds(self.x_axis.bounds)
.y_bounds(self.y_axis.bounds)
.marker(dataset.marker)
.paint(|ctx| {
ctx.draw(&Points {
coords: dataset.data,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
if let GraphType::Line = dataset.graph_type {
for data in dataset.data.windows(2) {
ctx.draw(&Line {
x1: data[0].0,
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color: dataset.style.fg.unwrap_or(Color::Reset),
})
}
}
})
.render(graph_area, buf);
}
if let Some(legend_area) = layout.legend_area {
buf.set_style(legend_area, original_style);
Block::default()
.borders(Borders::ALL)
.render(legend_area, buf);
for (i, dataset) in self.datasets.iter().enumerate() {
buf.set_string(
legend_area.x + 1,
legend_area.y + 1 + i as u16,
&dataset.name,
dataset.style,
);
}
}
if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x);
buf.set_style(
Rect {
x,
y,
width,
height: 1,
},
original_style,
);
buf.set_spans(x, y, &title, width);
}
if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap();
let width = graph_area.right().saturating_sub(x);
buf.set_style(
Rect {
x,
y,
width,
height: 1,
},
original_style,
);
buf.set_spans(x, y, &title, width);
}
}
}

View File

@ -495,7 +495,7 @@ impl NetGraph {
});
}
NetGraphCacheState::Cached(cache) => {
if current_max_value != cache.max_value {
if (current_max_value - cache.max_value).abs() > f64::EPSILON {
// Invalidated.
let (upper_bound, labels) = adjust_network_data_point(
current_max_value,
@ -692,7 +692,7 @@ impl Widget for OldNetGraph {
self.table.draw_tui_table(
painter,
f,
&vec![vec![
&[vec![
(
self.net_graph.rx_display.clone().into(),
None,

View File

@ -23,10 +23,12 @@ use crate::{
data_conversion::get_string_with_bytes,
data_harvester::processes::{self, ProcessSorting},
options::ProcessDefaults,
utils::error::BottomError,
};
use ProcessSorting::*;
use super::{
does_bound_intersect_coordinate,
sort_text_table::{SimpleSortableColumn, SortStatus, SortableColumn},
text_table::TextTableData,
AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection,
@ -559,7 +561,12 @@ impl ProcWidgetState {
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();
let parsed_query = parse_query(
self.get_current_search_query(),
self.process_search_state.is_searching_whole_word,
self.process_search_state.is_ignoring_case,
self.process_search_state.is_searching_with_regex,
);
// debug!("Parsed query: {:#?}", parsed_query);
if let Ok(parsed_query) = parsed_query {
@ -710,7 +717,7 @@ impl ProcessSortType {
match self {
ProcessSortType::Pid => Hard(Some(7)),
ProcessSortType::Count => Hard(Some(8)),
ProcessSortType::Name => Flex(0.35),
ProcessSortType::Name => Flex(0.3),
ProcessSortType::Command => Flex(0.7),
ProcessSortType::Cpu => Hard(Some(8)),
ProcessSortType::Mem => Hard(Some(8)),
@ -778,24 +785,6 @@ impl ProcessSortColumn {
sort_type,
}
}
pub fn sort_process(&self) {
match &self.sort_type {
ProcessSortType::Pid => {}
ProcessSortType::Count => {}
ProcessSortType::Name => {}
ProcessSortType::Command => {}
ProcessSortType::Cpu => {}
ProcessSortType::Mem => {}
ProcessSortType::MemPercent => {}
ProcessSortType::Rps => {}
ProcessSortType::Wps => {}
ProcessSortType::TotalRead => {}
ProcessSortType::TotalWrite => {}
ProcessSortType::User => {}
ProcessSortType::State => {}
}
}
}
impl SortableColumn for ProcessSortColumn {
@ -836,16 +825,12 @@ impl SortableColumn for ProcessSortColumn {
}
}
enum ProcessSortState {
Shown,
Hidden,
}
/// A searchable, sortable table to manage processes.
pub struct ProcessManager {
bounds: Rect,
process_table: SortableTextTable<ProcessSortColumn>,
sort_menu: SortMenu,
search_block_bounds: Rect,
search_input: TextInput,
@ -854,12 +839,14 @@ pub struct ProcessManager {
selected: ProcessManagerSelection,
in_tree_mode: bool,
sort_status: ProcessSortState,
show_sort: bool,
show_search: bool,
search_modifiers: SearchModifiers,
display_data: TextTableData,
process_filter: Option<Result<Query, BottomError>>,
}
impl ProcessManager {
@ -883,14 +870,16 @@ impl ProcessManager {
bounds: Rect::default(),
sort_menu: SortMenu::new(process_table_columns.len()),
process_table: SortableTextTable::new(process_table_columns).default_sort_index(2),
search_input: TextInput::new(),
search_input: TextInput::default(),
search_block_bounds: Rect::default(),
dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static...
selected: ProcessManagerSelection::Processes,
in_tree_mode: false,
sort_status: ProcessSortState::Hidden,
show_sort: false,
show_search: false,
search_modifiers: SearchModifiers::default(),
display_data: Default::default(),
process_filter: None,
};
manager.set_tree_mode(process_defaults.is_tree);
@ -917,7 +906,7 @@ impl ProcessManager {
} else {
self.sort_menu
.set_index(self.process_table.current_sorting_column_index());
self.sort_status = ProcessSortState::Shown;
self.show_sort = true;
self.selected = ProcessManagerSelection::Sort;
WidgetEventResult::Redraw
}
@ -952,17 +941,20 @@ impl Component for ProcessManager {
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
// "Global" handling:
match event.code {
KeyCode::Esc => {
if let ProcessSortState::Shown = self.sort_status {
self.sort_status = ProcessSortState::Hidden;
if let ProcessManagerSelection::Sort = self.selected {
self.selected = ProcessManagerSelection::Processes;
}
return WidgetEventResult::Redraw;
if let KeyCode::Esc = event.code {
if self.show_sort {
self.show_sort = false;
if let ProcessManagerSelection::Sort = self.selected {
self.selected = ProcessManagerSelection::Processes;
}
return WidgetEventResult::Redraw;
} else if self.show_search {
self.show_search = false;
if let ProcessManagerSelection::Search = self.selected {
self.selected = ProcessManagerSelection::Processes;
}
return WidgetEventResult::Redraw;
}
_ => {}
}
match self.selected {
@ -1023,13 +1015,18 @@ impl Component for ProcessManager {
self.process_table.handle_key_event(event)
}
ProcessManagerSelection::Sort => {
match event.code {
KeyCode::Enter if event.modifiers.is_empty() => {
self.process_table
.set_sort_index(self.sort_menu.current_index());
return WidgetEventResult::Signal(ReturnSignal::Update);
if event.modifiers.is_empty() {
match event.code {
KeyCode::Enter => {
self.process_table
.set_sort_index(self.sort_menu.current_index());
return WidgetEventResult::Signal(ReturnSignal::Update);
}
KeyCode::Char('/') => {
return self.open_search();
}
_ => {}
}
_ => {}
}
self.sort_menu.handle_key_event(event)
@ -1051,7 +1048,17 @@ impl Component for ProcessManager {
}
}
self.search_input.handle_key_event(event)
let handle_output = self.search_input.handle_key_event(event);
if let WidgetEventResult::Signal(ReturnSignal::Update) = handle_output {
self.process_filter = Some(parse_query(
self.search_input.query(),
self.is_searching_whole_word(),
!self.is_case_sensitive(),
self.is_searching_with_regex(),
));
}
handle_output
}
}
}
@ -1080,7 +1087,11 @@ impl Component for ProcessManager {
self.sort_menu.handle_mouse_event(event);
WidgetEventResult::Redraw
}
} else if self.search_input.does_border_intersect_mouse(&event) {
} else if does_bound_intersect_coordinate(
event.column,
event.row,
self.search_block_bounds,
) {
if let ProcessManagerSelection::Search = self.selected {
self.search_input.handle_mouse_event(event)
} else {
@ -1110,76 +1121,111 @@ impl Widget for ProcessManager {
fn draw<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
) {
match self.sort_status {
ProcessSortState::Shown => {
const SORT_CONSTRAINTS: [Constraint; 2] =
[Constraint::Length(10), Constraint::Min(0)];
let area = if self.show_search {
const SEARCH_CONSTRAINTS: [Constraint; 2] = [Constraint::Min(0), Constraint::Length(4)];
const INTERNAL_SEARCH_CONSTRAINTS: [Constraint; 2] = [Constraint::Length(1); 2];
let split_area = Layout::default()
.margin(0)
.direction(Direction::Horizontal)
.constraints(SORT_CONSTRAINTS)
.split(area);
let vertical_split_area = Layout::default()
.margin(0)
.direction(Direction::Vertical)
.constraints(SEARCH_CONSTRAINTS)
.split(area);
let sort_block = Block::default()
.border_style(if selected {
if let ProcessManagerSelection::Sort = self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
}
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
self.sort_menu.draw_sort_menu(
let is_search_selected = if selected {
matches!(self.selected, ProcessManagerSelection::Search)
} else {
false
};
let search_block = Block::default()
.border_style(if is_search_selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
self.search_block_bounds = vertical_split_area[1];
let internal_split_area = Layout::default()
.margin(0)
.direction(Direction::Vertical)
.constraints(INTERNAL_SEARCH_CONSTRAINTS)
.split(search_block.inner(vertical_split_area[1]));
if !internal_split_area.is_empty() {
self.search_input.draw_text_input(
painter,
f,
self.process_table.columns(),
sort_block,
split_area[0],
);
let process_block = Block::default()
.border_style(if selected {
if let ProcessManagerSelection::Processes = self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
}
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
self.process_table.draw_tui_table(
painter,
f,
&self.display_data,
process_block,
split_area[1],
selected,
internal_split_area[0],
is_search_selected,
);
}
ProcessSortState::Hidden => {
let block = Block::default()
.border_style(if selected {
if internal_split_area.len() == 2 {
// TODO: Draw buttons
}
f.render_widget(search_block, vertical_split_area[1]);
vertical_split_area[0]
} else {
area
};
let area = if self.show_sort {
const SORT_CONSTRAINTS: [Constraint; 2] = [Constraint::Length(10), Constraint::Min(0)];
let horizontal_split_area = Layout::default()
.margin(0)
.direction(Direction::Horizontal)
.constraints(SORT_CONSTRAINTS)
.split(area);
let sort_block = Block::default()
.border_style(if selected {
if let ProcessManagerSelection::Sort = self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
}
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
self.sort_menu.draw_sort_menu(
painter,
f,
self.process_table.columns(),
sort_block,
horizontal_split_area[0],
);
self.process_table.draw_tui_table(
painter,
f,
&self.display_data,
block,
area,
selected,
);
}
}
horizontal_split_area[1]
} else {
area
};
let process_block = Block::default()
.border_style(if selected {
if let ProcessManagerSelection::Processes = self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
}
} else {
painter.colours.border_style
})
.borders(Borders::ALL);
self.process_table.draw_tui_table(
painter,
f,
&self.display_data,
process_block,
area,
selected,
);
}
fn update_data(&mut self, data_collection: &DataCollection) {
@ -1187,8 +1233,17 @@ impl Widget for ProcessManager {
.process_harvest
.iter()
.filter(|process| {
// TODO: Filtering
true
if let Some(Ok(query)) = &self.process_filter {
query.check(
process,
matches!(
self.process_table.columns()[1].sort_type,
ProcessSortType::Command
),
)
} else {
true
}
})
.sorted_by(
match self.process_table.current_sorting_column().sort_type {

View File

@ -481,11 +481,7 @@ impl<'a> Widget for TimeChart<'a> {
) {
let interpolated_point = (
self.x_axis.bounds[0],
interpolate_point(
&older_point,
&newer_point,
self.x_axis.bounds[0],
),
interpolate_point(older_point, newer_point, self.x_axis.bounds[0]),
);
ctx.draw(&Points {
@ -522,11 +518,7 @@ impl<'a> Widget for TimeChart<'a> {
) {
let interpolated_point = (
self.x_axis.bounds[1],
interpolate_point(
&older_point,
&newer_point,
self.x_axis.bounds[1],
),
interpolate_point(older_point, newer_point, self.x_axis.bounds[1]),
);
ctx.draw(&Points {

View File

@ -603,6 +603,7 @@ pub fn create_input_thread(
sender: std::sync::mpsc::Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
) -> std::thread::JoinHandle<()> {
thread::spawn(move || {
// TODO: Maybe experiment with removing these timers. Look into using buffers instead?
let mut mouse_timer = Instant::now();
let mut keyboard_timer = Instant::now();