mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-23 13:45:12 +02:00
Switch to using unicode_segmentation's cursor as a basis on how we do cursor movement in search.
This commit is contained in:
parent
d96751b786
commit
a755a5d41c
@ -38,6 +38,7 @@ backtrace = "0.3"
|
|||||||
toml = "0.5.6"
|
toml = "0.5.6"
|
||||||
serde = {version = "1.0", features = ["derive"] }
|
serde = {version = "1.0", features = ["derive"] }
|
||||||
unicode-segmentation = "1.6.0"
|
unicode-segmentation = "1.6.0"
|
||||||
|
unicode-width = "0.1.7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "0.12"
|
assert_cmd = "0.12"
|
||||||
|
182
src/app.rs
182
src/app.rs
@ -6,9 +6,12 @@ pub mod data_farmer;
|
|||||||
use data_farmer::*;
|
use data_farmer::*;
|
||||||
|
|
||||||
use crate::{canvas, constants, utils::error::Result};
|
use crate::{canvas, constants, utils::error::Result};
|
||||||
|
|
||||||
mod process_killer;
|
mod process_killer;
|
||||||
|
|
||||||
|
use unicode_segmentation::GraphemeCursor;
|
||||||
|
|
||||||
|
const MAX_SEARCH_LENGTH: usize = 200;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum WidgetPosition {
|
pub enum WidgetPosition {
|
||||||
Cpu,
|
Cpu,
|
||||||
@ -60,9 +63,9 @@ pub struct AppSearchState {
|
|||||||
pub is_enabled: bool,
|
pub is_enabled: bool,
|
||||||
current_search_query: String,
|
current_search_query: String,
|
||||||
current_regex: Option<std::result::Result<regex::Regex, regex::Error>>,
|
current_regex: Option<std::result::Result<regex::Regex, regex::Error>>,
|
||||||
current_cursor_position: usize,
|
|
||||||
pub is_blank_search: bool,
|
pub is_blank_search: bool,
|
||||||
pub is_invalid_search: bool,
|
pub is_invalid_search: bool,
|
||||||
|
pub grapheme_cursor: GraphemeCursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppSearchState {
|
impl Default for AppSearchState {
|
||||||
@ -71,9 +74,9 @@ impl Default for AppSearchState {
|
|||||||
is_enabled: false,
|
is_enabled: false,
|
||||||
current_search_query: String::default(),
|
current_search_query: String::default(),
|
||||||
current_regex: None,
|
current_regex: None,
|
||||||
current_cursor_position: 0,
|
|
||||||
is_invalid_search: false,
|
is_invalid_search: false,
|
||||||
is_blank_search: true,
|
is_blank_search: true,
|
||||||
|
grapheme_cursor: GraphemeCursor::new(0, 0, true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -535,7 +538,8 @@ impl App {
|
|||||||
pub fn get_cursor_position(&self) -> usize {
|
pub fn get_cursor_position(&self) -> usize {
|
||||||
self.process_search_state
|
self.process_search_state
|
||||||
.search_state
|
.search_state
|
||||||
.current_cursor_position
|
.grapheme_cursor
|
||||||
|
.cur_cursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One of two functions allowed to run while in a dialog...
|
/// One of two functions allowed to run while in a dialog...
|
||||||
@ -576,23 +580,26 @@ impl App {
|
|||||||
WidgetPosition::Process => self.start_dd(),
|
WidgetPosition::Process => self.start_dd(),
|
||||||
WidgetPosition::ProcessSearch => {
|
WidgetPosition::ProcessSearch => {
|
||||||
if self.process_search_state.search_state.is_enabled
|
if self.process_search_state.search_state.is_enabled
|
||||||
&& self
|
&& self.get_cursor_position()
|
||||||
.process_search_state
|
< self
|
||||||
.search_state
|
.process_search_state
|
||||||
.current_cursor_position < self
|
.search_state
|
||||||
.process_search_state
|
.current_search_query
|
||||||
.search_state
|
.len()
|
||||||
.current_search_query
|
|
||||||
.len()
|
|
||||||
{
|
{
|
||||||
self.process_search_state
|
self.process_search_state
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.remove(
|
.remove(self.get_cursor_position());
|
||||||
self.process_search_state
|
|
||||||
.search_state
|
self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
|
||||||
.current_cursor_position,
|
self.get_cursor_position(),
|
||||||
);
|
self.process_search_state
|
||||||
|
.search_state
|
||||||
|
.current_search_query
|
||||||
|
.len(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
self.update_regex();
|
self.update_regex();
|
||||||
self.update_process_gui = true;
|
self.update_process_gui = true;
|
||||||
@ -619,9 +626,8 @@ impl App {
|
|||||||
|
|
||||||
pub fn clear_search(&mut self) {
|
pub fn clear_search(&mut self) {
|
||||||
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
||||||
self.process_search_state
|
self.process_search_state.search_state.grapheme_cursor =
|
||||||
.search_state
|
GraphemeCursor::new(0, 0, true);
|
||||||
.current_cursor_position = 0;
|
|
||||||
self.process_search_state.search_state.current_search_query = String::default();
|
self.process_search_state.search_state.current_search_query = String::default();
|
||||||
self.process_search_state.search_state.is_blank_search = true;
|
self.process_search_state.search_state.is_blank_search = true;
|
||||||
self.process_search_state.search_state.is_invalid_search = false;
|
self.process_search_state.search_state.is_invalid_search = false;
|
||||||
@ -629,26 +635,46 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn search_walk_forward(&mut self, start_position: usize) {
|
||||||
|
self.process_search_state
|
||||||
|
.search_state
|
||||||
|
.grapheme_cursor
|
||||||
|
.next_boundary(
|
||||||
|
&self.process_search_state.search_state.current_search_query[start_position..],
|
||||||
|
start_position,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_walk_back(&mut self, start_position: usize) {
|
||||||
|
self.process_search_state
|
||||||
|
.search_state
|
||||||
|
.grapheme_cursor
|
||||||
|
.prev_boundary(
|
||||||
|
&self.process_search_state.search_state.current_search_query[..start_position],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_backspace(&mut self) {
|
pub fn on_backspace(&mut self) {
|
||||||
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
||||||
if self.process_search_state.search_state.is_enabled
|
if self.process_search_state.search_state.is_enabled && self.get_cursor_position() > 0 {
|
||||||
&& self
|
self.search_walk_back(self.get_cursor_position());
|
||||||
.process_search_state
|
|
||||||
.search_state
|
|
||||||
.current_cursor_position
|
|
||||||
> 0
|
|
||||||
{
|
|
||||||
self.process_search_state
|
|
||||||
.search_state
|
|
||||||
.current_cursor_position -= 1;
|
|
||||||
self.process_search_state
|
self.process_search_state
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.remove(
|
.remove(self.get_cursor_position());
|
||||||
self.process_search_state
|
|
||||||
.search_state
|
self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
|
||||||
.current_cursor_position,
|
self.get_cursor_position(),
|
||||||
);
|
self.process_search_state
|
||||||
|
.search_state
|
||||||
|
.current_search_query
|
||||||
|
.len(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
self.update_regex();
|
self.update_regex();
|
||||||
self.update_process_gui = true;
|
self.update_process_gui = true;
|
||||||
@ -683,16 +709,7 @@ impl App {
|
|||||||
pub fn on_left_key(&mut self) {
|
pub fn on_left_key(&mut self) {
|
||||||
if !self.is_in_dialog() {
|
if !self.is_in_dialog() {
|
||||||
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
||||||
if self
|
self.search_walk_back(self.get_cursor_position());
|
||||||
.process_search_state
|
|
||||||
.search_state
|
|
||||||
.current_cursor_position
|
|
||||||
> 0
|
|
||||||
{
|
|
||||||
self.process_search_state
|
|
||||||
.search_state
|
|
||||||
.current_cursor_position -= 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if self.delete_dialog_state.is_showing_dd && !self.delete_dialog_state.is_on_yes {
|
} else if self.delete_dialog_state.is_showing_dd && !self.delete_dialog_state.is_on_yes {
|
||||||
self.delete_dialog_state.is_on_yes = true;
|
self.delete_dialog_state.is_on_yes = true;
|
||||||
@ -702,20 +719,7 @@ impl App {
|
|||||||
pub fn on_right_key(&mut self) {
|
pub fn on_right_key(&mut self) {
|
||||||
if !self.is_in_dialog() {
|
if !self.is_in_dialog() {
|
||||||
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
||||||
if self
|
self.search_walk_forward(self.get_cursor_position());
|
||||||
.process_search_state
|
|
||||||
.search_state
|
|
||||||
.current_cursor_position
|
|
||||||
< self
|
|
||||||
.process_search_state
|
|
||||||
.search_state
|
|
||||||
.current_search_query
|
|
||||||
.len()
|
|
||||||
{
|
|
||||||
self.process_search_state
|
|
||||||
.search_state
|
|
||||||
.current_cursor_position += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if self.delete_dialog_state.is_showing_dd && self.delete_dialog_state.is_on_yes {
|
} else if self.delete_dialog_state.is_showing_dd && self.delete_dialog_state.is_on_yes {
|
||||||
self.delete_dialog_state.is_on_yes = false;
|
self.delete_dialog_state.is_on_yes = false;
|
||||||
@ -725,9 +729,14 @@ impl App {
|
|||||||
pub fn skip_cursor_beginning(&mut self) {
|
pub fn skip_cursor_beginning(&mut self) {
|
||||||
if !self.is_in_dialog() {
|
if !self.is_in_dialog() {
|
||||||
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
||||||
self.process_search_state
|
self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
|
||||||
.search_state
|
0,
|
||||||
.current_cursor_position = 0;
|
self.process_search_state
|
||||||
|
.search_state
|
||||||
|
.current_search_query
|
||||||
|
.len(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -735,13 +744,17 @@ impl App {
|
|||||||
pub fn skip_cursor_end(&mut self) {
|
pub fn skip_cursor_end(&mut self) {
|
||||||
if !self.is_in_dialog() {
|
if !self.is_in_dialog() {
|
||||||
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
||||||
self.process_search_state
|
self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
|
||||||
.search_state
|
self.process_search_state
|
||||||
.current_cursor_position = self
|
.search_state
|
||||||
.process_search_state
|
.current_search_query
|
||||||
.search_state
|
.len(),
|
||||||
.current_search_query
|
self.process_search_state
|
||||||
.len();
|
.search_state
|
||||||
|
.current_search_query
|
||||||
|
.len(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -788,13 +801,12 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_char_key(&mut self, caught_char: char) {
|
pub fn on_char_key(&mut self, caught_char: char) {
|
||||||
// Forbid any char key presses when showing a dialog box...
|
|
||||||
|
|
||||||
// Skip control code chars
|
// Skip control code chars
|
||||||
if caught_char.is_control() {
|
if caught_char.is_control() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forbid any char key presses when showing a dialog box...
|
||||||
if !self.is_in_dialog() {
|
if !self.is_in_dialog() {
|
||||||
let current_key_press_inst = Instant::now();
|
let current_key_press_inst = Instant::now();
|
||||||
if current_key_press_inst
|
if current_key_press_inst
|
||||||
@ -806,21 +818,29 @@ impl App {
|
|||||||
self.last_key_press = current_key_press_inst;
|
self.last_key_press = current_key_press_inst;
|
||||||
|
|
||||||
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
if let WidgetPosition::ProcessSearch = self.current_widget_selected {
|
||||||
self.process_search_state
|
if self
|
||||||
|
.process_search_state
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.insert(
|
.len() <= MAX_SEARCH_LENGTH
|
||||||
|
{
|
||||||
|
self.process_search_state
|
||||||
|
.search_state
|
||||||
|
.current_search_query
|
||||||
|
.insert(self.get_cursor_position(), caught_char);
|
||||||
|
|
||||||
|
self.process_search_state.search_state.grapheme_cursor = GraphemeCursor::new(
|
||||||
|
self.get_cursor_position(),
|
||||||
self.process_search_state
|
self.process_search_state
|
||||||
.search_state
|
.search_state
|
||||||
.current_cursor_position,
|
.current_search_query
|
||||||
caught_char,
|
.len(),
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
self.process_search_state
|
self.search_walk_forward(self.get_cursor_position());
|
||||||
.search_state
|
self.update_regex();
|
||||||
.current_cursor_position += 1;
|
self.update_process_gui = true;
|
||||||
|
}
|
||||||
self.update_regex();
|
|
||||||
self.update_process_gui = true;
|
|
||||||
} else {
|
} else {
|
||||||
match caught_char {
|
match caught_char {
|
||||||
'/' => {
|
'/' => {
|
||||||
|
@ -4,7 +4,7 @@ use crate::{
|
|||||||
data_conversion::{ConvertedCpuData, ConvertedProcessData},
|
data_conversion::{ConvertedCpuData, ConvertedProcessData},
|
||||||
utils::error,
|
utils::error,
|
||||||
};
|
};
|
||||||
use std::cmp::max;
|
use std::cmp::{max, min};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tui::{
|
use tui::{
|
||||||
backend,
|
backend,
|
||||||
@ -14,6 +14,8 @@ use tui::{
|
|||||||
widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget},
|
widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Paragraph, Row, Table, Text, Widget},
|
||||||
Terminal,
|
Terminal,
|
||||||
};
|
};
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
mod canvas_colours;
|
mod canvas_colours;
|
||||||
use canvas_colours::*;
|
use canvas_colours::*;
|
||||||
@ -1155,55 +1157,60 @@ impl Painter {
|
|||||||
fn draw_search_field<B: backend::Backend>(
|
fn draw_search_field<B: backend::Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
|
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
|
||||||
) {
|
) {
|
||||||
let width = max(0, draw_loc.width as i64 - 34) as u64;
|
let width = max(0, draw_loc.width as i64 - 34) as u64; // TODO: [REFACTOR] Hard coding this is terrible.
|
||||||
let query = app_state.get_current_search_query();
|
let query = app_state.get_current_search_query().as_str();
|
||||||
let shrunk_query = if query.len() < width as usize {
|
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true)
|
||||||
query
|
.rev()
|
||||||
} else {
|
.enumerate(); // Reverse due to us wanting to draw from back -> front
|
||||||
&query[(query.len() - width as usize)..]
|
let num_graphemes = min(UnicodeWidthStr::width_cjk(query), width as usize);
|
||||||
};
|
|
||||||
|
|
||||||
let cursor_position = app_state.get_cursor_position();
|
let cursor_position = app_state.get_cursor_position();
|
||||||
|
|
||||||
let query_with_cursor: Vec<Text<'_>> =
|
let mut query_with_cursor: Vec<Text<'_>> = if let app::WidgetPosition::ProcessSearch =
|
||||||
if let app::WidgetPosition::ProcessSearch = app_state.current_widget_selected {
|
app_state.current_widget_selected
|
||||||
if cursor_position >= query.len() {
|
{
|
||||||
let mut q = vec![Text::styled(
|
let mut res = Vec::new();
|
||||||
shrunk_query.to_string(),
|
if cursor_position >= query.len() {
|
||||||
self.colours.text_style,
|
res.push(Text::styled(
|
||||||
)];
|
" ",
|
||||||
|
self.colours.currently_selected_text_style,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
q.push(Text::styled(
|
res.extend(
|
||||||
" ".to_string(),
|
grapheme_indices
|
||||||
self.colours.currently_selected_text_style,
|
.filter_map(|(itx, grapheme)| {
|
||||||
));
|
if itx >= num_graphemes {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let styled = if grapheme.0 == cursor_position {
|
||||||
|
Text::styled(grapheme.1, self.colours.currently_selected_text_style)
|
||||||
|
} else {
|
||||||
|
Text::styled(grapheme.1, self.colours.text_style)
|
||||||
|
};
|
||||||
|
Some(styled)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
|
||||||
q
|
res
|
||||||
} else {
|
} else {
|
||||||
shrunk_query
|
// This is easier - we just need to get a range of graphemes, rather than
|
||||||
.chars()
|
// dealing with possibly inserting a cursor (as none is shown!)
|
||||||
.enumerate()
|
grapheme_indices
|
||||||
.map(|(itx, c)| {
|
.filter_map(|(itx, grapheme)| {
|
||||||
if let app::WidgetPosition::ProcessSearch =
|
if itx >= num_graphemes {
|
||||||
app_state.current_widget_selected
|
None
|
||||||
{
|
} else {
|
||||||
if itx == cursor_position {
|
let styled = Text::styled(grapheme.1, self.colours.text_style);
|
||||||
return Text::styled(
|
Some(styled)
|
||||||
c.to_string(),
|
}
|
||||||
self.colours.currently_selected_text_style,
|
})
|
||||||
);
|
.collect::<Vec<_>>()
|
||||||
}
|
};
|
||||||
}
|
|
||||||
Text::styled(c.to_string(), self.colours.text_style)
|
// I feel like this is most definitely not the efficient way of doing this but eh
|
||||||
})
|
query_with_cursor.reverse();
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
vec![Text::styled(
|
|
||||||
shrunk_query.to_string(),
|
|
||||||
self.colours.text_style,
|
|
||||||
)]
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut search_text = vec![if app_state.is_grouped() {
|
let mut search_text = vec![if app_state.is_grouped() {
|
||||||
Text::styled("Search by Name: ", self.colours.table_header_style)
|
Text::styled("Search by Name: ", self.colours.table_header_style)
|
||||||
|
@ -315,12 +315,18 @@ fn handle_mouse_event(event: MouseEvent, app: &mut App) {
|
|||||||
fn handle_key_event_or_break(
|
fn handle_key_event_or_break(
|
||||||
event: KeyEvent, app: &mut App, rtx: &std::sync::mpsc::Sender<ResetEvent>,
|
event: KeyEvent, app: &mut App, rtx: &std::sync::mpsc::Sender<ResetEvent>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
//debug!("KeyEvent: {:?}", event);
|
||||||
|
|
||||||
|
// TODO: [PASTE] Note that this does NOT support some emojis like flags. This is due to us
|
||||||
|
// catching PER CHARACTER right now WITH A forced throttle! This means multi-char will not work.
|
||||||
|
// We can solve this (when we do paste probably) while keeping the throttle (mainly meant for movement)
|
||||||
|
// by throttling after *bulk+singular* actions, not just singular ones.
|
||||||
|
|
||||||
if event.modifiers.is_empty() {
|
if event.modifiers.is_empty() {
|
||||||
// Required catch for searching - otherwise you couldn't search with q.
|
// Required catch for searching - otherwise you couldn't search with q.
|
||||||
if event.code == KeyCode::Char('q') && !app.is_in_search_widget() {
|
if event.code == KeyCode::Char('q') && !app.is_in_search_widget() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::End => app.skip_to_last(),
|
KeyCode::End => app.skip_to_last(),
|
||||||
KeyCode::Home => app.skip_to_first(),
|
KeyCode::Home => app.skip_to_first(),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user