mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-23 13:45:12 +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]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.7.1"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
|
@ -60,7 +60,7 @@ thiserror = "1.0.24"
|
|||||||
toml = "0.5.8"
|
toml = "0.5.8"
|
||||||
tui = { version = "0.16.0", features = ["crossterm"], default-features = false }
|
tui = { version = "0.16.0", features = ["crossterm"], default-features = false }
|
||||||
typed-builder = "0.9.0"
|
typed-builder = "0.9.0"
|
||||||
unicode-segmentation = "1.7.1"
|
unicode-segmentation = "1.8.0"
|
||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
|
|
||||||
# For debugging only... disable on release builds with --no-default-target for no? TODO: Redo this.
|
# For debugging only... disable on release builds with --no-default-target for no? TODO: Redo this.
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
cognitive-complexity-threshold = 100
|
cognitive-complexity-threshold = 100
|
||||||
type-complexity-threshold = 500
|
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;
|
self.dd_err = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a global [`KeyEvent`], and returns [`Some(EventResult)`] if the global shortcut is consumed by some global
|
/// Handles a global [`KeyEvent`], and returns an [`EventResult`].
|
||||||
/// shortcut. If not, it returns [`None`].
|
fn handle_global_shortcut(&mut self, event: KeyEvent) -> EventResult {
|
||||||
fn handle_global_shortcut(&mut self, event: KeyEvent) -> Option<EventResult> {
|
|
||||||
// TODO: Write this.
|
// TODO: Write this.
|
||||||
|
|
||||||
if event.modifiers.is_empty() {
|
if event.modifiers.is_empty() {
|
||||||
@ -269,36 +268,36 @@ impl AppState {
|
|||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
if self.is_expanded {
|
if self.is_expanded {
|
||||||
self.is_expanded = false;
|
self.is_expanded = false;
|
||||||
Some(EventResult::Redraw)
|
EventResult::Redraw
|
||||||
} else if self.help_dialog_state.is_showing_help {
|
} else if self.help_dialog_state.is_showing_help {
|
||||||
self.help_dialog_state.is_showing_help = false;
|
self.help_dialog_state.is_showing_help = false;
|
||||||
self.help_dialog_state.scroll_state.current_scroll_index = 0;
|
self.help_dialog_state.scroll_state.current_scroll_index = 0;
|
||||||
Some(EventResult::Redraw)
|
EventResult::Redraw
|
||||||
} else if self.delete_dialog_state.is_showing_dd {
|
} else if self.delete_dialog_state.is_showing_dd {
|
||||||
self.close_dd();
|
self.close_dd();
|
||||||
Some(EventResult::Redraw)
|
EventResult::Redraw
|
||||||
} else {
|
} else {
|
||||||
None
|
EventResult::NoRedraw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') => Some(EventResult::Quit),
|
KeyCode::Char('q') => EventResult::Quit,
|
||||||
KeyCode::Char('e') => {
|
KeyCode::Char('e') => {
|
||||||
self.is_expanded = !self.is_expanded;
|
self.is_expanded = !self.is_expanded;
|
||||||
Some(EventResult::Redraw)
|
EventResult::Redraw
|
||||||
}
|
}
|
||||||
KeyCode::Char('?') => {
|
KeyCode::Char('?') => {
|
||||||
self.help_dialog_state.is_showing_help = true;
|
self.help_dialog_state.is_showing_help = true;
|
||||||
Some(EventResult::Redraw)
|
EventResult::Redraw
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => EventResult::NoRedraw,
|
||||||
}
|
}
|
||||||
} else if let KeyModifiers::CONTROL = event.modifiers {
|
} else if let KeyModifiers::CONTROL = event.modifiers {
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Char('c') => Some(EventResult::Quit),
|
KeyCode::Char('c') => EventResult::Quit,
|
||||||
_ => None,
|
_ => EventResult::NoRedraw,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
EventResult::NoRedraw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,15 +326,15 @@ impl AppState {
|
|||||||
pub fn handle_event(&mut self, event: BottomEvent) -> EventResult {
|
pub fn handle_event(&mut self, event: BottomEvent) -> EventResult {
|
||||||
match event {
|
match event {
|
||||||
BottomEvent::KeyInput(event) => {
|
BottomEvent::KeyInput(event) => {
|
||||||
if let Some(event_result) = self.handle_global_shortcut(event) {
|
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
|
||||||
// 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!
|
|
||||||
let result = widget.handle_key_event(event);
|
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 {
|
} else {
|
||||||
EventResult::NoRedraw
|
self.handle_global_shortcut(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BottomEvent::MouseInput(event) => {
|
BottomEvent::MouseInput(event) => {
|
||||||
@ -357,12 +356,11 @@ impl AppState {
|
|||||||
let was_id_already_selected = self.selected_widget == *id;
|
let was_id_already_selected = self.selected_widget == *id;
|
||||||
self.selected_widget = *id;
|
self.selected_widget = *id;
|
||||||
|
|
||||||
|
let result = widget.handle_mouse_event(event);
|
||||||
if was_id_already_selected {
|
if was_id_already_selected {
|
||||||
let result = widget.handle_mouse_event(event);
|
|
||||||
return self.convert_widget_event_result(result);
|
return self.convert_widget_event_result(result);
|
||||||
} else {
|
} else {
|
||||||
// If the aren't equal, *force* a redraw.
|
// If the aren't equal, *force* a redraw.
|
||||||
let result = widget.handle_mouse_event(event);
|
|
||||||
let _ = self.convert_widget_event_result(result);
|
let _ = self.convert_widget_event_result(result);
|
||||||
return EventResult::Redraw;
|
return EventResult::Redraw;
|
||||||
}
|
}
|
||||||
|
@ -218,7 +218,7 @@ fn read_proc(
|
|||||||
user: user_table
|
user: user_table
|
||||||
.get_uid_to_username_mapping(uid)
|
.get_uid_to_username_mapping(uid)
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.unwrap_or("N/A".into()),
|
.unwrap_or_else(|_| "N/A".into()),
|
||||||
},
|
},
|
||||||
new_process_times,
|
new_process_times,
|
||||||
))
|
))
|
||||||
|
776
src/app/query.rs
776
src/app/query.rs
@ -1,449 +1,434 @@
|
|||||||
use super::ProcWidgetState;
|
use crate::utils::error::{
|
||||||
use crate::{
|
BottomError::{self, QueryError},
|
||||||
data_conversion::ConvertedProcessData,
|
Result,
|
||||||
utils::error::{
|
|
||||||
BottomError::{self, QueryError},
|
|
||||||
Result,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::{borrow::Cow, collections::VecDeque};
|
use std::{borrow::Cow, collections::VecDeque};
|
||||||
|
|
||||||
|
use super::data_harvester::processes::ProcessHarvest;
|
||||||
|
|
||||||
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
|
const DELIMITER_LIST: [char; 6] = ['=', '>', '<', '(', ')', '\"'];
|
||||||
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
|
const COMPARISON_LIST: [&str; 3] = [">", "=", "<"];
|
||||||
const OR_LIST: [&str; 2] = ["or", "||"];
|
const OR_LIST: [&str; 2] = ["or", "||"];
|
||||||
const AND_LIST: [&str; 2] = ["and", "&&"];
|
const AND_LIST: [&str; 2] = ["and", "&&"];
|
||||||
|
|
||||||
/// I only separated this as otherwise, the states.rs file gets huge... and this should
|
/// In charge of parsing the given query.
|
||||||
/// belong in another file anyways, IMO.
|
/// We are defining the following language for a query (case-insensitive prefixes):
|
||||||
pub trait ProcessQuery {
|
///
|
||||||
/// In charge of parsing the given query.
|
/// - Process names: No prefix required, can use regex, match word, or case.
|
||||||
/// We are defining the following language for a query (case-insensitive prefixes):
|
/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
|
||||||
///
|
/// rather than a prefix.
|
||||||
/// - Process names: No prefix required, can use regex, match word, or case.
|
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
|
||||||
/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
|
/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare.
|
||||||
/// rather than a prefix.
|
/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare.
|
||||||
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
|
/// - STATE: Use prefix `state`, can use regex, match word, or case.
|
||||||
/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare.
|
/// - USER: Use prefix `user`, can use regex, match word, or case.
|
||||||
/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare.
|
/// - Read/s: Use prefix `r`. Can compare.
|
||||||
/// - STATE: Use prefix `state`, can use regex, match word, or case.
|
/// - Write/s: Use prefix `w`. Can compare.
|
||||||
/// - USER: Use prefix `user`, can use regex, match word, or case.
|
/// - Total read: Use prefix `read`. Can compare.
|
||||||
/// - Read/s: Use prefix `r`. Can compare.
|
/// - Total write: Use prefix `write`. Can compare.
|
||||||
/// - Write/s: Use prefix `w`. Can compare.
|
///
|
||||||
/// - Total read: Use prefix `read`. Can compare.
|
/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed
|
||||||
/// - Total write: Use prefix `write`. Can compare.
|
/// or quoted elements after splitting to treat as process names.
|
||||||
///
|
/// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
|
||||||
/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed
|
pub fn parse_query(
|
||||||
/// or quoted elements after splitting to treat as process names.
|
search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
|
||||||
/// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
|
is_searching_with_regex: bool,
|
||||||
fn parse_query(&self) -> Result<Query>;
|
) -> 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 {
|
while query.front().is_some() {
|
||||||
fn parse_query(&self) -> Result<Query> {
|
list_of_ors.push(process_or(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 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
|
Ok(Query { query: list_of_ors })
|
||||||
let mut lhs = process_and(query)?;
|
}
|
||||||
let mut rhs: Option<Box<And>> = None;
|
|
||||||
|
|
||||||
while let Some(queue_top) = query.front() {
|
fn process_or(query: &mut VecDeque<String>) -> Result<Or> {
|
||||||
// debug!("OR QT: {:?}", queue_top);
|
let mut lhs = process_and(query)?;
|
||||||
if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
let mut rhs: Option<Box<And>> = None;
|
||||||
query.pop_front();
|
|
||||||
rhs = Some(Box::new(process_and(query)?));
|
|
||||||
|
|
||||||
if let Some(queue_next) = query.front() {
|
while let Some(queue_top) = query.front() {
|
||||||
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
|
// debug!("OR QT: {:?}", queue_top);
|
||||||
// Must merge LHS and RHS
|
if OR_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
||||||
lhs = And {
|
query.pop_front();
|
||||||
lhs: Prefix {
|
rhs = Some(Box::new(process_and(query)?));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Or { lhs, rhs })
|
if let Some(queue_next) = query.front() {
|
||||||
}
|
if OR_LIST.contains(&queue_next.to_lowercase().as_str()) {
|
||||||
|
// Must merge LHS and RHS
|
||||||
fn process_and(query: &mut VecDeque<String>) -> Result<And> {
|
lhs = 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 {
|
|
||||||
lhs: Prefix {
|
lhs: Prefix {
|
||||||
or: list_of_ors.pop_front().map(Box::new),
|
or: Some(Box::new(Or { lhs, rhs })),
|
||||||
compare_prefix: None,
|
|
||||||
regex_prefix: None,
|
regex_prefix: None,
|
||||||
|
compare_prefix: None,
|
||||||
},
|
},
|
||||||
rhs: None,
|
rhs: None,
|
||||||
},
|
};
|
||||||
rhs: None,
|
rhs = None;
|
||||||
};
|
}
|
||||||
let returned_or = list_of_ors.into_iter().fold(initial_or, |lhs, rhs| Or {
|
} else {
|
||||||
lhs: And {
|
break;
|
||||||
lhs: Prefix {
|
}
|
||||||
or: Some(Box::new(lhs)),
|
} else if COMPARISON_LIST.contains(&queue_top.to_lowercase().as_str()) {
|
||||||
compare_prefix: None,
|
return Err(QueryError(Cow::Borrowed("Comparison not valid here")));
|
||||||
regex_prefix: None,
|
} else {
|
||||||
},
|
break;
|
||||||
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() {
|
Ok(Or { lhs, rhs })
|
||||||
if close_paren == ")" {
|
}
|
||||||
return Ok(Prefix {
|
|
||||||
or: Some(Box::new(returned_or)),
|
fn process_and(query: &mut VecDeque<String>) -> Result<And> {
|
||||||
regex_prefix: None,
|
let mut lhs = process_prefix(query, false)?;
|
||||||
compare_prefix: None,
|
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 {
|
} 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 {
|
} else {
|
||||||
return Err(QueryError("Missing closing parentheses".into()));
|
return Err(QueryError("Missing closing parentheses".into()));
|
||||||
}
|
}
|
||||||
} else if queue_top == ")" {
|
} else {
|
||||||
return Err(QueryError("Missing opening parentheses".into()));
|
return Err(QueryError("Missing closing parentheses".into()));
|
||||||
} else if queue_top == "\"" {
|
}
|
||||||
// Similar to parentheses, trap and check for missing closing quotes. Note, however, that we
|
} else if queue_top == ")" {
|
||||||
// will DIRECTLY call another process_prefix call...
|
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)?;
|
let prefix = process_prefix(query, true)?;
|
||||||
if let Some(close_paren) = query.pop_front() {
|
if let Some(close_paren) = query.pop_front() {
|
||||||
if close_paren == "\"" {
|
if close_paren == "\"" {
|
||||||
return Ok(prefix);
|
return Ok(prefix);
|
||||||
} else {
|
|
||||||
return Err(QueryError("Missing closing quotation".into()));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return Err(QueryError("Missing closing quotation".into()));
|
return Err(QueryError("Missing closing quotation".into()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Get prefix type...
|
return Err(QueryError("Missing closing quotation".into()));
|
||||||
let prefix_type = queue_top.parse::<PrefixType>()?;
|
}
|
||||||
let content = if let PrefixType::Name = prefix_type {
|
} else {
|
||||||
Some(queue_top)
|
// Get prefix type...
|
||||||
} else {
|
let prefix_type = queue_top.parse::<PrefixType>()?;
|
||||||
query.pop_front()
|
let content = if let PrefixType::Name = prefix_type {
|
||||||
};
|
Some(queue_top)
|
||||||
|
} else {
|
||||||
|
query.pop_front()
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(content) = content {
|
if let Some(content) = content {
|
||||||
match &prefix_type {
|
match &prefix_type {
|
||||||
PrefixType::Name => {
|
PrefixType::Name => {
|
||||||
return Ok(Prefix {
|
return Ok(Prefix {
|
||||||
or: None,
|
or: None,
|
||||||
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
|
regex_prefix: Some((prefix_type, StringQuery::Value(content))),
|
||||||
compare_prefix: None,
|
compare_prefix: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
PrefixType::Pid | PrefixType::State | PrefixType::User => {
|
PrefixType::Pid | PrefixType::State | PrefixType::User => {
|
||||||
// We have to check if someone put an "="...
|
// We have to check if someone put an "="...
|
||||||
if content == "=" {
|
if content == "=" {
|
||||||
// Check next string if possible
|
// Check next string if possible
|
||||||
if let Some(queue_next) = query.pop_front() {
|
if let Some(queue_next) = query.pop_front() {
|
||||||
// TODO: Need to consider the following cases:
|
// TODO: Need to consider the following cases:
|
||||||
// - (test)
|
// - (test)
|
||||||
// - (test
|
// - (test
|
||||||
// - test)
|
// - test)
|
||||||
// These are split into 2 to 3 different strings due to parentheses being
|
// These are split into 2 to 3 different strings due to parentheses being
|
||||||
// delimiters in our query system.
|
// delimiters in our query system.
|
||||||
//
|
//
|
||||||
// Do we want these to be valid? They should, as a string, right?
|
// 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 {
|
return Ok(Prefix {
|
||||||
or: None,
|
or: None,
|
||||||
regex_prefix: Some((
|
regex_prefix: Some((
|
||||||
prefix_type,
|
prefix_type,
|
||||||
StringQuery::Value(content),
|
StringQuery::Value(queue_next),
|
||||||
)),
|
)),
|
||||||
compare_prefix: None,
|
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 condition: Option<QueryComparison> = None;
|
||||||
let mut value: Option<f64> = None;
|
let mut value: Option<f64> = None;
|
||||||
|
|
||||||
if content == "=" {
|
if content == "=" {
|
||||||
condition = Some(QueryComparison::Equal);
|
condition = Some(QueryComparison::Equal);
|
||||||
if let Some(queue_next) = query.pop_front() {
|
if let Some(queue_next) = query.pop_front() {
|
||||||
value = queue_next.parse::<f64>().ok();
|
value = queue_next.parse::<f64>().ok();
|
||||||
} else {
|
} else {
|
||||||
return Err(QueryError("Missing value".into()));
|
return Err(QueryError("Missing value".into()));
|
||||||
}
|
}
|
||||||
} else if content == ">" || content == "<" {
|
} else if content == ">" || content == "<" {
|
||||||
// We also have to check if the next string is an "="...
|
// We also have to check if the next string is an "="...
|
||||||
if let Some(queue_next) = query.pop_front() {
|
if let Some(queue_next) = query.pop_front() {
|
||||||
if queue_next == "=" {
|
if queue_next == "=" {
|
||||||
condition = Some(if content == ">" {
|
condition = Some(if content == ">" {
|
||||||
QueryComparison::GreaterOrEqual
|
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()));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
condition = Some(if content == ">" {
|
QueryComparison::LessOrEqual
|
||||||
QueryComparison::Greater
|
});
|
||||||
} else {
|
if let Some(queue_next_next) = query.pop_front() {
|
||||||
QueryComparison::Less
|
value = queue_next_next.parse::<f64>().ok();
|
||||||
});
|
} else {
|
||||||
value = queue_next.parse::<f64>().ok();
|
return Err(QueryError("Missing value".into()));
|
||||||
}
|
}
|
||||||
} else {
|
} 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(condition) = condition {
|
||||||
if let Some(read_value) = value {
|
if let Some(read_value) = value {
|
||||||
// Now we want to check one last thing - is there a unit?
|
// Now we want to check one last thing - is there a unit?
|
||||||
// If no unit, assume base.
|
// If no unit, assume base.
|
||||||
// Furthermore, base must be PEEKED at initially, and will
|
// Furthermore, base must be PEEKED at initially, and will
|
||||||
// require (likely) prefix_type specific checks
|
// require (likely) prefix_type specific checks
|
||||||
// Lastly, if it *is* a unit, remember to POP!
|
// Lastly, if it *is* a unit, remember to POP!
|
||||||
|
|
||||||
let mut value = read_value;
|
let mut value = read_value;
|
||||||
|
|
||||||
match prefix_type {
|
match prefix_type {
|
||||||
PrefixType::MemBytes
|
PrefixType::MemBytes
|
||||||
| PrefixType::Rps
|
| PrefixType::Rps
|
||||||
| PrefixType::Wps
|
| PrefixType::Wps
|
||||||
| PrefixType::TRead
|
| PrefixType::TRead
|
||||||
| PrefixType::TWrite => {
|
| PrefixType::TWrite => {
|
||||||
if let Some(potential_unit) = query.front() {
|
if let Some(potential_unit) = query.front() {
|
||||||
match potential_unit.to_lowercase().as_str() {
|
match potential_unit.to_lowercase().as_str() {
|
||||||
"tb" => {
|
"tb" => {
|
||||||
value *= 1_000_000_000_000.0;
|
value *= 1_000_000_000_000.0;
|
||||||
query.pop_front();
|
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();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
"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()));
|
|
||||||
}
|
}
|
||||||
|
} else if inside_quotation {
|
||||||
Err(QueryError("Invalid query".into()))
|
// Uh oh, it's empty with quotes!
|
||||||
|
return Err(QueryError("Missing closing quotation".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut split_query = VecDeque::new();
|
Err(QueryError("Invalid query".into()))
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
pub struct Query {
|
||||||
@ -467,7 +452,7 @@ impl Query {
|
|||||||
Ok(())
|
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
|
self.query
|
||||||
.iter()
|
.iter()
|
||||||
.all(|ok| ok.check(process, is_using_command))
|
.all(|ok| ok.check(process, is_using_command))
|
||||||
@ -507,7 +492,7 @@ impl Or {
|
|||||||
Ok(())
|
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 {
|
if let Some(rhs) = &self.rhs {
|
||||||
self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command)
|
self.lhs.check(process, is_using_command) || rhs.check(process, is_using_command)
|
||||||
} else {
|
} else {
|
||||||
@ -552,7 +537,7 @@ impl And {
|
|||||||
Ok(())
|
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 {
|
if let Some(rhs) = &self.rhs {
|
||||||
self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command)
|
self.lhs.check(process, is_using_command) && rhs.check(process, is_using_command)
|
||||||
} else {
|
} else {
|
||||||
@ -662,7 +647,7 @@ impl Prefix {
|
|||||||
Ok(())
|
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 {
|
fn matches_condition(condition: &QueryComparison, lhs: f64, rhs: f64) -> bool {
|
||||||
match condition {
|
match condition {
|
||||||
QueryComparison::Equal => (lhs - rhs).abs() < std::f64::EPSILON,
|
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::Pid => r.is_match(process.pid.to_string().as_str()),
|
||||||
PrefixType::State => r.is_match(process.process_state.as_str()),
|
PrefixType::State => r.is_match(process.process_state.as_str()),
|
||||||
PrefixType::User => {
|
PrefixType::User => {
|
||||||
if let Some(user) = &process.user {
|
#[cfg(target_family = "unix")]
|
||||||
r.is_match(user.as_str())
|
{
|
||||||
} else {
|
r.is_match(process.user.as_ref())
|
||||||
|
}
|
||||||
|
#[cfg(not(target_family = "unix"))]
|
||||||
|
{
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -701,12 +689,12 @@ impl Prefix {
|
|||||||
match prefix_type {
|
match prefix_type {
|
||||||
PrefixType::PCpu => matches_condition(
|
PrefixType::PCpu => matches_condition(
|
||||||
&numerical_query.condition,
|
&numerical_query.condition,
|
||||||
process.cpu_percent_usage,
|
process.cpu_usage_percent,
|
||||||
numerical_query.value,
|
numerical_query.value,
|
||||||
),
|
),
|
||||||
PrefixType::PMem => matches_condition(
|
PrefixType::PMem => matches_condition(
|
||||||
&numerical_query.condition,
|
&numerical_query.condition,
|
||||||
process.mem_percent_usage,
|
process.mem_usage_percent,
|
||||||
numerical_query.value,
|
numerical_query.value,
|
||||||
),
|
),
|
||||||
PrefixType::MemBytes => matches_condition(
|
PrefixType::MemBytes => matches_condition(
|
||||||
@ -716,22 +704,22 @@ impl Prefix {
|
|||||||
),
|
),
|
||||||
PrefixType::Rps => matches_condition(
|
PrefixType::Rps => matches_condition(
|
||||||
&numerical_query.condition,
|
&numerical_query.condition,
|
||||||
process.rps_f64,
|
process.read_bytes_per_sec as f64,
|
||||||
numerical_query.value,
|
numerical_query.value,
|
||||||
),
|
),
|
||||||
PrefixType::Wps => matches_condition(
|
PrefixType::Wps => matches_condition(
|
||||||
&numerical_query.condition,
|
&numerical_query.condition,
|
||||||
process.wps_f64,
|
process.write_bytes_per_sec as f64,
|
||||||
numerical_query.value,
|
numerical_query.value,
|
||||||
),
|
),
|
||||||
PrefixType::TRead => matches_condition(
|
PrefixType::TRead => matches_condition(
|
||||||
&numerical_query.condition,
|
&numerical_query.condition,
|
||||||
process.tr_f64,
|
process.total_read_bytes as f64,
|
||||||
numerical_query.value,
|
numerical_query.value,
|
||||||
),
|
),
|
||||||
PrefixType::TWrite => matches_condition(
|
PrefixType::TWrite => matches_condition(
|
||||||
&numerical_query.condition,
|
&numerical_query.condition,
|
||||||
process.tw_f64,
|
process.total_write_bytes as f64,
|
||||||
numerical_query.value,
|
numerical_query.value,
|
||||||
),
|
),
|
||||||
_ => true,
|
_ => true,
|
||||||
|
@ -83,7 +83,7 @@ pub trait Component {
|
|||||||
let y = event.row;
|
let y = event.row;
|
||||||
let bounds = self.bounds();
|
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.
|
/// 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 y = event.row;
|
||||||
let bounds = self.border_bounds();
|
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.
|
/// A trait for actual fully-fledged widgets to be displayed in bottom.
|
||||||
#[enum_dispatch]
|
#[enum_dispatch]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
|
use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
|
||||||
use tui::{layout::Rect, widgets::TableState};
|
use tui::{layout::Rect, widgets::TableState};
|
||||||
|
|
||||||
@ -83,7 +85,6 @@ impl Scrollable {
|
|||||||
} else if self.current_index >= num_visible_rows {
|
} else if self.current_index >= num_visible_rows {
|
||||||
// Else if the current position past the last element visible in the list, omit
|
// 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.
|
// 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 = self.current_index - num_visible_rows + 1;
|
||||||
self.window_index.index
|
self.window_index.index
|
||||||
} else {
|
} else {
|
||||||
@ -96,6 +97,8 @@ impl Scrollable {
|
|||||||
// If it's past the first element, then show from that element downwards
|
// If it's past the first element, then show from that element downwards
|
||||||
self.window_index.index = self.current_index;
|
self.window_index.index = self.current_index;
|
||||||
} else if self.current_index >= self.window_index.index + num_visible_rows {
|
} 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;
|
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!
|
// 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!
|
/// Update the index with this! This will automatically update the scroll direction as well!
|
||||||
pub fn set_index(&mut self, new_index: usize) {
|
pub fn set_index(&mut self, new_index: usize) {
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
match new_index.cmp(&self.current_index) {
|
match new_index.cmp(&self.current_index) {
|
||||||
Ordering::Greater => {
|
Ordering::Greater => {
|
||||||
self.current_index = new_index;
|
self.current_index = new_index;
|
||||||
@ -156,9 +157,7 @@ impl Scrollable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let new_index = self.current_index + change_by;
|
let new_index = self.current_index + change_by;
|
||||||
if new_index >= self.num_items {
|
if new_index >= self.num_items || self.current_index == new_index {
|
||||||
WidgetEventResult::NoRedraw
|
|
||||||
} else if self.current_index == new_index {
|
|
||||||
WidgetEventResult::NoRedraw
|
WidgetEventResult::NoRedraw
|
||||||
} else {
|
} else {
|
||||||
self.set_index(new_index);
|
self.set_index(new_index);
|
||||||
@ -234,12 +233,16 @@ impl Component for Scrollable {
|
|||||||
let y = usize::from(event.row - self.bounds.top());
|
let y = usize::from(event.row - self.bounds.top());
|
||||||
|
|
||||||
if let Some(selected) = self.tui_state.selected() {
|
if let Some(selected) = self.tui_state.selected() {
|
||||||
if y > selected {
|
match y.cmp(&selected) {
|
||||||
let offset = y - selected;
|
Ordering::Less => {
|
||||||
return self.move_down(offset);
|
let offset = selected - y;
|
||||||
} else if y < selected {
|
return self.move_up(offset);
|
||||||
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
|
let data = columns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| vec![(c.original_name().clone().into(), None, None)])
|
.map(|c| vec![(c.original_name().clone(), None, None)])
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
self.table
|
self.table
|
||||||
|
@ -11,7 +11,7 @@ use crate::{
|
|||||||
canvas::Painter,
|
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 {
|
fn get_shortcut_name(e: &KeyEvent) -> String {
|
||||||
let modifier = if e.modifiers.is_empty() {
|
let modifier = if e.modifiers.is_empty() {
|
||||||
@ -48,7 +48,7 @@ fn get_shortcut_name(e: &KeyEvent) -> String {
|
|||||||
KeyCode::Esc => "Esc".into(),
|
KeyCode::Esc => "Esc".into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
format!("({}{})", modifier, key).into()
|
format!("({}{})", modifier, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
@ -346,8 +346,8 @@ where
|
|||||||
/// Note if the number of columns don't match in the [`SortableTextTable`] and data,
|
/// 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.
|
/// it will only create as many columns as it can grab data from both sources from.
|
||||||
pub fn draw_tui_table<B: Backend>(
|
pub fn draw_tui_table<B: Backend>(
|
||||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>,
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef,
|
||||||
block_area: Rect, show_selected_entry: bool,
|
block: Block<'_>, block_area: Rect, show_selected_entry: bool,
|
||||||
) {
|
) {
|
||||||
self.table
|
self.table
|
||||||
.draw_tui_table(painter, f, data, block, block_area, show_selected_entry);
|
.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 {
|
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
|
||||||
for (index, column) in self.table.columns.iter().enumerate() {
|
for (index, column) in self.table.columns.iter().enumerate() {
|
||||||
if let &Some((shortcut, _)) = column.shortcut() {
|
if let Some((shortcut, _)) = *column.shortcut() {
|
||||||
if shortcut == event {
|
if shortcut == event {
|
||||||
self.set_sort_index(index);
|
self.set_sort_index(index);
|
||||||
return WidgetEventResult::Signal(ReturnSignal::Update);
|
return WidgetEventResult::Signal(ReturnSignal::Update);
|
||||||
|
@ -1,47 +1,87 @@
|
|||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
|
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::{
|
use crate::{
|
||||||
event::WidgetEventResult::{self},
|
app::{
|
||||||
Component,
|
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)]
|
#[derive(Default)]
|
||||||
|
struct WindowIndex {
|
||||||
|
start_index: usize,
|
||||||
|
cached_area: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
/// A single-line component for taking text inputs.
|
/// A single-line component for taking text inputs.
|
||||||
pub struct TextInput {
|
pub struct TextInput {
|
||||||
text: String,
|
text: String,
|
||||||
cursor_index: usize,
|
|
||||||
bounds: Rect,
|
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 {
|
impl TextInput {
|
||||||
/// Creates a new [`TextInput`].
|
/// Returns a reference to the current query.
|
||||||
pub fn new() -> Self {
|
pub fn query(&self) -> &str {
|
||||||
Self {
|
&self.text
|
||||||
..Default::default()
|
}
|
||||||
|
|
||||||
|
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 {
|
fn move_forward(&mut self) -> usize {
|
||||||
if self.cursor_index == new_cursor_index {
|
let current_position = self.cursor.cur_cursor();
|
||||||
WidgetEventResult::NoRedraw
|
if let Ok(Some(new_position)) = self
|
||||||
|
.cursor
|
||||||
|
.next_boundary(&self.text[current_position..], current_position)
|
||||||
|
{
|
||||||
|
self.cursor_direction = CursorDirection::Right;
|
||||||
|
new_position
|
||||||
} else {
|
} else {
|
||||||
self.cursor_index = new_cursor_index;
|
current_position
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,38 +90,199 @@ impl TextInput {
|
|||||||
WidgetEventResult::NoRedraw
|
WidgetEventResult::NoRedraw
|
||||||
} else {
|
} else {
|
||||||
self.text = String::default();
|
self.text = String::default();
|
||||||
self.cursor_index = 0;
|
self.cursor = GraphemeCursor::new(0, 0, true);
|
||||||
WidgetEventResult::Redraw
|
self.window_index = Default::default();
|
||||||
|
self.cursor_direction = CursorDirection::Left;
|
||||||
|
WidgetEventResult::Signal(ReturnSignal::Update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_word_forward(&mut self) -> WidgetEventResult {
|
fn move_word_forward(&mut self) -> WidgetEventResult {
|
||||||
// TODO: Implement this
|
let current_index = self.cursor.cur_cursor();
|
||||||
WidgetEventResult::NoRedraw
|
|
||||||
|
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 {
|
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
|
WidgetEventResult::NoRedraw
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_previous_word(&mut self) -> WidgetEventResult {
|
fn clear_word_from_cursor(&mut self) -> WidgetEventResult {
|
||||||
// TODO: Implement this
|
// Fairly simple logic - create the word index iterator, skip the word that intersects with the current
|
||||||
WidgetEventResult::NoRedraw
|
// 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 {
|
fn clear_previous_grapheme(&mut self) -> WidgetEventResult {
|
||||||
// TODO: Implement this
|
let current_index = self.cursor.cur_cursor();
|
||||||
WidgetEventResult::NoRedraw
|
|
||||||
|
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) {
|
fn clear_current_grapheme(&mut self) -> WidgetEventResult {
|
||||||
self.text = new_text;
|
let current_index = self.cursor.cur_cursor();
|
||||||
|
|
||||||
if self.cursor_index >= self.text.len() {
|
if current_index < self.text.len() {
|
||||||
self.cursor_index = self.text.len() - 1;
|
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 {
|
impl Component for TextInput {
|
||||||
@ -93,35 +294,59 @@ impl Component for TextInput {
|
|||||||
self.bounds = new_bounds;
|
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 {
|
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
|
||||||
if event.modifiers.is_empty() {
|
if event.modifiers.is_empty() {
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Left => self.move_back(1),
|
KeyCode::Left => {
|
||||||
KeyCode::Right => self.move_forward(1),
|
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::Backspace => self.clear_previous_grapheme(),
|
||||||
|
KeyCode::Delete => self.clear_current_grapheme(),
|
||||||
|
KeyCode::Char(c) => self.insert_character(c),
|
||||||
_ => WidgetEventResult::NoRedraw,
|
_ => WidgetEventResult::NoRedraw,
|
||||||
}
|
}
|
||||||
} else if let KeyModifiers::CONTROL = event.modifiers {
|
} else if let KeyModifiers::CONTROL = event.modifiers {
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Char('a') => self.set_cursor(0),
|
KeyCode::Char('a') => {
|
||||||
KeyCode::Char('e') => self.set_cursor(self.text.len()),
|
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('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(),
|
KeyCode::Char('h') => self.clear_previous_grapheme(),
|
||||||
_ => WidgetEventResult::NoRedraw,
|
_ => WidgetEventResult::NoRedraw,
|
||||||
}
|
}
|
||||||
} else if let KeyModifiers::ALT = event.modifiers {
|
} else if let KeyModifiers::ALT = event.modifiers {
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Char('b') => self.move_word_forward(),
|
KeyCode::Char('b') => self.move_word_back(),
|
||||||
KeyCode::Char('f') => self.move_word_back(),
|
KeyCode::Char('f') => self.move_word_forward(),
|
||||||
_ => WidgetEventResult::NoRedraw,
|
_ => WidgetEventResult::NoRedraw,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -133,15 +358,12 @@ impl Component for TextInput {
|
|||||||
// We are assuming this is within bounds...
|
// We are assuming this is within bounds...
|
||||||
|
|
||||||
let x = event.column;
|
let x = event.column;
|
||||||
let widget_x = self.bounds.x;
|
let widget_x = self.bounds.x + 2;
|
||||||
let new_cursor_index = usize::from(x.saturating_sub(widget_x));
|
if x >= widget_x {
|
||||||
|
// TODO: do this
|
||||||
if new_cursor_index >= self.text.len() {
|
WidgetEventResult::Redraw
|
||||||
self.cursor_index = self.text.len() - 1;
|
|
||||||
} else {
|
} 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 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`].
|
/// A [`SimpleColumn`] represents some column in a [`TextTable`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -199,7 +200,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_desired_column_widths(
|
pub fn get_desired_column_widths(
|
||||||
columns: &[C], data: &TextTableData,
|
columns: &[C], data: &TextTableDataRef,
|
||||||
) -> Vec<DesiredColumnWidth> {
|
) -> Vec<DesiredColumnWidth> {
|
||||||
columns
|
columns
|
||||||
.iter()
|
.iter()
|
||||||
@ -237,7 +238,7 @@ where
|
|||||||
.collect::<Vec<_>>()
|
.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(
|
fn calculate_column_widths(
|
||||||
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
|
left_to_right: bool, mut desired_widths: Vec<DesiredColumnWidth>, total_width: u16,
|
||||||
) -> Vec<u16> {
|
) -> Vec<u16> {
|
||||||
@ -296,9 +297,18 @@ where
|
|||||||
column_widths
|
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() {
|
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 {
|
} else {
|
||||||
let was_cached: bool;
|
let was_cached: bool;
|
||||||
let column_widths = match &mut self.cached_column_widths {
|
let column_widths = match &mut self.cached_column_widths {
|
||||||
@ -351,12 +361,9 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a [`Table`] on screen corresponding to the [`TextTable`].
|
/// 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>(
|
pub fn draw_tui_table<B: Backend>(
|
||||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableData, block: Block<'_>,
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, data: &TextTableDataRef,
|
||||||
block_area: Rect, show_selected_entry: bool,
|
block: Block<'_>, block_area: Rect, show_selected_entry: bool,
|
||||||
) {
|
) {
|
||||||
use tui::widgets::Row;
|
use tui::widgets::Row;
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ impl TimeGraph {
|
|||||||
.style(painter.colours.graph_style)
|
.style(painter.colours.graph_style)
|
||||||
.labels(
|
.labels(
|
||||||
y_bound_labels
|
y_bound_labels
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|label| Span::styled(label.clone(), painter.colours.graph_style))
|
.map(|label| Span::styled(label.clone(), painter.colours.graph_style))
|
||||||
.collect(),
|
.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) => {
|
NetGraphCacheState::Cached(cache) => {
|
||||||
if current_max_value != cache.max_value {
|
if (current_max_value - cache.max_value).abs() > f64::EPSILON {
|
||||||
// Invalidated.
|
// Invalidated.
|
||||||
let (upper_bound, labels) = adjust_network_data_point(
|
let (upper_bound, labels) = adjust_network_data_point(
|
||||||
current_max_value,
|
current_max_value,
|
||||||
@ -692,7 +692,7 @@ impl Widget for OldNetGraph {
|
|||||||
self.table.draw_tui_table(
|
self.table.draw_tui_table(
|
||||||
painter,
|
painter,
|
||||||
f,
|
f,
|
||||||
&vec![vec![
|
&[vec![
|
||||||
(
|
(
|
||||||
self.net_graph.rx_display.clone().into(),
|
self.net_graph.rx_display.clone().into(),
|
||||||
None,
|
None,
|
||||||
|
@ -23,10 +23,12 @@ use crate::{
|
|||||||
data_conversion::get_string_with_bytes,
|
data_conversion::get_string_with_bytes,
|
||||||
data_harvester::processes::{self, ProcessSorting},
|
data_harvester::processes::{self, ProcessSorting},
|
||||||
options::ProcessDefaults,
|
options::ProcessDefaults,
|
||||||
|
utils::error::BottomError,
|
||||||
};
|
};
|
||||||
use ProcessSorting::*;
|
use ProcessSorting::*;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
does_bound_intersect_coordinate,
|
||||||
sort_text_table::{SimpleSortableColumn, SortStatus, SortableColumn},
|
sort_text_table::{SimpleSortableColumn, SortStatus, SortableColumn},
|
||||||
text_table::TextTableData,
|
text_table::TextTableData,
|
||||||
AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection,
|
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.is_invalid_search = false;
|
||||||
self.process_search_state.search_state.error_message = None;
|
self.process_search_state.search_state.error_message = None;
|
||||||
} else {
|
} 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);
|
// debug!("Parsed query: {:#?}", parsed_query);
|
||||||
|
|
||||||
if let Ok(parsed_query) = parsed_query {
|
if let Ok(parsed_query) = parsed_query {
|
||||||
@ -710,7 +717,7 @@ impl ProcessSortType {
|
|||||||
match self {
|
match self {
|
||||||
ProcessSortType::Pid => Hard(Some(7)),
|
ProcessSortType::Pid => Hard(Some(7)),
|
||||||
ProcessSortType::Count => Hard(Some(8)),
|
ProcessSortType::Count => Hard(Some(8)),
|
||||||
ProcessSortType::Name => Flex(0.35),
|
ProcessSortType::Name => Flex(0.3),
|
||||||
ProcessSortType::Command => Flex(0.7),
|
ProcessSortType::Command => Flex(0.7),
|
||||||
ProcessSortType::Cpu => Hard(Some(8)),
|
ProcessSortType::Cpu => Hard(Some(8)),
|
||||||
ProcessSortType::Mem => Hard(Some(8)),
|
ProcessSortType::Mem => Hard(Some(8)),
|
||||||
@ -778,24 +785,6 @@ impl ProcessSortColumn {
|
|||||||
sort_type,
|
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 {
|
impl SortableColumn for ProcessSortColumn {
|
||||||
@ -836,16 +825,12 @@ impl SortableColumn for ProcessSortColumn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProcessSortState {
|
|
||||||
Shown,
|
|
||||||
Hidden,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A searchable, sortable table to manage processes.
|
/// A searchable, sortable table to manage processes.
|
||||||
pub struct ProcessManager {
|
pub struct ProcessManager {
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
process_table: SortableTextTable<ProcessSortColumn>,
|
process_table: SortableTextTable<ProcessSortColumn>,
|
||||||
sort_menu: SortMenu,
|
sort_menu: SortMenu,
|
||||||
|
search_block_bounds: Rect,
|
||||||
|
|
||||||
search_input: TextInput,
|
search_input: TextInput,
|
||||||
|
|
||||||
@ -854,12 +839,14 @@ pub struct ProcessManager {
|
|||||||
selected: ProcessManagerSelection,
|
selected: ProcessManagerSelection,
|
||||||
|
|
||||||
in_tree_mode: bool,
|
in_tree_mode: bool,
|
||||||
sort_status: ProcessSortState,
|
show_sort: bool,
|
||||||
show_search: bool,
|
show_search: bool,
|
||||||
|
|
||||||
search_modifiers: SearchModifiers,
|
search_modifiers: SearchModifiers,
|
||||||
|
|
||||||
display_data: TextTableData,
|
display_data: TextTableData,
|
||||||
|
|
||||||
|
process_filter: Option<Result<Query, BottomError>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcessManager {
|
impl ProcessManager {
|
||||||
@ -883,14 +870,16 @@ impl ProcessManager {
|
|||||||
bounds: Rect::default(),
|
bounds: Rect::default(),
|
||||||
sort_menu: SortMenu::new(process_table_columns.len()),
|
sort_menu: SortMenu::new(process_table_columns.len()),
|
||||||
process_table: SortableTextTable::new(process_table_columns).default_sort_index(2),
|
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...
|
dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Maybe use something static...
|
||||||
selected: ProcessManagerSelection::Processes,
|
selected: ProcessManagerSelection::Processes,
|
||||||
in_tree_mode: false,
|
in_tree_mode: false,
|
||||||
sort_status: ProcessSortState::Hidden,
|
show_sort: false,
|
||||||
show_search: false,
|
show_search: false,
|
||||||
search_modifiers: SearchModifiers::default(),
|
search_modifiers: SearchModifiers::default(),
|
||||||
display_data: Default::default(),
|
display_data: Default::default(),
|
||||||
|
process_filter: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.set_tree_mode(process_defaults.is_tree);
|
manager.set_tree_mode(process_defaults.is_tree);
|
||||||
@ -917,7 +906,7 @@ impl ProcessManager {
|
|||||||
} else {
|
} else {
|
||||||
self.sort_menu
|
self.sort_menu
|
||||||
.set_index(self.process_table.current_sorting_column_index());
|
.set_index(self.process_table.current_sorting_column_index());
|
||||||
self.sort_status = ProcessSortState::Shown;
|
self.show_sort = true;
|
||||||
self.selected = ProcessManagerSelection::Sort;
|
self.selected = ProcessManagerSelection::Sort;
|
||||||
WidgetEventResult::Redraw
|
WidgetEventResult::Redraw
|
||||||
}
|
}
|
||||||
@ -952,17 +941,20 @@ impl Component for ProcessManager {
|
|||||||
|
|
||||||
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
|
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
|
||||||
// "Global" handling:
|
// "Global" handling:
|
||||||
match event.code {
|
if let KeyCode::Esc = event.code {
|
||||||
KeyCode::Esc => {
|
if self.show_sort {
|
||||||
if let ProcessSortState::Shown = self.sort_status {
|
self.show_sort = false;
|
||||||
self.sort_status = ProcessSortState::Hidden;
|
if let ProcessManagerSelection::Sort = self.selected {
|
||||||
if let ProcessManagerSelection::Sort = self.selected {
|
self.selected = ProcessManagerSelection::Processes;
|
||||||
self.selected = ProcessManagerSelection::Processes;
|
|
||||||
}
|
|
||||||
return WidgetEventResult::Redraw;
|
|
||||||
}
|
}
|
||||||
|
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 {
|
match self.selected {
|
||||||
@ -1023,13 +1015,18 @@ impl Component for ProcessManager {
|
|||||||
self.process_table.handle_key_event(event)
|
self.process_table.handle_key_event(event)
|
||||||
}
|
}
|
||||||
ProcessManagerSelection::Sort => {
|
ProcessManagerSelection::Sort => {
|
||||||
match event.code {
|
if event.modifiers.is_empty() {
|
||||||
KeyCode::Enter if event.modifiers.is_empty() => {
|
match event.code {
|
||||||
self.process_table
|
KeyCode::Enter => {
|
||||||
.set_sort_index(self.sort_menu.current_index());
|
self.process_table
|
||||||
return WidgetEventResult::Signal(ReturnSignal::Update);
|
.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)
|
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);
|
self.sort_menu.handle_mouse_event(event);
|
||||||
WidgetEventResult::Redraw
|
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 {
|
if let ProcessManagerSelection::Search = self.selected {
|
||||||
self.search_input.handle_mouse_event(event)
|
self.search_input.handle_mouse_event(event)
|
||||||
} else {
|
} else {
|
||||||
@ -1110,76 +1121,111 @@ impl Widget for ProcessManager {
|
|||||||
fn draw<B: Backend>(
|
fn draw<B: Backend>(
|
||||||
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
&mut self, painter: &Painter, f: &mut Frame<'_, B>, area: Rect, selected: bool,
|
||||||
) {
|
) {
|
||||||
match self.sort_status {
|
let area = if self.show_search {
|
||||||
ProcessSortState::Shown => {
|
const SEARCH_CONSTRAINTS: [Constraint; 2] = [Constraint::Min(0), Constraint::Length(4)];
|
||||||
const SORT_CONSTRAINTS: [Constraint; 2] =
|
const INTERNAL_SEARCH_CONSTRAINTS: [Constraint; 2] = [Constraint::Length(1); 2];
|
||||||
[Constraint::Length(10), Constraint::Min(0)];
|
|
||||||
|
|
||||||
let split_area = Layout::default()
|
let vertical_split_area = Layout::default()
|
||||||
.margin(0)
|
.margin(0)
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Vertical)
|
||||||
.constraints(SORT_CONSTRAINTS)
|
.constraints(SEARCH_CONSTRAINTS)
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let sort_block = Block::default()
|
let is_search_selected = if selected {
|
||||||
.border_style(if selected {
|
matches!(self.selected, ProcessManagerSelection::Search)
|
||||||
if let ProcessManagerSelection::Sort = self.selected {
|
} else {
|
||||||
painter.colours.highlighted_border_style
|
false
|
||||||
} else {
|
};
|
||||||
painter.colours.border_style
|
|
||||||
}
|
let search_block = Block::default()
|
||||||
} else {
|
.border_style(if is_search_selected {
|
||||||
painter.colours.border_style
|
painter.colours.highlighted_border_style
|
||||||
})
|
} else {
|
||||||
.borders(Borders::ALL);
|
painter.colours.border_style
|
||||||
self.sort_menu.draw_sort_menu(
|
})
|
||||||
|
.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,
|
painter,
|
||||||
f,
|
f,
|
||||||
self.process_table.columns(),
|
internal_split_area[0],
|
||||||
sort_block,
|
is_search_selected,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ProcessSortState::Hidden => {
|
|
||||||
let block = Block::default()
|
if internal_split_area.len() == 2 {
|
||||||
.border_style(if selected {
|
// 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
|
painter.colours.highlighted_border_style
|
||||||
} else {
|
} else {
|
||||||
painter.colours.border_style
|
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(
|
horizontal_split_area[1]
|
||||||
painter,
|
} else {
|
||||||
f,
|
area
|
||||||
&self.display_data,
|
};
|
||||||
block,
|
|
||||||
area,
|
let process_block = Block::default()
|
||||||
selected,
|
.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) {
|
fn update_data(&mut self, data_collection: &DataCollection) {
|
||||||
@ -1187,8 +1233,17 @@ impl Widget for ProcessManager {
|
|||||||
.process_harvest
|
.process_harvest
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|process| {
|
.filter(|process| {
|
||||||
// TODO: Filtering
|
if let Some(Ok(query)) = &self.process_filter {
|
||||||
true
|
query.check(
|
||||||
|
process,
|
||||||
|
matches!(
|
||||||
|
self.process_table.columns()[1].sort_type,
|
||||||
|
ProcessSortType::Command
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.sorted_by(
|
.sorted_by(
|
||||||
match self.process_table.current_sorting_column().sort_type {
|
match self.process_table.current_sorting_column().sort_type {
|
||||||
|
@ -481,11 +481,7 @@ impl<'a> Widget for TimeChart<'a> {
|
|||||||
) {
|
) {
|
||||||
let interpolated_point = (
|
let interpolated_point = (
|
||||||
self.x_axis.bounds[0],
|
self.x_axis.bounds[0],
|
||||||
interpolate_point(
|
interpolate_point(older_point, newer_point, self.x_axis.bounds[0]),
|
||||||
&older_point,
|
|
||||||
&newer_point,
|
|
||||||
self.x_axis.bounds[0],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.draw(&Points {
|
ctx.draw(&Points {
|
||||||
@ -522,11 +518,7 @@ impl<'a> Widget for TimeChart<'a> {
|
|||||||
) {
|
) {
|
||||||
let interpolated_point = (
|
let interpolated_point = (
|
||||||
self.x_axis.bounds[1],
|
self.x_axis.bounds[1],
|
||||||
interpolate_point(
|
interpolate_point(older_point, newer_point, self.x_axis.bounds[1]),
|
||||||
&older_point,
|
|
||||||
&newer_point,
|
|
||||||
self.x_axis.bounds[1],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.draw(&Points {
|
ctx.draw(&Points {
|
||||||
|
@ -603,6 +603,7 @@ pub fn create_input_thread(
|
|||||||
sender: std::sync::mpsc::Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
|
sender: std::sync::mpsc::Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
|
||||||
) -> std::thread::JoinHandle<()> {
|
) -> std::thread::JoinHandle<()> {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
// TODO: Maybe experiment with removing these timers. Look into using buffers instead?
|
||||||
let mut mouse_timer = Instant::now();
|
let mut mouse_timer = Instant::now();
|
||||||
let mut keyboard_timer = Instant::now();
|
let mut keyboard_timer = Instant::now();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user