Made search look prettier and organized it a bit... also added match whole word functionality.

This commit is contained in:
ClementTsang 2020-02-02 23:15:24 -05:00
parent 1360296b4e
commit fc3a2e69ec
3 changed files with 202 additions and 72 deletions

View File

@ -46,8 +46,64 @@ pub struct AppScrollWidgetState {
pub widget_scroll_position: i64,
}
/// AppSearchState only deals with the search's state.
pub struct AppSearchState {}
/// AppSearchState only deals with the search's current settings and state.
pub struct AppSearchState {
current_search_query: String,
searching_pid: bool,
ignore_case: bool,
current_regex: std::result::Result<regex::Regex, regex::Error>,
current_cursor_position: usize,
match_word: bool,
use_regex: bool,
}
impl Default for AppSearchState {
fn default() -> Self {
AppSearchState {
current_search_query: String::default(),
searching_pid: false,
ignore_case: false,
current_regex: BASE_REGEX.clone(),
current_cursor_position: 0,
match_word: false,
use_regex: false,
}
}
}
impl AppSearchState {
pub fn toggle_ignore_case(&mut self) {
self.ignore_case = !self.ignore_case;
}
pub fn toggle_search_whole_word(&mut self) {
self.match_word = !self.match_word;
}
pub fn toggle_search_regex(&mut self) {
self.use_regex = !self.use_regex;
}
pub fn toggle_search_with_pid(&mut self) {
self.searching_pid = !self.searching_pid;
}
pub fn is_ignoring_case(&self) -> bool {
self.ignore_case
}
pub fn is_searching_whole_word(&self) -> bool {
self.match_word
}
pub fn is_searching_with_regex(&self) -> bool {
self.use_regex
}
pub fn is_searching_with_pid(&self) -> bool {
self.searching_pid
}
}
// TODO: [OPT] Group like fields together... this is kinda gross to step through
pub struct App {
@ -84,12 +140,8 @@ pub struct App {
pub canvas_data: canvas::DisplayableData,
enable_grouping: bool,
enable_searching: bool,
current_search_query: String,
searching_pid: bool,
pub ignore_case: bool,
current_regex: std::result::Result<regex::Regex, regex::Error>,
current_cursor_position: usize,
pub data_collection: DataCollection,
pub search_state: AppSearchState,
}
impl App {
@ -130,12 +182,8 @@ impl App {
canvas_data: canvas::DisplayableData::default(),
enable_grouping: false,
enable_searching: false,
current_search_query: String::default(),
searching_pid: false,
ignore_case: false,
current_regex: BASE_REGEX.clone(), //TODO: [OPT] seems like a thing we can switch to lifetimes to avoid cloning
current_cursor_position: 0,
data_collection: DataCollection::default(),
search_state: AppSearchState::default(),
}
}
@ -147,8 +195,8 @@ impl App {
self.current_widget_selected = WidgetPosition::Process;
self.enable_searching = false;
}
self.current_search_query = String::new();
self.searching_pid = false;
self.search_state.current_search_query = String::new();
self.search_state.searching_pid = false;
self.to_delete_process_list = None;
self.dd_err = None;
}
@ -189,7 +237,13 @@ impl App {
match self.current_widget_selected {
WidgetPosition::Process => self.toggle_grouping(),
WidgetPosition::Disk => {}
WidgetPosition::ProcessSearch => self.toggle_ignore_case(),
WidgetPosition::ProcessSearch => {
if self.search_state.is_searching_with_pid() {
self.search_with_name();
} else {
self.search_with_pid();
}
}
_ => {}
}
}
@ -226,7 +280,7 @@ impl App {
pub fn search_with_pid(&mut self) {
if !self.is_in_dialog() && self.is_searching() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.searching_pid = true;
self.search_state.searching_pid = true;
}
}
}
@ -234,43 +288,50 @@ impl App {
pub fn search_with_name(&mut self) {
if !self.is_in_dialog() && self.is_searching() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.searching_pid = false;
self.search_state.searching_pid = false;
}
}
}
pub fn is_searching_with_pid(&self) -> bool {
self.searching_pid
}
pub fn get_current_search_query(&self) -> &String {
&self.current_search_query
&self.search_state.current_search_query
}
pub fn toggle_ignore_case(&mut self) {
if !self.is_in_dialog() && self.is_searching() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.ignore_case = !self.ignore_case;
self.search_state.toggle_ignore_case();
self.update_regex();
self.update_process_gui = true;
}
}
}
fn update_regex(&mut self) {
self.current_regex = if self.current_search_query.is_empty() {
pub fn update_regex(&mut self) {
self.search_state.current_regex = if self.search_state.current_search_query.is_empty() {
BASE_REGEX.clone()
} else if self.ignore_case {
regex::Regex::new(&(format!("(?i){}", self.current_search_query)))
} else {
regex::Regex::new(&(self.current_search_query))
let mut final_regex_string = self.search_state.current_search_query.clone();
if !self.search_state.is_searching_with_regex() {
final_regex_string = regex::escape(&final_regex_string);
}
if self.search_state.is_searching_whole_word() {
final_regex_string = format!("^{}$", final_regex_string);
}
if self.search_state.is_ignoring_case() {
final_regex_string = format!("(?i){}", final_regex_string);
}
regex::Regex::new(&final_regex_string)
};
self.previous_process_position = 0;
self.currently_selected_process_position = 0;
}
pub fn get_cursor_position(&self) -> usize {
self.current_cursor_position
self.search_state.current_cursor_position
}
/// One of two functions allowed to run while in a dialog...
@ -292,10 +353,11 @@ impl App {
pub fn on_backspace(&mut self) {
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
if self.current_cursor_position > 0 {
self.current_cursor_position -= 1;
self.current_search_query
.remove(self.current_cursor_position);
if self.search_state.current_cursor_position > 0 {
self.search_state.current_cursor_position -= 1;
self.search_state
.current_search_query
.remove(self.search_state.current_cursor_position);
self.update_regex();
self.update_process_gui = true;
@ -304,7 +366,7 @@ impl App {
}
pub fn get_current_regex_matcher(&self) -> &std::result::Result<regex::Regex, regex::Error> {
&self.current_regex
&self.search_state.current_regex
}
pub fn on_up_key(&mut self) {
@ -328,8 +390,8 @@ impl App {
pub fn on_left_key(&mut self) {
if !self.is_in_dialog() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
if self.current_cursor_position > 0 {
self.current_cursor_position -= 1;
if self.search_state.current_cursor_position > 0 {
self.search_state.current_cursor_position -= 1;
}
}
}
@ -338,8 +400,10 @@ impl App {
pub fn on_right_key(&mut self) {
if !self.is_in_dialog() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
if self.current_cursor_position < self.current_search_query.len() {
self.current_cursor_position += 1;
if self.search_state.current_cursor_position
< self.search_state.current_search_query.len()
{
self.search_state.current_cursor_position += 1;
}
}
}
@ -348,7 +412,7 @@ impl App {
pub fn skip_cursor_beginning(&mut self) {
if !self.is_in_dialog() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.current_cursor_position = 0;
self.search_state.current_cursor_position = 0;
}
}
}
@ -356,7 +420,8 @@ impl App {
pub fn skip_cursor_end(&mut self) {
if !self.is_in_dialog() {
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.current_cursor_position = self.current_search_query.len();
self.search_state.current_cursor_position =
self.search_state.current_search_query.len();
}
}
}
@ -374,9 +439,10 @@ impl App {
self.last_key_press = current_key_press_inst;
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
self.current_search_query
.insert(self.current_cursor_position, caught_char);
self.current_cursor_position += 1;
self.search_state
.current_search_query
.insert(self.search_state.current_cursor_position, caught_char);
self.search_state.current_cursor_position += 1;
self.update_regex();

View File

@ -376,10 +376,24 @@ pub fn draw_data<B: backend::Backend>(
let processes_chunk = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([Constraint::Percentage(25), Constraint::Percentage(75)].as_ref())
.constraints(
if (bottom_chunks[1].height as f64 * 0.25) as u16 >= 4 {
[Constraint::Percentage(75), Constraint::Percentage(25)]
} else {
let required = if bottom_chunks[1].height < 10 {
bottom_chunks[1].height / 2
} else {
5
};
let remaining = bottom_chunks[1].height - required;
[Constraint::Length(remaining), Constraint::Length(required)]
}
.as_ref(),
)
.split(bottom_chunks[1]);
draw_search_field(&mut f, app_state, processes_chunk[0]);
draw_processes_table(&mut f, app_state, processes_chunk[1]);
draw_processes_table(&mut f, app_state, processes_chunk[0]);
draw_search_field(&mut f, app_state, processes_chunk[1]);
} else {
draw_processes_table(&mut f, app_state, bottom_chunks[1]);
}
@ -873,7 +887,7 @@ fn draw_disk_table<B: backend::Backend>(
fn draw_search_field<B: backend::Backend>(
f: &mut Frame<B>, app_state: &mut app::App, draw_loc: Rect,
) {
let width = draw_loc.width - 18; // TODO [SEARCH] this is hard-coded... ew
let width = max(0, draw_loc.width as i64 - 20) as u64; // TODO [SEARCH] this is hard-coded... ew
let query = app_state.get_current_search_query();
let shrunk_query = if query.len() < width as usize {
query
@ -909,36 +923,66 @@ fn draw_search_field<B: backend::Backend>(
}
}
let mut search_text = vec![
if app_state.is_searching_with_pid() {
Text::styled("\nPID", Style::default().fg(TABLE_HEADER_COLOUR))
let mut search_text = vec![if app_state.search_state.is_searching_with_pid() {
Text::styled(
"Search by PID (Tab for Name): ",
Style::default().fg(TABLE_HEADER_COLOUR),
)
} else {
Text::styled(
"Search by Name (Tab for PID): ",
Style::default().fg(TABLE_HEADER_COLOUR),
)
}];
// Text options shamelessly stolen from VS Code.
let option_text = vec![
Text::styled("\n\n", Style::default().fg(TABLE_HEADER_COLOUR)),
Text::styled(
"Match Case (Alt+C)",
Style::default().fg(TABLE_HEADER_COLOUR),
),
if !app_state.search_state.is_ignoring_case() {
Text::styled("[*]", Style::default().fg(TABLE_HEADER_COLOUR))
} else {
Text::styled("\nName", Style::default().fg(TABLE_HEADER_COLOUR))
Text::styled("[ ]", Style::default().fg(TABLE_HEADER_COLOUR))
},
if app_state.ignore_case {
Text::styled(" (Ignore Case): ", Style::default().fg(TABLE_HEADER_COLOUR))
Text::styled(" ", Style::default().fg(TABLE_HEADER_COLOUR)),
Text::styled(
"Match Whole Word (Alt+W)",
Style::default().fg(TABLE_HEADER_COLOUR),
),
if app_state.search_state.is_searching_whole_word() {
Text::styled("[*]", Style::default().fg(TABLE_HEADER_COLOUR))
} else {
Text::styled(": ", Style::default().fg(TABLE_HEADER_COLOUR))
Text::styled("[ ]", Style::default().fg(TABLE_HEADER_COLOUR))
},
Text::styled(" ", Style::default().fg(TABLE_HEADER_COLOUR)),
Text::styled(
"Use Regex (Alt+R)",
Style::default().fg(TABLE_HEADER_COLOUR),
),
if app_state.search_state.is_searching_with_regex() {
Text::styled("[*]", Style::default().fg(TABLE_HEADER_COLOUR))
} else {
Text::styled("[ ]", Style::default().fg(TABLE_HEADER_COLOUR))
},
];
search_text.extend(query_with_cursor);
search_text.extend(option_text);
// TODO: [SEARCH] Gotta make this easier to understand... it's pretty ugly cramming controls like this
Paragraph::new(search_text.iter())
.block(
Block::default()
.title("Search (Esc or Ctrl-f to close)")
.borders(Borders::ALL)
.border_style(if app_state.get_current_regex_matcher().is_err() {
Style::default().fg(Color::Red)
} else {
match app_state.current_widget_selected {
app::WidgetPosition::ProcessSearch => *CANVAS_HIGHLIGHTED_BORDER_STYLE,
_ => *CANVAS_BORDER_STYLE,
}
}),
)
.block(Block::default().borders(Borders::ALL).border_style(
if app_state.get_current_regex_matcher().is_err() {
Style::default().fg(Color::Red)
} else {
match app_state.current_widget_selected {
app::WidgetPosition::ProcessSearch => *CANVAS_HIGHLIGHTED_BORDER_STYLE,
_ => *CANVAS_BORDER_STYLE,
}
},
))
.style(Style::default().fg(Color::Gray))
.alignment(Alignment::Left)
.wrap(false)

View File

@ -133,7 +133,7 @@ fn main() -> error::Result<()> {
// Set default search method
if matches.is_present("CASE_INSENSITIVE_DEFAULT") {
app.ignore_case = true;
app.search_state.toggle_ignore_case();
}
// Set up up tui and crossterm
@ -257,8 +257,6 @@ fn main() -> error::Result<()> {
KeyCode::Right => app.move_right(),
KeyCode::Up => app.move_up(),
KeyCode::Down => app.move_down(),
KeyCode::Char('p') => app.search_with_pid(),
KeyCode::Char('n') => app.search_with_name(),
KeyCode::Char('r') => {
if rtx.send(ResetEvent::Reset).is_ok() {
app.reset();
@ -276,6 +274,28 @@ fn main() -> error::Result<()> {
KeyCode::Down => app.move_down(),
_ => {}
}
} else if let KeyModifiers::ALT = event.modifiers {
match event.code {
KeyCode::Char('c') => {
if app.is_in_search_widget() {
app.search_state.toggle_ignore_case();
app.update_regex();
}
}
KeyCode::Char('w') => {
if app.is_in_search_widget() {
app.search_state.toggle_search_whole_word();
app.update_regex();
}
}
KeyCode::Char('r') => {
if app.is_in_search_widget() {
app.search_state.toggle_search_regex();
app.update_regex();
}
}
_ => {}
}
}
}
@ -417,7 +437,7 @@ fn update_final_process_list(app: &mut app::App) {
.iter()
.filter(|(_pid, process)| {
if let Ok(matcher) = app.get_current_regex_matcher() {
if app.is_searching_with_pid() {
if app.search_state.is_searching_with_pid() {
matcher.is_match(&process.pid.to_string())
} else {
matcher.is_match(&process.name)