diff --git a/CHANGELOG.md b/CHANGELOG.md index 6595092d..c2635ebe 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 ee21188f..3285a7a3 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"`). diff --git a/src/app.rs b/src/app.rs index 4bab7b2a..ea34f259 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,8 @@ use std::{ time::Instant, }; -use unicode_segmentation::GraphemeCursor; +use concat_string::concat_string; +use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use typed_builder::*; @@ -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,58 @@ 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 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 + .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()); + 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 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(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_char_width; + + proc_widget_state.update_query(); + proc_widget_state.proc_search.search_state.cursor_direction = + CursorDirection::Right; + } + } + } } diff --git a/src/app/widgets/process_table.rs b/src/app/widgets/process_table.rs index bb268356..970e1cc5 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 diff --git a/src/bin/main.rs b/src/bin/main.rs index a6a8a1a5..e615c5db 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 0ac83582..844a1efe 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!( @@ -410,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() { @@ -425,12 +432,14 @@ 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() { - break; - } - keyboard_timer = Instant::now(); + if sender.send(BottomEvent::KeyInput(key)).is_err() { + break; } } Event::Mouse(mouse) => {