mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-25 22:55:06 +02:00
refactor + change: write new movement logic
This commit is contained in:
parent
e657fec2c0
commit
64c6d0c898
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -251,6 +251,7 @@ dependencies = [
|
||||
"fxhash",
|
||||
"heim",
|
||||
"indexmap",
|
||||
"indextree",
|
||||
"itertools",
|
||||
"libc",
|
||||
"log",
|
||||
@ -882,6 +883,12 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indextree"
|
||||
version = "4.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990980c3d268c9b99df35e813eca2b8d1ee08606f6d2bb325edbd0b0c68f9ffe"
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.9"
|
||||
|
@ -48,6 +48,7 @@ futures = "0.3.14"
|
||||
futures-timer = "3.0.2"
|
||||
fxhash = "0.2.1"
|
||||
indexmap = "1.6.2"
|
||||
indextree = "4.3.1"
|
||||
itertools = "0.10.0"
|
||||
once_cell = "1.5.2"
|
||||
regex = "1.5.4"
|
||||
|
@ -1,11 +1,30 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const MAX_TIMEOUT: Duration = Duration::from_millis(400);
|
||||
|
||||
/// The results of handling some user input event, like a mouse or key event, signifying what
|
||||
/// the program should then do next.
|
||||
pub enum EventResult {
|
||||
/// Kill the program.
|
||||
Quit,
|
||||
|
||||
/// Trigger a redraw.
|
||||
Redraw,
|
||||
Continue,
|
||||
|
||||
/// Don't trigger a redraw.
|
||||
NoRedraw,
|
||||
}
|
||||
|
||||
/// How a widget should handle a widget selection request.
|
||||
pub enum SelectionAction {
|
||||
/// This event occurs if the widget internally handled the selection action.
|
||||
Handled,
|
||||
|
||||
/// 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,
|
||||
@ -36,16 +55,14 @@ pub enum MultiKeyResult {
|
||||
pub struct MultiKey {
|
||||
state: MultiKeyState,
|
||||
pattern: Vec<char>,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl MultiKey {
|
||||
/// Creates a new [`MultiKey`] with a given pattern and timeout.
|
||||
pub fn register(pattern: Vec<char>, timeout: Duration) -> Self {
|
||||
pub fn register(pattern: Vec<char>) -> Self {
|
||||
Self {
|
||||
state: MultiKeyState::Idle,
|
||||
pattern,
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +98,7 @@ impl MultiKey {
|
||||
trigger_instant,
|
||||
checked_index,
|
||||
} => {
|
||||
if trigger_instant.elapsed() > self.timeout {
|
||||
if trigger_instant.elapsed() > MAX_TIMEOUT {
|
||||
// Just reset and recursively call (putting it into Idle).
|
||||
self.reset();
|
||||
self.input(c)
|
||||
|
0
src/app/widgets/base/carousel.rs
Normal file
0
src/app/widgets/base/carousel.rs
Normal file
@ -1,4 +1,4 @@
|
||||
//! A collection of basic widgets.
|
||||
//! A collection of basic components.
|
||||
|
||||
pub mod text_table;
|
||||
pub use text_table::TextTable;
|
||||
|
@ -1,11 +1,9 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent};
|
||||
use tui::{layout::Rect, widgets::TableState};
|
||||
|
||||
use crate::app::{
|
||||
event::{EventResult, MultiKey, MultiKeyResult},
|
||||
Widget,
|
||||
Component,
|
||||
};
|
||||
|
||||
pub enum ScrollDirection {
|
||||
@ -36,7 +34,7 @@ impl Scrollable {
|
||||
scroll_direction: ScrollDirection::Down,
|
||||
num_items,
|
||||
tui_state: TableState::default(),
|
||||
gg_manager: MultiKey::register(vec!['g', 'g'], Duration::from_millis(400)),
|
||||
gg_manager: MultiKey::register(vec!['g', 'g']), // TODO: Use a static arrayvec
|
||||
bounds: Rect::default(),
|
||||
}
|
||||
}
|
||||
@ -79,7 +77,7 @@ impl Scrollable {
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,7 +88,7 @@ impl Scrollable {
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +102,7 @@ impl Scrollable {
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
} else {
|
||||
self.update_index(new_index);
|
||||
@ -121,18 +119,28 @@ impl Scrollable {
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
} else {
|
||||
self.update_index(new_index);
|
||||
EventResult::Redraw
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_num_items(&mut self, num_items: usize) {
|
||||
self.num_items = num_items;
|
||||
|
||||
if num_items <= self.current_index {
|
||||
self.current_index = num_items - 1;
|
||||
}
|
||||
|
||||
if num_items <= self.previous_index {
|
||||
self.previous_index = num_items - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Scrollable {
|
||||
type UpdateData = usize;
|
||||
|
||||
impl Component for Scrollable {
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
use crossterm::event::KeyCode::{Char, Down, Up};
|
||||
|
||||
@ -144,14 +152,14 @@ impl Widget for Scrollable {
|
||||
Char('k') => self.move_up(1),
|
||||
Char('g') => match self.gg_manager.input('g') {
|
||||
MultiKeyResult::Completed => self.skip_to_first(),
|
||||
MultiKeyResult::Accepted => EventResult::Continue,
|
||||
MultiKeyResult::Rejected => EventResult::Continue,
|
||||
MultiKeyResult::Accepted => EventResult::NoRedraw,
|
||||
MultiKeyResult::Rejected => EventResult::NoRedraw,
|
||||
},
|
||||
Char('G') => self.skip_to_last(),
|
||||
_ => EventResult::Continue,
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
} else {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,23 +184,11 @@ impl Widget for Scrollable {
|
||||
}
|
||||
}
|
||||
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
crossterm::event::MouseEventKind::ScrollDown => self.move_down(1),
|
||||
crossterm::event::MouseEventKind::ScrollUp => self.move_up(1),
|
||||
_ => EventResult::Continue,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, new_num_items: usize) {
|
||||
self.num_items = new_num_items;
|
||||
|
||||
if new_num_items <= self.current_index {
|
||||
self.current_index = new_num_items - 1;
|
||||
}
|
||||
|
||||
if new_num_items <= self.previous_index {
|
||||
self.previous_index = new_num_items - 1;
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +1,138 @@
|
||||
pub struct TextInput {}
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::app::{
|
||||
event::EventResult::{self},
|
||||
Component,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
/// A single-line component for taking text inputs.
|
||||
pub struct TextInput {
|
||||
text: String,
|
||||
cursor_index: usize,
|
||||
bounds: Rect,
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
/// Creates a new [`TextInput`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, new_cursor_index: usize) -> EventResult {
|
||||
if self.cursor_index == new_cursor_index {
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
self.cursor_index = new_cursor_index;
|
||||
EventResult::Redraw
|
||||
}
|
||||
}
|
||||
|
||||
fn move_back(&mut self, amount_to_subtract: usize) -> EventResult {
|
||||
self.set_cursor(self.cursor_index.saturating_sub(amount_to_subtract))
|
||||
}
|
||||
|
||||
fn move_forward(&mut self, amount_to_add: usize) -> EventResult {
|
||||
let new_cursor = self.cursor_index + amount_to_add;
|
||||
if new_cursor >= self.text.len() {
|
||||
self.set_cursor(self.text.len() - 1)
|
||||
} else {
|
||||
self.set_cursor(new_cursor)
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_text(&mut self) -> EventResult {
|
||||
if self.text.is_empty() {
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
self.text = String::default();
|
||||
self.cursor_index = 0;
|
||||
EventResult::Redraw
|
||||
}
|
||||
}
|
||||
|
||||
fn move_word_forward(&mut self) -> EventResult {
|
||||
// TODO: Implement this
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
|
||||
fn move_word_back(&mut self) -> EventResult {
|
||||
// TODO: Implement this
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
|
||||
fn clear_previous_word(&mut self) -> EventResult {
|
||||
// TODO: Implement this
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
|
||||
fn clear_previous_grapheme(&mut self) -> EventResult {
|
||||
// TODO: Implement this
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
|
||||
pub fn update(&mut self, new_text: String) {
|
||||
self.text = new_text;
|
||||
|
||||
if self.cursor_index >= self.text.len() {
|
||||
self.cursor_index = self.text.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for TextInput {
|
||||
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) -> EventResult {
|
||||
if event.modifiers.is_empty() {
|
||||
match event.code {
|
||||
KeyCode::Left => self.move_back(1),
|
||||
KeyCode::Right => self.move_forward(1),
|
||||
KeyCode::Backspace => self.clear_previous_grapheme(),
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
} else if let KeyModifiers::CONTROL = event.modifiers {
|
||||
match event.code {
|
||||
KeyCode::Char('a') => self.set_cursor(0),
|
||||
KeyCode::Char('e') => self.set_cursor(self.text.len()),
|
||||
KeyCode::Char('u') => self.clear_text(),
|
||||
KeyCode::Char('w') => self.clear_previous_word(),
|
||||
KeyCode::Char('h') => self.clear_previous_grapheme(),
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
} else if let KeyModifiers::ALT = event.modifiers {
|
||||
match event.code {
|
||||
KeyCode::Char('b') => self.move_word_forward(),
|
||||
KeyCode::Char('f') => self.move_word_back(),
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||
// We are assuming this is within bounds...
|
||||
|
||||
let x = event.column;
|
||||
let widget_x = self.bounds.x;
|
||||
let new_cursor_index = usize::from(x.saturating_sub(widget_x));
|
||||
|
||||
if new_cursor_index >= self.text.len() {
|
||||
self.cursor_index = self.text.len() - 1;
|
||||
} else {
|
||||
self.cursor_index = new_cursor_index;
|
||||
}
|
||||
|
||||
EventResult::Redraw
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,32 @@
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::app::{event::EventResult, Scrollable, Widget};
|
||||
use crate::app::{event::EventResult, Component, Scrollable};
|
||||
|
||||
struct Column {
|
||||
name: &'static str,
|
||||
/// A [`Column`] represents some column in a [`TextTable`].
|
||||
pub struct Column {
|
||||
pub name: &'static str,
|
||||
pub shortcut: Option<KeyEvent>,
|
||||
pub default_descending: bool,
|
||||
|
||||
// TODO: I would remove these in the future, storing them here feels weird...
|
||||
desired_column_width: u16,
|
||||
calculated_column_width: u16,
|
||||
|
||||
x_bounds: (u16, u16),
|
||||
pub desired_column_width: u16,
|
||||
pub calculated_column_width: u16,
|
||||
pub x_bounds: (u16, u16),
|
||||
}
|
||||
|
||||
impl Column {}
|
||||
|
||||
/// The [`Widget::UpdateState`] of a [`TextTable`].
|
||||
pub struct TextTableUpdateData {
|
||||
num_items: Option<usize>,
|
||||
columns: Option<Vec<Column>>,
|
||||
impl Column {
|
||||
/// Creates a new [`Column`], given a name and optional shortcut.
|
||||
pub fn new(name: &'static str, shortcut: Option<KeyEvent>, default_descending: bool) -> Self {
|
||||
Self {
|
||||
name,
|
||||
desired_column_width: 0,
|
||||
calculated_column_width: 0,
|
||||
x_bounds: (0, 0),
|
||||
shortcut,
|
||||
default_descending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sortable, scrollable table with columns.
|
||||
@ -37,24 +45,30 @@ pub struct TextTable {
|
||||
|
||||
/// Which index we're sorting by.
|
||||
sort_index: usize,
|
||||
|
||||
/// Whether we're sorting by ascending order.
|
||||
sort_ascending: bool,
|
||||
}
|
||||
|
||||
impl TextTable {
|
||||
pub fn new(num_items: usize, columns: Vec<&'static str>) -> Self {
|
||||
pub fn new(num_items: usize, columns: Vec<(&'static str, Option<KeyEvent>, bool)>) -> Self {
|
||||
Self {
|
||||
scrollable: Scrollable::new(num_items),
|
||||
columns: columns
|
||||
.into_iter()
|
||||
.map(|name| Column {
|
||||
.map(|(name, shortcut, default_descending)| Column {
|
||||
name,
|
||||
desired_column_width: 0,
|
||||
calculated_column_width: 0,
|
||||
x_bounds: (0, 0),
|
||||
shortcut,
|
||||
default_descending,
|
||||
})
|
||||
.collect(),
|
||||
show_gap: true,
|
||||
bounds: Rect::default(),
|
||||
sort_index: 0,
|
||||
sort_ascending: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,12 +103,42 @@ impl TextTable {
|
||||
pub fn column_names(&self) -> Vec<&'static str> {
|
||||
self.columns.iter().map(|column| column.name).collect()
|
||||
}
|
||||
|
||||
pub fn update_num_items(&mut self, num_items: usize) {
|
||||
self.scrollable.update_num_items(num_items);
|
||||
}
|
||||
|
||||
pub fn update_a_column(&mut self, index: usize, column: Column) {
|
||||
if let Some(c) = self.columns.get_mut(index) {
|
||||
*c = column;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_columns(&mut self, columns: Vec<Column>) {
|
||||
self.columns = columns;
|
||||
if self.columns.len() <= self.sort_index {
|
||||
self.sort_index = self.columns.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TextTable {
|
||||
type UpdateData = TextTableUpdateData;
|
||||
|
||||
impl Component for TextTable {
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
for (index, column) in self.columns.iter().enumerate() {
|
||||
if let Some(shortcut) = column.shortcut {
|
||||
if shortcut == event {
|
||||
if self.sort_index == index {
|
||||
// Just flip the sort if we're already sorting by this.
|
||||
self.sort_ascending = !self.sort_ascending;
|
||||
} else {
|
||||
self.sort_index = index;
|
||||
self.sort_ascending = !column.default_descending;
|
||||
}
|
||||
return EventResult::Redraw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.scrollable.handle_key_event(event)
|
||||
}
|
||||
|
||||
@ -107,29 +151,22 @@ impl Widget for TextTable {
|
||||
for (index, column) in self.columns.iter().enumerate() {
|
||||
let (start, end) = column.x_bounds;
|
||||
if start >= x && end <= y {
|
||||
self.sort_index = index;
|
||||
if self.sort_index == index {
|
||||
// Just flip the sort if we're already sorting by this.
|
||||
self.sort_ascending = !self.sort_ascending;
|
||||
} else {
|
||||
self.sort_index = index;
|
||||
self.sort_ascending = !column.default_descending;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
self.scrollable.handle_mouse_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, update_data: Self::UpdateData) {
|
||||
if let Some(num_items) = update_data.num_items {
|
||||
self.scrollable.update(num_items);
|
||||
}
|
||||
|
||||
if let Some(columns) = update_data.columns {
|
||||
self.columns = columns;
|
||||
if self.columns.len() <= self.sort_index {
|
||||
self.sort_index = self.columns.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Rect {
|
||||
self.bounds
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use std::time::{Duration, Instant};
|
||||
use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::app::{event::EventResult, Widget};
|
||||
use crate::app::{event::EventResult, Component};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AutohideTimerState {
|
||||
@ -23,7 +23,9 @@ pub enum AutohideTimer {
|
||||
impl AutohideTimer {
|
||||
fn trigger_display_timer(&mut self) {
|
||||
match self {
|
||||
AutohideTimer::Disabled => todo!(),
|
||||
AutohideTimer::Disabled => {
|
||||
// Does nothing.
|
||||
}
|
||||
AutohideTimer::Enabled {
|
||||
state,
|
||||
show_duration: _,
|
||||
@ -35,7 +37,9 @@ impl AutohideTimer {
|
||||
|
||||
pub fn update_display_timer(&mut self) {
|
||||
match self {
|
||||
AutohideTimer::Disabled => {}
|
||||
AutohideTimer::Disabled => {
|
||||
// Does nothing.
|
||||
}
|
||||
AutohideTimer::Enabled {
|
||||
state,
|
||||
show_duration,
|
||||
@ -87,7 +91,7 @@ impl TimeGraph {
|
||||
'-' => self.zoom_out(),
|
||||
'+' => self.zoom_in(),
|
||||
'=' => self.reset_zoom(),
|
||||
_ => EventResult::Continue,
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +109,7 @@ impl TimeGraph {
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,13 +127,13 @@ impl TimeGraph {
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_zoom(&mut self) -> EventResult {
|
||||
if self.current_display_time == self.default_time_value {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
self.current_display_time = self.default_time_value;
|
||||
self.autohide_timer.trigger_display_timer();
|
||||
@ -138,19 +142,17 @@ impl TimeGraph {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TimeGraph {
|
||||
type UpdateData = ();
|
||||
|
||||
impl Component for TimeGraph {
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
use crossterm::event::KeyCode::Char;
|
||||
|
||||
if event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::SHIFT {
|
||||
match event.code {
|
||||
Char(c) => self.handle_char(c),
|
||||
_ => EventResult::Continue,
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
} else {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +160,7 @@ impl Widget for TimeGraph {
|
||||
match event.kind {
|
||||
crossterm::event::MouseEventKind::ScrollDown => self.zoom_out(),
|
||||
crossterm::event::MouseEventKind::ScrollUp => self.zoom_in(),
|
||||
_ => EventResult::Continue,
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
}
|
||||
|
||||
|
0
src/app/widgets/basic_cpu.rs
Normal file
0
src/app/widgets/basic_cpu.rs
Normal file
0
src/app/widgets/basic_mem.rs
Normal file
0
src/app/widgets/basic_mem.rs
Normal file
0
src/app/widgets/basic_net.rs
Normal file
0
src/app/widgets/basic_net.rs
Normal file
@ -1,5 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tui::layout::Rect;
|
||||
|
||||
use super::{Component, Widget};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BatteryWidgetState {
|
||||
pub currently_selected_battery_index: usize,
|
||||
@ -23,3 +27,30 @@ impl BatteryState {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement battery widget.
|
||||
/// A table displaying battery information on a per-battery basis.
|
||||
pub struct BatteryTable {
|
||||
bounds: Rect,
|
||||
}
|
||||
|
||||
impl BatteryTable {
|
||||
/// Creates a new [`BatteryTable`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bounds: Rect::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for BatteryTable {
|
||||
fn bounds(&self) -> tui::layout::Rect {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
fn set_bounds(&mut self, new_bounds: tui::layout::Rect) {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for BatteryTable {}
|
||||
|
@ -6,8 +6,8 @@ use tui::layout::Rect;
|
||||
use crate::app::event::EventResult;
|
||||
|
||||
use super::{
|
||||
does_point_intersect_rect, text_table::TextTableUpdateData, AppScrollWidgetState,
|
||||
CanvasTableWidthState, TextTable, TimeGraph, Widget,
|
||||
does_point_intersect_rect, AppScrollWidgetState, CanvasTableWidthState, Component, TextTable,
|
||||
TimeGraph, Widget,
|
||||
};
|
||||
|
||||
pub struct CpuWidgetState {
|
||||
@ -66,10 +66,6 @@ pub enum CpuGraphLegendPosition {
|
||||
Right,
|
||||
}
|
||||
|
||||
pub struct CpuGraphUpdateData {
|
||||
pub legend_data: Option<TextTableUpdateData>,
|
||||
}
|
||||
|
||||
/// A widget designed to show CPU usage via a graph, along with a side legend implemented as a [`TextTable`].
|
||||
pub struct CpuGraph {
|
||||
graph: TimeGraph,
|
||||
@ -95,21 +91,16 @@ impl CpuGraph {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for CpuGraph {
|
||||
type UpdateData = CpuGraphUpdateData;
|
||||
|
||||
impl Component for CpuGraph {
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
match self.selected {
|
||||
CpuGraphSelection::Graph => self.graph.handle_key_event(event),
|
||||
CpuGraphSelection::Legend => self.legend.handle_key_event(event),
|
||||
CpuGraphSelection::None => EventResult::Continue,
|
||||
CpuGraphSelection::None => EventResult::NoRedraw,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||
// Check where we clicked (and switch the selected field if required) and fire the handler from there.
|
||||
// Note we assume that the
|
||||
|
||||
let global_x = event.column;
|
||||
let global_y = event.row;
|
||||
|
||||
@ -120,13 +111,7 @@ impl Widget for CpuGraph {
|
||||
self.selected = CpuGraphSelection::Legend;
|
||||
self.legend.handle_mouse_event(event)
|
||||
} else {
|
||||
EventResult::Continue
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, update_data: Self::UpdateData) {
|
||||
if let Some(legend_data) = update_data.legend_data {
|
||||
self.legend.update(legend_data);
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,3 +123,5 @@ impl Widget for CpuGraph {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for CpuGraph {}
|
||||
|
@ -5,10 +5,7 @@ use tui::layout::Rect;
|
||||
|
||||
use crate::app::event::EventResult;
|
||||
|
||||
use super::{
|
||||
text_table::TextTableUpdateData, AppScrollWidgetState, CanvasTableWidthState, TextTable,
|
||||
Widget,
|
||||
};
|
||||
use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, Widget};
|
||||
|
||||
pub struct DiskWidgetState {
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
@ -58,9 +55,7 @@ impl DiskTable {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for DiskTable {
|
||||
type UpdateData = TextTableUpdateData;
|
||||
|
||||
impl Component for DiskTable {
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
self.table.handle_key_event(event)
|
||||
}
|
||||
@ -69,10 +64,6 @@ impl Widget for DiskTable {
|
||||
self.table.handle_mouse_event(event)
|
||||
}
|
||||
|
||||
fn update(&mut self, update_data: Self::UpdateData) {
|
||||
self.table.update(update_data);
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Rect {
|
||||
self.bounds
|
||||
}
|
||||
@ -81,3 +72,5 @@ impl Widget for DiskTable {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for DiskTable {}
|
||||
|
@ -5,7 +5,7 @@ use tui::layout::Rect;
|
||||
|
||||
use crate::app::event::EventResult;
|
||||
|
||||
use super::{TimeGraph, Widget};
|
||||
use super::{Component, TimeGraph, Widget};
|
||||
|
||||
pub struct MemWidgetState {
|
||||
pub current_display_time: u64,
|
||||
@ -56,9 +56,7 @@ impl MemGraph {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MemGraph {
|
||||
type UpdateData = ();
|
||||
|
||||
impl Component for MemGraph {
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
self.graph.handle_key_event(event)
|
||||
}
|
||||
@ -75,3 +73,5 @@ impl Widget for MemGraph {
|
||||
self.graph.set_bounds(new_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for MemGraph {}
|
||||
|
@ -2,10 +2,14 @@ use std::time::Instant;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use indextree::{Arena, NodeId};
|
||||
use tui::{layout::Rect, widgets::TableState};
|
||||
|
||||
use crate::{
|
||||
app::{event::EventResult, layout_manager::BottomWidgetType},
|
||||
app::{
|
||||
event::{EventResult, SelectionAction},
|
||||
layout_manager::BottomWidgetType,
|
||||
},
|
||||
constants,
|
||||
};
|
||||
|
||||
@ -33,37 +37,65 @@ pub use self::battery::*;
|
||||
pub mod temp;
|
||||
pub use temp::*;
|
||||
|
||||
/// A trait for things that are drawn with state.
|
||||
#[enum_dispatch]
|
||||
#[allow(unused_variables)]
|
||||
pub trait Widget {
|
||||
type UpdateData;
|
||||
|
||||
pub trait Component {
|
||||
/// Handles a [`KeyEvent`].
|
||||
///
|
||||
/// Defaults to returning [`EventResult::Continue`], indicating nothing should be done.
|
||||
/// Defaults to returning [`EventResult::NoRedraw`], indicating nothing should be done.
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
|
||||
/// Handles a [`MouseEvent`].
|
||||
///
|
||||
/// Defaults to returning [`EventResult::Continue`], indicating nothing should be done.
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||
EventResult::Continue
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
|
||||
/// Updates a [`Widget`] with new data from some state outside of its control. Defaults to doing nothing.
|
||||
fn update(&mut self, update_data: Self::UpdateData) {}
|
||||
|
||||
/// Returns a [`Widget`]'s bounding box. Note that these are defined in *global*, *absolute*
|
||||
/// Returns a [`Component`]'s bounding box. Note that these are defined in *global*, *absolute*
|
||||
/// coordinates.
|
||||
fn bounds(&self) -> Rect;
|
||||
|
||||
/// Updates a [`Widget`]s bounding box.
|
||||
/// Updates a [`Component`]s bounding box to `new_bounds`.
|
||||
fn set_bounds(&mut self, new_bounds: Rect);
|
||||
}
|
||||
|
||||
#[enum_dispatch(BottomWidget)]
|
||||
/// A trait for actual fully-fledged widgets to be displayed in bottom.
|
||||
#[enum_dispatch]
|
||||
pub trait Widget {
|
||||
/// Updates a [`Widget`] given some data. Defaults to doing nothing.
|
||||
fn update(&mut self) {}
|
||||
|
||||
/// Handles what to do when trying to respond to a widget selection movement to the left.
|
||||
/// Defaults to just moving to the next-possible widget in that direction.
|
||||
fn handle_widget_selection_left(&mut self) -> SelectionAction {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
|
||||
/// Handles what to do when trying to respond to a widget selection movement to the right.
|
||||
/// Defaults to just moving to the next-possible widget in that direction.
|
||||
fn handle_widget_selection_right(&mut self) -> SelectionAction {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
|
||||
/// Handles what to do when trying to respond to a widget selection movement upward.
|
||||
/// Defaults to just moving to the next-possible widget in that direction.
|
||||
fn handle_widget_selection_up(&mut self) -> SelectionAction {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
|
||||
/// Handles what to do when trying to respond to a widget selection movement downward.
|
||||
/// Defaults to just moving to the next-possible widget in that direction.
|
||||
fn handle_widget_selection_down(&mut self) -> SelectionAction {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
}
|
||||
|
||||
/// The "main" widgets that are used by bottom to display information!
|
||||
#[enum_dispatch(Component, Widget)]
|
||||
enum BottomWidget {
|
||||
MemGraph,
|
||||
TempTable,
|
||||
@ -71,12 +103,293 @@ enum BottomWidget {
|
||||
CpuGraph,
|
||||
NetGraph,
|
||||
OldNetGraph,
|
||||
ProcessManager,
|
||||
BatteryTable,
|
||||
}
|
||||
|
||||
/// Checks whether points `(x, y)` intersect a given [`Rect`].
|
||||
pub fn does_point_intersect_rect(x: u16, y: u16, rect: Rect) -> bool {
|
||||
x >= rect.left() && x <= rect.right() && y >= rect.top() && y <= rect.bottom()
|
||||
}
|
||||
|
||||
/// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of:
|
||||
/// - [`LayoutNode::Row`] (a a non-leaf that distributes its children horizontally)
|
||||
/// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically)
|
||||
/// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with)
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum LayoutNode {
|
||||
Row(BottomRow),
|
||||
Col(BottomCol),
|
||||
Widget,
|
||||
}
|
||||
|
||||
/// A simple struct representing a row and its state.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct BottomRow {
|
||||
last_selected_index: usize,
|
||||
}
|
||||
|
||||
/// A simple struct representing a column and its state.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct BottomCol {
|
||||
last_selected_index: usize,
|
||||
}
|
||||
|
||||
/// Relative movement direction from the currently selected widget.
|
||||
pub enum MovementDirection {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
/// Attempts to find and return the selected [`BottomWidgetId`] after moving in a direction.
|
||||
///
|
||||
/// Note this function assumes a properly built tree - if not, bad things may happen! We generally assume that:
|
||||
/// - Only [`LayoutNode::Widget`]s are leaves.
|
||||
/// - Only [`LayoutNode::Row`]s or [`LayoutNode::Col`]s are non-leaves.
|
||||
fn move_widget_selection(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_widget: &mut BottomWidget,
|
||||
current_widget_id: NodeId, direction: MovementDirection,
|
||||
) -> NodeId {
|
||||
// We first give our currently-selected widget a chance to react to the movement - it may handle it internally!
|
||||
let handled = match direction {
|
||||
MovementDirection::Left => current_widget.handle_widget_selection_left(),
|
||||
MovementDirection::Right => current_widget.handle_widget_selection_right(),
|
||||
MovementDirection::Up => current_widget.handle_widget_selection_up(),
|
||||
MovementDirection::Down => current_widget.handle_widget_selection_down(),
|
||||
};
|
||||
|
||||
match handled {
|
||||
SelectionAction::Handled => {
|
||||
// If it was handled by the widget, then we don't have to do anything - return the current one.
|
||||
current_widget_id
|
||||
}
|
||||
SelectionAction::NotHandled => {
|
||||
/// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent
|
||||
/// is a [`LayoutNode::Row`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order).
|
||||
/// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`].
|
||||
fn find_first_row(
|
||||
layout_tree: &Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> Option<(NodeId, NodeId)> {
|
||||
layout_tree
|
||||
.get(current_id)
|
||||
.and_then(|current_node| current_node.parent())
|
||||
.and_then(|parent_id| {
|
||||
layout_tree
|
||||
.get(parent_id)
|
||||
.map(|parent_node| (parent_id, parent_node))
|
||||
})
|
||||
.and_then(|(parent_id, parent_node)| match parent_node.get() {
|
||||
LayoutNode::Row(_) => Some((parent_id, current_id)),
|
||||
LayoutNode::Col(_) => find_first_row(layout_tree, parent_id),
|
||||
LayoutNode::Widget => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent
|
||||
/// is a [`LayoutNode::Col`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order).
|
||||
/// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`].
|
||||
fn find_first_col(
|
||||
layout_tree: &Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> Option<(NodeId, NodeId)> {
|
||||
layout_tree
|
||||
.get(current_id)
|
||||
.and_then(|current_node| current_node.parent())
|
||||
.and_then(|parent_id| {
|
||||
layout_tree
|
||||
.get(parent_id)
|
||||
.map(|parent_node| (parent_id, parent_node))
|
||||
})
|
||||
.and_then(|(parent_id, parent_node)| match parent_node.get() {
|
||||
LayoutNode::Row(_) => find_first_col(layout_tree, parent_id),
|
||||
LayoutNode::Col(_) => Some((parent_id, current_id)),
|
||||
LayoutNode::Widget => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Descends to a leaf.
|
||||
fn descend_to_leaf(layout_tree: &Arena<LayoutNode>, current_id: NodeId) -> NodeId {
|
||||
if let Some(current_node) = layout_tree.get(current_id) {
|
||||
match current_node.get() {
|
||||
LayoutNode::Row(BottomRow {
|
||||
last_selected_index,
|
||||
})
|
||||
| LayoutNode::Col(BottomCol {
|
||||
last_selected_index,
|
||||
}) => {
|
||||
if let Some(next_child) =
|
||||
current_id.children(layout_tree).nth(*last_selected_index)
|
||||
{
|
||||
descend_to_leaf(layout_tree, next_child)
|
||||
} else {
|
||||
current_id
|
||||
}
|
||||
}
|
||||
LayoutNode::Widget => {
|
||||
// Halt!
|
||||
current_id
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_id
|
||||
}
|
||||
}
|
||||
|
||||
// If it was NOT handled by the current widget, then move in the correct direction; we can rely
|
||||
// on the tree layout to help us decide where to go.
|
||||
// Movement logic is inspired by i3. When we enter a new column/row, we go to the *last* selected
|
||||
// element; if we can't, go to the nearest one.
|
||||
match direction {
|
||||
MovementDirection::Left => {
|
||||
// When we move "left":
|
||||
// 1. Look for the parent of the current widget.
|
||||
// 2. Depending on whether it is a Row or Col:
|
||||
// a) If we are in a Row, try to move to the child (it can be a Row, Col, or Widget) before it,
|
||||
// and update the last-selected index. If we can't (i.e. we are the first element), then
|
||||
// instead move to the parent, and try again to select the element before it. If there is
|
||||
// no parent (i.e. we hit the root), then just return the original index.
|
||||
// b) If we are in a Col, then just try to move to the parent. If there is no
|
||||
// parent (i.e. we hit the root), then just return the original index.
|
||||
// c) A Widget should be impossible to select.
|
||||
// 3. Assuming we have now selected a new child, then depending on what the child is:
|
||||
// a) If we are in a Row or Col, then take the last selected index, and repeat step 3 until you hit
|
||||
// a Widget.
|
||||
// b) If we are in a Widget, return the corresponding NodeId.
|
||||
|
||||
fn find_left(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.preceding_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Subtract one from the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Row(row) = parent.get_mut() {
|
||||
row.last_selected_index =
|
||||
row.last_selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_left(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_left(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Right => {
|
||||
// When we move "right", repeat the steps for "left", but instead try to move to the child *after*
|
||||
// it in all cases.
|
||||
|
||||
fn find_right(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.following_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Add one to the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Row(row) = parent.get_mut() {
|
||||
row.last_selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_right(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_right(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Up => {
|
||||
// When we move "up", copy the steps for "left", but switch "Row" and "Col". We instead want to move
|
||||
// vertically, so we want to now avoid Rows and look for Cols!
|
||||
|
||||
fn find_above(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.preceding_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Subtract one from the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Col(row) = parent.get_mut() {
|
||||
row.last_selected_index =
|
||||
row.last_selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_above(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_above(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Down => {
|
||||
// See "up"'s steps, but now we're going for the child *after* the currently selected one in all
|
||||
// cases.
|
||||
|
||||
fn find_below(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.following_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Add one to the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Col(row) = parent.get_mut() {
|
||||
row.last_selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_below(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_below(layout_tree, current_widget_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Old stuff below -----
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScrollDirection {
|
||||
// UP means scrolling up --- this usually DECREMENTS
|
||||
|
@ -2,7 +2,7 @@ use std::{collections::HashMap, time::Instant};
|
||||
|
||||
use tui::layout::Rect;
|
||||
|
||||
use super::{TimeGraph, Widget};
|
||||
use super::{Component, TimeGraph, Widget};
|
||||
|
||||
pub struct NetWidgetState {
|
||||
pub current_display_time: u64,
|
||||
@ -55,7 +55,9 @@ impl NetState {
|
||||
}
|
||||
}
|
||||
|
||||
struct NetGraphCache {
|
||||
/// A struct containing useful cached information for a [`NetGraph`].
|
||||
#[derive(Clone)]
|
||||
pub struct NetGraphCache {
|
||||
max_range: f64,
|
||||
labels: Vec<String>,
|
||||
time_start: f64,
|
||||
@ -71,9 +73,10 @@ enum NetGraphCacheState {
|
||||
///
|
||||
/// As of now, this is essentially just a wrapper around a [`TimeGraph`].
|
||||
pub struct NetGraph {
|
||||
/// The graph itself. Just a [`TimeGraph`].
|
||||
graph: TimeGraph,
|
||||
|
||||
// Cached details; probably want to move at some point...
|
||||
// Cached details for drawing purposes; probably want to move at some point...
|
||||
draw_cache: NetGraphCacheState,
|
||||
}
|
||||
|
||||
@ -86,6 +89,7 @@ impl NetGraph {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the associated cache on a [`NetGraph`].
|
||||
pub fn set_cache(&mut self, max_range: f64, labels: Vec<String>, time_start: f64) {
|
||||
self.draw_cache = NetGraphCacheState::Cached(NetGraphCache {
|
||||
max_range,
|
||||
@ -94,17 +98,32 @@ impl NetGraph {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether the [`NetGraph`] contains a cache from drawing.
|
||||
pub fn is_cached(&self) -> bool {
|
||||
match self.draw_cache {
|
||||
NetGraphCacheState::Uncached => false,
|
||||
NetGraphCacheState::Cached(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the [`NetGraphCache`] tied to the [`NetGraph`] if there is one.
|
||||
pub fn get_cache(&self) -> Option<&NetGraphCache> {
|
||||
match &self.draw_cache {
|
||||
NetGraphCacheState::Uncached => None,
|
||||
NetGraphCacheState::Cached(cache) => Some(cache),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an owned copy of the [`NetGraphCache`] tied to the [`NetGraph`] if there is one.
|
||||
pub fn get_cache_owned(&self) -> Option<NetGraphCache> {
|
||||
match &self.draw_cache {
|
||||
NetGraphCacheState::Uncached => None,
|
||||
NetGraphCacheState::Cached(cache) => Some(cache.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for NetGraph {
|
||||
type UpdateData = ();
|
||||
|
||||
impl Component for NetGraph {
|
||||
fn bounds(&self) -> Rect {
|
||||
self.graph.bounds()
|
||||
}
|
||||
@ -126,6 +145,8 @@ impl Widget for NetGraph {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for NetGraph {}
|
||||
|
||||
/// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`],
|
||||
/// and the main difference is that it also contains a bounding box for the graph + text.
|
||||
pub struct OldNetGraph {
|
||||
@ -143,9 +164,7 @@ impl OldNetGraph {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for OldNetGraph {
|
||||
type UpdateData = ();
|
||||
|
||||
impl Component for OldNetGraph {
|
||||
fn bounds(&self) -> Rect {
|
||||
self.bounds
|
||||
}
|
||||
@ -166,3 +185,5 @@ impl Widget for OldNetGraph {
|
||||
self.graph.handle_mouse_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for OldNetGraph {}
|
||||
|
@ -1,16 +1,23 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
|
||||
use tui::widgets::TableState;
|
||||
use tui::{layout::Rect, widgets::TableState};
|
||||
|
||||
use crate::{
|
||||
app::query::*,
|
||||
app::{
|
||||
event::{EventResult, MultiKey, MultiKeyResult},
|
||||
query::*,
|
||||
},
|
||||
data_harvester::processes::{self, ProcessSorting},
|
||||
};
|
||||
use ProcessSorting::*;
|
||||
|
||||
use super::{AppScrollWidgetState, CanvasTableWidthState, CursorDirection, ScrollDirection};
|
||||
use super::{
|
||||
does_point_intersect_rect, AppScrollWidgetState, CanvasTableWidthState, Component,
|
||||
CursorDirection, ScrollDirection, TextInput, TextTable, Widget,
|
||||
};
|
||||
|
||||
/// AppSearchState deals with generic searching (I might do this in the future).
|
||||
pub struct AppSearchState {
|
||||
@ -606,3 +613,202 @@ impl ProcState {
|
||||
self.widget_states.get(&widget_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// The currently selected part of a [`ProcessManager`]
|
||||
enum ProcessManagerSelection {
|
||||
Processes,
|
||||
Sort,
|
||||
Search,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// The state of the search modifiers.
|
||||
struct SearchModifiers {
|
||||
enable_case_sensitive: bool,
|
||||
enable_whole_word: bool,
|
||||
enable_regex: bool,
|
||||
}
|
||||
|
||||
/// A searchable, sortable table to manage processes.
|
||||
pub struct ProcessManager {
|
||||
bounds: Rect,
|
||||
process_table: TextTable,
|
||||
sort_table: TextTable,
|
||||
search_input: TextInput,
|
||||
|
||||
dd_multi: MultiKey,
|
||||
|
||||
selected: ProcessManagerSelection,
|
||||
|
||||
in_tree_mode: bool,
|
||||
show_sort: bool,
|
||||
show_search: bool,
|
||||
|
||||
search_modifiers: SearchModifiers,
|
||||
}
|
||||
|
||||
impl ProcessManager {
|
||||
/// Creates a new [`ProcessManager`].
|
||||
pub fn new(default_in_tree_mode: bool) -> Self {
|
||||
Self {
|
||||
bounds: Rect::default(),
|
||||
process_table: TextTable::new(0, vec![]), // TODO: Do this
|
||||
sort_table: TextTable::new(0, vec![]), // TODO: Do this too
|
||||
search_input: TextInput::new(),
|
||||
dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Use a static arrayvec
|
||||
selected: ProcessManagerSelection::Processes,
|
||||
in_tree_mode: default_in_tree_mode,
|
||||
show_sort: false,
|
||||
show_search: false,
|
||||
search_modifiers: SearchModifiers::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_search(&mut self) -> EventResult {
|
||||
if let ProcessManagerSelection::Search = self.selected {
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
self.show_search = true;
|
||||
self.selected = ProcessManagerSelection::Search;
|
||||
EventResult::Redraw
|
||||
}
|
||||
}
|
||||
|
||||
fn open_sort(&mut self) -> EventResult {
|
||||
if let ProcessManagerSelection::Sort = self.selected {
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
self.show_sort = true;
|
||||
self.selected = ProcessManagerSelection::Sort;
|
||||
EventResult::Redraw
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the process manager is searching the current term with the restriction that it must
|
||||
/// match entire word.
|
||||
pub fn is_searching_whole_word(&self) -> bool {
|
||||
self.search_modifiers.enable_whole_word
|
||||
}
|
||||
|
||||
/// Returns whether the process manager is searching the current term using regex.
|
||||
pub fn is_searching_with_regex(&self) -> bool {
|
||||
self.search_modifiers.enable_regex
|
||||
}
|
||||
|
||||
/// Returns whether the process manager is searching the current term with the restriction that case-sensitivity
|
||||
/// matters.
|
||||
pub fn is_case_sensitive(&self) -> bool {
|
||||
self.search_modifiers.enable_case_sensitive
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ProcessManager {
|
||||
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) -> EventResult {
|
||||
match self.selected {
|
||||
ProcessManagerSelection::Processes => {
|
||||
// Try to catch some stuff first...
|
||||
if event.modifiers.is_empty() {
|
||||
match event.code {
|
||||
KeyCode::Tab => {
|
||||
// Handle grouping/ungrouping
|
||||
}
|
||||
KeyCode::Char('P') => {
|
||||
// Show full command/process name
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
match self.dd_multi.input('d') {
|
||||
MultiKeyResult::Completed => {
|
||||
// Kill the selected process(es)
|
||||
}
|
||||
MultiKeyResult::Accepted | MultiKeyResult::Rejected => {
|
||||
return EventResult::NoRedraw;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('/') => {
|
||||
return self.open_search();
|
||||
}
|
||||
KeyCode::Char('%') => {
|
||||
// Handle switching memory usage type
|
||||
}
|
||||
KeyCode::Char('+') => {
|
||||
// Expand a branch
|
||||
}
|
||||
KeyCode::Char('-') => {
|
||||
// Collapse a branch
|
||||
}
|
||||
KeyCode::Char('t') | KeyCode::F(5) => {
|
||||
self.in_tree_mode = !self.in_tree_mode;
|
||||
return EventResult::Redraw;
|
||||
}
|
||||
KeyCode::F(6) => {
|
||||
return self.open_sort();
|
||||
}
|
||||
KeyCode::F(9) => {
|
||||
// Kill the selected process(es)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if let KeyModifiers::CONTROL = event.modifiers {
|
||||
if let KeyCode::Char('f') = event.code {
|
||||
return self.open_search();
|
||||
}
|
||||
} else if let KeyModifiers::SHIFT = event.modifiers {
|
||||
if let KeyCode::Char('P') = event.code {
|
||||
// Show full command/process name
|
||||
}
|
||||
}
|
||||
|
||||
self.process_table.handle_key_event(event)
|
||||
}
|
||||
ProcessManagerSelection::Sort => {
|
||||
if event.modifiers.is_empty() {
|
||||
match event.code {
|
||||
KeyCode::F(1) => {}
|
||||
KeyCode::F(2) => {}
|
||||
KeyCode::F(3) => {}
|
||||
_ => {}
|
||||
}
|
||||
} else if let KeyModifiers::ALT = event.modifiers {
|
||||
match event.code {
|
||||
KeyCode::Char('c') | KeyCode::Char('C') => {}
|
||||
KeyCode::Char('w') | KeyCode::Char('W') => {}
|
||||
KeyCode::Char('r') | KeyCode::Char('R') => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.sort_table.handle_key_event(event)
|
||||
}
|
||||
ProcessManagerSelection::Search => self.search_input.handle_key_event(event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||
let global_x = event.column;
|
||||
let global_y = event.row;
|
||||
|
||||
if does_point_intersect_rect(global_x, global_y, self.process_table.bounds()) {
|
||||
self.selected = ProcessManagerSelection::Processes;
|
||||
self.process_table.handle_mouse_event(event)
|
||||
} else if does_point_intersect_rect(global_x, global_y, self.sort_table.bounds()) {
|
||||
self.selected = ProcessManagerSelection::Sort;
|
||||
self.sort_table.handle_mouse_event(event)
|
||||
} else if does_point_intersect_rect(global_x, global_y, self.search_input.bounds()) {
|
||||
self.selected = ProcessManagerSelection::Search;
|
||||
self.search_input.handle_mouse_event(event)
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ProcessManager {}
|
||||
|
@ -5,10 +5,7 @@ use tui::layout::Rect;
|
||||
|
||||
use crate::app::event::EventResult;
|
||||
|
||||
use super::{
|
||||
text_table::TextTableUpdateData, AppScrollWidgetState, CanvasTableWidthState, TextTable,
|
||||
Widget,
|
||||
};
|
||||
use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, Widget};
|
||||
|
||||
pub struct TempWidgetState {
|
||||
pub scroll_state: AppScrollWidgetState,
|
||||
@ -58,9 +55,7 @@ impl TempTable {
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TempTable {
|
||||
type UpdateData = TextTableUpdateData;
|
||||
|
||||
impl Component for TempTable {
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
self.table.handle_key_event(event)
|
||||
}
|
||||
@ -69,10 +64,6 @@ impl Widget for TempTable {
|
||||
self.table.handle_mouse_event(event)
|
||||
}
|
||||
|
||||
fn update(&mut self, update_data: Self::UpdateData) {
|
||||
self.table.update(update_data);
|
||||
}
|
||||
|
||||
fn bounds(&self) -> Rect {
|
||||
self.bounds
|
||||
}
|
||||
@ -81,3 +72,5 @@ impl Widget for TempTable {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TempTable {}
|
||||
|
@ -140,7 +140,7 @@ fn main() -> Result<()> {
|
||||
force_redraw(&mut app);
|
||||
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
||||
}
|
||||
EventResult::Continue => {}
|
||||
EventResult::NoRedraw => {}
|
||||
}
|
||||
}
|
||||
BottomEvent::MouseInput(event) => match handle_mouse_event(event, &mut app) {
|
||||
@ -152,7 +152,7 @@ fn main() -> Result<()> {
|
||||
force_redraw(&mut app);
|
||||
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
||||
}
|
||||
EventResult::Continue => {}
|
||||
EventResult::NoRedraw => {}
|
||||
},
|
||||
BottomEvent::Update(data) => {
|
||||
app.data_collection.eat_data(data);
|
||||
|
@ -89,7 +89,7 @@ pub fn handle_mouse_event(event: MouseEvent, app: &mut AppState) -> EventResult
|
||||
app.handle_scroll_down();
|
||||
EventResult::Redraw
|
||||
}
|
||||
_ => EventResult::Continue,
|
||||
_ => EventResult::NoRedraw,
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,7 +128,7 @@ pub fn handle_key_event(
|
||||
KeyCode::F(6) => app.toggle_sort(),
|
||||
KeyCode::F(9) => app.start_killing_process(),
|
||||
_ => {
|
||||
return EventResult::Continue;
|
||||
return EventResult::NoRedraw;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -171,7 +171,7 @@ pub fn handle_key_event(
|
||||
// are hard to iter while truncating last (eloquently).
|
||||
// KeyCode::Backspace => app.skip_word_backspace(),
|
||||
_ => {
|
||||
return EventResult::Continue;
|
||||
return EventResult::NoRedraw;
|
||||
}
|
||||
}
|
||||
} else if let KeyModifiers::SHIFT = event.modifiers {
|
||||
@ -182,7 +182,7 @@ pub fn handle_key_event(
|
||||
KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down),
|
||||
KeyCode::Char(caught_char) => app.on_char_key(caught_char),
|
||||
_ => {
|
||||
return EventResult::Continue;
|
||||
return EventResult::NoRedraw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user