mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-09-24 10:18:38 +02:00
bug: fix search scrolling with wider Unicode characters. (#938)
This should help make search scrolling more reliable larger Unicode characters like CJK/flag characters.
This commit is contained in:
parent
efcf2bde29
commit
3b5774117f
@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- [#805](https://github.com/ClementTsang/bottom/pull/805): Fix bottom keeping devices awake in certain scenarios.
|
- [#805](https://github.com/ClementTsang/bottom/pull/805): Fix bottom keeping devices awake in certain scenarios.
|
||||||
- [#825](https://github.com/ClementTsang/bottom/pull/825): Use alternative method of getting parent PID in some cases on macOS devices to avoid needing root access.
|
- [#825](https://github.com/ClementTsang/bottom/pull/825): Use alternative method of getting parent PID in some cases on macOS devices to avoid needing root access.
|
||||||
- [#916](https://github.com/ClementTsang/bottom/pull/916): Fix possible gaps with widget layout spacing.
|
- [#916](https://github.com/ClementTsang/bottom/pull/916): Fix possible gaps with widget layout spacing.
|
||||||
|
- [#938](https://github.com/ClementTsang/bottom/pull/938): Fix search scrolling with wider Unicode characters.
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
|
165
src/app.rs
165
src/app.rs
@ -11,7 +11,6 @@ use layout_manager::*;
|
|||||||
pub use states::*;
|
pub use states::*;
|
||||||
use typed_builder::*;
|
use typed_builder::*;
|
||||||
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
|
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
|
||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
|
||||||
|
|
||||||
use crate::widgets::{ProcWidget, ProcWidgetMode};
|
use crate::widgets::{ProcWidget, ProcWidgetMode};
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -32,8 +31,6 @@ pub mod states;
|
|||||||
|
|
||||||
use frozen_state::FrozenState;
|
use frozen_state::FrozenState;
|
||||||
|
|
||||||
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 {
|
||||||
Log,
|
Log,
|
||||||
@ -504,23 +501,21 @@ impl App {
|
|||||||
{
|
{
|
||||||
if is_in_search_widget {
|
if is_in_search_widget {
|
||||||
if proc_widget_state.proc_search.search_state.is_enabled
|
if proc_widget_state.proc_search.search_state.is_enabled
|
||||||
&& proc_widget_state.get_search_cursor_position()
|
&& proc_widget_state.cursor_char_index()
|
||||||
< proc_widget_state
|
< proc_widget_state
|
||||||
.proc_search
|
.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.len()
|
.len()
|
||||||
{
|
{
|
||||||
let current_cursor = proc_widget_state.get_search_cursor_position();
|
let current_cursor = proc_widget_state.cursor_char_index();
|
||||||
proc_widget_state
|
proc_widget_state.search_walk_forward();
|
||||||
.search_walk_forward(proc_widget_state.get_search_cursor_position());
|
|
||||||
|
|
||||||
let _removed_chars: String = proc_widget_state
|
let _ = proc_widget_state
|
||||||
.proc_search
|
.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.drain(current_cursor..proc_widget_state.get_search_cursor_position())
|
.drain(current_cursor..proc_widget_state.cursor_char_index());
|
||||||
.collect();
|
|
||||||
|
|
||||||
proc_widget_state.proc_search.search_state.grapheme_cursor =
|
proc_widget_state.proc_search.search_state.grapheme_cursor =
|
||||||
GraphemeCursor::new(
|
GraphemeCursor::new(
|
||||||
@ -552,22 +547,21 @@ impl App {
|
|||||||
{
|
{
|
||||||
if is_in_search_widget
|
if is_in_search_widget
|
||||||
&& proc_widget_state.proc_search.search_state.is_enabled
|
&& proc_widget_state.proc_search.search_state.is_enabled
|
||||||
&& proc_widget_state.get_search_cursor_position() > 0
|
&& proc_widget_state.cursor_char_index() > 0
|
||||||
{
|
{
|
||||||
let current_cursor = proc_widget_state.get_search_cursor_position();
|
let current_cursor = proc_widget_state.cursor_char_index();
|
||||||
proc_widget_state
|
proc_widget_state.search_walk_back();
|
||||||
.search_walk_back(proc_widget_state.get_search_cursor_position());
|
|
||||||
|
|
||||||
let removed_chars: String = proc_widget_state
|
// Remove the indices in between.
|
||||||
|
let _ = proc_widget_state
|
||||||
.proc_search
|
.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.drain(proc_widget_state.get_search_cursor_position()..current_cursor)
|
.drain(proc_widget_state.cursor_char_index()..current_cursor);
|
||||||
.collect();
|
|
||||||
|
|
||||||
proc_widget_state.proc_search.search_state.grapheme_cursor =
|
proc_widget_state.proc_search.search_state.grapheme_cursor =
|
||||||
GraphemeCursor::new(
|
GraphemeCursor::new(
|
||||||
proc_widget_state.get_search_cursor_position(),
|
proc_widget_state.cursor_char_index(),
|
||||||
proc_widget_state
|
proc_widget_state
|
||||||
.proc_search
|
.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
@ -576,11 +570,6 @@ impl App {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.char_cursor_position -= UnicodeWidthStr::width(removed_chars.as_str());
|
|
||||||
|
|
||||||
proc_widget_state.proc_search.search_state.cursor_direction =
|
proc_widget_state.proc_search.search_state.cursor_direction =
|
||||||
CursorDirection::Left;
|
CursorDirection::Left;
|
||||||
|
|
||||||
@ -684,19 +673,9 @@ impl App {
|
|||||||
.get_mut_widget_state(self.current_widget.widget_id - 1)
|
.get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||||
{
|
{
|
||||||
if is_in_search_widget {
|
if is_in_search_widget {
|
||||||
let prev_cursor = proc_widget_state.get_search_cursor_position();
|
let prev_cursor = proc_widget_state.cursor_char_index();
|
||||||
proc_widget_state
|
proc_widget_state.search_walk_back();
|
||||||
.search_walk_back(proc_widget_state.get_search_cursor_position());
|
if proc_widget_state.cursor_char_index() < prev_cursor {
|
||||||
if proc_widget_state.get_search_cursor_position() < prev_cursor {
|
|
||||||
let str_slice = &proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.current_search_query
|
|
||||||
[proc_widget_state.get_search_cursor_position()..prev_cursor];
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.char_cursor_position -= UnicodeWidthStr::width(str_slice);
|
|
||||||
proc_widget_state.proc_search.search_state.cursor_direction =
|
proc_widget_state.proc_search.search_state.cursor_direction =
|
||||||
CursorDirection::Left;
|
CursorDirection::Left;
|
||||||
}
|
}
|
||||||
@ -753,20 +732,9 @@ impl App {
|
|||||||
.get_mut_widget_state(self.current_widget.widget_id - 1)
|
.get_mut_widget_state(self.current_widget.widget_id - 1)
|
||||||
{
|
{
|
||||||
if is_in_search_widget {
|
if is_in_search_widget {
|
||||||
let prev_cursor = proc_widget_state.get_search_cursor_position();
|
let prev_cursor = proc_widget_state.cursor_char_index();
|
||||||
proc_widget_state.search_walk_forward(
|
proc_widget_state.search_walk_forward();
|
||||||
proc_widget_state.get_search_cursor_position(),
|
if proc_widget_state.cursor_char_index() > prev_cursor {
|
||||||
);
|
|
||||||
if proc_widget_state.get_search_cursor_position() > prev_cursor {
|
|
||||||
let str_slice = &proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.current_search_query
|
|
||||||
[prev_cursor..proc_widget_state.get_search_cursor_position()];
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.char_cursor_position += UnicodeWidthStr::width(str_slice);
|
|
||||||
proc_widget_state.proc_search.search_state.cursor_direction =
|
proc_widget_state.proc_search.search_state.cursor_direction =
|
||||||
CursorDirection::Right;
|
CursorDirection::Right;
|
||||||
}
|
}
|
||||||
@ -932,10 +900,7 @@ impl App {
|
|||||||
.len(),
|
.len(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.char_cursor_position = 0;
|
|
||||||
proc_widget_state.proc_search.search_state.cursor_direction =
|
proc_widget_state.proc_search.search_state.cursor_direction =
|
||||||
CursorDirection::Left;
|
CursorDirection::Left;
|
||||||
}
|
}
|
||||||
@ -954,30 +919,14 @@ impl App {
|
|||||||
.get_mut(&(self.current_widget.widget_id - 1))
|
.get_mut(&(self.current_widget.widget_id - 1))
|
||||||
{
|
{
|
||||||
if is_in_search_widget {
|
if is_in_search_widget {
|
||||||
proc_widget_state.proc_search.search_state.grapheme_cursor =
|
let query_len = proc_widget_state
|
||||||
GraphemeCursor::new(
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.current_search_query
|
|
||||||
.len(),
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.current_search_query
|
|
||||||
.len(),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
.char_cursor_position = UnicodeWidthStr::width(
|
.current_search_query
|
||||||
proc_widget_state
|
.len();
|
||||||
.proc_search
|
|
||||||
.search_state
|
proc_widget_state.proc_search.search_state.grapheme_cursor =
|
||||||
.current_search_query
|
GraphemeCursor::new(query_len, query_len, true);
|
||||||
.as_str(),
|
|
||||||
);
|
|
||||||
proc_widget_state.proc_search.search_state.cursor_direction =
|
proc_widget_state.proc_search.search_state.cursor_direction =
|
||||||
CursorDirection::Right;
|
CursorDirection::Right;
|
||||||
}
|
}
|
||||||
@ -1008,11 +957,11 @@ impl App {
|
|||||||
// Traverse backwards from the current cursor location until you hit non-whitespace characters,
|
// Traverse backwards from the current cursor location until you hit non-whitespace characters,
|
||||||
// then continue to traverse (and delete) backwards until you hit a whitespace character. Halt.
|
// then continue to traverse (and delete) backwards until you hit a whitespace character. Halt.
|
||||||
|
|
||||||
// So... first, let's get our current cursor position using graphemes...
|
// So... first, let's get our current cursor position in terms of char indices.
|
||||||
let end_index = proc_widget_state.get_char_cursor_position();
|
let end_index = proc_widget_state.cursor_char_index();
|
||||||
|
|
||||||
// Then, let's crawl backwards until we hit our location, and store the "head"...
|
// Then, let's crawl backwards until we hit our location, and store the "head"...
|
||||||
let query = proc_widget_state.get_current_search_query();
|
let query = proc_widget_state.current_search_query();
|
||||||
let mut start_index = 0;
|
let mut start_index = 0;
|
||||||
let mut saw_non_whitespace = false;
|
let mut saw_non_whitespace = false;
|
||||||
|
|
||||||
@ -1032,12 +981,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let removed_chars: String = proc_widget_state
|
let _ = proc_widget_state
|
||||||
.proc_search
|
.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.drain(start_index..end_index)
|
.drain(start_index..end_index);
|
||||||
.collect();
|
|
||||||
|
|
||||||
proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new(
|
proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new(
|
||||||
start_index,
|
start_index,
|
||||||
@ -1049,11 +997,6 @@ impl App {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.char_cursor_position -= UnicodeWidthStr::width(removed_chars.as_str());
|
|
||||||
|
|
||||||
proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left;
|
proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left;
|
||||||
|
|
||||||
proc_widget_state.update_query();
|
proc_widget_state.update_query();
|
||||||
@ -1113,25 +1056,16 @@ impl App {
|
|||||||
.widget_states
|
.widget_states
|
||||||
.get_mut(&(self.current_widget.widget_id - 1))
|
.get_mut(&(self.current_widget.widget_id - 1))
|
||||||
{
|
{
|
||||||
if is_in_search_widget
|
if is_in_search_widget && proc_widget_state.is_search_enabled() {
|
||||||
&& proc_widget_state.is_search_enabled()
|
|
||||||
&& UnicodeWidthStr::width(
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.current_search_query
|
|
||||||
.as_str(),
|
|
||||||
) <= MAX_SEARCH_LENGTH
|
|
||||||
{
|
|
||||||
proc_widget_state
|
proc_widget_state
|
||||||
.proc_search
|
.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
.current_search_query
|
.current_search_query
|
||||||
.insert(proc_widget_state.get_search_cursor_position(), caught_char);
|
.insert(proc_widget_state.cursor_char_index(), caught_char);
|
||||||
|
|
||||||
proc_widget_state.proc_search.search_state.grapheme_cursor =
|
proc_widget_state.proc_search.search_state.grapheme_cursor =
|
||||||
GraphemeCursor::new(
|
GraphemeCursor::new(
|
||||||
proc_widget_state.get_search_cursor_position(),
|
proc_widget_state.cursor_char_index(),
|
||||||
proc_widget_state
|
proc_widget_state
|
||||||
.proc_search
|
.proc_search
|
||||||
.search_state
|
.search_state
|
||||||
@ -1139,14 +1073,7 @@ impl App {
|
|||||||
.len(),
|
.len(),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
proc_widget_state
|
proc_widget_state.search_walk_forward();
|
||||||
.search_walk_forward(proc_widget_state.get_search_cursor_position());
|
|
||||||
|
|
||||||
proc_widget_state
|
|
||||||
.proc_search
|
|
||||||
.search_state
|
|
||||||
.char_cursor_position +=
|
|
||||||
UnicodeWidthChar::width(caught_char).unwrap_or(0);
|
|
||||||
|
|
||||||
proc_widget_state.update_query();
|
proc_widget_state.update_query();
|
||||||
proc_widget_state.proc_search.search_state.cursor_direction =
|
proc_widget_state.proc_search.search_state.cursor_direction =
|
||||||
@ -2724,22 +2651,10 @@ impl App {
|
|||||||
.widget_states
|
.widget_states
|
||||||
.get_mut(&(self.current_widget.widget_id - 1))
|
.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();
|
let num_runes = UnicodeSegmentation::graphemes(paste.as_str(), true).count();
|
||||||
|
|
||||||
if is_in_search_widget
|
if is_in_search_widget && proc_widget_state.is_search_enabled() {
|
||||||
&& proc_widget_state.is_search_enabled()
|
let left_bound = proc_widget_state.cursor_char_index();
|
||||||
&& 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
|
let curr_query = &mut proc_widget_state
|
||||||
.proc_search
|
.proc_search
|
||||||
@ -2752,15 +2667,9 @@ impl App {
|
|||||||
GraphemeCursor::new(left_bound, curr_query.len(), true);
|
GraphemeCursor::new(left_bound, curr_query.len(), true);
|
||||||
|
|
||||||
for _ in 0..num_runes {
|
for _ in 0..num_runes {
|
||||||
let cursor = proc_widget_state.get_search_cursor_position();
|
proc_widget_state.search_walk_forward();
|
||||||
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.update_query();
|
||||||
proc_widget_state.proc_search.search_state.cursor_direction =
|
proc_widget_state.proc_search.search_state.cursor_direction =
|
||||||
CursorDirection::Right;
|
CursorDirection::Right;
|
||||||
|
@ -253,7 +253,7 @@ pub fn parse_query(
|
|||||||
if content == "=" {
|
if content == "=" {
|
||||||
// Check next string if possible
|
// Check next string if possible
|
||||||
if let Some(queue_next) = query.pop_front() {
|
if let Some(queue_next) = query.pop_front() {
|
||||||
// TODO: [Query, ???] Need to consider the following cases:
|
// TODO: [Query] Need to consider the following cases:
|
||||||
// - (test)
|
// - (test)
|
||||||
// - (test
|
// - (test
|
||||||
// - test)
|
// - test)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use std::{collections::HashMap, time::Instant};
|
use std::{collections::HashMap, ops::Range, time::Instant};
|
||||||
|
|
||||||
use unicode_segmentation::GraphemeCursor;
|
use indexmap::IndexMap;
|
||||||
|
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentation};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{layout_manager::BottomWidgetType, query::*},
|
app::{layout_manager::BottomWidgetType, query::*},
|
||||||
constants,
|
constants,
|
||||||
|
utils::gen_util::str_width,
|
||||||
widgets::{
|
widgets::{
|
||||||
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
|
BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState,
|
||||||
ProcWidget, TempWidgetState,
|
ProcWidget, TempWidgetState,
|
||||||
@ -71,10 +73,11 @@ pub struct AppSearchState {
|
|||||||
pub is_invalid_search: bool,
|
pub is_invalid_search: bool,
|
||||||
pub grapheme_cursor: GraphemeCursor,
|
pub grapheme_cursor: GraphemeCursor,
|
||||||
pub cursor_direction: CursorDirection,
|
pub cursor_direction: CursorDirection,
|
||||||
pub cursor_bar: usize,
|
|
||||||
/// This represents the position in terms of CHARACTERS, not graphemes
|
pub display_start_char_index: usize,
|
||||||
pub char_cursor_position: usize,
|
pub size_mappings: IndexMap<usize, Range<usize>>,
|
||||||
/// The query
|
|
||||||
|
/// The query. TODO: Merge this as one enum.
|
||||||
pub query: Option<Query>,
|
pub query: Option<Query>,
|
||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
}
|
}
|
||||||
@ -88,8 +91,8 @@ impl Default for AppSearchState {
|
|||||||
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_direction: CursorDirection::Right,
|
||||||
cursor_bar: 0,
|
display_start_char_index: 0,
|
||||||
char_cursor_position: 0,
|
size_mappings: IndexMap::default(),
|
||||||
query: None,
|
query: None,
|
||||||
error_message: None,
|
error_message: None,
|
||||||
}
|
}
|
||||||
@ -109,6 +112,142 @@ impl AppSearchState {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the starting grapheme index to draw from.
|
||||||
|
pub fn get_start_position(&mut self, available_width: usize, is_force_redraw: bool) {
|
||||||
|
// Remember - the number of columns != the number of grapheme slots/sizes, you
|
||||||
|
// cannot use index to determine this reliably!
|
||||||
|
|
||||||
|
let start_index = if is_force_redraw {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.display_start_char_index
|
||||||
|
};
|
||||||
|
let cursor_index = self.grapheme_cursor.cur_cursor();
|
||||||
|
|
||||||
|
if let Some(start_range) = self.size_mappings.get(&start_index) {
|
||||||
|
let cursor_range = self
|
||||||
|
.size_mappings
|
||||||
|
.get(&cursor_index)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.size_mappings
|
||||||
|
.last()
|
||||||
|
.map(|(_, r)| r.end..(r.end + 1))
|
||||||
|
.unwrap_or(start_range.end..(start_range.end + 1))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cases to handle in both cases:
|
||||||
|
// - The current start index can show the cursor's word.
|
||||||
|
// - The current start index cannot show the cursor's word.
|
||||||
|
//
|
||||||
|
// What differs is how we "scroll" based on the cursor movement direction.
|
||||||
|
|
||||||
|
self.display_start_char_index = match self.cursor_direction {
|
||||||
|
CursorDirection::Right => {
|
||||||
|
if start_range.start + available_width >= cursor_range.end {
|
||||||
|
// Use the current index.
|
||||||
|
start_index
|
||||||
|
} else if cursor_range.end >= available_width {
|
||||||
|
// If the current position is past the last visible element, skip until we see it.
|
||||||
|
|
||||||
|
let mut index = 0;
|
||||||
|
for i in 0..(cursor_index + 1) {
|
||||||
|
if let Some(r) = self.size_mappings.get(&i) {
|
||||||
|
if r.start + available_width >= cursor_range.end {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CursorDirection::Left => {
|
||||||
|
if cursor_range.start < start_range.end {
|
||||||
|
let mut index = 0;
|
||||||
|
for i in cursor_index..(self.current_search_query.len()) {
|
||||||
|
if let Some(r) = self.size_mappings.get(&i) {
|
||||||
|
if r.start + available_width >= cursor_range.end {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index
|
||||||
|
} else {
|
||||||
|
start_index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// If we fail here somehow, just reset to 0 index + scroll left.
|
||||||
|
self.display_start_char_index = 0;
|
||||||
|
self.cursor_direction = CursorDirection::Left;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn walk_forward(&mut self) {
|
||||||
|
// TODO: Add tests for this.
|
||||||
|
let start_position = self.grapheme_cursor.cur_cursor();
|
||||||
|
let chunk = &self.current_search_query[start_position..];
|
||||||
|
|
||||||
|
match self.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.grapheme_cursor
|
||||||
|
.provide_context(&self.current_search_query[0..ctx], 0);
|
||||||
|
|
||||||
|
self.grapheme_cursor
|
||||||
|
.next_boundary(chunk, start_position)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
_ => Err(err).unwrap(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn walk_backward(&mut self) {
|
||||||
|
// TODO: Add tests for this.
|
||||||
|
let start_position = self.grapheme_cursor.cur_cursor();
|
||||||
|
let chunk = &self.current_search_query[..start_position];
|
||||||
|
|
||||||
|
match self.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.grapheme_cursor
|
||||||
|
.provide_context(&self.current_search_query[0..ctx], 0);
|
||||||
|
|
||||||
|
self.grapheme_cursor.prev_boundary(chunk, 0).unwrap();
|
||||||
|
}
|
||||||
|
_ => Err(err).unwrap(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_sizes(&mut self) {
|
||||||
|
self.size_mappings.clear();
|
||||||
|
let mut curr_offset = 0;
|
||||||
|
for (index, grapheme) in
|
||||||
|
UnicodeSegmentation::grapheme_indices(self.current_search_query.as_str(), true)
|
||||||
|
{
|
||||||
|
let width = str_width(grapheme);
|
||||||
|
let end = curr_offset + width;
|
||||||
|
|
||||||
|
self.size_mappings.insert(index, curr_offset..end);
|
||||||
|
|
||||||
|
curr_offset = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.size_mappings.shrink_to_fit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProcState {
|
pub struct ProcState {
|
||||||
@ -266,3 +405,92 @@ pub struct ParagraphScrollState {
|
|||||||
pub current_scroll_index: u16,
|
pub current_scroll_index: u16,
|
||||||
pub max_scroll_index: u16,
|
pub max_scroll_index: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn move_right(state: &mut AppSearchState) {
|
||||||
|
state.walk_forward();
|
||||||
|
state.cursor_direction = CursorDirection::Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_left(state: &mut AppSearchState) {
|
||||||
|
state.walk_backward();
|
||||||
|
state.cursor_direction = CursorDirection::Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_cursor_moves() {
|
||||||
|
let mut state = AppSearchState::default();
|
||||||
|
state.current_search_query = "Hi, 你好! 🇦🇶".to_string();
|
||||||
|
state.grapheme_cursor = GraphemeCursor::new(0, state.current_search_query.len(), true);
|
||||||
|
state.update_sizes();
|
||||||
|
|
||||||
|
// Moving right.
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 0);
|
||||||
|
assert_eq!(state.display_start_char_index, 0);
|
||||||
|
|
||||||
|
move_right(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 1);
|
||||||
|
assert_eq!(state.display_start_char_index, 0);
|
||||||
|
|
||||||
|
move_right(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 2);
|
||||||
|
assert_eq!(state.display_start_char_index, 0);
|
||||||
|
|
||||||
|
move_right(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 3);
|
||||||
|
assert_eq!(state.display_start_char_index, 0);
|
||||||
|
|
||||||
|
move_right(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 4);
|
||||||
|
assert_eq!(state.display_start_char_index, 2);
|
||||||
|
|
||||||
|
move_right(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 7);
|
||||||
|
assert_eq!(state.display_start_char_index, 4);
|
||||||
|
|
||||||
|
move_right(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 10);
|
||||||
|
assert_eq!(state.display_start_char_index, 7);
|
||||||
|
|
||||||
|
move_right(&mut state);
|
||||||
|
move_right(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 12);
|
||||||
|
assert_eq!(state.display_start_char_index, 10);
|
||||||
|
|
||||||
|
// Moving left.
|
||||||
|
move_left(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 11);
|
||||||
|
assert_eq!(state.display_start_char_index, 10);
|
||||||
|
|
||||||
|
move_left(&mut state);
|
||||||
|
move_left(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 7);
|
||||||
|
assert_eq!(state.display_start_char_index, 7);
|
||||||
|
|
||||||
|
move_left(&mut state);
|
||||||
|
move_left(&mut state);
|
||||||
|
move_left(&mut state);
|
||||||
|
move_left(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 1);
|
||||||
|
assert_eq!(state.display_start_char_index, 1);
|
||||||
|
|
||||||
|
move_left(&mut state);
|
||||||
|
state.get_start_position(4, false);
|
||||||
|
assert_eq!(state.grapheme_cursor.cur_cursor(), 0);
|
||||||
|
assert_eq!(state.display_start_char_index, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,45 +2,6 @@ use std::{cmp::min, time::Instant};
|
|||||||
|
|
||||||
use tui::layout::Rect;
|
use tui::layout::Rect;
|
||||||
|
|
||||||
use crate::app::CursorDirection;
|
|
||||||
|
|
||||||
pub fn get_search_start_position(
|
|
||||||
num_columns: usize, cursor_direction: &CursorDirection, cursor_bar: &mut usize,
|
|
||||||
current_cursor_position: usize, is_force_redraw: bool,
|
|
||||||
) -> usize {
|
|
||||||
if is_force_redraw {
|
|
||||||
*cursor_bar = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
match cursor_direction {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
} else if current_cursor_position >= *cursor_bar + num_columns {
|
|
||||||
*cursor_bar = current_cursor_position - num_columns;
|
|
||||||
}
|
|
||||||
// Else, don't change what our start position is from whatever it is set to!
|
|
||||||
*cursor_bar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate how many bars are to be drawn within basic mode's components.
|
/// Calculate how many bars are to be drawn within basic mode's components.
|
||||||
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
|
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
|
||||||
min(
|
min(
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
style::Style,
|
||||||
terminal::Frame,
|
terminal::Frame,
|
||||||
text::{Span, Spans},
|
text::{Span, Spans},
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
};
|
};
|
||||||
use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation};
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::App,
|
app::{App, AppSearchState},
|
||||||
canvas::{drawing_utils::get_search_start_position, Painter},
|
canvas::Painter,
|
||||||
components::data_table::{DrawInfo, SelectionState},
|
components::data_table::{DrawInfo, SelectionState},
|
||||||
constants::*,
|
constants::*,
|
||||||
};
|
};
|
||||||
@ -68,8 +68,6 @@ impl Painter {
|
|||||||
|
|
||||||
/// Draws the process sort box.
|
/// Draws the process sort box.
|
||||||
/// - `widget_id` represents the widget ID of the process widget itself.an
|
/// - `widget_id` represents the widget ID of the process widget itself.an
|
||||||
///
|
|
||||||
/// This should not be directly called.
|
|
||||||
fn draw_processes_table<B: Backend>(
|
fn draw_processes_table<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
@ -99,38 +97,42 @@ impl Painter {
|
|||||||
/// Draws the process search field.
|
/// Draws the process search field.
|
||||||
/// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget
|
/// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget
|
||||||
/// state that is stored.
|
/// state that is stored.
|
||||||
///
|
|
||||||
/// This should not be directly called.
|
|
||||||
fn draw_search_field<B: Backend>(
|
fn draw_search_field<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
) {
|
) {
|
||||||
fn build_query<'a>(
|
fn build_query_span(
|
||||||
is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize,
|
search_state: &AppSearchState, available_width: usize, is_on_widget: bool,
|
||||||
cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style,
|
currently_selected_text_style: Style, text_style: Style,
|
||||||
text_style: tui::style::Style,
|
) -> Vec<Span<'_>> {
|
||||||
) -> Vec<Span<'a>> {
|
let start_index = search_state.display_start_char_index;
|
||||||
let mut current_grapheme_pos = 0;
|
let cursor_index = search_state.grapheme_cursor.cur_cursor();
|
||||||
|
let mut current_width = 0;
|
||||||
|
let query = search_state.current_search_query.as_str();
|
||||||
|
|
||||||
if is_on_widget {
|
if is_on_widget {
|
||||||
let mut res = grapheme_indices
|
let mut res = Vec::with_capacity(available_width);
|
||||||
.filter_map(|grapheme| {
|
for ((index, grapheme), lengths) in
|
||||||
current_grapheme_pos += UnicodeWidthStr::width(grapheme.1);
|
UnicodeSegmentation::grapheme_indices(query, true)
|
||||||
|
.zip(search_state.size_mappings.values())
|
||||||
if current_grapheme_pos <= start_position {
|
{
|
||||||
None
|
if index < start_index {
|
||||||
|
continue;
|
||||||
|
} else if current_width > available_width {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
let styled = if index == cursor_index {
|
||||||
|
Span::styled(grapheme, currently_selected_text_style)
|
||||||
} else {
|
} else {
|
||||||
let styled = if grapheme.0 == cursor_position {
|
Span::styled(grapheme, text_style)
|
||||||
Span::styled(grapheme.1, currently_selected_text_style)
|
};
|
||||||
} else {
|
|
||||||
Span::styled(grapheme.1, text_style)
|
|
||||||
};
|
|
||||||
Some(styled)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if cursor_position == query.len() {
|
res.push(styled);
|
||||||
|
current_width += lengths.end - lengths.start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor_index == query.len() {
|
||||||
res.push(Span::styled(" ", currently_selected_text_style))
|
res.push(Span::styled(" ", currently_selected_text_style))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,44 +145,36 @@ impl Painter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make the cursor scroll back if there's space!
|
|
||||||
if let Some(proc_widget_state) =
|
if let Some(proc_widget_state) =
|
||||||
app_state.proc_state.widget_states.get_mut(&(widget_id - 1))
|
app_state.proc_state.widget_states.get_mut(&(widget_id - 1))
|
||||||
{
|
{
|
||||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||||
let num_columns = usize::from(draw_loc.width);
|
let num_columns = usize::from(draw_loc.width);
|
||||||
let search_title = "> ";
|
const SEARCH_TITLE: &str = "> ";
|
||||||
|
let offset = if draw_border { 4 } else { 2 }; // width of 3 removed for >_|
|
||||||
|
let available_width = if num_columns > (offset + 3) {
|
||||||
|
num_columns - offset
|
||||||
|
} else {
|
||||||
|
num_columns
|
||||||
|
};
|
||||||
|
|
||||||
let num_chars_for_text = search_title.len();
|
proc_widget_state
|
||||||
let cursor_position = proc_widget_state.get_search_cursor_position();
|
.proc_search
|
||||||
let current_cursor_position = proc_widget_state.get_char_cursor_position();
|
.search_state
|
||||||
|
.get_start_position(available_width, app_state.is_force_redraw);
|
||||||
|
|
||||||
let start_position: usize = get_search_start_position(
|
|
||||||
num_columns - num_chars_for_text - 5,
|
|
||||||
&proc_widget_state.proc_search.search_state.cursor_direction,
|
|
||||||
&mut proc_widget_state.proc_search.search_state.cursor_bar,
|
|
||||||
current_cursor_position,
|
|
||||||
app_state.is_force_redraw,
|
|
||||||
);
|
|
||||||
|
|
||||||
let query = proc_widget_state.get_current_search_query().as_str();
|
|
||||||
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true);
|
|
||||||
|
|
||||||
// TODO: [CURSOR] blank cursor if not selected
|
|
||||||
// TODO: [CURSOR] blinking cursor?
|
// TODO: [CURSOR] blinking cursor?
|
||||||
let query_with_cursor = build_query(
|
let query_with_cursor = build_query_span(
|
||||||
|
&proc_widget_state.proc_search.search_state,
|
||||||
|
available_width,
|
||||||
is_on_widget,
|
is_on_widget,
|
||||||
grapheme_indices,
|
|
||||||
start_position,
|
|
||||||
cursor_position,
|
|
||||||
query,
|
|
||||||
self.colours.currently_selected_text_style,
|
self.colours.currently_selected_text_style,
|
||||||
self.colours.text_style,
|
self.colours.text_style,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut search_text = vec![Spans::from({
|
let mut search_text = vec![Spans::from({
|
||||||
let mut search_vec = vec![Span::styled(
|
let mut search_vec = vec![Span::styled(
|
||||||
search_title,
|
SEARCH_TITLE,
|
||||||
if is_on_widget {
|
if is_on_widget {
|
||||||
self.colours.table_header_style
|
self.colours.table_header_style
|
||||||
} else {
|
} else {
|
||||||
@ -300,8 +294,6 @@ impl Painter {
|
|||||||
/// Draws the process sort box.
|
/// Draws the process sort box.
|
||||||
/// - `widget_id` represents the widget ID of the sort box itself --- NOT the process widget
|
/// - `widget_id` represents the widget ID of the sort box itself --- NOT the process widget
|
||||||
/// state that is stored.
|
/// state that is stored.
|
||||||
///
|
|
||||||
/// This should not be directly called.
|
|
||||||
fn draw_sort_table<B: Backend>(
|
fn draw_sort_table<B: Backend>(
|
||||||
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
) {
|
) {
|
||||||
|
@ -52,26 +52,27 @@ impl Default for DataTableState {
|
|||||||
impl DataTableState {
|
impl DataTableState {
|
||||||
/// Gets the starting position of a table.
|
/// Gets the starting position of a table.
|
||||||
pub fn get_start_position(&mut self, num_rows: usize, is_force_redraw: bool) {
|
pub fn get_start_position(&mut self, num_rows: usize, is_force_redraw: bool) {
|
||||||
let mut start_index = self.display_start_index;
|
let start_index = if is_force_redraw {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
self.display_start_index
|
||||||
|
};
|
||||||
let current_scroll_position = self.current_index;
|
let current_scroll_position = self.current_index;
|
||||||
let scroll_direction = self.scroll_direction;
|
let scroll_direction = self.scroll_direction;
|
||||||
|
|
||||||
if is_force_redraw {
|
|
||||||
start_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.display_start_index = match scroll_direction {
|
self.display_start_index = match scroll_direction {
|
||||||
ScrollDirection::Down => {
|
ScrollDirection::Down => {
|
||||||
if current_scroll_position < start_index + num_rows {
|
if current_scroll_position < start_index + num_rows {
|
||||||
// If, using previous_scrolled_position, we can see the element
|
// If, using the current scroll position, we can see the element
|
||||||
// (so within that and + num_rows) just reuse the current previously scrolled position
|
// (so within that and + num_rows) just reuse the current previously
|
||||||
|
// scrolled position.
|
||||||
start_index
|
start_index
|
||||||
} else if current_scroll_position >= num_rows {
|
} else if current_scroll_position >= num_rows {
|
||||||
// Else if the current position past the last element visible in the list, omit
|
// If the current position past the last element visible in the list,
|
||||||
// until we can see that element
|
// then skip until we can see that element.
|
||||||
current_scroll_position - num_rows + 1
|
current_scroll_position - num_rows + 1
|
||||||
} else {
|
} else {
|
||||||
// Else, if it is not past the last element visible, do not omit anything
|
// Else, if it is not past the last element visible, do not omit anything.
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,10 +103,39 @@ pub fn truncate_to_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the width of a str `s`. This takes into account some things like
|
||||||
|
/// joiners when calculating width.
|
||||||
|
pub fn str_width(s: &str) -> usize {
|
||||||
|
UnicodeSegmentation::graphemes(s, true)
|
||||||
|
.map(|g| {
|
||||||
|
if g.contains('\u{200d}') {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
UnicodeWidthStr::width(g)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the "width" of grapheme `g`. This takes into account some things like
|
||||||
|
/// joiners when calculating width.
|
||||||
|
///
|
||||||
|
/// Note that while you *can* pass in an entire string, the point is to check
|
||||||
|
/// individual graphemes (e.g. `"a"`, `"💎"`, `"大"`, `"🇨🇦"`).
|
||||||
|
#[inline]
|
||||||
|
fn grapheme_width(g: &str) -> usize {
|
||||||
|
if g.contains('\u{200d}') {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
UnicodeWidthStr::width(g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Truncates a string with an ellipsis character.
|
/// Truncates a string with an ellipsis character.
|
||||||
///
|
///
|
||||||
/// NB: This probably does not handle EVERY case, but I think it handles most cases
|
/// NB: This probably does not handle EVERY case, but I think it handles most cases
|
||||||
/// we will use this function for fine... hopefully.
|
/// we will use this function for fine... hopefully.
|
||||||
|
#[inline]
|
||||||
fn truncate_str<U: Into<usize>>(content: &str, width: U) -> String {
|
fn truncate_str<U: Into<usize>>(content: &str, width: U) -> String {
|
||||||
let width = width.into();
|
let width = width.into();
|
||||||
let mut text = String::with_capacity(width);
|
let mut text = String::with_capacity(width);
|
||||||
@ -124,11 +153,7 @@ fn truncate_str<U: Into<usize>>(content: &str, width: U) -> String {
|
|||||||
// - Adds a character not up to the boundary, then fails.
|
// - Adds a character not up to the boundary, then fails.
|
||||||
// Inspired by https://tomdebruijn.com/posts/rust-string-length-width-calculations/
|
// Inspired by https://tomdebruijn.com/posts/rust-string-length-width-calculations/
|
||||||
for g in UnicodeSegmentation::graphemes(content, true) {
|
for g in UnicodeSegmentation::graphemes(content, true) {
|
||||||
let g_width = if g.contains('\u{200d}') {
|
let g_width = grapheme_width(g);
|
||||||
2
|
|
||||||
} else {
|
|
||||||
UnicodeWidthStr::width(g)
|
|
||||||
};
|
|
||||||
|
|
||||||
if curr_width + g_width <= width {
|
if curr_width + g_width <= width {
|
||||||
curr_width += g_width;
|
curr_width += g_width;
|
||||||
@ -164,7 +189,6 @@ pub const fn sort_partial_fn<T: std::cmp::PartialOrd>(is_descending: bool) -> fn
|
|||||||
/// Returns an [`Ordering`] between two [`PartialOrd`]s.
|
/// Returns an [`Ordering`] between two [`PartialOrd`]s.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn partial_ordering<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
|
pub fn partial_ordering<T: std::cmp::PartialOrd>(a: T, b: T) -> Ordering {
|
||||||
// TODO: Switch to `total_cmp` on 1.62
|
|
||||||
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@ 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 {
|
||||||
@ -710,19 +709,15 @@ impl ProcWidget {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_search_cursor_position(&self) -> usize {
|
pub fn cursor_char_index(&self) -> usize {
|
||||||
self.proc_search.search_state.grapheme_cursor.cur_cursor()
|
self.proc_search.search_state.grapheme_cursor.cur_cursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_char_cursor_position(&self) -> usize {
|
|
||||||
self.proc_search.search_state.char_cursor_position
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_search_enabled(&self) -> bool {
|
pub fn is_search_enabled(&self) -> bool {
|
||||||
self.proc_search.search_state.is_enabled
|
self.proc_search.search_state.is_enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_search_query(&self) -> &String {
|
pub fn current_search_query(&self) -> &str {
|
||||||
&self.proc_search.search_state.current_search_query
|
&self.proc_search.search_state.current_search_query
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -759,6 +754,9 @@ impl ProcWidget {
|
|||||||
self.table.state.display_start_index = 0;
|
self.table.state.display_start_index = 0;
|
||||||
self.table.state.current_index = 0;
|
self.table.state.current_index = 0;
|
||||||
|
|
||||||
|
// Update the internal sizes too.
|
||||||
|
self.proc_search.search_state.update_sizes();
|
||||||
|
|
||||||
self.force_data_update();
|
self.force_data_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,69 +765,12 @@ impl ProcWidget {
|
|||||||
self.force_data_update();
|
self.force_data_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search_walk_forward(&mut self, start_position: usize) {
|
pub fn search_walk_forward(&mut self) {
|
||||||
// TODO: Add tests for this.
|
self.proc_search.search_state.walk_forward();
|
||||||
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
|
|
||||||
.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) {
|
pub fn search_walk_back(&mut self) {
|
||||||
// TODO: Add tests for this.
|
self.proc_search.search_state.walk_backward();
|
||||||
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
|
|
||||||
.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
|
/// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not
|
||||||
|
Loading…
x
Reference in New Issue
Block a user