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:
parent
5f849e81e6
commit
938c4ccd52
|
@ -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
|
||||
|
||||
|
|
|
@ -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"`).
|
||||
|
|
59
src/app.rs
59
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
27
src/lib.rs
27
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<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) => {
|
||||
|
|
Loading…
Reference in New Issue