mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-21 12:45:05 +02:00
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.
|
- [#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.
|
- [#868](https://github.com/ClementTsang/bottom/pull/868): Make temperature widget sortable.
|
||||||
- [#870](https://github.com/ClementTsang/bottom/pull/870): Make disk 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
|
## [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."/>
|
<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>
|
</figure>
|
||||||
|
|
||||||
|
You can also paste search queries (e.g. ++shift+insert++, ++ctrl+shift+v++).
|
||||||
|
|
||||||
#### Keywords
|
#### 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"`).
|
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,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use unicode_segmentation::GraphemeCursor;
|
use concat_string::concat_string;
|
||||||
|
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
|
||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
use typed_builder::*;
|
use typed_builder::*;
|
||||||
@ -35,7 +36,7 @@ pub mod widgets;
|
|||||||
|
|
||||||
use frozen_state::FrozenState;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum AxisScaling {
|
pub enum AxisScaling {
|
||||||
@ -2714,4 +2715,58 @@ impl App {
|
|||||||
1 + self.app_config_fields.table_gap
|
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;
|
mod sort_table;
|
||||||
use sort_table::SortTableColumn;
|
use sort_table::SortTableColumn;
|
||||||
|
use unicode_segmentation::GraphemeIncomplete;
|
||||||
|
|
||||||
/// ProcessSearchState only deals with process' search's current settings and state.
|
/// ProcessSearchState only deals with process' search's current settings and state.
|
||||||
pub struct ProcessSearchState {
|
pub struct ProcessSearchState {
|
||||||
@ -775,26 +776,69 @@ impl ProcWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn search_walk_forward(&mut self, start_position: usize) {
|
pub fn search_walk_forward(&mut self, start_position: usize) {
|
||||||
|
// 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(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
|
self.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
.grapheme_cursor
|
.grapheme_cursor
|
||||||
.next_boundary(
|
.provide_context(
|
||||||
&self.proc_search.search_state.current_search_query[start_position..],
|
&self.proc_search.search_state.current_search_query[0..ctx],
|
||||||
start_position,
|
0,
|
||||||
)
|
);
|
||||||
|
|
||||||
|
self.proc_search
|
||||||
|
.search_state
|
||||||
|
.grapheme_cursor
|
||||||
|
.next_boundary(chunk, start_position)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
_ => Err(err).unwrap(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn search_walk_back(&mut self, start_position: usize) {
|
pub fn search_walk_back(&mut self, start_position: usize) {
|
||||||
|
// 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(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
|
self.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
.grapheme_cursor
|
.grapheme_cursor
|
||||||
.prev_boundary(
|
.provide_context(
|
||||||
&self.proc_search.search_state.current_search_query[..start_position],
|
&self.proc_search.search_state.current_search_query[0..ctx],
|
||||||
0,
|
0,
|
||||||
)
|
);
|
||||||
|
|
||||||
|
self.proc_search
|
||||||
|
.search_state
|
||||||
|
.grapheme_cursor
|
||||||
|
.prev_boundary(chunk, 0)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
_ => Err(err).unwrap(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not
|
/// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not
|
||||||
/// visible (e.g. off screen).
|
/// visible (e.g. off screen).
|
||||||
|
@ -26,7 +26,7 @@ use std::{
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::EnableMouseCapture,
|
event::{EnableBracketedPaste, EnableMouseCapture},
|
||||||
execute,
|
execute,
|
||||||
terminal::{enable_raw_mode, EnterAlternateScreen},
|
terminal::{enable_raw_mode, EnterAlternateScreen},
|
||||||
};
|
};
|
||||||
@ -120,7 +120,12 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
// Set up up tui and crossterm
|
// Set up up tui and crossterm
|
||||||
let mut stdout_val = stdout();
|
let mut stdout_val = stdout();
|
||||||
execute!(stdout_val, EnterAlternateScreen, EnableMouseCapture)?;
|
execute!(
|
||||||
|
stdout_val,
|
||||||
|
EnterAlternateScreen,
|
||||||
|
EnableMouseCapture,
|
||||||
|
EnableBracketedPaste
|
||||||
|
)?;
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?;
|
let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?;
|
||||||
@ -151,6 +156,10 @@ fn main() -> Result<()> {
|
|||||||
handle_mouse_event(event, &mut app);
|
handle_mouse_event(event, &mut app);
|
||||||
update_data(&mut app);
|
update_data(&mut app);
|
||||||
}
|
}
|
||||||
|
BottomEvent::PasteEvent(paste) => {
|
||||||
|
app.handle_paste(paste);
|
||||||
|
update_data(&mut app);
|
||||||
|
}
|
||||||
BottomEvent::Update(data) => {
|
BottomEvent::Update(data) => {
|
||||||
app.data_collection.eat_data(data);
|
app.data_collection.eat_data(data);
|
||||||
|
|
||||||
|
25
src/lib.rs
25
src/lib.rs
@ -28,8 +28,8 @@ use std::{
|
|||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{
|
event::{
|
||||||
poll, read, DisableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent,
|
poll, read, DisableBracketedPaste, DisableMouseCapture, Event, KeyCode, KeyEvent,
|
||||||
MouseEventKind,
|
KeyModifiers, MouseEvent, MouseEventKind,
|
||||||
},
|
},
|
||||||
execute,
|
execute,
|
||||||
style::Print,
|
style::Print,
|
||||||
@ -71,6 +71,7 @@ pub type Pid = libc::pid_t;
|
|||||||
pub enum BottomEvent<I, J> {
|
pub enum BottomEvent<I, J> {
|
||||||
KeyInput(I),
|
KeyInput(I),
|
||||||
MouseInput(J),
|
MouseInput(J),
|
||||||
|
PasteEvent(String),
|
||||||
Update(Box<data_harvester::Data>),
|
Update(Box<data_harvester::Data>),
|
||||||
Clean,
|
Clean,
|
||||||
}
|
}
|
||||||
@ -273,6 +274,7 @@ pub fn cleanup_terminal(
|
|||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
execute!(
|
execute!(
|
||||||
terminal.backend_mut(),
|
terminal.backend_mut(),
|
||||||
|
DisableBracketedPaste,
|
||||||
DisableMouseCapture,
|
DisableMouseCapture,
|
||||||
LeaveAlternateScreen
|
LeaveAlternateScreen
|
||||||
)?;
|
)?;
|
||||||
@ -311,7 +313,13 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
|
|||||||
let stacktrace: String = format!("{:?}", backtrace::Backtrace::new());
|
let stacktrace: String = format!("{:?}", backtrace::Backtrace::new());
|
||||||
|
|
||||||
disable_raw_mode().unwrap();
|
disable_raw_mode().unwrap();
|
||||||
execute!(stdout, DisableMouseCapture, LeaveAlternateScreen).unwrap();
|
execute!(
|
||||||
|
stdout,
|
||||||
|
DisableBracketedPaste,
|
||||||
|
DisableMouseCapture,
|
||||||
|
LeaveAlternateScreen
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Print stack trace. Must be done after!
|
// Print stack trace. Must be done after!
|
||||||
execute!(
|
execute!(
|
||||||
@ -410,7 +418,6 @@ pub fn create_input_thread(
|
|||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut mouse_timer = Instant::now();
|
let mut mouse_timer = Instant::now();
|
||||||
let mut keyboard_timer = Instant::now();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok(is_terminated) = termination_ctrl_lock.try_lock() {
|
if let Ok(is_terminated) = termination_ctrl_lock.try_lock() {
|
||||||
@ -425,12 +432,14 @@ pub fn create_input_thread(
|
|||||||
if let Ok(event) = read() {
|
if let Ok(event) = read() {
|
||||||
// FIXME: Handle all other event cases.
|
// FIXME: Handle all other event cases.
|
||||||
match event {
|
match event {
|
||||||
Event::Key(key) => {
|
Event::Paste(paste) => {
|
||||||
if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 {
|
if sender.send(BottomEvent::PasteEvent(paste)).is_err() {
|
||||||
if sender.send(BottomEvent::KeyInput(key)).is_err() {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
keyboard_timer = Instant::now();
|
}
|
||||||
|
Event::Key(key) => {
|
||||||
|
if sender.send(BottomEvent::KeyInput(key)).is_err() {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Mouse(mouse) => {
|
Event::Mouse(mouse) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user