refactor: finish help menu

This commit is contained in:
ClementTsang 2021-09-22 01:16:33 -04:00
parent e7b9c72912
commit 7ee85a82f7
36 changed files with 1011 additions and 852 deletions

View File

@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Changes
- [#557](https://github.com/ClementTsang/bottom/pull/557): Add '/s' to network usage legend.
- [#557](https://github.com/ClementTsang/bottom/pull/557): Add '/s' to network usage legend to indicate "per second".
## Internal Changes

29
Cargo.lock generated
View File

@ -263,6 +263,7 @@ dependencies = [
"serde",
"smol",
"sysinfo",
"textwrap 0.14.2",
"thiserror",
"toml",
"tui",
@ -348,7 +349,7 @@ dependencies = [
"atty",
"bitflags",
"strsim",
"textwrap",
"textwrap 0.11.0",
"unicode-width",
"vec_map",
]
@ -1410,6 +1411,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "smol"
version = "1.2.5"
@ -1482,6 +1489,17 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.24"
@ -1558,6 +1576,15 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
[[package]]
name = "unicode-linebreak"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
dependencies = [
"regex",
]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"

View File

@ -57,6 +57,7 @@ serde = { version = "1.0.125", features = ["derive"] }
# Sysinfo is still used in Linux for the ProcessStatus
sysinfo = "0.18.2"
thiserror = "1.0.24"
textwrap = "0.14.2"
toml = "0.5.8"
tui = { version = "0.16.0", features = ["crossterm"], default-features = false }
typed-builder = "0.9.0"

View File

@ -42,7 +42,7 @@ Note that key bindings are generally case-sensitive.
| ++q++ , ++ctrl+c++ | Quit |
| ++esc++ | Close dialog windows, search, widgets, or exit expanded mode |
| ++ctrl+r++ | Resets any collected data |
| ++f++ | Toggles freezing, which stops new data from being shown |
| ++f++ | Toggles freezing, stopping new data from being shown |
| ++question++ | Open help menu |
| ++e++ | Toggle expanding the currently selected widget |
| ++ctrl+up++ <br/> ++shift+up++ <br/> ++K++ <br/> ++W++ | Select the widget above |

View File

@ -13,7 +13,7 @@ use std::{
time::Instant,
};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
use fxhash::FxHashMap;
use indextree::{Arena, NodeId};
use unicode_segmentation::GraphemeCursor;
@ -33,7 +33,7 @@ use crate::{
BottomEvent, Pid,
};
use self::event::{EventResult, ReturnSignal, WidgetEventResult};
use self::event::{ComponentEventResult, EventResult, ReturnSignal};
const MAX_SEARCH_LENGTH: usize = 200;
@ -140,8 +140,6 @@ pub struct AppState {
// --- Eventually delete/rewrite ---
pub delete_dialog_state: AppDeleteDialogState,
pub help_dialog_state: AppHelpDialogState,
// --- TO DELETE ---
pub cpu_state: CpuState,
pub mem_state: MemState,
@ -172,6 +170,8 @@ pub struct AppState {
pub layout_tree: Arena<LayoutNode>,
pub layout_tree_root: NodeId,
frozen_state: FrozenState,
pub help_dialog: DialogState<HelpDialog>,
}
impl AppState {
@ -204,7 +204,6 @@ impl AppState {
data_collection: Default::default(),
is_expanded: Default::default(),
delete_dialog_state: Default::default(),
help_dialog_state: Default::default(),
cpu_state: Default::default(),
mem_state: Default::default(),
net_state: Default::default(),
@ -222,6 +221,7 @@ impl AppState {
is_force_redraw: Default::default(),
is_determining_widget_boundary: Default::default(),
frozen_state: Default::default(),
help_dialog: Default::default(),
}
}
@ -277,7 +277,7 @@ impl AppState {
let c = c.to_ascii_lowercase();
match c {
'q' => EventResult::Quit,
'e' => {
'e' if !self.help_dialog.is_showing() => {
if self.app_config_fields.use_basic_mode {
EventResult::NoRedraw
} else {
@ -285,11 +285,11 @@ impl AppState {
EventResult::Redraw
}
}
'?' => {
self.help_dialog_state.is_showing_help = true;
'?' if !self.help_dialog.is_showing() => {
self.help_dialog.show();
EventResult::Redraw
}
'f' => {
'f' if !self.help_dialog.is_showing() => {
self.toggle_freeze();
if !self.is_frozen() {
let data_collection = &self.data_collection;
@ -331,17 +331,59 @@ impl AppState {
}
}
/// Quick and dirty handler to convert [`ComponentEventResult`]s to [`EventResult`]s, and handle [`ReturnSignal`]s.
fn convert_widget_event_result(&mut self, w: ComponentEventResult) -> EventResult {
match w {
ComponentEventResult::Unhandled => EventResult::NoRedraw,
ComponentEventResult::Redraw => EventResult::Redraw,
ComponentEventResult::NoRedraw => EventResult::NoRedraw,
ComponentEventResult::Signal(signal) => match signal {
ReturnSignal::KillProcess => {
todo!()
}
ReturnSignal::Update => {
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
match &self.frozen_state {
FrozenState::NotFrozen => {
widget.update_data(&self.data_collection);
}
FrozenState::Frozen(frozen_data) => {
widget.update_data(frozen_data);
}
}
}
EventResult::Redraw
}
},
}
}
/// Handles a [`KeyEvent`], and returns an [`EventResult`].
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
let result = if let DialogState::Shown(help_dialog) = &mut self.help_dialog {
help_dialog.handle_key_event(event)
} else if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
widget.handle_key_event(event)
} else {
ComponentEventResult::Unhandled
};
match result {
ComponentEventResult::Unhandled => self.handle_global_key_event(event),
_ => self.convert_widget_event_result(result),
}
}
/// Handles a global [`KeyEvent`], and returns an [`EventResult`].
fn handle_global_shortcut(&mut self, event: KeyEvent) -> EventResult {
fn handle_global_key_event(&mut self, event: KeyEvent) -> EventResult {
if event.modifiers.is_empty() {
match event.code {
KeyCode::Esc => {
if self.is_expanded {
self.is_expanded = false;
EventResult::Redraw
} else if self.help_dialog_state.is_showing_help {
self.help_dialog_state.is_showing_help = false;
self.help_dialog_state.scroll_state.current_scroll_index = 0;
} else if self.help_dialog.is_showing() {
self.help_dialog.hide();
EventResult::Redraw
} else if self.delete_dialog_state.is_showing_dd {
self.close_dd();
@ -350,8 +392,13 @@ impl AppState {
EventResult::NoRedraw
}
}
KeyCode::Char(c) => self.handle_global_char(c),
_ => EventResult::NoRedraw,
_ => {
if let KeyCode::Char(c) = event.code {
self.handle_global_char(c)
} else {
EventResult::NoRedraw
}
}
}
} else if let KeyModifiers::CONTROL = event.modifiers {
match event.code {
@ -384,30 +431,55 @@ impl AppState {
}
}
/// Quick and dirty handler to convert [`WidgetEventResult`]s to [`EventResult`]s, and handle [`ReturnSignal`]s.
fn convert_widget_event_result(&mut self, w: WidgetEventResult) -> EventResult {
match w {
WidgetEventResult::Quit => EventResult::Quit,
WidgetEventResult::Redraw => EventResult::Redraw,
WidgetEventResult::NoRedraw => EventResult::NoRedraw,
WidgetEventResult::Signal(signal) => match signal {
ReturnSignal::KillProcess => {
todo!()
}
ReturnSignal::Update => {
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
match &self.frozen_state {
FrozenState::NotFrozen => {
widget.update_data(&self.data_collection);
}
FrozenState::Frozen(frozen_data) => {
widget.update_data(frozen_data);
}
/// Handles a [`MouseEvent`].
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
if let DialogState::Shown(help_dialog) = &mut self.help_dialog {
let result = help_dialog.handle_mouse_event(event);
self.convert_widget_event_result(result)
} else if self.is_expanded {
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
let result = widget.handle_mouse_event(event);
self.convert_widget_event_result(result)
} else {
EventResult::NoRedraw
}
} else {
let mut returned_result = EventResult::NoRedraw;
for (id, widget) in self.widget_lookup_map.iter_mut() {
if widget.does_border_intersect_mouse(&event) {
let result = widget.handle_mouse_event(event);
let new_id;
match widget.selectable_type() {
SelectableType::Selectable => {
new_id = *id;
}
SelectableType::Unselectable => {
let result = widget.handle_mouse_event(event);
return self.convert_widget_event_result(result);
}
SelectableType::Redirect(redirected_id) => {
new_id = redirected_id;
}
}
EventResult::Redraw
let was_id_already_selected = self.selected_widget == new_id;
self.selected_widget = new_id;
if was_id_already_selected {
returned_result = self.convert_widget_event_result(result);
break;
} else {
// If the weren't equal, *force* a redraw, and correct the layout tree.
correct_layout_last_selections(&mut self.layout_tree, self.selected_widget);
let _ = self.convert_widget_event_result(result);
returned_result = EventResult::Redraw;
break;
}
}
},
}
returned_result
}
}
@ -415,83 +487,16 @@ impl AppState {
/// whether the app now requires a redraw.
pub fn handle_event(&mut self, event: BottomEvent) -> EventResult {
match event {
BottomEvent::KeyInput(event) => {
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
let result = widget.handle_key_event(event);
match self.convert_widget_event_result(result) {
EventResult::Quit => EventResult::Quit,
EventResult::Redraw => EventResult::Redraw,
EventResult::NoRedraw => self.handle_global_shortcut(event),
}
} else {
self.handle_global_shortcut(event)
}
}
BottomEvent::KeyInput(event) => self.handle_key_event(event),
BottomEvent::MouseInput(event) => {
// Not great, but basically a blind lookup through the table for anything that clips the click location.
// TODO: Would be cool to use a kd-tree or something like that in the future.
match &event.kind {
MouseEventKind::Down(MouseButton::Left) => {
if self.is_expanded {
if let Some(widget) =
self.widget_lookup_map.get_mut(&self.selected_widget)
{
let result = widget.handle_mouse_event(event);
return self.convert_widget_event_result(result);
}
} else {
for (id, widget) in self.widget_lookup_map.iter_mut() {
if widget.does_border_intersect_mouse(&event) {
let result = widget.handle_mouse_event(event);
let new_id;
match widget.selectable_type() {
SelectableType::Selectable => {
new_id = *id;
}
SelectableType::Unselectable => {
let result = widget.handle_mouse_event(event);
return self.convert_widget_event_result(result);
}
SelectableType::Redirect(redirected_id) => {
new_id = redirected_id;
}
}
let was_id_already_selected = self.selected_widget == new_id;
self.selected_widget = new_id;
if was_id_already_selected {
return self.convert_widget_event_result(result);
} else {
// If the weren't equal, *force* a redraw, and correct the layout tree.
correct_layout_last_selections(
&mut self.layout_tree,
self.selected_widget,
);
let _ = self.convert_widget_event_result(result);
return EventResult::Redraw;
}
}
}
}
}
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {
if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget)
{
let result = widget.handle_mouse_event(event);
return self.convert_widget_event_result(result);
}
}
_ => {}
}
EventResult::NoRedraw
self.handle_mouse_event(event)
}
BottomEvent::Update(new_data) => {
self.data_collection.eat_data(new_data);
// TODO: Optimization for dialogs; don't redraw here.
if !self.is_frozen() {
let data_collection = &self.data_collection;
self.widget_lookup_map
@ -515,78 +520,6 @@ impl AppState {
}
}
pub fn on_esc(&mut self) {
self.reset_multi_tap_keys();
if self.is_in_dialog() {
if self.help_dialog_state.is_showing_help {
self.help_dialog_state.is_showing_help = false;
self.help_dialog_state.scroll_state.current_scroll_index = 0;
} else {
self.close_dd();
}
self.is_force_redraw = true;
} else {
match self.current_widget.widget_type {
BottomWidgetType::Proc => {
if let Some(current_proc_state) = self
.proc_state
.get_mut_widget_state(self.current_widget.widget_id)
{
if current_proc_state.is_search_enabled() || current_proc_state.is_sort_open
{
current_proc_state
.process_search_state
.search_state
.is_enabled = false;
current_proc_state.is_sort_open = false;
self.is_force_redraw = true;
return;
}
}
}
BottomWidgetType::ProcSearch => {
if let Some(current_proc_state) = self
.proc_state
.get_mut_widget_state(self.current_widget.widget_id - 1)
{
if current_proc_state.is_search_enabled() {
current_proc_state
.process_search_state
.search_state
.is_enabled = false;
self.move_widget_selection(&WidgetDirection::Up);
self.is_force_redraw = true;
return;
}
}
}
BottomWidgetType::ProcSort => {
if let Some(current_proc_state) = self
.proc_state
.get_mut_widget_state(self.current_widget.widget_id - 2)
{
if current_proc_state.is_sort_open {
current_proc_state.columns.current_scroll_position =
current_proc_state.columns.backup_prev_scroll_position;
current_proc_state.is_sort_open = false;
self.move_widget_selection(&WidgetDirection::Right);
self.is_force_redraw = true;
return;
}
}
}
_ => {}
}
if self.is_expanded {
self.is_expanded = false;
self.is_force_redraw = true;
}
}
}
pub fn is_in_search_widget(&self) -> bool {
matches!(
self.current_widget.widget_type,
@ -600,7 +533,7 @@ impl AppState {
}
fn is_in_dialog(&self) -> bool {
self.help_dialog_state.is_showing_help || self.delete_dialog_state.is_showing_dd
self.delete_dialog_state.is_showing_dd
}
fn ignore_normal_keybinds(&self) -> bool {
@ -1097,7 +1030,7 @@ impl AppState {
pub fn on_up_key(&mut self) {
if !self.is_in_dialog() {
self.decrement_position_count();
} else if self.help_dialog_state.is_showing_help {
} else if self.help_dialog.is_showing() {
self.help_scroll_up();
} else if self.delete_dialog_state.is_showing_dd {
#[cfg(target_os = "windows")]
@ -1118,7 +1051,7 @@ impl AppState {
pub fn on_down_key(&mut self) {
if !self.is_in_dialog() {
self.increment_position_count();
} else if self.help_dialog_state.is_showing_help {
} else if self.help_dialog.is_showing() {
self.help_scroll_down();
} else if self.delete_dialog_state.is_showing_dd {
#[cfg(target_os = "windows")]
@ -1596,19 +1529,9 @@ impl AppState {
}
}
self.handle_char(caught_char);
} else if self.help_dialog_state.is_showing_help {
} else if self.help_dialog.is_showing() {
match caught_char {
'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
let potential_index = caught_char.to_digit(10);
if let Some(potential_index) = potential_index {
if (potential_index as usize) < self.help_dialog_state.index_shortcuts.len()
{
self.help_scroll_to_or_max(
self.help_dialog_state.index_shortcuts[potential_index as usize],
);
}
}
}
'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {}
'j' | 'k' | 'g' | 'G' => self.handle_char(caught_char),
_ => {}
}
@ -1792,7 +1715,7 @@ impl AppState {
}
}
'?' => {
self.help_dialog_state.is_showing_help = true;
self.help_dialog.show();
self.is_force_redraw = true;
}
'H' | 'A' => self.move_widget_selection(&WidgetDirection::Left),
@ -1863,7 +1786,6 @@ impl AppState {
}
fn expand_widget(&mut self) {
// TODO: [BASIC] Expansion in basic mode.
if !self.ignore_normal_keybinds() && !self.app_config_fields.use_basic_mode {
// Pop-out mode. We ignore if in process search.
@ -2256,7 +2178,7 @@ impl AppState {
{
if proc_widget_state.is_sort_open {
if let Some(proc_sort_widget) = self.widget_map.get(&new_widget_id) {
self.current_widget = proc_sort_widget.clone(); // TODO: Could I remove this clone w/ static references?
self.current_widget = proc_sort_widget.clone();
}
}
}
@ -2378,8 +2300,8 @@ impl AppState {
_ => {}
}
self.reset_multi_tap_keys();
} else if self.help_dialog_state.is_showing_help {
self.help_dialog_state.scroll_state.current_scroll_index = 0;
} else if self.help_dialog.is_showing() {
// self.help_dialog_state.scroll_state.current_scroll_index = 0;
} else if self.delete_dialog_state.is_showing_dd {
self.delete_dialog_state.selected_signal = KillSignal::Cancel;
}
@ -2517,26 +2439,17 @@ impl AppState {
}
fn help_scroll_up(&mut self) {
if self.help_dialog_state.scroll_state.current_scroll_index > 0 {
self.help_dialog_state.scroll_state.current_scroll_index -= 1;
}
// if self.help_dialog_state.scroll_state.current_scroll_index > 0 {
// self.help_dialog_state.scroll_state.current_scroll_index -= 1;
// }
}
fn help_scroll_down(&mut self) {
if self.help_dialog_state.scroll_state.current_scroll_index + 1
< self.help_dialog_state.scroll_state.max_scroll_index
{
self.help_dialog_state.scroll_state.current_scroll_index += 1;
}
}
fn help_scroll_to_or_max(&mut self, new_position: u16) {
if new_position < self.help_dialog_state.scroll_state.max_scroll_index {
self.help_dialog_state.scroll_state.current_scroll_index = new_position;
} else {
self.help_dialog_state.scroll_state.current_scroll_index =
self.help_dialog_state.scroll_state.max_scroll_index - 1;
}
// if self.help_dialog_state.scroll_state.current_scroll_index + 1
// < self.help_dialog_state.scroll_state.max_scroll_index
// {
// self.help_dialog_state.scroll_state.current_scroll_index += 1;
// }
}
fn on_plus(&mut self) {

View File

@ -224,15 +224,12 @@ impl DataCollection {
}
fn eat_temp(&mut self, temperature_sensors: Vec<temperature::TempHarvest>) {
// TODO: [PO] To implement
self.temp_harvest = temperature_sensors.to_vec();
}
fn eat_disks(
&mut self, disks: Vec<disks::DiskHarvest>, io: disks::IoHarvest, harvested_time: Instant,
) {
// TODO: [PO] To implement
let time_since_last_harvest = harvested_time
.duration_since(self.current_instant)
.as_secs_f64();

View File

@ -27,7 +27,6 @@ use std::borrow::Cow;
use crate::Pid;
// TODO: Add value so we know if it's sorted ascending or descending by default?
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum ProcessSorting {
CpuPercent,

View File

@ -57,7 +57,6 @@ fn is_temp_filtered(filter: &Option<Filter>, text: &str) -> bool {
fn temp_vec_sort(temperature_vec: &mut Vec<TempHarvest>) {
// By default, sort temperature, then by alphabetically!
// TODO: [TEMPS] Allow users to control this.
// Note we sort in reverse here; we want greater temps to be higher priority.
temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) {

View File

@ -1,6 +1,5 @@
use std::time::{Duration, Instant};
const MAX_TIMEOUT: Duration = Duration::from_millis(400);
pub mod multi_key;
pub use multi_key::*;
/// These are "signals" that are sent along with an [`WidgetEventResult`] to signify a potential additional action
/// that the caller must do, along with the "core" result of either drawing or redrawing.
@ -32,9 +31,9 @@ pub enum EventResult {
/// The results of a widget handling some event, like a mouse or key event,
/// signifying what the program should then do next.
#[derive(Debug)]
pub enum WidgetEventResult {
/// Kill the program.
Quit,
pub enum ComponentEventResult {
/// The event isn't handled by the widget, and should be propagated to the parent.
Unhandled,
/// Trigger a redraw.
Redraw,
/// Don't trigger a redraw.
@ -50,101 +49,3 @@ pub enum SelectionAction {
/// This event occurs if the widget did not handle the selection action; the caller must handle it.
NotHandled,
}
/// The states a [`MultiKey`] can be in.
enum MultiKeyState {
/// Currently not waiting on any next input.
Idle,
/// Waiting for the next input, with a given trigger [`Instant`].
Waiting {
/// When it was triggered.
trigger_instant: Instant,
/// What part of the pattern it is at.
checked_index: usize,
},
}
/// The possible outcomes of calling [`MultiKey::input`].
pub enum MultiKeyResult {
/// Returned when a character was *accepted*, but has not completed the sequence required.
Accepted,
/// Returned when a character is accepted and completes the sequence.
Completed,
/// Returned if a character breaks the sequence or if it has already timed out.
Rejected,
}
/// A struct useful for managing multi-key keybinds.
pub struct MultiKey {
state: MultiKeyState,
pattern: Vec<char>,
}
impl MultiKey {
/// Creates a new [`MultiKey`] with a given pattern and timeout.
pub fn register(pattern: Vec<char>) -> Self {
Self {
state: MultiKeyState::Idle,
pattern,
}
}
/// Resets to an idle state.
fn reset(&mut self) {
self.state = MultiKeyState::Idle;
}
/// Handles a char input and returns the current status of the [`MultiKey`] after, which is one of:
/// - Accepting the char and moving to the next state
/// - Completing the multi-key pattern
/// - Rejecting it
///
/// Note that if a [`MultiKey`] only "times out" upon calling this - if it has timed out, it will first reset
/// before trying to check the char.
pub fn input(&mut self, c: char) -> MultiKeyResult {
match &mut self.state {
MultiKeyState::Idle => {
if let Some(first) = self.pattern.first() {
if *first == c {
self.state = MultiKeyState::Waiting {
trigger_instant: Instant::now(),
checked_index: 0,
};
return MultiKeyResult::Accepted;
}
}
MultiKeyResult::Rejected
}
MultiKeyState::Waiting {
trigger_instant,
checked_index,
} => {
if trigger_instant.elapsed() > MAX_TIMEOUT {
// Just reset and recursively call (putting it into Idle).
self.reset();
self.input(c)
} else if let Some(next) = self.pattern.get(*checked_index + 1) {
if *next == c {
*checked_index += 1;
if *checked_index == self.pattern.len() - 1 {
self.reset();
MultiKeyResult::Completed
} else {
MultiKeyResult::Accepted
}
} else {
self.reset();
MultiKeyResult::Rejected
}
} else {
self.reset();
MultiKeyResult::Rejected
}
}
}
}
}

101
src/app/event/multi_key.rs Normal file
View File

@ -0,0 +1,101 @@
use std::time::{Duration, Instant};
const MAX_TIMEOUT: Duration = Duration::from_millis(400);
/// The states a [`MultiKey`] can be in.
enum MultiKeyState {
/// Currently not waiting on any next input.
Idle,
/// Waiting for the next input, with a given trigger [`Instant`].
Waiting {
/// When it was triggered.
trigger_instant: Instant,
/// What part of the pattern it is at.
checked_index: usize,
},
}
/// The possible outcomes of calling [`MultiKey::input`].
pub enum MultiKeyResult {
/// Returned when a character was *accepted*, but has not completed the sequence required.
Accepted,
/// Returned when a character is accepted and completes the sequence.
Completed,
/// Returned if a character breaks the sequence or if it has already timed out.
Rejected,
}
/// A struct useful for managing multi-key keybinds.
pub struct MultiKey {
state: MultiKeyState,
pattern: Vec<char>,
}
impl MultiKey {
/// Creates a new [`MultiKey`] with a given pattern and timeout.
pub fn register(pattern: Vec<char>) -> Self {
Self {
state: MultiKeyState::Idle,
pattern,
}
}
/// Resets to an idle state.
fn reset(&mut self) {
self.state = MultiKeyState::Idle;
}
/// Handles a char input and returns the current status of the [`MultiKey`] after, which is one of:
/// - Accepting the char and moving to the next state
/// - Completing the multi-key pattern
/// - Rejecting it
///
/// Note that if a [`MultiKey`] only "times out" upon calling this - if it has timed out, it will first reset
/// before trying to check the char.
pub fn input(&mut self, c: char) -> MultiKeyResult {
match &mut self.state {
MultiKeyState::Idle => {
if let Some(first) = self.pattern.first() {
if *first == c {
self.state = MultiKeyState::Waiting {
trigger_instant: Instant::now(),
checked_index: 0,
};
return MultiKeyResult::Accepted;
}
}
MultiKeyResult::Rejected
}
MultiKeyState::Waiting {
trigger_instant,
checked_index,
} => {
if trigger_instant.elapsed() > MAX_TIMEOUT {
// Just reset and recursively call (putting it into Idle).
self.reset();
self.input(c)
} else if let Some(next) = self.pattern.get(*checked_index + 1) {
if *next == c {
*checked_index += 1;
if *checked_index == self.pattern.len() - 1 {
self.reset();
MultiKeyResult::Completed
} else {
MultiKeyResult::Accepted
}
} else {
self.reset();
MultiKeyResult::Rejected
}
} else {
self.reset();
MultiKeyResult::Rejected
}
}
}
}
}

View File

@ -1511,7 +1511,6 @@ pub fn move_widget_selection(
}
LayoutNode::Widget(_) => {
// Halt!
// TODO: How does this handle carousel?
current_id
}
}

View File

@ -7,11 +7,10 @@ use tui::{backend::Backend, layout::Rect, widgets::TableState, Frame};
use crate::{
app::{
event::{SelectionAction, WidgetEventResult},
event::{ComponentEventResult, SelectionAction},
layout_manager::BottomWidgetType,
},
canvas::Painter,
constants,
options::layout_options::LayoutRule,
};
@ -20,12 +19,15 @@ mod tui_stuff;
pub mod base;
pub use base::*;
pub mod dialogs;
pub use dialogs::*;
pub mod bottom_widgets;
pub use bottom_widgets::*;
use self::tui_stuff::BlockBuilder;
use super::data_farmer::DataCollection;
use super::{data_farmer::DataCollection, event::EventResult};
/// A trait for things that are drawn with state.
#[enum_dispatch]
@ -33,16 +35,16 @@ use super::data_farmer::DataCollection;
pub trait Component {
/// Handles a [`KeyEvent`].
///
/// Defaults to returning [`EventResult::NoRedraw`], indicating nothing should be done.
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
WidgetEventResult::NoRedraw
/// Defaults to returning [`ComponentEventResult::Unhandled`], indicating the component does not handle this event.
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
ComponentEventResult::Unhandled
}
/// Handles a [`MouseEvent`].
///
/// Defaults to returning [`EventResult::Continue`], indicating nothing should be done.
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
WidgetEventResult::NoRedraw
/// Defaults to returning [`ComponentEventResult::Unhandled`], indicating the component does not handle this event.
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
ComponentEventResult::Unhandled
}
/// Returns a [`Component`]'s bounding box. Note that these are defined in *global*, *absolute*
@ -180,6 +182,41 @@ pub enum TmpBottomWidget {
Empty,
}
/// The states a dialog can be in. Consists of either:
/// - [`DialogState::Hidden`] - the dialog is currently not showing.
/// - [`DialogState::Shown`] - the dialog is showing.
#[derive(Debug)]
pub enum DialogState<D: Default + Component> {
Hidden,
Shown(D),
}
impl<D> Default for DialogState<D>
where
D: Default + Component,
{
fn default() -> Self {
DialogState::Hidden
}
}
impl<D> DialogState<D>
where
D: Default + Component,
{
pub fn is_showing(&self) -> bool {
matches!(self, DialogState::Shown(_))
}
pub fn hide(&mut self) {
*self = DialogState::Hidden;
}
pub fn show(&mut self) {
*self = DialogState::Shown(D::default());
}
}
// ----- Old stuff below -----
#[derive(Debug)]
@ -250,7 +287,27 @@ impl Default for AppHelpDialogState {
AppHelpDialogState {
is_showing_help: false,
scroll_state: ParagraphScrollState::default(),
index_shortcuts: vec![0; constants::HELP_TEXT.len()],
index_shortcuts: vec![],
}
}
}
impl AppHelpDialogState {
pub fn increment(&mut self) -> EventResult {
if self.scroll_state.current_scroll_index < self.scroll_state.max_scroll_index {
self.scroll_state.current_scroll_index += 1;
EventResult::Redraw
} else {
EventResult::NoRedraw
}
}
pub fn decrement(&mut self) -> EventResult {
if self.scroll_state.current_scroll_index > 0 {
self.scroll_state.current_scroll_index -= 1;
EventResult::Redraw
} else {
EventResult::NoRedraw
}
}
}

View File

@ -4,7 +4,7 @@ use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEve
use tui::{layout::Rect, widgets::TableState};
use crate::app::{
event::{MultiKey, MultiKeyResult, WidgetEventResult},
event::{ComponentEventResult, MultiKey, MultiKeyResult},
Component,
};
@ -138,54 +138,54 @@ impl Scrollable {
}
}
fn skip_to_first(&mut self) -> WidgetEventResult {
fn skip_to_first(&mut self) -> ComponentEventResult {
if self.current_index != 0 {
self.set_index(0);
WidgetEventResult::Redraw
ComponentEventResult::Redraw
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
}
fn skip_to_last(&mut self) -> WidgetEventResult {
fn skip_to_last(&mut self) -> ComponentEventResult {
let last_index = self.num_items - 1;
if self.current_index != last_index {
self.set_index(last_index);
WidgetEventResult::Redraw
ComponentEventResult::Redraw
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
}
/// Moves *downward* by *incrementing* the current index.
fn move_down(&mut self, change_by: usize) -> WidgetEventResult {
fn move_down(&mut self, change_by: usize) -> ComponentEventResult {
if self.num_items == 0 {
return WidgetEventResult::NoRedraw;
return ComponentEventResult::NoRedraw;
}
let new_index = self.current_index + change_by;
if new_index >= self.num_items || self.current_index == new_index {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
self.set_index(new_index);
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
/// Moves *upward* by *decrementing* the current index.
fn move_up(&mut self, change_by: usize) -> WidgetEventResult {
fn move_up(&mut self, change_by: usize) -> ComponentEventResult {
if self.num_items == 0 {
return WidgetEventResult::NoRedraw;
return ComponentEventResult::NoRedraw;
}
let new_index = self.current_index.saturating_sub(change_by);
if self.current_index == new_index {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
self.set_index(new_index);
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
@ -207,7 +207,7 @@ impl Scrollable {
}
impl Component for Scrollable {
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
use crossterm::event::KeyCode::{Char, Down, Up};
if event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::SHIFT {
@ -218,18 +218,19 @@ impl Component for Scrollable {
Char('k') => self.move_up(1),
Char('g') => match self.gg_manager.input('g') {
MultiKeyResult::Completed => self.skip_to_first(),
MultiKeyResult::Accepted => WidgetEventResult::NoRedraw,
MultiKeyResult::Rejected => WidgetEventResult::NoRedraw,
MultiKeyResult::Accepted | MultiKeyResult::Rejected => {
ComponentEventResult::NoRedraw
}
},
Char('G') => self.skip_to_last(),
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
match event.kind {
MouseEventKind::Down(MouseButton::Left) => {
if self.does_bounds_intersect_mouse(&event) {
@ -256,11 +257,11 @@ impl Component for Scrollable {
}
}
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
MouseEventKind::ScrollDown => self.move_down(1),
MouseEventKind::ScrollUp => self.move_up(1),
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
}

View File

@ -3,7 +3,7 @@ use tui::{backend::Backend, layout::Rect, Frame};
use crate::{
app::{
event::WidgetEventResult, text_table::SimpleColumn, widgets::tui_stuff::BlockBuilder,
event::ComponentEventResult, text_table::SimpleColumn, widgets::tui_stuff::BlockBuilder,
Component, TextTable,
},
canvas::Painter,
@ -69,11 +69,11 @@ impl Component for SortMenu {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
self.table.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
self.table.handle_mouse_event(event)
}
}

View File

@ -5,7 +5,7 @@ use tui::{backend::Backend, layout::Rect, Frame};
use crate::{
app::{
event::{ReturnSignal, WidgetEventResult},
event::{ReturnSignal, ComponentEventResult},
widgets::tui_stuff::BlockBuilder,
Component, TextTable,
},
@ -391,12 +391,12 @@ impl<S> Component for SortableTextTable<S>
where
S: SortableColumn,
{
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
for (index, column) in self.table.columns.iter().enumerate() {
if let Some((shortcut, _)) = *column.shortcut() {
if shortcut == event {
self.set_sort_index(index);
return WidgetEventResult::Signal(ReturnSignal::Update);
return ComponentEventResult::Signal(ReturnSignal::Update);
}
}
}
@ -404,10 +404,10 @@ where
self.table.scrollable.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
if let MouseEventKind::Down(MouseButton::Left) = event.kind {
if !self.does_bounds_intersect_mouse(&event) {
return WidgetEventResult::NoRedraw;
return ComponentEventResult::NoRedraw;
}
// Note these are representing RELATIVE coordinates! They *need* the above intersection check for validity!
@ -419,7 +419,7 @@ where
if let Some((start, end)) = column.get_x_bounds() {
if x >= start && x <= end {
self.set_sort_index(index);
return WidgetEventResult::Signal(ReturnSignal::Update);
return ComponentEventResult::Signal(ReturnSignal::Update);
}
}
}

View File

@ -13,8 +13,8 @@ use unicode_width::UnicodeWidthStr;
use crate::{
app::{
event::{
ComponentEventResult::{self},
ReturnSignal,
WidgetEventResult::{self},
},
Component,
},
@ -85,19 +85,19 @@ impl TextInput {
}
}
fn clear_text(&mut self) -> WidgetEventResult {
fn clear_text(&mut self) -> ComponentEventResult {
if self.text.is_empty() {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
self.text = String::default();
self.cursor = GraphemeCursor::new(0, 0, true);
self.window_index = Default::default();
self.cursor_direction = CursorDirection::Left;
WidgetEventResult::Signal(ReturnSignal::Update)
ComponentEventResult::Signal(ReturnSignal::Update)
}
}
fn move_word_forward(&mut self) -> WidgetEventResult {
fn move_word_forward(&mut self) -> ComponentEventResult {
let current_index = self.cursor.cur_cursor();
if current_index < self.text.len() {
@ -105,30 +105,30 @@ impl TextInput {
if index > 0 {
self.cursor.set_cursor(index + current_index);
self.cursor_direction = CursorDirection::Right;
return WidgetEventResult::Redraw;
return ComponentEventResult::Redraw;
}
}
self.cursor.set_cursor(self.text.len());
}
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
fn move_word_back(&mut self) -> WidgetEventResult {
fn move_word_back(&mut self) -> ComponentEventResult {
let current_index = self.cursor.cur_cursor();
for (index, _word) in self.text[..current_index].unicode_word_indices().rev() {
if index < current_index {
self.cursor.set_cursor(index);
self.cursor_direction = CursorDirection::Left;
return WidgetEventResult::Redraw;
return ComponentEventResult::Redraw;
}
}
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
fn clear_word_from_cursor(&mut self) -> WidgetEventResult {
fn clear_word_from_cursor(&mut self) -> ComponentEventResult {
// Fairly simple logic - create the word index iterator, skip the word that intersects with the current
// cursor location, draw the rest, update the string.
let current_index = self.cursor.cur_cursor();
@ -147,16 +147,16 @@ impl TextInput {
}
if start_delete_index == current_index {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
self.text.drain(start_delete_index..current_index);
self.cursor = GraphemeCursor::new(start_delete_index, self.text.len(), true);
self.cursor_direction = CursorDirection::Left;
WidgetEventResult::Signal(ReturnSignal::Update)
ComponentEventResult::Signal(ReturnSignal::Update)
}
}
fn clear_previous_grapheme(&mut self) -> WidgetEventResult {
fn clear_previous_grapheme(&mut self) -> ComponentEventResult {
let current_index = self.cursor.cur_cursor();
if current_index > 0 {
@ -166,13 +166,13 @@ impl TextInput {
self.cursor = GraphemeCursor::new(new_index, self.text.len(), true);
self.cursor_direction = CursorDirection::Left;
WidgetEventResult::Signal(ReturnSignal::Update)
ComponentEventResult::Signal(ReturnSignal::Update)
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
}
fn clear_current_grapheme(&mut self) -> WidgetEventResult {
fn clear_current_grapheme(&mut self) -> ComponentEventResult {
let current_index = self.cursor.cur_cursor();
if current_index < self.text.len() {
@ -182,19 +182,19 @@ impl TextInput {
self.cursor = GraphemeCursor::new(current_index, self.text.len(), true);
self.cursor_direction = CursorDirection::Left;
WidgetEventResult::Signal(ReturnSignal::Update)
ComponentEventResult::Signal(ReturnSignal::Update)
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
}
fn insert_character(&mut self, c: char) -> WidgetEventResult {
fn insert_character(&mut self, c: char) -> ComponentEventResult {
let current_index = self.cursor.cur_cursor();
self.text.insert(current_index, c);
self.cursor = GraphemeCursor::new(current_index, self.text.len(), true);
self.move_forward();
WidgetEventResult::Signal(ReturnSignal::Update)
ComponentEventResult::Signal(ReturnSignal::Update)
}
/// Updates the window indexes and returns the start index.
@ -296,29 +296,29 @@ impl Component for TextInput {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
if event.modifiers.is_empty() {
match event.code {
KeyCode::Left => {
let original_cursor = self.cursor.cur_cursor();
if self.move_back() == original_cursor {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
KeyCode::Right => {
let original_cursor = self.cursor.cur_cursor();
if self.move_forward() == original_cursor {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
KeyCode::Backspace => self.clear_previous_grapheme(),
KeyCode::Delete => self.clear_current_grapheme(),
KeyCode::Char(c) => self.insert_character(c),
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
} else if let KeyModifiers::CONTROL = event.modifiers {
match event.code {
@ -326,46 +326,46 @@ impl Component for TextInput {
let prev_index = self.cursor.cur_cursor();
self.cursor.set_cursor(0);
if self.cursor.cur_cursor() == prev_index {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
KeyCode::Char('e') => {
let prev_index = self.cursor.cur_cursor();
self.cursor.set_cursor(self.text.len());
if self.cursor.cur_cursor() == prev_index {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
KeyCode::Char('u') => self.clear_text(),
KeyCode::Char('w') => self.clear_word_from_cursor(),
KeyCode::Char('h') => self.clear_previous_grapheme(),
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
} else if let KeyModifiers::ALT = event.modifiers {
match event.code {
KeyCode::Char('b') => self.move_word_back(),
KeyCode::Char('f') => self.move_word_forward(),
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
// We are assuming this is within bounds...
let x = event.column;
let widget_x = self.bounds.x + 2;
if x >= widget_x {
// TODO: do this
WidgetEventResult::Redraw
// TODO: Do this at some point after refactor
ComponentEventResult::Redraw
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
}
}

View File

@ -15,7 +15,7 @@ use tui::{
use unicode_segmentation::UnicodeSegmentation;
use crate::{
app::{event::WidgetEventResult, widgets::tui_stuff::BlockBuilder, Component, Scrollable},
app::{event::ComponentEventResult, widgets::tui_stuff::BlockBuilder, Component, Scrollable},
canvas::Painter,
constants::TABLE_GAP_HEIGHT_LIMIT,
};
@ -130,7 +130,7 @@ where
pub show_gap: bool,
/// The bounding box of the [`TextTable`].
pub bounds: Rect, // TODO: Consider moving bounds to something else???
pub bounds: Rect, // TODO: Consider moving bounds to something else?
/// The bounds including the border, if there is one.
pub border_bounds: Rect,
@ -492,19 +492,19 @@ impl<C> Component for TextTable<C>
where
C: TableColumn,
{
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
if self.selectable {
self.scrollable.handle_key_event(event)
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
if self.selectable {
self.scrollable.handle_mouse_event(event)
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}

View File

@ -15,7 +15,7 @@ use tui::{
use crate::{
app::{
event::WidgetEventResult,
event::ComponentEventResult,
widgets::tui_stuff::{
custom_legend_chart::{Axis, Dataset},
TimeChart,
@ -160,62 +160,62 @@ impl TimeGraph {
}
/// Handles a char `c`.
fn handle_char(&mut self, c: char) -> WidgetEventResult {
fn handle_char(&mut self, c: char) -> ComponentEventResult {
match c {
'-' => self.zoom_out(),
'+' => self.zoom_in(),
'=' => self.reset_zoom(),
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::NoRedraw,
}
}
fn zoom_in(&mut self) -> WidgetEventResult {
fn zoom_in(&mut self) -> ComponentEventResult {
let new_time = self.current_display_time.saturating_sub(self.time_interval);
if self.current_display_time == new_time {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else if new_time >= self.min_duration {
self.current_display_time = new_time;
self.autohide_timer.start_display_timer();
WidgetEventResult::Redraw
ComponentEventResult::Redraw
} else if new_time != self.min_duration {
self.current_display_time = self.min_duration;
self.autohide_timer.start_display_timer();
WidgetEventResult::Redraw
ComponentEventResult::Redraw
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
}
fn zoom_out(&mut self) -> WidgetEventResult {
fn zoom_out(&mut self) -> ComponentEventResult {
let new_time = self.current_display_time + self.time_interval;
if self.current_display_time == new_time {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else if new_time <= self.max_duration {
self.current_display_time = new_time;
self.autohide_timer.start_display_timer();
WidgetEventResult::Redraw
ComponentEventResult::Redraw
} else if new_time != self.max_duration {
self.current_display_time = self.max_duration;
self.autohide_timer.start_display_timer();
WidgetEventResult::Redraw
ComponentEventResult::Redraw
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
}
}
fn reset_zoom(&mut self) -> WidgetEventResult {
fn reset_zoom(&mut self) -> ComponentEventResult {
if self.current_display_time == self.default_time_value {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
self.current_display_time = self.default_time_value;
self.autohide_timer.start_display_timer();
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
@ -307,24 +307,24 @@ impl TimeGraph {
}
impl Component for TimeGraph {
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
use crossterm::event::KeyCode::Char;
if event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::SHIFT {
match event.code {
Char(c) => self.handle_char(c),
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
match event.kind {
MouseEventKind::ScrollDown => self.zoom_out(),
MouseEventKind::ScrollUp => self.zoom_in(),
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
}

View File

@ -8,7 +8,8 @@ use tui::{
use crate::{
app::{
event::WidgetEventResult, widgets::tui_stuff::PipeGauge, Component, DataCollection, Widget,
event::ComponentEventResult, widgets::tui_stuff::PipeGauge, Component, DataCollection,
Widget,
},
canvas::Painter,
constants::SIDE_BORDERS,
@ -54,13 +55,13 @@ impl Component for BasicMem {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
match event.code {
KeyCode::Char('%') if event.modifiers.is_empty() => {
KeyCode::Char('%') => {
self.use_percent = !self.use_percent;
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
}
}

View File

@ -14,7 +14,7 @@ use tui::{
use crate::{
app::{
data_farmer::DataCollection, does_bound_intersect_coordinate, event::WidgetEventResult,
data_farmer::DataCollection, does_bound_intersect_coordinate, event::ComponentEventResult,
widgets::tui_stuff::PipeGauge, Component, Widget,
},
canvas::Painter,
@ -114,44 +114,44 @@ impl Component for BatteryTable {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
if event.modifiers.is_empty() {
match event.code {
KeyCode::Left => {
let current_index = self.selected_index;
self.decrement_index();
if current_index == self.selected_index {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
KeyCode::Right => {
let current_index = self.selected_index;
self.increment_index();
if current_index == self.selected_index {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
for (itx, bound) in self.tab_bounds.iter().enumerate() {
if does_bound_intersect_coordinate(event.column, event.row, *bound)
&& itx < self.battery_data.len()
{
self.selected_index = itx;
return WidgetEventResult::Redraw;
return ComponentEventResult::Redraw;
}
}
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
@ -183,7 +183,7 @@ impl Widget for BatteryTable {
.block()
.selected(selected)
.borders(self.block_border)
.expanded(expanded)
.show_esc(expanded)
.build(painter, area);
let inner_area = block.inner(area);

View File

@ -12,7 +12,7 @@ use tui::{
use crate::{
app::{
does_bound_intersect_coordinate, event::WidgetEventResult, Component, SelectableType,
does_bound_intersect_coordinate, event::ComponentEventResult, Component, SelectableType,
Widget,
},
canvas::Painter,
@ -164,7 +164,7 @@ impl Component for Carousel {
self.bounds = new_bounds;
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
match event.kind {
crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left) => {
let x = event.column;
@ -172,15 +172,15 @@ impl Component for Carousel {
if does_bound_intersect_coordinate(x, y, self.left_button_bounds) {
self.decrement_index();
WidgetEventResult::Redraw
ComponentEventResult::Redraw
} else if does_bound_intersect_coordinate(x, y, self.right_button_bounds) {
self.increment_index();
WidgetEventResult::Redraw
ComponentEventResult::Redraw
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
}
}

View File

@ -9,7 +9,7 @@ use tui::{
use crate::{
app::{
event::{SelectionAction, WidgetEventResult},
event::{ComponentEventResult, SelectionAction},
text_table::SimpleColumn,
time_graph::TimeGraphData,
AppConfigFields, AppScrollWidgetState, CanvasTableWidthState, Component, DataCollection,
@ -118,21 +118,21 @@ impl CpuGraph {
}
impl Component for CpuGraph {
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
match self.selected {
CpuGraphSelection::Graph => self.graph.handle_key_event(event),
CpuGraphSelection::Legend => self.legend.handle_key_event(event),
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
if self.graph.does_border_intersect_mouse(&event) {
if let CpuGraphSelection::Graph = self.selected {
self.graph.handle_mouse_event(event)
} else {
self.selected = CpuGraphSelection::Graph;
self.graph.handle_mouse_event(event);
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
} else if self.legend.does_border_intersect_mouse(&event) {
if let CpuGraphSelection::Legend = self.selected {
@ -140,10 +140,10 @@ impl Component for CpuGraph {
} else {
self.selected = CpuGraphSelection::Legend;
self.legend.handle_mouse_event(event);
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
@ -193,7 +193,7 @@ impl Widget for CpuGraph {
let legend_block = self
.block()
.selected(selected && matches!(&self.selected, CpuGraphSelection::Legend))
.expanded(expanded)
.show_esc(expanded)
.hide_title(true);
let legend_data = self
@ -279,7 +279,7 @@ impl Widget for CpuGraph {
let graph_block = self
.block()
.selected(selected && matches!(&self.selected, CpuGraphSelection::Graph))
.expanded(expanded)
.show_esc(expanded)
.build(painter, graph_block_area);
self.graph.draw_tui_chart(

View File

@ -5,7 +5,7 @@ use tui::{backend::Backend, layout::Rect, widgets::Borders, Frame};
use crate::{
app::{
data_farmer::DataCollection, event::WidgetEventResult,
data_farmer::DataCollection, event::ComponentEventResult,
sort_text_table::SimpleSortableColumn, text_table::TextTableData, AppScrollWidgetState,
CanvasTableWidthState, Component, TextTable, Widget,
},
@ -105,11 +105,11 @@ impl DiskTable {
}
impl Component for DiskTable {
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
self.table.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
self.table.handle_mouse_event(event)
}
@ -135,7 +135,7 @@ impl Widget for DiskTable {
.block()
.selected(selected)
.borders(self.block_border)
.expanded(expanded);
.show_esc(expanded);
self.table.draw_tui_table(
painter,

View File

@ -4,7 +4,7 @@ use crossterm::event::{KeyEvent, MouseEvent};
use tui::{backend::Backend, layout::Rect};
use crate::{
app::{event::WidgetEventResult, time_graph::TimeGraphData, DataCollection},
app::{event::ComponentEventResult, time_graph::TimeGraphData, DataCollection},
app::{Component, TimeGraph, Widget},
data_conversion::{convert_mem_data_points, convert_mem_labels, convert_swap_data_points},
options::layout_options::LayoutRule,
@ -63,11 +63,11 @@ impl MemGraph {
}
impl Component for MemGraph {
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
self.graph.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
self.graph.handle_mouse_event(event)
}
@ -92,7 +92,7 @@ impl Widget for MemGraph {
let block = self
.block()
.selected(selected)
.expanded(expanded)
.show_esc(expanded)
.build(painter, area);
let mut chart_data = Vec::with_capacity(2);

View File

@ -1,5 +1,6 @@
use std::{borrow::Cow, collections::HashMap, time::Instant};
use crossterm::event::{KeyEvent, MouseEvent};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
@ -8,9 +9,9 @@ use tui::{
use crate::{
app::{
data_farmer::DataCollection, text_table::SimpleColumn, time_graph::TimeGraphData,
widgets::tui_stuff::BlockBuilder, AppConfigFields, AxisScaling, Component, TextTable,
TimeGraph, Widget,
data_farmer::DataCollection, event::ComponentEventResult, text_table::SimpleColumn,
time_graph::TimeGraphData, widgets::tui_stuff::BlockBuilder, AppConfigFields, AxisScaling,
Component, TextTable, TimeGraph, Widget,
},
canvas::Painter,
data_conversion::convert_network_data_points,
@ -497,15 +498,11 @@ impl Component for NetGraph {
self.bounds = new_bounds;
}
fn handle_key_event(
&mut self, event: crossterm::event::KeyEvent,
) -> crate::app::event::WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
self.graph.handle_key_event(event)
}
fn handle_mouse_event(
&mut self, event: crossterm::event::MouseEvent,
) -> crate::app::event::WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
self.graph.handle_mouse_event(event)
}
}
@ -522,7 +519,7 @@ impl Widget for NetGraph {
let block = self
.block()
.selected(selected)
.expanded(expanded)
.show_esc(expanded)
.build(painter, area);
self.set_draw_cache();
@ -644,15 +641,11 @@ impl Component for OldNetGraph {
self.bounds = new_bounds;
}
fn handle_key_event(
&mut self, event: crossterm::event::KeyEvent,
) -> crate::app::event::WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
self.net_graph.handle_key_event(event)
}
fn handle_mouse_event(
&mut self, event: crossterm::event::MouseEvent,
) -> crate::app::event::WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
self.net_graph.handle_mouse_event(event)
}
}

View File

@ -15,7 +15,7 @@ use tui::{
use crate::{
app::{
data_harvester::processes::ProcessHarvest,
event::{MultiKey, MultiKeyResult, ReturnSignal, SelectionAction, WidgetEventResult},
event::{ComponentEventResult, MultiKey, MultiKeyResult, ReturnSignal, SelectionAction},
query::*,
text_table::DesiredColumnWidth,
widgets::tui_stuff::BlockBuilder,
@ -869,27 +869,27 @@ impl ProcessManager {
self
}
fn open_search(&mut self) -> WidgetEventResult {
fn open_search(&mut self) -> ComponentEventResult {
if let ProcessManagerSelection::Search = self.selected {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
self.show_search = true;
self.prev_selected = self.selected;
self.selected = ProcessManagerSelection::Search;
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
fn open_sort(&mut self) -> WidgetEventResult {
fn open_sort(&mut self) -> ComponentEventResult {
if let ProcessManagerSelection::Sort = self.selected {
WidgetEventResult::NoRedraw
ComponentEventResult::NoRedraw
} else {
self.sort_menu
.set_index(self.process_table.current_sorting_column_index());
self.show_sort = true;
self.prev_selected = self.selected;
self.selected = ProcessManagerSelection::Sort;
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
}
@ -917,7 +917,7 @@ impl ProcessManager {
)
}
fn toggle_command(&mut self) -> WidgetEventResult {
fn toggle_command(&mut self) -> ComponentEventResult {
if self.is_using_command() {
self.process_table
.set_column(ProcessSortColumn::new(ProcessSortType::Name), 1);
@ -929,7 +929,7 @@ impl ProcessManager {
// Invalidate row cache.
self.process_table.invalidate_cached_columns();
WidgetEventResult::Signal(ReturnSignal::Update)
ComponentEventResult::Signal(ReturnSignal::Update)
}
fn is_grouped(&self) -> bool {
@ -939,7 +939,7 @@ impl ProcessManager {
)
}
fn toggle_grouped(&mut self) -> WidgetEventResult {
fn toggle_grouped(&mut self) -> ComponentEventResult {
if self.is_grouped() {
self.process_table
.set_column(ProcessSortColumn::new(ProcessSortType::Pid), 0);
@ -965,7 +965,7 @@ impl ProcessManager {
// Invalidate row cache.
self.process_table.invalidate_cached_columns();
WidgetEventResult::Signal(ReturnSignal::Update)
ComponentEventResult::Signal(ReturnSignal::Update)
}
fn hide_sort(&mut self) {
@ -994,7 +994,7 @@ impl Component for ProcessManager {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
// "Global" handling:
if let KeyCode::Esc = event.code {
@ -1002,19 +1002,19 @@ impl Component for ProcessManager {
ProcessManagerSelection::Processes => {
if self.show_sort {
self.hide_sort();
return WidgetEventResult::Redraw;
return ComponentEventResult::Redraw;
} else if self.show_search {
self.hide_search();
return WidgetEventResult::Redraw;
return ComponentEventResult::Redraw;
}
}
ProcessManagerSelection::Sort if self.show_sort => {
self.hide_sort();
return WidgetEventResult::Redraw;
return ComponentEventResult::Redraw;
}
ProcessManagerSelection::Search if self.show_search => {
self.hide_search();
return WidgetEventResult::Redraw;
return ComponentEventResult::Redraw;
}
_ => {}
}
@ -1039,7 +1039,7 @@ impl Component for ProcessManager {
// Kill the selected process(es)
}
MultiKeyResult::Accepted | MultiKeyResult::Rejected => {
return WidgetEventResult::NoRedraw;
return ComponentEventResult::NoRedraw;
}
}
}
@ -1057,7 +1057,7 @@ impl Component for ProcessManager {
}
KeyCode::Char('t') | KeyCode::F(5) => {
self.in_tree_mode = !self.in_tree_mode;
return WidgetEventResult::Redraw;
return ComponentEventResult::Redraw;
}
KeyCode::Char('s') | KeyCode::F(6) => {
return self.open_sort();
@ -1086,7 +1086,7 @@ impl Component for ProcessManager {
KeyCode::Enter => {
self.process_table
.set_sort_index(self.sort_menu.current_index());
return WidgetEventResult::Signal(ReturnSignal::Update);
return ComponentEventResult::Signal(ReturnSignal::Update);
}
KeyCode::Char('/') => {
return self.open_search();
@ -1115,7 +1115,7 @@ impl Component for ProcessManager {
}
let handle_output = self.search_input.handle_key_event(event);
if let WidgetEventResult::Signal(ReturnSignal::Update) = handle_output {
if let ComponentEventResult::Signal(ReturnSignal::Update) = handle_output {
self.process_filter = Some(parse_query(
self.search_input.query(),
self.is_searching_whole_word(),
@ -1129,7 +1129,7 @@ impl Component for ProcessManager {
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
match &event.kind {
MouseEventKind::Down(MouseButton::Left) => {
if self.process_table.does_border_intersect_mouse(&event) {
@ -1139,11 +1139,10 @@ impl Component for ProcessManager {
self.prev_selected = self.selected;
self.selected = ProcessManagerSelection::Processes;
match self.process_table.handle_mouse_event(event) {
WidgetEventResult::Quit => WidgetEventResult::Quit,
WidgetEventResult::Redraw | WidgetEventResult::NoRedraw => {
WidgetEventResult::Redraw
}
WidgetEventResult::Signal(s) => WidgetEventResult::Signal(s),
ComponentEventResult::Unhandled
| ComponentEventResult::Redraw
| ComponentEventResult::NoRedraw => ComponentEventResult::Redraw,
ComponentEventResult::Signal(s) => ComponentEventResult::Signal(s),
}
}
} else if self.sort_menu.does_border_intersect_mouse(&event) {
@ -1153,7 +1152,7 @@ impl Component for ProcessManager {
self.prev_selected = self.selected;
self.selected = ProcessManagerSelection::Sort;
self.sort_menu.handle_mouse_event(event);
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
} else if does_bound_intersect_coordinate(
event.column,
@ -1166,10 +1165,10 @@ impl Component for ProcessManager {
self.prev_selected = self.selected;
self.selected = ProcessManagerSelection::Search;
self.search_input.handle_mouse_event(event);
WidgetEventResult::Redraw
ComponentEventResult::Redraw
}
} else {
WidgetEventResult::NoRedraw
ComponentEventResult::Unhandled
}
}
MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => match self.selected {
@ -1177,7 +1176,7 @@ impl Component for ProcessManager {
ProcessManagerSelection::Sort => self.sort_menu.handle_mouse_event(event),
ProcessManagerSelection::Search => self.search_input.handle_mouse_event(event),
},
_ => WidgetEventResult::NoRedraw,
_ => ComponentEventResult::Unhandled,
}
}
}
@ -1195,9 +1194,9 @@ impl Widget for ProcessManager {
let search_constraints: [Constraint; 2] = [
Constraint::Min(0),
if self.block_border.contains(Borders::TOP) {
Constraint::Length(4)
Constraint::Length(5)
} else {
Constraint::Length(2)
Constraint::Length(3)
},
];
const INTERNAL_SEARCH_CONSTRAINTS: [Constraint; 2] = [Constraint::Length(1); 2];
@ -1277,7 +1276,7 @@ impl Widget for ProcessManager {
.block()
.selected(process_selected)
.borders(self.block_border)
.expanded(expanded && !self.show_sort && !self.show_search);
.show_esc(expanded && !self.show_sort && !self.show_search);
self.process_table.draw_tui_table(
painter,

View File

@ -6,8 +6,9 @@ use tui::{backend::Backend, layout::Rect, widgets::Borders, Frame};
use crate::{
app::{
data_farmer::DataCollection, data_harvester::temperature::TemperatureType,
event::WidgetEventResult, sort_text_table::SimpleSortableColumn, text_table::TextTableData,
AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, Widget,
event::ComponentEventResult, sort_text_table::SimpleSortableColumn,
text_table::TextTableData, AppScrollWidgetState, CanvasTableWidthState, Component,
TextTable, Widget,
},
canvas::Painter,
data_conversion::convert_temp_row,
@ -116,11 +117,11 @@ impl TempTable {
}
impl Component for TempTable {
fn handle_key_event(&mut self, event: KeyEvent) -> WidgetEventResult {
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
self.table.handle_key_event(event)
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> WidgetEventResult {
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
self.table.handle_mouse_event(event)
}
@ -146,7 +147,7 @@ impl Widget for TempTable {
.block()
.selected(selected)
.borders(self.block_border)
.expanded(expanded);
.show_esc(expanded);
self.table.draw_tui_table(
painter,

View File

@ -0,0 +1,2 @@
pub mod help;
pub use help::HelpDialog;

View File

@ -0,0 +1,304 @@
use std::cmp::min;
use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use fxhash::FxHashMap;
use itertools::{EitherOrBoth, Itertools};
use tui::{
backend::Backend,
layout::{Constraint, Layout, Rect},
text::{Span, Spans},
widgets::{Borders, Paragraph},
Frame,
};
use unicode_width::UnicodeWidthStr;
use crate::{
app::{
event::{ComponentEventResult, MultiKey, MultiKeyResult},
widgets::tui_stuff::BlockBuilder,
Component,
},
canvas::Painter,
constants::*,
};
pub struct HelpDialog {
current_index: usize,
max_index: usize,
bounds: Rect,
wrapped_text: Vec<Vec<Spans<'static>>>,
left_column_width: Constraint,
right_column_width: Constraint,
/// Manages the `gg` double-tap shortcut.
gg_manager: MultiKey,
/// A jury-rigged solution for shortcut indices.
/// TODO: THIS DOES NOT SCALE WELL!
shortcut_indices: FxHashMap<u32, usize>,
}
impl Default for HelpDialog {
fn default() -> Self {
Self {
current_index: Default::default(),
max_index: Default::default(),
bounds: Default::default(),
left_column_width: Constraint::Length(0),
right_column_width: Constraint::Length(0),
wrapped_text: Default::default(),
gg_manager: MultiKey::register(vec!['g', 'g']),
shortcut_indices: FxHashMap::default(),
}
}
}
impl HelpDialog {
pub fn rebuild_wrapped_text(&mut self, painter: &Painter) {
let left_column_width = HELP_TEXT
.iter()
.map(|text| {
text.iter()
.map(|[labels, _details]| {
labels
.lines()
.map(|line| UnicodeWidthStr::width(line))
.max()
.unwrap_or(0)
})
.max()
.unwrap_or(0)
})
.max()
.unwrap_or(0)
+ 2;
let right_column_width = (self.bounds.width as usize).saturating_sub(left_column_width);
self.left_column_width = Constraint::Length(left_column_width as u16);
self.right_column_width = Constraint::Length(right_column_width as u16);
let mut shortcut_index = 1;
let mut current_index = HELP_TITLES.len() + 2;
// let mut section_indices: Vec<usize> = Vec::with_capacity(HELP_TITLES.len());
// let mut help_title_index = HELP_CONTENTS_TEXT.len() + 1;
// Behold, this monstrosity of an iterator (I'm sorry).
// Be warned, for when you stare into the iterator, the iterator stares back.
let wrapped_details_iter = HELP_TEXT.iter().map(|text| {
text.iter()
.map(|[labels, details]| {
let labels = textwrap::fill(labels, left_column_width);
let details = textwrap::fill(details, right_column_width);
labels
.lines()
.zip_longest(details.lines())
.map(|z| match z {
EitherOrBoth::Both(a, b) => vec![
Spans::from(Span::styled(
a.to_string(),
painter.colours.text_style,
)),
Spans::from(Span::styled(
b.to_string(),
painter.colours.text_style,
)),
],
EitherOrBoth::Left(s) => {
vec![Spans::from(Span::styled(
s.to_string(),
painter.colours.text_style,
))]
}
EitherOrBoth::Right(s) => vec![
Spans::default(),
Spans::from(Span::styled(
s.to_string(),
painter.colours.text_style,
)),
],
})
.collect::<Vec<_>>()
})
.flatten()
.collect::<Vec<_>>()
});
self.wrapped_text = HELP_CONTENTS_TEXT
.iter()
.map(|t| vec![Spans::from(Span::styled(*t, painter.colours.text_style))])
.chain(
HELP_TITLES
.iter()
.zip(wrapped_details_iter)
.map(|(title, text)| {
self.shortcut_indices.insert(shortcut_index, current_index);
shortcut_index += 1;
current_index += 2 + text.len();
std::iter::once(vec![Spans::default()])
.chain(std::iter::once(vec![Spans::from(Span::styled(
*title,
painter.colours.highlighted_border_style,
))]))
.chain(text)
})
.flatten(),
)
.collect();
self.max_index = self
.wrapped_text
.len()
.saturating_sub(self.bounds.height as usize);
for value in self.shortcut_indices.values_mut() {
*value = min(*value, self.max_index);
}
if self.current_index > self.max_index {
self.current_index = self.max_index;
}
}
pub fn draw_help<B: Backend>(
&mut self, painter: &Painter, f: &mut Frame<'_, B>, block_area: Rect,
) {
let block = BlockBuilder::new("Help")
.borders(Borders::all())
.show_esc(true)
.build(painter, block_area);
let inner_area = block.inner(block_area);
if inner_area != self.bounds {
self.bounds = inner_area;
self.rebuild_wrapped_text(painter);
}
let end_index = self.current_index + inner_area.height as usize;
let split_area = Layout::default()
.constraints(vec![Constraint::Length(1); inner_area.height as usize])
.direction(tui::layout::Direction::Vertical)
.split(inner_area);
self.wrapped_text[self.current_index..end_index]
.iter()
.zip(split_area)
.for_each(|(row, area)| {
if row.len() > 1 {
let row_split_area = Layout::default()
.constraints(vec![self.left_column_width, self.right_column_width])
.direction(tui::layout::Direction::Horizontal)
.split(area);
let left_area = row_split_area[0];
let right_area = row_split_area[1];
f.render_widget(Paragraph::new(row[0].clone()), left_area);
f.render_widget(Paragraph::new(row[1].clone()), right_area);
} else if let Some(line) = row.get(0) {
f.render_widget(Paragraph::new(line.clone()), area);
}
});
f.render_widget(block, block_area);
}
fn move_up(&mut self, amount: usize) -> ComponentEventResult {
let new_index = self.current_index.saturating_sub(amount);
if self.current_index == new_index {
ComponentEventResult::NoRedraw
} else {
self.current_index = new_index;
ComponentEventResult::Redraw
}
}
fn move_down(&mut self, amount: usize) -> ComponentEventResult {
let new_index = self.current_index + amount;
if new_index > self.max_index || self.current_index == new_index {
ComponentEventResult::NoRedraw
} else {
self.current_index = new_index;
ComponentEventResult::Redraw
}
}
fn skip_to_first(&mut self) -> ComponentEventResult {
if self.current_index == 0 {
ComponentEventResult::NoRedraw
} else {
self.current_index = 0;
ComponentEventResult::Redraw
}
}
fn skip_to_last(&mut self) -> ComponentEventResult {
if self.current_index == self.max_index {
ComponentEventResult::NoRedraw
} else {
self.current_index = self.max_index;
ComponentEventResult::Redraw
}
}
}
impl Component for HelpDialog {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
}
fn handle_key_event(&mut self, event: KeyEvent) -> ComponentEventResult {
use crossterm::event::KeyCode::{Char, Down, Up};
if event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::SHIFT {
match event.code {
Down if event.modifiers == KeyModifiers::NONE => self.move_down(1),
Up if event.modifiers == KeyModifiers::NONE => self.move_up(1),
Char(c) => match c {
'j' => self.move_down(1),
'k' => self.move_up(1),
'g' => match self.gg_manager.input('g') {
MultiKeyResult::Completed => self.skip_to_first(),
MultiKeyResult::Accepted | MultiKeyResult::Rejected => {
ComponentEventResult::NoRedraw
}
},
'G' => self.skip_to_last(),
'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
if let Some(potential_index) = c.to_digit(10) {
if let Some(&new_index) = self.shortcut_indices.get(&potential_index) {
if new_index != self.current_index {
self.current_index = new_index;
ComponentEventResult::Redraw
} else {
ComponentEventResult::NoRedraw
}
} else {
ComponentEventResult::Unhandled
}
} else {
ComponentEventResult::Unhandled
}
}
_ => ComponentEventResult::Unhandled,
},
_ => ComponentEventResult::Unhandled,
}
} else {
ComponentEventResult::Unhandled
}
}
fn handle_mouse_event(&mut self, event: MouseEvent) -> ComponentEventResult {
match event.kind {
MouseEventKind::ScrollDown => self.move_down(1),
MouseEventKind::ScrollUp => self.move_up(1),
_ => ComponentEventResult::Unhandled,
}
}
}

View File

@ -10,7 +10,7 @@ use crate::canvas::Painter;
pub struct BlockBuilder {
borders: Borders,
selected: bool,
expanded: bool,
show_esc: bool,
name: &'static str,
hide_title: bool,
extra_text: Option<String>,
@ -22,7 +22,7 @@ impl BlockBuilder {
Self {
borders: Borders::ALL,
selected: false,
expanded: false,
show_esc: false,
name,
hide_title: false,
extra_text: None,
@ -35,9 +35,9 @@ impl BlockBuilder {
self
}
/// Indicates that this block is currently expanded, and should be drawn as such.
pub fn expanded(mut self, expanded: bool) -> Self {
self.expanded = expanded;
/// Indicates that this block should show esc, and should be drawn as such.
pub fn show_esc(mut self, show_esc: bool) -> Self {
self.show_esc = show_esc;
self
}
@ -64,12 +64,14 @@ impl BlockBuilder {
let has_title = !self.hide_title
&& (self.borders.contains(Borders::TOP) || self.borders.contains(Borders::BOTTOM));
let border_style = if self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
};
let block = Block::default()
.border_style(if self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
})
.border_style(border_style)
.borders(self.borders);
let inner_width = block.inner(area).width as usize;
@ -82,12 +84,11 @@ impl BlockBuilder {
let mut title_len = name.width();
let mut title = vec![name, Span::from(""), Span::from(""), Span::from("")];
if self.expanded {
if self.show_esc {
const EXPAND_TEXT: &str = " Esc to go back ";
const EXPAND_TEXT_LEN: usize = EXPAND_TEXT.len();
let expand_span =
Span::styled(EXPAND_TEXT, painter.colours.highlighted_border_style);
let expand_span = Span::styled(EXPAND_TEXT, border_style);
if title_len + EXPAND_TEXT_LEN <= inner_width {
title_len += EXPAND_TEXT_LEN;
@ -107,16 +108,9 @@ impl BlockBuilder {
}
}
if self.expanded {
if self.show_esc {
let difference = inner_width.saturating_sub(title_len);
title[2] = Span::styled(
"".repeat(difference),
if self.selected {
painter.colours.highlighted_border_style
} else {
painter.colours.border_style
},
);
title[2] = Span::styled("".repeat(difference), border_style);
}
block.title(title)

View File

@ -50,7 +50,7 @@ fn main() -> Result<()> {
let thread_termination_cvar = Arc::new(Condvar::new());
// Set up input handling
let (sender, receiver) = mpsc::channel(); // FIXME: Make this bounded, prevents overloading.
let (sender, receiver) = mpsc::channel();
let input_thread = create_input_thread(sender.clone(), thread_termination_lock.clone());
// Cleaning loop

View File

@ -5,7 +5,7 @@ use indextree::{Arena, NodeId};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
text::{Span, Spans},
text::Span,
widgets::Paragraph,
Frame, Terminal,
};
@ -19,7 +19,7 @@ use crate::{
layout_manager::{generate_layout, ColLayout, LayoutNode, RowLayout},
text_table::TextTableData,
widgets::{Component, Widget},
TmpBottomWidget,
DialogState, TmpBottomWidget,
},
constants::*,
data_conversion::{ConvertedBatteryData, ConvertedCpuData, ConvertedProcessData},
@ -92,14 +92,12 @@ impl FromStr for ColourScheme {
/// Handles the canvas' state.
pub struct Painter {
pub colours: CanvasColours,
styled_help_text: Vec<Spans<'static>>,
}
impl Painter {
pub fn init(config: &Config, colour_scheme: ColourScheme) -> anyhow::Result<Self> {
let mut painter = Painter {
colours: CanvasColours::default(),
styled_help_text: Vec::default(),
};
if let ColourScheme::Custom = colour_scheme {
@ -107,7 +105,6 @@ impl Painter {
} else {
painter.generate_colour_scheme(colour_scheme)?;
}
painter.complete_painter_init();
Ok(painter)
}
@ -153,43 +150,6 @@ impl Painter {
Ok(())
}
/// Must be run once before drawing, but after setting colours.
/// This is to set some remaining styles and text.
fn complete_painter_init(&mut self) {
let mut styled_help_spans = Vec::new();
// Init help text:
(*HELP_TEXT).iter().enumerate().for_each(|(itx, section)| {
if itx == 0 {
styled_help_spans.extend(
section
.iter()
.map(|&text| Span::styled(text, self.colours.text_style))
.collect::<Vec<_>>(),
);
} else {
// Not required check but it runs only a few times... so whatever ig, prevents me from
// being dumb and leaving a help text section only one line long.
if section.len() > 1 {
styled_help_spans.push(Span::raw(""));
styled_help_spans
.push(Span::styled(section[0], self.colours.table_header_style));
styled_help_spans.extend(
section[1..]
.iter()
.map(|&text| Span::styled(text, self.colours.text_style))
.collect::<Vec<_>>(),
);
}
}
});
self.styled_help_text = styled_help_spans.into_iter().map(Spans::from).collect();
}
// TODO: [CONFIG] write this, should call painter init and any changed colour functions...
pub fn update_painter_colours(&mut self) {}
fn draw_frozen_indicator<B: Backend>(&self, f: &mut Frame<'_, B>, draw_loc: Rect) {
f.render_widget(
Paragraph::new(Span::styled(
@ -218,7 +178,7 @@ impl Painter {
let terminal_height = draw_area.height;
let terminal_width = draw_area.width;
if app_state.help_dialog_state.is_showing_help {
if let DialogState::Shown(help_dialog) = &mut app_state.help_dialog {
let gen_help_len = GENERAL_HELP_TEXT.len() as u16 + 3;
let border_len = terminal_height.saturating_sub(gen_help_len) / 2;
let vertical_dialog_chunk = Layout::default()
@ -248,7 +208,7 @@ impl Painter {
})
.split(vertical_dialog_chunk[1]);
self.draw_help_dialog(&mut f, app_state, middle_dialog_chunk[1]);
help_dialog.draw_help(&self, f, middle_dialog_chunk[1]);
} else if app_state.delete_dialog_state.is_showing_dd {
// TODO: This needs the paragraph wrap feature from tui-rs to be pushed to complete... but for now it's pretty close!
// The main problem right now is that I cannot properly calculate the height offset since

View File

@ -1,5 +1,2 @@
pub mod dd_dialog;
pub mod help_dialog;
pub use dd_dialog::KillDialog;
pub use help_dialog::HelpDialog;

View File

@ -1,121 +0,0 @@
use unicode_width::UnicodeWidthStr;
use crate::{app::AppState, canvas::Painter, constants};
use tui::{
backend::Backend,
layout::{Alignment, Rect},
terminal::Frame,
text::Span,
text::Spans,
widgets::{Block, Borders, Paragraph, Wrap},
};
const HELP_BASE: &str = " Help ── Esc to close ";
pub trait HelpDialog {
fn draw_help_dialog<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect,
);
}
// TODO: [REFACTOR] Make generic dialog boxes to build off of instead?
impl HelpDialog for Painter {
fn draw_help_dialog<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect,
) {
let help_title = Spans::from(vec![
Span::styled(" Help ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to close ",
"".repeat(
usize::from(draw_loc.width).saturating_sub(HELP_BASE.chars().count() + 2)
)
),
self.colours.border_style,
),
]);
if app_state.should_get_widget_bounds() {
// We must also recalculate how many lines are wrapping to properly get scrolling to work on
// small terminal sizes... oh joy.
let mut overflow_buffer = 0;
let paragraph_width = std::cmp::max(draw_loc.width.saturating_sub(2), 1);
let mut prev_section_len = 0;
constants::HELP_TEXT
.iter()
.enumerate()
.for_each(|(itx, section)| {
let mut buffer = 0;
if itx == 0 {
section.iter().for_each(|text_line| {
buffer += UnicodeWidthStr::width(*text_line).saturating_sub(1) as u16
/ paragraph_width;
});
app_state.help_dialog_state.index_shortcuts[itx] = 0;
} else {
section.iter().for_each(|text_line| {
buffer += UnicodeWidthStr::width(*text_line).saturating_sub(1) as u16
/ paragraph_width;
});
app_state.help_dialog_state.index_shortcuts[itx] =
app_state.help_dialog_state.index_shortcuts[itx - 1]
+ 1
+ prev_section_len;
}
prev_section_len = section.len() as u16 + buffer;
overflow_buffer += buffer;
});
app_state.help_dialog_state.scroll_state.max_scroll_index =
(self.styled_help_text.len() as u16
+ (constants::HELP_TEXT.len() as u16 - 5)
+ overflow_buffer)
.saturating_sub(draw_loc.height);
// Fix if over-scrolled
if app_state
.help_dialog_state
.scroll_state
.current_scroll_index
>= app_state.help_dialog_state.scroll_state.max_scroll_index
{
app_state
.help_dialog_state
.scroll_state
.current_scroll_index = app_state
.help_dialog_state
.scroll_state
.max_scroll_index
.saturating_sub(1);
}
}
f.render_widget(
Paragraph::new(self.styled_help_text.clone())
.block(
Block::default()
.title(help_title)
.style(self.colours.border_style)
.borders(Borders::ALL)
.border_style(self.colours.border_style),
)
.style(self.colours.text_style)
.alignment(Alignment::Left)
.wrap(Wrap { trim: true })
.scroll((
app_state
.help_dialog_state
.scroll_state
.current_scroll_index,
0,
)),
draw_loc,
);
}
}

View File

@ -227,159 +227,193 @@ pub static NORD_LIGHT_COLOUR_PALETTE: Lazy<ConfigColours> = Lazy::new(|| ConfigC
// Help text
pub const HELP_CONTENTS_TEXT: [&str; 8] = [
"Press the corresponding numbers to jump to the section, or scroll:",
"1 - General",
"2 - CPU widget",
"3 - Process widget",
"4 - Process search widget",
"5 - Process sort widget",
"6 - Battery widget",
"7 - Basic memory widget",
"Press the corresponding numbers to jump to that section, or just scroll down:",
"[1] General",
"[2] CPU widget",
"[3] Process widget",
"[4] Process search widget",
"[5] Process sort widget",
"[6] Battery widget",
"[7] Basic memory widget",
];
// TODO [Help]: Search in help?
// TODO [Help]: Move to using tables for easier formatting?
pub const GENERAL_HELP_TEXT: [&str; 30] = [
"1 - General",
"q, Ctrl-c Quit",
"Esc Close dialog windows, search, widgets, or exit expanded mode",
"Ctrl-r Resets any collected data",
"f Toggles freezing, which stops new data from being shown",
"Ctrl-Left, ",
"Shift-Left, Move widget selection left",
"H, A ",
"Ctrl-Right, ",
"Shift-Right, Move widget selection right",
"L, D ",
"Ctrl-Up, ",
"Shift-Up, Move widget selection up",
"K, W ",
"Ctrl-Down, ",
"Shift-Down, Move widget selection down",
"J, S ",
"Left, h Move left within widget",
"Down, j Move down within widget",
"Up, k Move up within widget",
"Right, l Move right within widget",
"? Open help menu",
"gg Jump to the first entry",
"G Jump to the last entry",
"e Toggle expanding the currently selected widget",
"+ Zoom in on chart (decrease time range)",
"- Zoom out on chart (increase time range)",
"= Reset zoom",
"Mouse scroll Scroll through the tables or zoom in/out of charts by scrolling up/down",
"Mouse click Selects the clicked widget, table entry, dialog option, or tab",
pub const GENERAL_HELP_TITLE: &str = "General";
pub const GENERAL_HELP_TEXT: [[&str; 2]; 21] = [
["q, Ctrl-c", "Quit"],
[
"Esc",
"Close dialog windows, search, widgets, or exit expanded mode",
],
["Ctrl-r", "Resets any collected data"],
["f", "Toggles freezing, stopping new data from being shown"],
["Ctrl-Left\nShift-Left\nH, A", "Move widget selection left"],
[
"Ctrl-Right\nShift-Right\nL, D",
"Move widget selection right",
],
["Ctrl-Up\nShift-Up\nK, W", "Move widget selection up"],
["Ctrl-Down\nShift-Dow\nJ, S", "Move widget selection down"],
["Left, h", "Move left within widget"],
["Down, j", "Move down within widget"],
["Up, k", "Move up within widget"],
["Right, l", "Move right within widget"],
["?", "Open help menu"],
["gg", "Jump to the first entry"],
["G", "Jump to the last entry"],
["e", "Toggle expanding the currently selected widget"],
["+", "Zoom in on chart (decrease time range)"],
["-", "Zoom out on chart (increase time range)"],
["=", "Reset zoom"],
[
"Mouse scroll",
"Scroll through the tables or zoom in/out of charts by scrolling up/down",
],
[
"Mouse click",
"Selects the clicked widget, table entry, dialog option, or tab",
],
];
pub const CPU_HELP_TEXT: [&str; 2] = [
"2 - CPU widget\n",
"Mouse scroll Scrolling over an CPU core/average shows only that entry on the chart",
pub const CPU_HELP_TITLE: &str = "CPU Widget";
pub const CPU_HELP_TEXT: [[&str; 2]; 1] = [[
"Mouse scroll",
"Scrolling over an CPU core/average shows only that entry on the chart",
]];
pub const PROCESS_HELP_TITLE: &str = "Process Widget";
pub const PROCESS_HELP_TEXT: [[&str; 2]; 14] = [
["dd, F9", "Kill the selected process"],
[
"c",
"Sort by CPU usage, press again to reverse sorting order",
],
[
"m",
"Sort by memory usage, press again to reverse sorting order",
],
[
"p",
"Sort by PID name, press again to reverse sorting order",
],
[
"n",
"Sort by process name, press again to reverse sorting order",
],
["Tab", "Group/un-group processes with the same name"],
["Ctrl-f, /", "Open process search widget"],
[
"P",
"Toggle between showing the full command or just the process name",
],
["s, F6", "Open process sort widget"],
["I", "Invert current sort"],
[
"%",
"Toggle between values and percentages for memory usage",
],
["t, F5", "Toggle tree mode"],
["+, -, click", "Collapse/expand a branch while in tree mode"],
[
"click on header",
"Sorts the entries by that column, click again to invert the sort",
],
];
pub const PROCESS_HELP_TEXT: [&str; 15] = [
"3 - Process widget",
"dd, F9 Kill the selected process",
"c Sort by CPU usage, press again to reverse sorting order",
"m Sort by memory usage, press again to reverse sorting order",
"p Sort by PID name, press again to reverse sorting order",
"n Sort by process name, press again to reverse sorting order",
"Tab Group/un-group processes with the same name",
"Ctrl-f, / Open process search widget",
"P Toggle between showing the full command or just the process name",
"s, F6 Open process sort widget",
"I Invert current sort",
"% Toggle between values and percentages for memory usage",
"t, F5 Toggle tree mode",
"+, -, click Collapse/expand a branch while in tree mode",
"click on header Sorts the entries by that column, click again to invert the sort",
pub const SEARCH_TEXT_HELP_TITLE: &str = "Process Search";
pub const SEARCH_HELP_TEXT: [[&str; 2]; 48] = [
["Tab", "Toggle between searching for PID and name"],
["Esc", "Close the search widget (retains the filter)"],
["Ctrl-a", "Skip to the start of the search query"],
["Ctrl-e", "Skip to the end of the search query"],
["Ctrl-u", "Clear the current search query"],
["Ctrl-w", "Delete a word behind the cursor"],
["Ctrl-h", "Delete the character behind the cursor"],
["Backspace", "Delete the character behind the cursor"],
["Delete", "Delete the character at the cursor"],
["Alt-c, F1", "Toggle matching case"],
["Alt-w, F2", "Toggle matching the entire word"],
["Alt-r, F3", "Toggle using regex"],
["Left, Alt-h", "Move cursor left"],
["Right, Alt-l", "Move cursor right"],
["\n", "\n"],
["Supported search types:", ""],
["<by name/cmd>", "ex: btm"],
["pid", "ex: pid 825"],
["cpu, cpu%", "ex: cpu > 4.2"],
["mem, mem%", "ex: mem < 4.2"],
["memb", "ex: memb < 100 kb"],
["read, r/s", "ex: read >= 1 b"],
["write, w/s", "ex: write <= 1 tb"],
["tread, t.read", "ex: tread = 1"],
["twrite, t.write", "ex: twrite = 1"],
["user", "ex: user = root"],
["state", "ex: state = running"],
["\n", "\n"],
["Comparison operators:", ""],
["=", "ex: cpu = 1"],
[">", "ex: cpu > 1"],
["<", "ex: cpu < 1"],
[">=", "ex: cpu >= 1"],
["<=", "ex: cpu <= 1"],
["\n", "\n"],
["Logical operators:", ""],
["and, &&, <Space> ", ": btm and cpu > 1 and mem > 1"],
["or, ||", "ex: btm or firefox"],
["\n", "\n"],
["Supported units:", ""],
["B", "ex: read > 1 b"],
["KB", "ex: read > 1 kb"],
["MB", "ex: read > 1 mb"],
["TB", "ex: read > 1 tb"],
["KiB", "ex: read > 1 kib"],
["MiB", "ex: read > 1 mib"],
["GiB", "ex: read > 1 gib"],
["TiB", "ex: read > 1 tib"],
];
pub const SEARCH_HELP_TEXT: [&str; 49] = [
"4 - Process search widget",
"Tab Toggle between searching for PID and name",
"Esc Close the search widget (retains the filter)",
"Ctrl-a Skip to the start of the search query",
"Ctrl-e Skip to the end of the search query",
"Ctrl-u Clear the current search query",
"Ctrl-w Delete a word behind the cursor",
"Ctrl-h Delete the character behind the cursor",
"Backspace Delete the character behind the cursor",
"Delete Delete the character at the cursor",
"Alt-c, F1 Toggle matching case",
"Alt-w, F2 Toggle matching the entire word",
"Alt-r, F3 Toggle using regex",
"Left, Alt-h Move cursor left",
"Right, Alt-l Move cursor right",
"",
"Supported search types:",
"<by name/cmd> ex: btm",
"pid ex: pid 825",
"cpu, cpu% ex: cpu > 4.2",
"mem, mem% ex: mem < 4.2",
"memb ex: memb < 100 kb",
"read, r/s ex: read >= 1 b",
"write, w/s ex: write <= 1 tb",
"tread, t.read ex: tread = 1",
"twrite, t.write ex: twrite = 1",
"user ex: user = root",
"state ex: state = running",
"",
"Comparison operators:",
"= ex: cpu = 1",
"> ex: cpu > 1",
"< ex: cpu < 1",
">= ex: cpu >= 1",
"<= ex: cpu <= 1",
"",
"Logical operators:",
"and, &&, <Space> ex: btm and cpu > 1 and mem > 1",
"or, || ex: btm or firefox",
"",
"Supported units:",
"B ex: read > 1 b",
"KB ex: read > 1 kb",
"MB ex: read > 1 mb",
"TB ex: read > 1 tb",
"KiB ex: read > 1 kib",
"MiB ex: read > 1 mib",
"GiB ex: read > 1 gib",
"TiB ex: read > 1 tib",
pub const PROCESS_SORT_HELP_TITLE: &str = "Process Sort";
pub const PROCESS_SORT_HELP_TEXT: [[&str; 2]; 5] = [
["Down, 'j'", "Scroll down in list"],
["Up, 'k'", "Scroll up in list"],
["Mouse scroll", "Scroll through sort widget"],
["Esc", "Close the sort widget"],
["Enter", "Sort by current selected column"],
];
pub const SORT_HELP_TEXT: [&str; 6] = [
"5 - Sort widget\n",
"Down, 'j' Scroll down in list",
"Up, 'k' Scroll up in list",
"Mouse scroll Scroll through sort widget",
"Esc Close the sort widget",
"Enter Sort by current selected column",
pub const BATTERY_HELP_TITLE: &str = "Battery Widget";
pub const BATTERY_HELP_TEXT: [[&str; 2]; 2] = [
["Left", "Go to previous battery"],
["Right", "Go to next battery"],
];
pub const BATTERY_HELP_TEXT: [&str; 3] = [
"6 - Battery widget",
"Left Go to previous battery",
"Right Go to next battery",
];
pub const BASIC_MEM_HELP_TITLE: &str = "Basic Memory Widget";
pub const BASIC_MEM_HELP_TEXT: [[&str; 2]; 1] = [[
"%",
"Toggle between values and percentages for memory usage",
]];
pub const BASIC_MEM_HELP_TEXT: [&str; 2] = [
"7 - Basic memory widget",
"% Toggle between values and percentages for memory usage",
];
pub static HELP_TEXT: Lazy<Vec<Vec<&'static str>>> = Lazy::new(|| {
vec![
HELP_CONTENTS_TEXT.to_vec(),
pub static HELP_TEXT: Lazy<[Vec<[&'static str; 2]>; 7]> = Lazy::new(|| {
[
GENERAL_HELP_TEXT.to_vec(),
CPU_HELP_TEXT.to_vec(),
PROCESS_HELP_TEXT.to_vec(),
SEARCH_HELP_TEXT.to_vec(),
SORT_HELP_TEXT.to_vec(),
PROCESS_SORT_HELP_TEXT.to_vec(),
BATTERY_HELP_TEXT.to_vec(),
BASIC_MEM_HELP_TEXT.to_vec(),
]
});
pub const HELP_TITLES: [&'static str; 7] = [
GENERAL_HELP_TITLE,
CPU_HELP_TITLE,
PROCESS_HELP_TITLE,
SEARCH_TEXT_HELP_TITLE,
PROCESS_SORT_HELP_TITLE,
BATTERY_HELP_TITLE,
BASIC_MEM_HELP_TITLE,
];
// Default layouts
pub const DEFAULT_LAYOUT: &str = r##"
[[row]]

View File

@ -98,7 +98,7 @@ pub fn handle_key_event(
KeyCode::Left => app.on_left_key(),
KeyCode::Right => app.on_right_key(),
KeyCode::Char(caught_char) => app.on_char_key(caught_char),
KeyCode::Esc => app.on_esc(),
// KeyCode::Esc => app.on_esc(),
KeyCode::Enter => app.on_enter(),
KeyCode::Tab => app.on_tab(),
KeyCode::Backspace => app.on_backspace(),