mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-04-08 17:05:59 +02:00
refactor: add text input
This commit is contained in:
parent
27736b7fc0
commit
b1889b0934
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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"
|
||||
|
@ -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.
|
||||
|
@ -1,3 +1,3 @@
|
||||
cognitive-complexity-threshold = 100
|
||||
type-complexity-threshold = 500
|
||||
too-many-arguments-threshold = 8
|
||||
too-many-arguments-threshold = 10
|
||||
|
44
src/app.rs
44
src/app.rs
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
))
|
||||
|
776
src/app/query.rs
776
src/app/query.rs
@ -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,
|
||||
|
@ -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)]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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(),
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user