mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-23 13:45:12 +02:00
Merge pull request #50 from ClementTsang/fix_cursor_screen_bug
Fix cursor screen bug
This commit is contained in:
commit
d922f85b95
3
.github/pull_request_template.md
vendored
3
.github/pull_request_template.md
vendored
@ -14,6 +14,7 @@ _Remove the irrelevant one:_
|
|||||||
|
|
||||||
- [x] Bug fix (non-breaking change which fixes an issue)
|
- [x] Bug fix (non-breaking change which fixes an issue)
|
||||||
- [x] New feature (non-breaking change which adds functionality)
|
- [x] New feature (non-breaking change which adds functionality)
|
||||||
|
- [x] Other (something else)
|
||||||
|
|
||||||
## Test methodology
|
## Test methodology
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ _Please state how this was tested:_
|
|||||||
_Please ensure all are ticked (and actually done):_
|
_Please ensure all are ticked (and actually done):_
|
||||||
|
|
||||||
- [ ] Change has been tested to work
|
- [ ] Change has been tested to work
|
||||||
- [ ] Code has been linted using rustfmt
|
- [ ] Areas your change affects has been linted using rustfmt
|
||||||
- [ ] Code has been self-reviewed
|
- [ ] Code has been self-reviewed
|
||||||
- [ ] Code has been tested and no new breakage is introduced
|
- [ ] Code has been tested and no new breakage is introduced
|
||||||
- [ ] Documentation has been added/updated if needed
|
- [ ] Documentation has been added/updated if needed
|
||||||
|
@ -30,7 +30,6 @@ heim = "0.0.10"
|
|||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
regex = "1.3.4"
|
regex = "1.3.4"
|
||||||
sysinfo = "0.11"
|
sysinfo = "0.11"
|
||||||
winapi = "0.3.8"
|
|
||||||
crossterm = "0.16"
|
crossterm = "0.16"
|
||||||
tui = {version = "0.8", features = ["crossterm"], default-features = false }
|
tui = {version = "0.8", features = ["crossterm"], default-features = false }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
@ -40,6 +39,9 @@ serde = {version = "1.0", features = ["derive"] }
|
|||||||
unicode-segmentation = "1.6.0"
|
unicode-segmentation = "1.6.0"
|
||||||
unicode-width = "0.1.7"
|
unicode-width = "0.1.7"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
winapi = "0.3.8"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "0.12"
|
assert_cmd = "0.12"
|
||||||
predicates = "1"
|
predicates = "1"
|
||||||
|
@ -206,7 +206,7 @@ Note that `q` is disabled while in the search widget.
|
|||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
|
|
||||||
Contribution is welcome! Just submit a PR.
|
Contribution is welcome! Just submit a PR. Note that I develop and test on stable Rust.
|
||||||
|
|
||||||
If you spot any issue with nobody assigned to it, or it seems like no work has started on it, feel free to try and do it!
|
If you spot any issue with nobody assigned to it, or it seems like no work has started on it, feel free to try and do it!
|
||||||
|
|
||||||
|
@ -11,4 +11,3 @@ reorder_modules = true
|
|||||||
reorder_impl_items = true
|
reorder_impl_items = true
|
||||||
tab_spaces = 4
|
tab_spaces = 4
|
||||||
format_strings = true
|
format_strings = true
|
||||||
space_after_colon = true
|
|
||||||
|
71
src/app.rs
71
src/app.rs
@ -8,8 +8,8 @@ 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};
|
use unicode_segmentation::GraphemeCursor;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
const MAX_SEARCH_LENGTH: usize = 200;
|
const MAX_SEARCH_LENGTH: usize = 200;
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ pub enum ScrollDirection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SearchDirection {
|
pub enum CursorDirection {
|
||||||
LEFT,
|
LEFT,
|
||||||
RIGHT,
|
RIGHT,
|
||||||
}
|
}
|
||||||
@ -73,6 +73,10 @@ pub struct AppSearchState {
|
|||||||
pub is_blank_search: bool,
|
pub is_blank_search: bool,
|
||||||
pub is_invalid_search: bool,
|
pub is_invalid_search: bool,
|
||||||
pub grapheme_cursor: GraphemeCursor,
|
pub grapheme_cursor: GraphemeCursor,
|
||||||
|
pub cursor_direction: CursorDirection,
|
||||||
|
pub cursor_bar: usize,
|
||||||
|
/// This represents the position in terms of CHARACTERS, not graphemes
|
||||||
|
pub char_cursor_position: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppSearchState {
|
impl Default for AppSearchState {
|
||||||
@ -84,11 +88,21 @@ impl Default for AppSearchState {
|
|||||||
is_invalid_search: false,
|
is_invalid_search: false,
|
||||||
is_blank_search: true,
|
is_blank_search: true,
|
||||||
grapheme_cursor: GraphemeCursor::new(0, 0, true),
|
grapheme_cursor: GraphemeCursor::new(0, 0, true),
|
||||||
|
cursor_direction: CursorDirection::RIGHT,
|
||||||
|
cursor_bar: 0,
|
||||||
|
char_cursor_position: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppSearchState {
|
impl AppSearchState {
|
||||||
|
/// Returns a reset but still enabled app search state
|
||||||
|
pub fn reset() -> Self {
|
||||||
|
let mut app_search_state = AppSearchState::default();
|
||||||
|
app_search_state.is_enabled = true;
|
||||||
|
app_search_state
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_invalid_or_blank_search(&self) -> bool {
|
pub fn is_invalid_or_blank_search(&self) -> bool {
|
||||||
self.is_blank_search || self.is_invalid_search
|
self.is_blank_search || self.is_invalid_search
|
||||||
}
|
}
|
||||||
@ -549,6 +563,10 @@ impl App {
|
|||||||
.cur_cursor()
|
.cur_cursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_char_cursor_position(&self) -> usize {
|
||||||
|
self.process_search_state.search_state.char_cursor_position
|
||||||
|
}
|
||||||
|
|
||||||
/// One of two functions allowed to run while in a dialog...
|
/// One of two functions allowed to run while in a dialog...
|
||||||
pub fn on_enter(&mut self) {
|
pub fn on_enter(&mut self) {
|
||||||
if self.delete_dialog_state.is_showing_dd {
|
if self.delete_dialog_state.is_showing_dd {
|
||||||
@ -620,19 +638,14 @@ impl App {
|
|||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub fn skip_word_backspace(&mut self) {
|
pub fn skip_word_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 {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.search_state.grapheme_cursor =
|
|
||||||
GraphemeCursor::new(0, 0, true);
|
|
||||||
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_invalid_search = false;
|
|
||||||
self.update_process_gui = true;
|
self.update_process_gui = true;
|
||||||
|
self.process_search_state.search_state = AppSearchState::reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -663,7 +676,8 @@ impl App {
|
|||||||
if self.process_search_state.search_state.is_enabled && self.get_cursor_position() > 0 {
|
if self.process_search_state.search_state.is_enabled && self.get_cursor_position() > 0 {
|
||||||
self.search_walk_back(self.get_cursor_position());
|
self.search_walk_back(self.get_cursor_position());
|
||||||
|
|
||||||
self.process_search_state
|
let removed_char = self
|
||||||
|
.process_search_state
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.remove(self.get_cursor_position());
|
.remove(self.get_cursor_position());
|
||||||
@ -677,6 +691,10 @@ impl App {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.process_search_state.search_state.char_cursor_position -=
|
||||||
|
UnicodeWidthChar::width(removed_char).unwrap_or(0);
|
||||||
|
self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT;
|
||||||
|
|
||||||
self.update_regex();
|
self.update_regex();
|
||||||
self.update_process_gui = true;
|
self.update_process_gui = true;
|
||||||
}
|
}
|
||||||
@ -710,7 +728,15 @@ 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 {
|
||||||
|
let prev_cursor = self.get_cursor_position();
|
||||||
self.search_walk_back(self.get_cursor_position());
|
self.search_walk_back(self.get_cursor_position());
|
||||||
|
if self.get_cursor_position() < prev_cursor {
|
||||||
|
let str_slice = &self.process_search_state.search_state.current_search_query
|
||||||
|
[self.get_cursor_position()..prev_cursor];
|
||||||
|
self.process_search_state.search_state.char_cursor_position -=
|
||||||
|
UnicodeWidthStr::width(str_slice);
|
||||||
|
self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} 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;
|
||||||
@ -720,7 +746,16 @@ 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 {
|
||||||
|
let prev_cursor = self.get_cursor_position();
|
||||||
self.search_walk_forward(self.get_cursor_position());
|
self.search_walk_forward(self.get_cursor_position());
|
||||||
|
if self.get_cursor_position() > prev_cursor {
|
||||||
|
let str_slice = &self.process_search_state.search_state.current_search_query
|
||||||
|
[prev_cursor..self.get_cursor_position()];
|
||||||
|
self.process_search_state.search_state.char_cursor_position +=
|
||||||
|
UnicodeWidthStr::width(str_slice);
|
||||||
|
self.process_search_state.search_state.cursor_direction =
|
||||||
|
CursorDirection::RIGHT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} 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;
|
||||||
@ -738,6 +773,8 @@ impl App {
|
|||||||
.len(),
|
.len(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
self.process_search_state.search_state.char_cursor_position = 0;
|
||||||
|
self.process_search_state.search_state.cursor_direction = CursorDirection::LEFT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -756,6 +793,14 @@ impl App {
|
|||||||
.len(),
|
.len(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
self.process_search_state.search_state.char_cursor_position =
|
||||||
|
UnicodeWidthStr::width(
|
||||||
|
self.process_search_state
|
||||||
|
.search_state
|
||||||
|
.current_search_query
|
||||||
|
.as_str(),
|
||||||
|
);
|
||||||
|
self.process_search_state.search_state.cursor_direction = CursorDirection::RIGHT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -840,6 +885,10 @@ impl App {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
self.search_walk_forward(self.get_cursor_position());
|
self.search_walk_forward(self.get_cursor_position());
|
||||||
|
|
||||||
|
self.process_search_state.search_state.char_cursor_position +=
|
||||||
|
UnicodeWidthChar::width(caught_char).unwrap_or(0);
|
||||||
|
|
||||||
self.update_regex();
|
self.update_regex();
|
||||||
self.update_process_gui = true;
|
self.update_process_gui = true;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use crate::{
|
|||||||
data_conversion::{ConvertedCpuData, ConvertedProcessData},
|
data_conversion::{ConvertedCpuData, ConvertedProcessData},
|
||||||
utils::error,
|
utils::error,
|
||||||
};
|
};
|
||||||
use std::cmp::{max, min};
|
use std::cmp::max;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tui::{
|
use tui::{
|
||||||
backend,
|
backend,
|
||||||
@ -1162,27 +1162,27 @@ impl Painter {
|
|||||||
&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; // TODO: [REFACTOR] Hard coding this is terrible.
|
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().as_str();
|
|
||||||
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true).rev(); // Reverse due to us wanting to draw from back -> front
|
|
||||||
let cursor_position = app_state.get_cursor_position();
|
let cursor_position = app_state.get_cursor_position();
|
||||||
let right_border = min(UnicodeWidthStr::width(query), width as usize);
|
let char_cursor_position = app_state.get_char_cursor_position();
|
||||||
|
|
||||||
let mut itx = 0;
|
let start_position: usize = get_search_start_position(
|
||||||
let mut query_with_cursor: Vec<Text<'_>> = if let app::WidgetPosition::ProcessSearch =
|
width as usize,
|
||||||
app_state.current_widget_selected
|
&app_state.process_search_state.search_state.cursor_direction,
|
||||||
{
|
&mut app_state.process_search_state.search_state.cursor_bar,
|
||||||
let mut res = Vec::new();
|
char_cursor_position,
|
||||||
if cursor_position >= query.len() {
|
app_state.is_resized,
|
||||||
res.push(Text::styled(
|
);
|
||||||
" ",
|
|
||||||
self.colours.currently_selected_text_style,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
res.extend(
|
let query = app_state.get_current_search_query().as_str();
|
||||||
grapheme_indices
|
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true);
|
||||||
|
let mut current_grapheme_posn = 0;
|
||||||
|
let query_with_cursor: Vec<Text<'_>> =
|
||||||
|
if let app::WidgetPosition::ProcessSearch = app_state.current_widget_selected {
|
||||||
|
let mut res = grapheme_indices
|
||||||
.filter_map(|grapheme| {
|
.filter_map(|grapheme| {
|
||||||
if itx >= right_border {
|
current_grapheme_posn += UnicodeWidthStr::width(grapheme.1);
|
||||||
|
|
||||||
|
if current_grapheme_posn <= start_position {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let styled = if grapheme.0 == cursor_position {
|
let styled = if grapheme.0 == cursor_position {
|
||||||
@ -1190,12 +1190,17 @@ impl Painter {
|
|||||||
} else {
|
} else {
|
||||||
Text::styled(grapheme.1, self.colours.text_style)
|
Text::styled(grapheme.1, self.colours.text_style)
|
||||||
};
|
};
|
||||||
itx += UnicodeWidthStr::width(grapheme.1);
|
|
||||||
Some(styled)
|
Some(styled)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>();
|
||||||
);
|
|
||||||
|
if cursor_position >= query.len() {
|
||||||
|
res.push(Text::styled(
|
||||||
|
" ",
|
||||||
|
self.colours.currently_selected_text_style,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
res
|
res
|
||||||
} else {
|
} else {
|
||||||
@ -1203,20 +1208,17 @@ impl Painter {
|
|||||||
// dealing with possibly inserting a cursor (as none is shown!)
|
// dealing with possibly inserting a cursor (as none is shown!)
|
||||||
grapheme_indices
|
grapheme_indices
|
||||||
.filter_map(|grapheme| {
|
.filter_map(|grapheme| {
|
||||||
if itx >= right_border {
|
current_grapheme_posn += UnicodeWidthStr::width(grapheme.1);
|
||||||
|
if current_grapheme_posn <= start_position {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let styled = Text::styled(grapheme.1, self.colours.text_style);
|
let styled = Text::styled(grapheme.1, self.colours.text_style);
|
||||||
itx += UnicodeWidthStr::width(grapheme.1);
|
|
||||||
Some(styled)
|
Some(styled)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
|
|
||||||
// I feel like this is most definitely not the efficient way of doing this but eh
|
|
||||||
query_with_cursor.reverse();
|
|
||||||
|
|
||||||
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)
|
||||||
} else if app_state.process_search_state.is_searching_with_pid {
|
} else if app_state.process_search_state.is_searching_with_pid {
|
||||||
|
@ -72,11 +72,43 @@ pub fn get_variable_intrinsic_widths(
|
|||||||
|
|
||||||
#[allow(dead_code, unused_variables)]
|
#[allow(dead_code, unused_variables)]
|
||||||
pub fn get_search_start_position(
|
pub fn get_search_start_position(
|
||||||
num_rows: u64, scroll_direction: &app::ScrollDirection, scroll_position_bar: &mut u64,
|
num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize,
|
||||||
currently_selected_position: u64, is_resized: bool,
|
current_cursor_position: usize, is_resized: bool,
|
||||||
) -> u64 {
|
) -> usize {
|
||||||
//TODO: [Scroll] Gotta fix this too lol
|
if is_resized {
|
||||||
|
*cursor_bar = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
match cursor_direction {
|
||||||
|
app::CursorDirection::RIGHT => {
|
||||||
|
if current_cursor_position < *cursor_bar + num_columns {
|
||||||
|
// If, using previous_scrolled_position, we can see the element
|
||||||
|
// (so within that and + num_rows) just reuse the current previously scrolled position
|
||||||
|
*cursor_bar
|
||||||
|
} else if current_cursor_position >= num_columns {
|
||||||
|
// Else if the current position past the last element visible in the list, omit
|
||||||
|
// until we can see that element
|
||||||
|
*cursor_bar = current_cursor_position - num_columns;
|
||||||
|
*cursor_bar
|
||||||
|
} else {
|
||||||
|
// Else, if it is not past the last element visible, do not omit anything
|
||||||
0
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app::CursorDirection::LEFT => {
|
||||||
|
if current_cursor_position <= *cursor_bar {
|
||||||
|
// If it's past the first element, then show from that element downwards
|
||||||
|
*cursor_bar = current_cursor_position;
|
||||||
|
*cursor_bar
|
||||||
|
} else if current_cursor_position >= *cursor_bar + num_columns {
|
||||||
|
*cursor_bar = current_cursor_position - num_columns;
|
||||||
|
*cursor_bar
|
||||||
|
} else {
|
||||||
|
// Else, don't change what our start position is from whatever it is set to!
|
||||||
|
*cursor_bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_start_position(
|
pub fn get_start_position(
|
||||||
|
@ -399,6 +399,8 @@ fn handle_key_event_or_break(
|
|||||||
KeyCode::Char('u') => app.clear_search(),
|
KeyCode::Char('u') => app.clear_search(),
|
||||||
KeyCode::Char('a') => app.skip_cursor_beginning(),
|
KeyCode::Char('a') => app.skip_cursor_beginning(),
|
||||||
KeyCode::Char('e') => app.skip_cursor_end(),
|
KeyCode::Char('e') => app.skip_cursor_end(),
|
||||||
|
// Can't do now, CTRL+BACKSPACE doesn't work and graphemes
|
||||||
|
// are hard to iter while truncating last (eloquently).
|
||||||
// KeyCode::Backspace => app.skip_word_backspace(),
|
// KeyCode::Backspace => app.skip_word_backspace(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user