Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: search paste support #881

Merged
merged 7 commits into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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