Skip to content

Commit

Permalink
feature: search paste support (#881)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ClementTsang authored Nov 10, 2022
1 parent 5f849e8 commit 938c4cc
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions docs/content/usage/widgets/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`).
Expand Down
59 changes: 57 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
}
}
}
68 changes: 56 additions & 12 deletions src/app/widgets/process_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use std::{

use anyhow::{Context, Result};
use crossterm::{
event::EnableMouseCapture,
event::{EnableBracketedPaste, EnableMouseCapture},
execute,
terminal::{enable_raw_mode, EnterAlternateScreen},
};
Expand Down Expand Up @@ -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))?;
Expand Down Expand Up @@ -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);

Expand Down
27 changes: 18 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -273,6 +274,7 @@ pub fn cleanup_terminal(
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
DisableBracketedPaste,
DisableMouseCapture,
LeaveAlternateScreen
)?;
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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() {
Expand All @@ -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) => {
Expand Down

0 comments on commit 938c4cc

Please sign in to comment.