From e7135d6b91818344074c7835f45ec17c72033b54 Mon Sep 17 00:00:00 2001 From: ClementTsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 9 Nov 2022 01:52:20 -0500 Subject: [PATCH 1/7] feature: add pasting to search Supports pasting events to the search bar (e.g. shift-insert, ctrl-shift-v). --- src/app.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++- src/bin/main.rs | 13 +++++++++-- src/lib.rs | 19 +++++++++++++--- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4bab7b2a5..369ef13b9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,7 @@ use std::{ time::Instant, }; +use concat_string::concat_string; use unicode_segmentation::GraphemeCursor; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -35,7 +36,7 @@ pub mod widgets; use frozen_state::FrozenState; -const MAX_SEARCH_LENGTH: usize = 200; +const MAX_SEARCH_LENGTH: usize = 200; // FIXME: Remove this limit, it's unnecessary. #[derive(Debug, Clone)] pub enum AxisScaling { @@ -2714,4 +2715,60 @@ impl App { 1 + self.app_config_fields.table_gap } } + + /// A quick and dirty way to handle paste events. + pub fn handle_paste(&mut self, paste: String) { + // Partially copy-pasted from the single-char variant; should probably optimize this process in the future. + let is_in_search_widget = self.is_in_search_widget(); + if let Some(proc_widget_state) = self + .proc_state + .widget_states + .get_mut(&(self.current_widget.widget_id - 1)) + { + let curr_width = UnicodeWidthStr::width( + proc_widget_state + .proc_search + .search_state + .current_search_query + .as_str(), + ); + let paste_width = UnicodeWidthStr::width(paste.as_str()); + + if is_in_search_widget + && proc_widget_state.is_search_enabled() + && curr_width + paste_width <= MAX_SEARCH_LENGTH + { + let left_bound = proc_widget_state.get_search_cursor_position(); + let new_left_bound = (left_bound + paste_width).saturating_sub(1); + let curr_query = &mut proc_widget_state + .proc_search + .search_state + .current_search_query; + let (left, right) = curr_query.split_at(left_bound); + *curr_query = concat_string!(left, paste, right); + + proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new( + new_left_bound, + proc_widget_state + .proc_search + .search_state + .current_search_query + .len(), + true, + ); + proc_widget_state.search_walk_forward(new_left_bound); + + proc_widget_state + .proc_search + .search_state + .char_cursor_position += paste_width; + + proc_widget_state.update_query(); + proc_widget_state.proc_search.search_state.cursor_direction = + CursorDirection::Right; + + return; + } + } + } } diff --git a/src/bin/main.rs b/src/bin/main.rs index a6a8a1a56..e615c5db7 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -26,7 +26,7 @@ use std::{ use anyhow::{Context, Result}; use crossterm::{ - event::EnableMouseCapture, + event::{EnableBracketedPaste, EnableMouseCapture}, execute, terminal::{enable_raw_mode, EnterAlternateScreen}, }; @@ -120,7 +120,12 @@ fn main() -> Result<()> { // Set up up tui and crossterm let mut stdout_val = stdout(); - execute!(stdout_val, EnterAlternateScreen, EnableMouseCapture)?; + execute!( + stdout_val, + EnterAlternateScreen, + EnableMouseCapture, + EnableBracketedPaste + )?; enable_raw_mode()?; let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?; @@ -151,6 +156,10 @@ fn main() -> Result<()> { handle_mouse_event(event, &mut app); update_data(&mut app); } + BottomEvent::PasteEvent(paste) => { + app.handle_paste(paste); + update_data(&mut app); + } BottomEvent::Update(data) => { app.data_collection.eat_data(data); diff --git a/src/lib.rs b/src/lib.rs index 0ac83582b..7d8a3fe9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,8 +28,8 @@ use std::{ use crossterm::{ event::{ - poll, read, DisableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, - MouseEventKind, + poll, read, DisableBracketedPaste, DisableMouseCapture, Event, KeyCode, KeyEvent, + KeyModifiers, MouseEvent, MouseEventKind, }, execute, style::Print, @@ -71,6 +71,7 @@ pub type Pid = libc::pid_t; pub enum BottomEvent { KeyInput(I), MouseInput(J), + PasteEvent(String), Update(Box), Clean, } @@ -273,6 +274,7 @@ pub fn cleanup_terminal( disable_raw_mode()?; execute!( terminal.backend_mut(), + DisableBracketedPaste, DisableMouseCapture, LeaveAlternateScreen )?; @@ -311,7 +313,13 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) { let stacktrace: String = format!("{:?}", backtrace::Backtrace::new()); disable_raw_mode().unwrap(); - execute!(stdout, DisableMouseCapture, LeaveAlternateScreen).unwrap(); + execute!( + stdout, + DisableBracketedPaste, + DisableMouseCapture, + LeaveAlternateScreen + ) + .unwrap(); // Print stack trace. Must be done after! execute!( @@ -425,6 +433,11 @@ pub fn create_input_thread( if let Ok(event) = read() { // FIXME: Handle all other event cases. match event { + Event::Paste(paste) => { + if sender.send(BottomEvent::PasteEvent(paste)).is_err() { + break; + } + } Event::Key(key) => { if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 { if sender.send(BottomEvent::KeyInput(key)).is_err() { From 1acc3f8eb82ade7832b6f68fac7a150c72e5d8c3 Mon Sep 17 00:00:00 2001 From: ClementTsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 9 Nov 2022 01:56:11 -0500 Subject: [PATCH 2/7] update docs --- CHANGELOG.md | 1 + docs/content/usage/widgets/process.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6595092d3..c2635ebef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#841](https://github.com/ClementTsang/bottom/pull/841): Add page up/page down support for the help screen. - [#868](https://github.com/ClementTsang/bottom/pull/868): Make temperature widget sortable. - [#870](https://github.com/ClementTsang/bottom/pull/870): Make disk widget sortable. +- [#881](https://github.com/ClementTsang/bottom/pull/881): Add pasting to the search bar. ## [0.6.8] - 2022-02-01 diff --git a/docs/content/usage/widgets/process.md b/docs/content/usage/widgets/process.md index ee21188f9..3285a7a34 100644 --- a/docs/content/usage/widgets/process.md +++ b/docs/content/usage/widgets/process.md @@ -102,6 +102,8 @@ Lastly, we can refine our search even further based on the other columns, like P A picture of searching for a process with a search condition that uses the CPU keyword. +You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++). + #### Keywords Note all keywords are case-insensitive. To search for a process/command that collides with a keyword, surround the term with quotes (e.x. `"cpu"`). From e5ba852a0b984cc5e1c0d46891e515b012a15372 Mon Sep 17 00:00:00 2001 From: ClementTsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 9 Nov 2022 01:56:33 -0500 Subject: [PATCH 3/7] clippy --- src/app.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 369ef13b9..da21edcde 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2766,8 +2766,6 @@ impl App { proc_widget_state.update_query(); proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Right; - - return; } } } From 5271210542d837260d51f347a17e912fa465bbc7 Mon Sep 17 00:00:00 2001 From: ClementTsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 9 Nov 2022 02:03:41 -0500 Subject: [PATCH 4/7] comment --- docs/content/usage/widgets/process.md | 2 +- src/app.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/content/usage/widgets/process.md b/docs/content/usage/widgets/process.md index 3285a7a34..17d192986 100644 --- a/docs/content/usage/widgets/process.md +++ b/docs/content/usage/widgets/process.md @@ -102,7 +102,7 @@ Lastly, we can refine our search even further based on the other columns, like P A picture of searching for a process with a search condition that uses the CPU keyword. -You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++). +You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++), though this may not work in Windows. #### Keywords diff --git a/src/app.rs b/src/app.rs index da21edcde..2b2c9998d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2718,7 +2718,8 @@ impl App { /// A quick and dirty way to handle paste events. pub fn handle_paste(&mut self, paste: String) { - // Partially copy-pasted from the single-char variant; should probably optimize this process in the future. + // Partially copy-pasted from the single-char variant; should probably clean up this process in the future. + // In particular, encapsulate this entire logic and add some tests to make it less potentially error-prone. let is_in_search_widget = self.is_in_search_widget(); if let Some(proc_widget_state) = self .proc_state From 7675ffac9cc02f33b0e15493b8eac9375f953f4b Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 9 Nov 2022 02:44:30 -0500 Subject: [PATCH 5/7] Update process.md --- docs/content/usage/widgets/process.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/usage/widgets/process.md b/docs/content/usage/widgets/process.md index 17d192986..4da2cecbf 100644 --- a/docs/content/usage/widgets/process.md +++ b/docs/content/usage/widgets/process.md @@ -102,7 +102,7 @@ Lastly, we can refine our search even further based on the other columns, like P A picture of searching for a process with a search condition that uses the CPU keyword. -You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++), though this may not work in Windows. +You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++), though this may not work in some Windows terminals. #### Keywords From 81636c9f101a1c72117227f290fbe25569c39e50 Mon Sep 17 00:00:00 2001 From: ClementTsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 9 Nov 2022 22:41:39 -0500 Subject: [PATCH 6/7] remove keyboard event throttle --- docs/content/usage/widgets/process.md | 2 +- src/lib.rs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/content/usage/widgets/process.md b/docs/content/usage/widgets/process.md index 4da2cecbf..3285a7a34 100644 --- a/docs/content/usage/widgets/process.md +++ b/docs/content/usage/widgets/process.md @@ -102,7 +102,7 @@ Lastly, we can refine our search even further based on the other columns, like P A picture of searching for a process with a search condition that uses the CPU keyword. -You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++), though this may not work in some Windows terminals. +You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++). #### Keywords diff --git a/src/lib.rs b/src/lib.rs index 7d8a3fe9d..844a1efea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -418,7 +418,6 @@ pub fn create_input_thread( ) -> JoinHandle<()> { thread::spawn(move || { let mut mouse_timer = Instant::now(); - let mut keyboard_timer = Instant::now(); loop { if let Ok(is_terminated) = termination_ctrl_lock.try_lock() { @@ -439,11 +438,8 @@ pub fn create_input_thread( } } Event::Key(key) => { - if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 { - if sender.send(BottomEvent::KeyInput(key)).is_err() { - break; - } - keyboard_timer = Instant::now(); + if sender.send(BottomEvent::KeyInput(key)).is_err() { + break; } } Event::Mouse(mouse) => { From 17a6c122043083fcc6bb522ad56416023c18a312 Mon Sep 17 00:00:00 2001 From: ClementTsang <34804052+ClementTsang@users.noreply.github.com> Date: Thu, 10 Nov 2022 00:15:06 -0500 Subject: [PATCH 7/7] fix issues with cjk/flag characters --- src/app.rs | 25 ++++++------ src/app/widgets/process_table.rs | 68 ++++++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2b2c9998d..ea34f2596 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,7 @@ use std::{ }; use concat_string::concat_string; -use unicode_segmentation::GraphemeCursor; +use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use typed_builder::*; @@ -2734,13 +2734,15 @@ impl App { .as_str(), ); let paste_width = UnicodeWidthStr::width(paste.as_str()); + let num_runes = UnicodeSegmentation::graphemes(paste.as_str(), true).count(); if is_in_search_widget && proc_widget_state.is_search_enabled() && curr_width + paste_width <= MAX_SEARCH_LENGTH { + let paste_char_width = paste.len(); let left_bound = proc_widget_state.get_search_cursor_position(); - let new_left_bound = (left_bound + paste_width).saturating_sub(1); + let curr_query = &mut proc_widget_state .proc_search .search_state @@ -2748,21 +2750,18 @@ impl App { let (left, right) = curr_query.split_at(left_bound); *curr_query = concat_string!(left, paste, right); - proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new( - new_left_bound, - proc_widget_state - .proc_search - .search_state - .current_search_query - .len(), - true, - ); - proc_widget_state.search_walk_forward(new_left_bound); + proc_widget_state.proc_search.search_state.grapheme_cursor = + GraphemeCursor::new(left_bound, curr_query.len(), true); + + for _ in 0..num_runes { + let cursor = proc_widget_state.get_search_cursor_position(); + proc_widget_state.search_walk_forward(cursor); + } proc_widget_state .proc_search .search_state - .char_cursor_position += paste_width; + .char_cursor_position += paste_char_width; proc_widget_state.update_query(); proc_widget_state.proc_search.search_state.cursor_direction = diff --git a/src/app/widgets/process_table.rs b/src/app/widgets/process_table.rs index bb2683567..970e1cc56 100644 --- a/src/app/widgets/process_table.rs +++ b/src/app/widgets/process_table.rs @@ -26,6 +26,7 @@ pub use proc_widget_data::*; mod sort_table; use sort_table::SortTableColumn; +use unicode_segmentation::GraphemeIncomplete; /// ProcessSearchState only deals with process' search's current settings and state. pub struct ProcessSearchState { @@ -775,25 +776,68 @@ impl ProcWidget { } pub fn search_walk_forward(&mut self, start_position: usize) { - self.proc_search + // TODO: Add tests for this. + let chunk = &self.proc_search.search_state.current_search_query[start_position..]; + + match self + .proc_search .search_state .grapheme_cursor - .next_boundary( - &self.proc_search.search_state.current_search_query[start_position..], - start_position, - ) - .unwrap(); + .next_boundary(chunk, start_position) + { + Ok(_) => {} + Err(err) => match err { + GraphemeIncomplete::PreContext(ctx) => { + // Provide the entire string as context. Not efficient but should resolve failures. + self.proc_search + .search_state + .grapheme_cursor + .provide_context( + &self.proc_search.search_state.current_search_query[0..ctx], + 0, + ); + + self.proc_search + .search_state + .grapheme_cursor + .next_boundary(chunk, start_position) + .unwrap(); + } + _ => Err(err).unwrap(), + }, + } } pub fn search_walk_back(&mut self, start_position: usize) { - self.proc_search + // TODO: Add tests for this. + let chunk = &self.proc_search.search_state.current_search_query[..start_position]; + match self + .proc_search .search_state .grapheme_cursor - .prev_boundary( - &self.proc_search.search_state.current_search_query[..start_position], - 0, - ) - .unwrap(); + .prev_boundary(chunk, 0) + { + Ok(_) => {} + Err(err) => match err { + GraphemeIncomplete::PreContext(ctx) => { + // Provide the entire string as context. Not efficient but should resolve failures. + self.proc_search + .search_state + .grapheme_cursor + .provide_context( + &self.proc_search.search_state.current_search_query[0..ctx], + 0, + ); + + self.proc_search + .search_state + .grapheme_cursor + .prev_boundary(chunk, 0) + .unwrap(); + } + _ => Err(err).unwrap(), + }, + } } /// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not