feature: search paste support (#881)

* feature: add pasting to search

Supports pasting events to the search bar (e.g. shift-insert, ctrl-shift-v).

* update docs

* clippy

* comment

* Update process.md

* remove keyboard event throttle

* fix issues with cjk/flag characters
This commit is contained in:
Clement Tsang 2022-11-10 00:40:04 -05:00 committed by GitHub
parent 5f849e81e6
commit 938c4ccd52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 145 additions and 25 deletions

View File

@ -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

View File

@ -102,6 +102,8 @@ Lastly, we can refine our search even further based on the other columns, like P
<img src="../../../assets/screenshots/process/search/cpu.webp" alt="A picture of searching for a process with a search condition that uses the CPU keyword."/>
</figure>
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"`).

View File

@ -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;
}
}
}
}

View File

@ -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

View File

@ -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);

View File

@ -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<I, J> {
KeyInput(I),
MouseInput(J),
PasteEvent(String),
Update(Box<data_harvester::Data>),
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) => {