tuice tuice

This commit is contained in:
ClementTsang 2021-12-05 19:23:19 -05:00
parent 7029412929
commit f1ec2fd70f
37 changed files with 590 additions and 1344 deletions

View File

@ -1,28 +1,44 @@
pub mod data_farmer;
use std::sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
};
pub use data_farmer::*;
pub mod data_harvester;
use data_harvester::temperature;
pub mod event;
pub mod filter;
pub use filter::*;
pub mod layout_manager;
use layout_manager::*;
pub mod widgets;
pub use widgets::*;
mod process_killer;
pub mod query;
pub mod widgets;
use std::time::Instant;
mod frozen_state;
use frozen_state::FrozenState;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
use crate::{
canvas::Painter,
constants,
tuice::{Application, Row},
units::data_units::DataUnit,
Pid,
};
use anyhow::Result;
use indextree::{Arena, NodeId};
use rustc_hash::FxHashMap;
pub use data_farmer::*;
use data_harvester::temperature;
pub use filter::*;
use layout_manager::*;
pub use widgets::*;
use crate::{constants, units::data_units::DataUnit, utils::error::Result, BottomEvent, Pid};
use self::event::{ComponentEventResult, EventResult, ReturnSignal};
// FIXME: Move this!
#[derive(Debug, Clone)]
pub enum AxisScaling {
Log,
@ -95,54 +111,55 @@ pub struct AppConfigFields {
pub network_use_binary_prefix: bool,
}
/// The [`FrozenState`] indicates whether the application state should be frozen; if it is, save a snapshot of
/// the data collected at that instant.
pub enum FrozenState {
NotFrozen,
Frozen(Box<DataCollection>),
#[derive(PartialEq, Eq)]
enum CurrentScreen {
Main,
Expanded,
Help,
Delete,
}
impl Default for FrozenState {
impl Default for CurrentScreen {
fn default() -> Self {
Self::NotFrozen
Self::Main
}
}
#[derive(Debug)]
pub enum AppMessages {
Update(Box<data_harvester::Data>),
OpenHelp,
KillProcess { to_kill: Vec<Pid> },
ToggleFreeze,
Clean,
Stop,
}
pub struct AppState {
pub dd_err: Option<String>,
to_delete_process_list: Option<(String, Vec<Pid>)>,
pub data_collection: DataCollection,
pub is_expanded: bool,
pub used_widgets: UsedWidgets,
pub filters: DataFilters,
pub app_config_fields: AppConfigFields,
// --- FIXME: TO DELETE/REWRITE ---
pub delete_dialog_state: AppDeleteDialogState,
pub is_force_redraw: bool,
pub is_determining_widget_boundary: bool,
// --- NEW STUFF ---
pub selected_widget: NodeId,
pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>,
pub widget_lookup_map: FxHashMap<NodeId, BottomWidget>,
pub layout_tree: Arena<LayoutNode>,
pub layout_tree_root: NodeId,
frozen_state: FrozenState,
pub help_dialog: DialogState<HelpDialog>,
frozen_state: FrozenState,
current_screen: CurrentScreen,
painter: Painter,
terminator: Arc<AtomicBool>,
}
impl AppState {
/// Creates a new [`AppState`].
pub fn new(
app_config_fields: AppConfigFields, filters: DataFilters,
layout_tree_output: LayoutCreationOutput,
) -> Self {
layout_tree_output: LayoutCreationOutput, painter: Painter,
) -> Result<Self> {
let LayoutCreationOutput {
layout_tree,
root: layout_tree_root,
@ -151,7 +168,7 @@ impl AppState {
used_widgets,
} = layout_tree_output;
Self {
Ok(Self {
app_config_fields,
filters,
used_widgets,
@ -159,546 +176,83 @@ impl AppState {
widget_lookup_map,
layout_tree,
layout_tree_root,
painter,
// Use defaults.
dd_err: Default::default(),
to_delete_process_list: Default::default(),
data_collection: Default::default(),
is_expanded: Default::default(),
delete_dialog_state: Default::default(),
is_force_redraw: Default::default(),
is_determining_widget_boundary: Default::default(),
frozen_state: Default::default(),
help_dialog: Default::default(),
current_screen: Default::default(),
terminator: Self::register_terminator()?,
})
}
fn register_terminator() -> Result<Arc<AtomicBool>> {
let it = Arc::new(AtomicBool::new(false));
let it_clone = it.clone();
ctrlc::set_handler(move || {
it_clone.store(true, SeqCst);
})?;
Ok(it)
}
fn set_current_screen(&mut self, screen_type: CurrentScreen) {
if self.current_screen == screen_type {
self.current_screen = screen_type;
// TODO: Redraw
}
}
}
impl Application for AppState {
type Message = AppMessages;
fn update(&mut self, message: Self::Message) {
match message {
AppMessages::Update(new_data) => {
self.data_collection.eat_data(new_data);
}
AppMessages::OpenHelp => {
self.set_current_screen(CurrentScreen::Help);
}
AppMessages::KillProcess { to_kill } => {}
AppMessages::ToggleFreeze => {
self.frozen_state.toggle(&self.data_collection);
}
AppMessages::Clean => {
self.data_collection
.clean_data(constants::STALE_MAX_MILLISECONDS);
}
AppMessages::Stop => {
self.terminator.store(true, SeqCst);
}
}
}
fn is_terminated(&self) -> bool {
self.terminator.load(SeqCst)
}
fn view(
&mut self,
) -> Box<dyn crate::tuice::Component<Self::Message, crate::tuice::CrosstermBackend>> {
Box::new(Row::with_children(vec![crate::tuice::TextTable::new(
vec!["A", "B", "C"],
)]))
}
fn destroy(&mut self) {
// TODO: Eventually move some thread logic into the app creation, and destroy here?
}
fn global_event_handler(
&mut self, event: crate::tuice::Event, _messages: &mut Vec<Self::Message>,
) {
use crate::tuice::Event;
match event {
Event::Keyboard(_) => {}
Event::Mouse(_) => {}
}
}
pub fn is_frozen(&self) -> bool {
matches!(self.frozen_state, FrozenState::Frozen(_))
}
pub fn toggle_freeze(&mut self) {
if matches!(self.frozen_state, FrozenState::Frozen(_)) {
self.frozen_state = FrozenState::NotFrozen;
} else {
self.frozen_state = FrozenState::Frozen(Box::new(self.data_collection.clone()));
}
}
pub fn reset(&mut self) {
// Call reset on all widgets.
self.widget_lookup_map
.values_mut()
.for_each(|widget| widget.reset());
// Unfreeze.
self.frozen_state = FrozenState::NotFrozen;
// Reset data
self.data_collection.reset();
}
pub fn should_get_widget_bounds(&self) -> bool {
self.is_force_redraw || self.is_determining_widget_boundary
}
fn close_dd(&mut self) {
self.delete_dialog_state.is_showing_dd = false;
self.delete_dialog_state.selected_signal = KillSignal::default();
self.delete_dialog_state.scroll_pos = 0;
self.to_delete_process_list = None;
self.dd_err = None;
}
/// Handles a global event involving a char.
fn handle_global_char(&mut self, c: char) -> EventResult {
if c.is_ascii_control() {
EventResult::NoRedraw
} else {
// Check for case-sensitive bindings first.
match c {
'H' | 'A' => self.move_to_widget(MovementDirection::Left),
'L' | 'D' => self.move_to_widget(MovementDirection::Right),
'K' | 'W' => self.move_to_widget(MovementDirection::Up),
'J' | 'S' => self.move_to_widget(MovementDirection::Down),
_ => {
let c = c.to_ascii_lowercase();
match c {
'q' => EventResult::Quit,
'e' if !self.help_dialog.is_showing() => {
if self.app_config_fields.use_basic_mode {
EventResult::NoRedraw
} else {
self.is_expanded = !self.is_expanded;
EventResult::Redraw
}
}
'?' if !self.help_dialog.is_showing() => {
self.help_dialog.show();
EventResult::Redraw
}
'f' if !self.help_dialog.is_showing() => {
self.toggle_freeze();
if !self.is_frozen() {
let data_collection = &self.data_collection;
self.widget_lookup_map
.iter_mut()
.for_each(|(_id, widget)| widget.update_data(data_collection));
}
EventResult::Redraw
}
_ => EventResult::NoRedraw,
}
}
}
}
}
/// Moves to a widget.
fn move_to_widget(&mut self, direction: MovementDirection) -> EventResult {
match if self.is_expanded {
move_expanded_widget_selection(
&mut self.widget_lookup_map,
self.selected_widget,
direction,
)
} else {
let layout_tree = &mut self.layout_tree;
move_widget_selection(
layout_tree,
&mut self.widget_lookup_map,
self.selected_widget,
direction,
)
} {
MoveWidgetResult::ForceRedraw(new_widget_id) => {
self.selected_widget = new_widget_id;
EventResult::Redraw
}
MoveWidgetResult::NodeId(new_widget_id) => {
let previous_selected = self.selected_widget;
self.selected_widget = new_widget_id;
if previous_selected != self.selected_widget {
EventResult::Redraw
} else {
EventResult::NoRedraw
}
}
}
}
/// 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_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.is_showing() {
self.help_dialog.hide();
EventResult::Redraw
} else if self.delete_dialog_state.is_showing_dd {
self.close_dd();
EventResult::Redraw
} else {
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 {
KeyCode::Char('c') | KeyCode::Char('C') => EventResult::Quit,
KeyCode::Char('r') | KeyCode::Char('R') => {
self.reset();
let data_collection = &self.data_collection;
self.widget_lookup_map
.iter_mut()
.for_each(|(_id, widget)| widget.update_data(data_collection));
EventResult::Redraw
}
KeyCode::Left => self.move_to_widget(MovementDirection::Left),
KeyCode::Right => self.move_to_widget(MovementDirection::Right),
KeyCode::Up => self.move_to_widget(MovementDirection::Up),
KeyCode::Down => self.move_to_widget(MovementDirection::Down),
_ => EventResult::NoRedraw,
}
} else if let KeyModifiers::SHIFT = event.modifiers {
match event.code {
KeyCode::Left => self.move_to_widget(MovementDirection::Left),
KeyCode::Right => self.move_to_widget(MovementDirection::Right),
KeyCode::Up => self.move_to_widget(MovementDirection::Up),
KeyCode::Down => self.move_to_widget(MovementDirection::Down),
KeyCode::Char(c) => self.handle_global_char(c),
_ => EventResult::NoRedraw,
}
} else {
EventResult::NoRedraw
}
}
/// 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);
match widget.selectable_type() {
SelectableType::Selectable => {
let was_id_already_selected = self.selected_widget == *id;
self.selected_widget = *id;
if was_id_already_selected {
returned_result = 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);
returned_result = EventResult::Redraw;
}
break;
}
SelectableType::Unselectable => {
let result = widget.handle_mouse_event(event);
return self.convert_widget_event_result(result);
}
}
}
}
returned_result
}
}
/// Handles a [`BottomEvent`] and updates the [`AppState`] if needed. Returns an [`EventResult`] indicating
/// whether the app now requires a redraw.
pub fn handle_event(&mut self, event: BottomEvent) -> EventResult {
match 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.
self.handle_mouse_event(event)
}
BottomEvent::Update(new_data) => {
self.data_collection.eat_data(new_data);
// TODO: [Optimization] Optimization for dialogs - don't redraw on an update!
if !self.is_frozen() {
let data_collection = &self.data_collection;
self.widget_lookup_map
.iter_mut()
.for_each(|(_id, widget)| widget.update_data(data_collection));
EventResult::Redraw
} else {
EventResult::NoRedraw
}
}
BottomEvent::Resize {
width: _,
height: _,
} => EventResult::Redraw,
BottomEvent::Clean => {
self.data_collection
.clean_data(constants::STALE_MAX_MILLISECONDS);
EventResult::NoRedraw
}
}
}
#[cfg(target_family = "unix")]
fn on_number(&mut self, number_char: char) {
if self.delete_dialog_state.is_showing_dd {
if self
.delete_dialog_state
.last_number_press
.map_or(100, |ins| ins.elapsed().as_millis())
>= 400
{
self.delete_dialog_state.keyboard_signal_select = 0;
}
let mut kbd_signal = self.delete_dialog_state.keyboard_signal_select * 10;
kbd_signal += number_char.to_digit(10).unwrap() as usize;
if kbd_signal > 64 {
kbd_signal %= 100;
}
#[cfg(target_os = "linux")]
if kbd_signal > 64 || kbd_signal == 32 || kbd_signal == 33 {
kbd_signal %= 10;
}
#[cfg(target_os = "macos")]
if kbd_signal > 31 {
kbd_signal %= 10;
}
self.delete_dialog_state.selected_signal = KillSignal::Kill(kbd_signal);
if kbd_signal < 10 {
self.delete_dialog_state.keyboard_signal_select = kbd_signal;
} else {
self.delete_dialog_state.keyboard_signal_select = 0;
}
self.delete_dialog_state.last_number_press = Some(Instant::now());
}
}
fn on_left_key(&mut self) {
// if !self.is_in_dialog() {
// match self.current_widget.widget_type {
// BottomWidgetType::ProcSearch => {
// let is_in_search_widget = self.is_in_search_widget();
// if let Some(proc_widget_state) = self
// .proc_state
// .get_mut_widget_state(self.current_widget.widget_id - 1)
// {
// if is_in_search_widget {
// let prev_cursor = proc_widget_state.get_search_cursor_position();
// proc_widget_state
// .search_walk_back(proc_widget_state.get_search_cursor_position());
// if proc_widget_state.get_search_cursor_position() < prev_cursor {
// let str_slice = &proc_widget_state
// .process_search_state
// .search_state
// .current_search_query
// [proc_widget_state.get_search_cursor_position()..prev_cursor];
// proc_widget_state
// .process_search_state
// .search_state
// .char_cursor_position -= UnicodeWidthStr::width(str_slice);
// proc_widget_state
// .process_search_state
// .search_state
// .cursor_direction = CursorDirection::Left;
// }
// }
// }
// }
// _ => {}
// }
// } else if self.delete_dialog_state.is_showing_dd {
// #[cfg(target_family = "unix")]
// {
// if self.app_config_fields.is_advanced_kill {
// match self.delete_dialog_state.selected_signal {
// KillSignal::Kill(prev_signal) => {
// self.delete_dialog_state.selected_signal = match prev_signal - 1 {
// 0 => KillSignal::Cancel,
// // 32+33 are skipped
// 33 => KillSignal::Kill(31),
// signal => KillSignal::Kill(signal),
// };
// }
// KillSignal::Cancel => {}
// };
// } else {
// self.delete_dialog_state.selected_signal = KillSignal::default();
// }
// }
// #[cfg(target_os = "windows")]
// {
// self.delete_dialog_state.selected_signal = KillSignal::Kill(1);
// }
// }
}
fn on_right_key(&mut self) {
// if !self.is_in_dialog() {
// match self.current_widget.widget_type {
// BottomWidgetType::ProcSearch => {
// let is_in_search_widget = self.is_in_search_widget();
// if let Some(proc_widget_state) = self
// .proc_state
// .get_mut_widget_state(self.current_widget.widget_id - 1)
// {
// if is_in_search_widget {
// let prev_cursor = proc_widget_state.get_search_cursor_position();
// proc_widget_state.search_walk_forward(
// proc_widget_state.get_search_cursor_position(),
// );
// if proc_widget_state.get_search_cursor_position() > prev_cursor {
// let str_slice = &proc_widget_state
// .process_search_state
// .search_state
// .current_search_query
// [prev_cursor..proc_widget_state.get_search_cursor_position()];
// proc_widget_state
// .process_search_state
// .search_state
// .char_cursor_position += UnicodeWidthStr::width(str_slice);
// proc_widget_state
// .process_search_state
// .search_state
// .cursor_direction = CursorDirection::Right;
// }
// }
// }
// }
// _ => {}
// }
// } else if self.delete_dialog_state.is_showing_dd {
// #[cfg(target_family = "unix")]
// {
// if self.app_config_fields.is_advanced_kill {
// let new_signal = match self.delete_dialog_state.selected_signal {
// KillSignal::Cancel => 1,
// // 32+33 are skipped
// #[cfg(target_os = "linux")]
// KillSignal::Kill(31) => 34,
// #[cfg(target_os = "macos")]
// KillSignal::Kill(31) => 31,
// KillSignal::Kill(64) => 64,
// KillSignal::Kill(signal) => signal + 1,
// };
// self.delete_dialog_state.selected_signal = KillSignal::Kill(new_signal);
// } else {
// self.delete_dialog_state.selected_signal = KillSignal::Cancel;
// }
// }
// #[cfg(target_os = "windows")]
// {
// self.delete_dialog_state.selected_signal = KillSignal::Cancel;
// }
// }
}
fn start_killing_process(&mut self) {
todo!()
// if let Some(proc_widget_state) = self
// .proc_state
// .widget_states
// .get(&self.current_widget.widget_id)
// {
// if let Some(corresponding_filtered_process_list) = self
// .canvas_data
// .finalized_process_data_map
// .get(&self.current_widget.widget_id)
// {
// if proc_widget_state.scroll_state.current_scroll_position
// < corresponding_filtered_process_list.len()
// {
// let current_process: (String, Vec<Pid>);
// if self.is_grouped(self.current_widget.widget_id) {
// if let Some(process) = &corresponding_filtered_process_list
// .get(proc_widget_state.scroll_state.current_scroll_position)
// {
// current_process = (process.name.to_string(), process.group_pids.clone())
// } else {
// return;
// }
// } else {
// let process = corresponding_filtered_process_list
// [proc_widget_state.scroll_state.current_scroll_position]
// .clone();
// current_process = (process.name.clone(), vec![process.pid])
// };
// self.to_delete_process_list = Some(current_process);
// self.delete_dialog_state.is_showing_dd = true;
// self.is_determining_widget_boundary = true;
// }
// }
// }
}
fn kill_highlighted_process(&mut self) -> Result<()> {
// if let BottomWidgetType::Proc = self.current_widget.widget_type {
// if let Some(current_selected_processes) = &self.to_delete_process_list {
// #[cfg(target_family = "unix")]
// let signal = match self.delete_dialog_state.selected_signal {
// KillSignal::Kill(sig) => sig,
// KillSignal::Cancel => 15, // should never happen, so just TERM
// };
// for pid in &current_selected_processes.1 {
// #[cfg(target_family = "unix")]
// {
// process_killer::kill_process_given_pid(*pid, signal)?;
// }
// #[cfg(target_os = "windows")]
// {
// process_killer::kill_process_given_pid(*pid)?;
// }
// }
// }
// self.to_delete_process_list = None;
// Ok(())
// } else {
// Err(BottomError::GenericError(
// "Cannot kill processes if the current widget is not the Process widget!"
// .to_string(),
// ))
// }
Ok(())
}
pub fn get_to_delete_processes(&self) -> Option<(String, Vec<Pid>)> {
// self.to_delete_process_list.clone()
todo!()
}
}

44
src/app/frozen_state.rs Normal file
View File

@ -0,0 +1,44 @@
use super::DataCollection;
/// The [`FrozenState`] indicates whether the application state should be frozen. It is either not frozen or
/// frozen and containing a copy of the state at the time.
pub enum FrozenState {
NotFrozen,
Frozen(Box<DataCollection>),
}
impl Default for FrozenState {
fn default() -> Self {
Self::NotFrozen
}
}
pub type IsFrozen = bool;
impl FrozenState {
/// Checks whether the [`FrozenState`] is currently frozen.
pub fn is_frozen(&self) -> IsFrozen {
matches!(self, FrozenState::Frozen(_))
}
/// Freezes the [`FrozenState`].
pub fn freeze(&mut self, data: Box<DataCollection>) {
*self = FrozenState::Frozen(data);
}
/// Unfreezes the [`FrozenState`].
pub fn thaw(&mut self) {
*self = FrozenState::NotFrozen;
}
/// Toggles the [`FrozenState`] and returns whether it is now frozen.
pub fn toggle(&mut self, data: &DataCollection) -> IsFrozen {
if self.is_frozen() {
self.thaw();
false
} else {
self.freeze(Box::new(data.clone()));
true
}
}
}

View File

@ -17,7 +17,7 @@ use tui::layout::Rect;
use crate::app::widgets::Widget;
use super::{
event::SelectionAction, AppConfigFields, CpuGraph, TimeGraph, TmpBottomWidget, UsedWidgets,
event::SelectionAction, AppConfigFields, BottomWidget, CpuGraph, TimeGraph, UsedWidgets,
};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
@ -197,7 +197,7 @@ pub enum MovementDirection {
pub struct LayoutCreationOutput {
pub layout_tree: Arena<LayoutNode>,
pub root: NodeId,
pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>,
pub widget_lookup_map: FxHashMap<NodeId, BottomWidget>,
pub selected: NodeId,
pub used_widgets: UsedWidgets,
}
@ -210,7 +210,7 @@ pub fn create_layout_tree(
rows: &[Row], process_defaults: ProcessDefaults, app_config_fields: &AppConfigFields,
) -> Result<LayoutCreationOutput> {
fn add_widget_to_map(
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: BottomWidgetType,
widget_lookup_map: &mut FxHashMap<NodeId, BottomWidget>, widget_type: BottomWidgetType,
widget_id: NodeId, process_defaults: &ProcessDefaults, app_config_fields: &AppConfigFields,
width: LayoutRule, height: LayoutRule,
) -> Result<()> {
@ -518,7 +518,7 @@ pub enum MoveWidgetResult {
/// A more restricted movement, only within a single widget.
pub fn move_expanded_widget_selection(
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, current_widget_id: NodeId,
widget_lookup_map: &mut FxHashMap<NodeId, BottomWidget>, current_widget_id: NodeId,
direction: MovementDirection,
) -> MoveWidgetResult {
if let Some(current_widget) = widget_lookup_map.get_mut(&current_widget_id) {
@ -542,9 +542,8 @@ pub fn move_expanded_widget_selection(
/// - Only [`LayoutNode::Widget`]s are leaves.
/// - Only [`LayoutNode::Row`]s or [`LayoutNode::Col`]s are non-leaves.
pub fn move_widget_selection(
layout_tree: &mut Arena<LayoutNode>,
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, current_widget_id: NodeId,
direction: MovementDirection,
layout_tree: &mut Arena<LayoutNode>, widget_lookup_map: &mut FxHashMap<NodeId, BottomWidget>,
current_widget_id: NodeId, direction: MovementDirection,
) -> MoveWidgetResult {
// We first give our currently-selected widget a chance to react to the movement - it may handle it internally!
let handled = {
@ -832,7 +831,7 @@ pub fn move_widget_selection(
/// - [How Flutter does sublinear layout](https://flutter.dev/docs/resources/inside-flutter#sublinear-layout)
pub fn generate_layout(
root: NodeId, arena: &mut Arena<LayoutNode>, area: Rect,
lookup_map: &FxHashMap<NodeId, TmpBottomWidget>,
lookup_map: &FxHashMap<NodeId, BottomWidget>,
) {
// TODO: [Optimization, Layout] Add some caching/dirty mechanisms to reduce calls.
@ -887,8 +886,8 @@ pub fn generate_layout(
/// The internal recursive call to build a layout. Builds off of `arena` and stores bounds inside it.
fn layout(
node: NodeId, arena: &mut Arena<LayoutNode>,
lookup_map: &FxHashMap<NodeId, TmpBottomWidget>, mut constraints: LayoutConstraints,
node: NodeId, arena: &mut Arena<LayoutNode>, lookup_map: &FxHashMap<NodeId, BottomWidget>,
mut constraints: LayoutConstraints,
) -> Size {
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
match layout_node {

View File

@ -156,7 +156,7 @@ pub enum SelectableType {
/// The "main" widgets that are used by bottom to display information!
#[allow(clippy::large_enum_variant)]
#[enum_dispatch(Component, Widget)]
pub enum TmpBottomWidget {
pub enum BottomWidget {
MemGraph,
TempTable,
DiskTable,
@ -172,7 +172,7 @@ pub enum TmpBottomWidget {
Empty,
}
impl Debug for TmpBottomWidget {
impl Debug for BottomWidget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MemGraph(_) => write!(f, "MemGraph"),
@ -192,41 +192,6 @@ impl Debug for TmpBottomWidget {
}
}
/// 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());
}
}
// ----- FIXME: Delete the old stuff below -----
#[derive(PartialEq)]

View File

@ -4,16 +4,13 @@
#[macro_use]
extern crate log;
use bottom::{app::event::EventResult, canvas, options::*, *};
use bottom::{app::AppMessages, options::*, tuice::RuntimeEvent, *};
use std::{
boxed::Box,
io::stdout,
panic,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc, Condvar, Mutex,
},
sync::{mpsc, Arc, Condvar, Mutex},
thread,
time::Duration,
};
@ -39,10 +36,7 @@ fn main() -> Result<()> {
.context("Unable to properly parse or create the config file.")?;
// Create "app" struct, which will control most of the program and store settings/state
let mut app = build_app(&matches, &mut config)?;
// Create painter and set colours.
let mut painter = canvas::Painter::init(&config, get_color_scheme(&matches, &config)?)?;
let app = build_app(&matches, &mut config)?;
// Create termination mutex and cvar
#[allow(clippy::mutex_atomic)]
@ -71,7 +65,10 @@ fn main() -> Result<()> {
break;
}
}
if cleaning_sender.send(BottomEvent::Clean).is_err() {
if cleaning_sender
.send(RuntimeEvent::Custom(AppMessages::Clean))
.is_err()
{
// debug!("Failed to send cleaning sender...");
break;
}
@ -105,33 +102,7 @@ fn main() -> Result<()> {
// TODO: [Threads, Panic] Make this close all the child threads too!
panic::set_hook(Box::new(|info| panic_hook(info)));
// Set termination hook
let is_terminated = Arc::new(AtomicBool::new(false));
let ist_clone = is_terminated.clone();
ctrlc::set_handler(move || {
ist_clone.store(true, Ordering::SeqCst);
})?;
// Paint once first.
try_drawing(&mut terminal, &mut app, &mut painter)?;
while !is_terminated.load(Ordering::SeqCst) {
if let Ok(recv) = receiver.recv() {
match app.handle_event(recv) {
EventResult::Quit => {
break;
}
EventResult::Redraw => {
try_drawing(&mut terminal, &mut app, &mut painter)?;
}
EventResult::NoRedraw => {
continue;
}
}
} else {
break;
}
}
tuice::launch_with_application(app, receiver);
// I think doing it in this order is safe...
*thread_termination_lock.lock().unwrap() = true;

View File

@ -4,21 +4,20 @@ use indextree::{Arena, NodeId};
use rustc_hash::FxHashMap;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
layout::{Constraint, Layout, Rect},
text::Span,
widgets::Paragraph,
Frame, Terminal,
};
use canvas_colours::*;
use dialogs::*;
use crate::{
app::{
self,
layout_manager::{generate_layout, ColLayout, LayoutNode, RowLayout},
widgets::{Component, Widget},
DialogState, TmpBottomWidget,
BottomWidget,
},
constants::*,
options::Config,
@ -27,7 +26,6 @@ use crate::{
};
mod canvas_colours;
mod dialogs;
#[derive(Debug)]
pub enum ColourScheme {
@ -138,7 +136,7 @@ impl Painter {
&mut self, terminal: &mut Terminal<B>, app_state: &mut app::AppState,
) -> error::Result<()> {
terminal.draw(|mut f| {
let (draw_area, frozen_draw_loc) = if app_state.is_frozen() {
let (draw_area, frozen_draw_loc) = if false {
let split_loc = Layout::default()
.constraints([Constraint::Min(0), Constraint::Length(1)])
.split(f.size());
@ -149,118 +147,7 @@ impl Painter {
let terminal_height = draw_area.height;
let terminal_width = draw_area.width;
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()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(border_len),
Constraint::Length(gen_help_len),
Constraint::Length(border_len),
])
.split(draw_area);
let middle_dialog_chunk = Layout::default()
.direction(Direction::Horizontal)
.constraints(if terminal_width < 100 {
// TODO: [Drawing, Hard-coded] The point we start changing size at currently hard-coded in.
[
Constraint::Percentage(0),
Constraint::Percentage(100),
Constraint::Percentage(0),
]
} else {
[
Constraint::Percentage(20),
Constraint::Percentage(60),
Constraint::Percentage(20),
]
})
.split(vertical_dialog_chunk[1]);
help_dialog.draw_help(self, f, middle_dialog_chunk[1]);
} else if app_state.delete_dialog_state.is_showing_dd {
// TODO: [Drawing] Better dd sizing 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
// line-wrapping is NOT the same as taking the width of the text and dividing by width.
// So, I need the height AFTER wrapping.
// See: https://github.com/fdehau/tui-rs/pull/349. Land this after this pushes to release.
//
// ADDENDUM: I could probably use the same textwrap trick I did with the help menu for this.
let dd_text = self.get_dd_spans(app_state);
let text_width = if terminal_width < 100 {
terminal_width * 90 / 100
} else {
terminal_width * 50 / 100
};
let text_height = if cfg!(target_os = "windows")
|| !app_state.app_config_fields.is_advanced_kill
{
7
} else {
22
};
// let (text_width, text_height) = if let Some(dd_text) = &dd_text {
// let width = if current_width < 100 {
// current_width * 90 / 100
// } else {
// let min_possible_width = (current_width * 50 / 100) as usize;
// let mut width = dd_text.width();
// // This should theoretically never allow width to be 0... we can be safe and do an extra check though.
// while width > (current_width as usize) && width / 2 > min_possible_width {
// width /= 2;
// }
// std::cmp::max(width, min_possible_width) as u16
// };
// (
// width,
// (dd_text.height() + 2 + (dd_text.width() / width as usize)) as u16,
// )
// } else {
// // AFAIK this shouldn't happen, unless something went wrong...
// (
// if current_width < 100 {
// current_width * 90 / 100
// } else {
// current_width * 50 / 100
// },
// 7,
// )
// };
let vertical_bordering = terminal_height.saturating_sub(text_height) / 2;
let vertical_dialog_chunk = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(vertical_bordering),
Constraint::Length(text_height),
Constraint::Length(vertical_bordering),
])
.split(draw_area);
let horizontal_bordering = terminal_width.saturating_sub(text_width) / 2;
let middle_dialog_chunk = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(horizontal_bordering),
Constraint::Length(text_width),
Constraint::Length(horizontal_bordering),
])
.split(vertical_dialog_chunk[1]);
// This is a bit nasty, but it works well... I guess.
app_state.delete_dialog_state.is_showing_dd =
self.draw_dd_dialog(&mut f, dd_text, app_state, middle_dialog_chunk[1]);
} else if app_state.is_expanded {
if false {
if let Some(frozen_draw_loc) = frozen_draw_loc {
self.draw_frozen_indicator(&mut f, frozen_draw_loc);
}
@ -276,7 +163,7 @@ impl Painter {
/// A simple traversal through the `arena`, drawing all leaf elements.
fn traverse_and_draw_tree<B: Backend>(
node: NodeId, arena: &Arena<LayoutNode>, f: &mut Frame<'_, B>,
lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, painter: &Painter,
lookup_map: &mut FxHashMap<NodeId, BottomWidget>, painter: &Painter,
selected_id: NodeId, offset_x: u16, offset_y: u16,
) {
if let Some(layout_node) = arena.get(node).map(|n| n.get()) {
@ -306,7 +193,7 @@ impl Painter {
);
if let Some(widget) = lookup_map.get_mut(&node) {
if let TmpBottomWidget::Carousel(carousel) = widget {
if let BottomWidget::Carousel(carousel) = widget {
let remaining_area: Rect =
carousel.draw_carousel(painter, f, area);
if let Some(to_draw_node) =

View File

@ -1,2 +0,0 @@
pub mod dd_dialog;
pub use dd_dialog::KillDialog;

View File

@ -1,409 +0,0 @@
#[cfg(target_family = "unix")]
use std::cmp::min;
use tui::{
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
terminal::Frame,
text::{Span, Spans, Text},
widgets::{Block, Borders, Paragraph, Wrap},
};
use crate::{
app::{AppState, KillSignal},
canvas::Painter,
};
const DD_BASE: &str = " Confirm Kill Process ── Esc to close ";
const DD_ERROR_BASE: &str = " Error ── Esc to close ";
pub trait KillDialog {
fn get_dd_spans(&self, app_state: &AppState) -> Option<Text<'_>>;
fn draw_dd_confirm_buttons<B: Backend>(
&self, f: &mut Frame<'_, B>, button_draw_loc: &Rect, app_state: &mut AppState,
);
fn draw_dd_dialog<B: Backend>(
&self, f: &mut Frame<'_, B>, dd_text: Option<Text<'_>>, app_state: &mut AppState,
draw_loc: Rect,
) -> bool;
}
impl KillDialog for Painter {
fn get_dd_spans(&self, app_state: &AppState) -> Option<Text<'_>> {
if let Some(dd_err) = &app_state.dd_err {
return Some(Text::from(vec![
Spans::default(),
Spans::from("Failed to kill process."),
Spans::from(dd_err.clone()),
Spans::from("Please press ENTER or ESC to close this dialog."),
]));
} else if let Some(_to_kill_processes) = app_state.get_to_delete_processes() {
// if let Some(first_pid) = to_kill_processes.1.first() {
// return Some(Text::from(vec![
// Spans::from(""),
// if app_state.is_grouped(app_state.current_widget.widget_id) {
// if to_kill_processes.1.len() != 1 {
// Spans::from(format!(
// "Kill {} processes with the name \"{}\"? Press ENTER to confirm.",
// to_kill_processes.1.len(),
// to_kill_processes.0
// ))
// } else {
// Spans::from(format!(
// "Kill 1 process with the name \"{}\"? Press ENTER to confirm.",
// to_kill_processes.0
// ))
// }
// } else {
// Spans::from(format!(
// "Kill process \"{}\" with PID {}? Press ENTER to confirm.",
// to_kill_processes.0, first_pid
// ))
// },
// ]));
// }
}
None
}
fn draw_dd_confirm_buttons<B: Backend>(
&self, f: &mut Frame<'_, B>, button_draw_loc: &Rect, app_state: &mut AppState,
) {
if cfg!(target_os = "windows") || !app_state.app_config_fields.is_advanced_kill {
let (yes_button, no_button) = match app_state.delete_dialog_state.selected_signal {
KillSignal::Kill(_) => (
Span::styled("Yes", self.colours.currently_selected_text_style),
Span::raw("No"),
),
KillSignal::Cancel => (
Span::raw("Yes"),
Span::styled("No", self.colours.currently_selected_text_style),
),
};
let button_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage(35),
Constraint::Percentage(30),
Constraint::Percentage(35),
]
.as_ref(),
)
.split(*button_draw_loc);
f.render_widget(
Paragraph::new(yes_button)
.block(Block::default())
.alignment(Alignment::Right),
button_layout[0],
);
f.render_widget(
Paragraph::new(no_button)
.block(Block::default())
.alignment(Alignment::Left),
button_layout[2],
);
if app_state.should_get_widget_bounds() {
// This is kinda weird, but the gist is:
// - We have three sections; we put our mouse bounding box for the "yes" button at the very right edge
// of the left section and 3 characters back. We then give it a buffer size of 1 on the x-coordinate.
// - Same for the "no" button, except it is the right section and we do it from the start of the right
// section.
//
// Lastly, note that mouse detection for the dd buttons assume correct widths. As such, we correct
// them here and check with >= and <= mouse bound checks, as opposed to how we do it elsewhere with
// >= and <. See https://github.com/ClementTsang/bottom/pull/459 for details.
app_state.delete_dialog_state.button_positions = vec![
// Yes
(
button_layout[0].x + button_layout[0].width - 4,
button_layout[0].y,
button_layout[0].x + button_layout[0].width,
button_layout[0].y,
if cfg!(target_os = "windows") { 1 } else { 15 },
),
// No
(
button_layout[2].x - 1,
button_layout[2].y,
button_layout[2].x + 2,
button_layout[2].y,
0,
),
];
}
} else {
#[cfg(target_family = "unix")]
{
// TODO: [Optimization, Const] Can probably make this const.
let signal_text;
#[cfg(target_os = "linux")]
{
signal_text = vec![
"0: Cancel",
"1: HUP",
"2: INT",
"3: QUIT",
"4: ILL",
"5: TRAP",
"6: ABRT",
"7: BUS",
"8: FPE",
"9: KILL",
"10: USR1",
"11: SEGV",
"12: USR2",
"13: PIPE",
"14: ALRM",
"15: TERM",
"16: STKFLT",
"17: CHLD",
"18: CONT",
"19: STOP",
"20: TSTP",
"21: TTIN",
"22: TTOU",
"23: URG",
"24: XCPU",
"25: XFSZ",
"26: VTALRM",
"27: PROF",
"28: WINCH",
"29: IO",
"30: PWR",
"31: SYS",
"34: RTMIN",
"35: RTMIN+1",
"36: RTMIN+2",
"37: RTMIN+3",
"38: RTMIN+4",
"39: RTMIN+5",
"40: RTMIN+6",
"41: RTMIN+7",
"42: RTMIN+8",
"43: RTMIN+9",
"44: RTMIN+10",
"45: RTMIN+11",
"46: RTMIN+12",
"47: RTMIN+13",
"48: RTMIN+14",
"49: RTMIN+15",
"50: RTMAX-14",
"51: RTMAX-13",
"52: RTMAX-12",
"53: RTMAX-11",
"54: RTMAX-10",
"55: RTMAX-9",
"56: RTMAX-8",
"57: RTMAX-7",
"58: RTMAX-6",
"59: RTMAX-5",
"60: RTMAX-4",
"61: RTMAX-3",
"62: RTMAX-2",
"63: RTMAX-1",
"64: RTMAX",
];
}
#[cfg(target_os = "macos")]
{
signal_text = vec![
"0: Cancel",
"1: HUP",
"2: INT",
"3: QUIT",
"4: ILL",
"5: TRAP",
"6: ABRT",
"7: EMT",
"8: FPE",
"9: KILL",
"10: BUS",
"11: SEGV",
"12: SYS",
"13: PIPE",
"14: ALRM",
"15: TERM",
"16: URG",
"17: STOP",
"18: TSTP",
"19: CONT",
"20: CHLD",
"21: TTIN",
"22: TTOU",
"23: IO",
"24: XCPU",
"25: XFSZ",
"26: VTALRM",
"27: PROF",
"28: WINCH",
"29: INFO",
"30: USR1",
"31: USR2",
];
}
let button_rect = Layout::default()
.direction(Direction::Horizontal)
.margin(1)
.constraints(
[
Constraint::Length((button_draw_loc.width - 14) / 2),
Constraint::Min(0),
Constraint::Length((button_draw_loc.width - 14) / 2),
]
.as_ref(),
)
.split(*button_draw_loc)[1];
let mut selected = match app_state.delete_dialog_state.selected_signal {
KillSignal::Cancel => 0,
KillSignal::Kill(signal) => signal,
};
// 32+33 are skipped
if selected > 31 {
selected -= 2;
}
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Min(1); button_rect.height as usize])
.split(button_rect);
let prev_offset: usize = app_state.delete_dialog_state.scroll_pos;
app_state.delete_dialog_state.scroll_pos = if selected == 0 {
0
} else if selected < prev_offset + 1 {
selected - 1
} else if selected > prev_offset + (layout.len() as usize) - 1 {
selected - (layout.len() as usize) + 1
} else {
prev_offset
};
let scroll_offset: usize = app_state.delete_dialog_state.scroll_pos;
let mut buttons = signal_text[scroll_offset + 1
..min((layout.len() as usize) + scroll_offset, signal_text.len())]
.iter()
.map(|text| Span::raw(*text))
.collect::<Vec<Span<'_>>>();
buttons.insert(0, Span::raw(signal_text[0]));
buttons[selected - scroll_offset] = Span::styled(
signal_text[selected],
self.colours.currently_selected_text_style,
);
app_state.delete_dialog_state.button_positions = layout
.iter()
.enumerate()
.map(|(i, pos)| {
(
pos.x,
pos.y,
pos.x + pos.width - 1,
pos.y + pos.height - 1,
if i == 0 { 0 } else { scroll_offset } + i,
)
})
.collect::<Vec<(u16, u16, u16, u16, usize)>>();
for (btn, pos) in buttons.into_iter().zip(layout.into_iter()) {
f.render_widget(Paragraph::new(btn).alignment(Alignment::Left), pos);
}
}
}
}
fn draw_dd_dialog<B: Backend>(
&self, f: &mut Frame<'_, B>, dd_text: Option<Text<'_>>, app_state: &mut AppState,
draw_loc: Rect,
) -> bool {
if let Some(dd_text) = dd_text {
let dd_title = if app_state.dd_err.is_some() {
Spans::from(vec![
Span::styled(" Error ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to close ",
"".repeat(
usize::from(draw_loc.width)
.saturating_sub(DD_ERROR_BASE.chars().count() + 2)
)
),
self.colours.border_style,
),
])
} else {
Spans::from(vec![
Span::styled(" Confirm Kill Process ", self.colours.widget_title_style),
Span::styled(
format!(
"─{}─ Esc to close ",
"".repeat(
usize::from(draw_loc.width)
.saturating_sub(DD_BASE.chars().count() + 2)
)
),
self.colours.border_style,
),
])
};
f.render_widget(
Paragraph::new(dd_text)
.block(
Block::default()
.title(dd_title)
.style(self.colours.border_style)
.borders(Borders::ALL)
.border_style(self.colours.border_style),
)
.style(self.colours.text_style)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true }),
draw_loc,
);
let btn_height =
if cfg!(target_os = "windows") || !app_state.app_config_fields.is_advanced_kill {
3
} else {
20
};
// Now draw buttons if needed...
let split_draw_loc = Layout::default()
.direction(Direction::Vertical)
.constraints(
if app_state.dd_err.is_some() {
vec![Constraint::Percentage(100)]
} else {
vec![Constraint::Min(3), Constraint::Length(btn_height)]
}
.as_ref(),
)
.split(draw_loc);
// This being true implies that dd_err is none.
if let Some(button_draw_loc) = split_draw_loc.get(1) {
self.draw_dd_confirm_buttons(f, button_draw_loc, app_state);
}
if app_state.dd_err.is_some() {
return app_state.delete_dialog_state.is_showing_dd;
} else {
return true;
}
}
// Currently we just return "false" if things go wrong finding
// the process or a first PID (if an error arises it should be caught).
// I don't really like this, and I find it ugly, but it works for now.
false
}
}

View File

@ -20,15 +20,16 @@ use std::{
};
use crossterm::{
event::{poll, read, DisableMouseCapture, Event, KeyEvent, MouseEvent, MouseEventKind},
event::{poll, read, DisableMouseCapture, MouseEventKind},
execute,
style::Print,
terminal::{disable_raw_mode, LeaveAlternateScreen},
};
use app::{data_harvester, AppState, UsedWidgets};
use app::{data_harvester, AppMessages, UsedWidgets};
use constants::*;
use options::*;
use tuice::{Event, RuntimeEvent};
use utils::error;
pub mod app;
@ -42,7 +43,7 @@ pub mod clap;
pub mod constants;
pub mod data_conversion;
pub mod options;
pub(crate) mod tuine;
pub mod tuice;
pub(crate) mod units;
// FIXME: Use newtype pattern for PID
@ -52,15 +53,6 @@ pub type Pid = usize;
#[cfg(target_family = "unix")]
pub type Pid = libc::pid_t;
#[derive(Debug)]
pub enum BottomEvent {
KeyInput(KeyEvent),
MouseInput(MouseEvent),
Update(Box<data_harvester::Data>),
Resize { width: u16, height: u16 },
Clean,
}
#[derive(Debug)]
pub enum ThreadControlEvent {
Reset,
@ -124,18 +116,6 @@ pub fn create_or_get_config(config_path: &Option<PathBuf>) -> error::Result<Conf
}
}
pub fn try_drawing(
terminal: &mut tui::terminal::Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>,
app: &mut AppState, painter: &mut canvas::Painter,
) -> error::Result<()> {
if let Err(err) = painter.draw_data(terminal, app) {
cleanup_terminal(terminal)?;
return Err(err);
}
Ok(())
}
pub fn cleanup_terminal(
terminal: &mut tui::terminal::Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>,
) -> error::Result<()> {
@ -182,7 +162,8 @@ pub fn panic_hook(panic_info: &PanicInfo<'_>) {
}
pub fn create_input_thread(
sender: std::sync::mpsc::Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
sender: std::sync::mpsc::Sender<RuntimeEvent<AppMessages>>,
termination_ctrl_lock: Arc<Mutex<bool>>,
) -> std::thread::JoinHandle<()> {
thread::spawn(move || {
// TODO: [Optimization, Input] Maybe experiment with removing these timers. Look into using buffers instead?
@ -202,29 +183,35 @@ pub fn create_input_thread(
if poll {
if let Ok(event) = read() {
match event {
Event::Key(event) => {
crossterm::event::Event::Key(event) => {
if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 {
if sender.send(BottomEvent::KeyInput(event)).is_err() {
if sender
.send(RuntimeEvent::UserInterface(Event::Keyboard(event)))
.is_err()
{
break;
}
keyboard_timer = Instant::now();
}
}
Event::Mouse(event) => match &event.kind {
crossterm::event::Event::Mouse(event) => match &event.kind {
MouseEventKind::Drag(_) => {}
MouseEventKind::Moved => {}
_ => {
if Instant::now().duration_since(mouse_timer).as_millis() >= 20
{
if sender.send(BottomEvent::MouseInput(event)).is_err() {
if sender
.send(RuntimeEvent::UserInterface(Event::Mouse(event)))
.is_err()
{
break;
}
mouse_timer = Instant::now();
}
}
},
Event::Resize(width, height) => {
if sender.send(BottomEvent::Resize { width, height }).is_err() {
crossterm::event::Event::Resize(width, height) => {
if sender.send(RuntimeEvent::Resize { width, height }).is_err() {
break;
}
}
@ -237,7 +224,7 @@ pub fn create_input_thread(
}
pub fn create_collection_thread(
sender: std::sync::mpsc::Sender<BottomEvent>,
sender: std::sync::mpsc::Sender<RuntimeEvent<AppMessages>>,
control_receiver: std::sync::mpsc::Receiver<ThreadControlEvent>,
termination_ctrl_lock: Arc<Mutex<bool>>, termination_ctrl_cvar: Arc<Condvar>,
app_config_fields: &app::AppConfigFields, filters: app::DataFilters,
@ -300,7 +287,7 @@ pub fn create_collection_thread(
}
}
let event = BottomEvent::Update(Box::from(data_state.data));
let event = RuntimeEvent::Custom(AppMessages::Update(Box::from(data_state.data)));
data_state.data = data_harvester::Data::default();
if sender.send(event).is_err() {
break;

View File

@ -4,7 +4,7 @@ use std::str::FromStr;
use crate::{
app::{layout_manager::*, *},
canvas::ColourScheme,
canvas::{ColourScheme, Painter},
constants::*,
units::data_units::DataUnit,
utils::error::{self, BottomError},
@ -255,11 +255,8 @@ pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Re
net_filter,
};
Ok(AppState::new(
app_config_fields,
data_filter,
layout_tree_output,
))
let painter = Painter::init(&config, get_color_scheme(&matches, &config)?)?;
AppState::new(app_config_fields, data_filter, layout_tree_output, painter)
}
fn get_update_rate_in_milliseconds(

40
src/tuice/application.rs Normal file
View File

@ -0,0 +1,40 @@
use std::{fmt::Debug, sync::mpsc::Receiver};
use super::{
runtime::{self, RuntimeEvent},
Component, Event,
};
/// An alias to the [`tui::backend::CrosstermBackend`] writing to [`std::io::Stdout`].
pub type CrosstermBackend = tui::backend::CrosstermBackend<std::io::Stdout>;
#[allow(unused_variables)]
pub trait Application: Sized {
type Message: Debug;
/// Determines how to handle a given message.
fn update(&mut self, message: Self::Message);
/// Returns whether to stop the application. Defaults to
/// always returning false.
fn is_terminated(&self) -> bool;
fn view(&mut self) -> Box<dyn Component<Self::Message, CrosstermBackend>>;
/// To run upon stopping the application.
fn destroy(&mut self) {}
/// An optional event handler, intended for use with global shortcuts or events.
/// This will be run *after* trying to send the events into the user interface, and
/// *only* if it is not handled at all by it.
///
/// Defaults to not doing anything.
fn global_event_handler(&mut self, event: Event, messages: &mut Vec<Self::Message>) {}
}
/// Launches some application with tuice.
pub fn launch_with_application<A: Application + 'static>(
application: A, receiver: Receiver<RuntimeEvent<A::Message>>,
) {
runtime::launch(application, receiver);
}

24
src/tuice/component.rs Normal file
View File

@ -0,0 +1,24 @@
pub mod base;
pub use base::*;
use tui::{layout::Rect, Frame};
use super::{Event, Status};
/// A component displays information and can be interacted with.
#[allow(unused_variables)]
pub trait Component<Message, Backend>
where
Backend: tui::backend::Backend,
{
/// Handles an [`Event`]. Defaults to just ignoring the event.
fn on_event(&mut self, bounds: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
/// Returns the desired layout of the component. Defaults to returning
fn layout(&self) {}
/// Draws the component.
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, Backend>);
}

View File

@ -0,0 +1,18 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
pub struct Block {}
impl<Message, B> Component<Message, B> for Block
where
B: Backend,
{
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -0,0 +1,18 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
pub struct Carousel {}
impl<Message, B> Component<Message, B> for Carousel
where
B: Backend,
{
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -0,0 +1,18 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
pub struct Column {}
impl<Message, B> Component<Message, B> for Column
where
B: Backend,
{
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -0,0 +1,17 @@
pub mod text_table;
pub use text_table::{TextColumn, TextColumnConstraint, TextTable};
pub mod shortcut;
pub use shortcut::Shortcut;
pub mod row;
pub use row::Row;
pub mod column;
pub use column::Column;
pub mod block;
pub use block::Block;
pub mod carousel;
pub use carousel::Carousel;

View File

@ -0,0 +1,42 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
#[derive(Default)]
pub struct Row<'a, Message, B>
where
B: Backend,
{
children: Vec<Box<dyn Component<Message, B> + 'a>>, // FIXME: For performance purposes, let's cheat and use enum-dispatch
}
impl<'a, Message, B> Row<'a, Message, B>
where
B: Backend,
{
/// Creates a new [`Row`] with the given children.
pub fn with_children<C>(children: Vec<C>) -> Self
where
C: Into<Box<dyn Component<Message, B> + 'a>>,
{
Self {
children: children.into_iter().map(Into::into).collect(),
}
}
}
impl<'a, Message, B> Component<Message, B> for Row<'a, Message, B>
where
B: Backend,
{
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, B>) {
self.children.iter_mut().for_each(|child| {
// TODO: This is just temp! We need layout!
child.draw(bounds, frame);
})
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -0,0 +1,21 @@
use tui::{backend::Backend, layout::Rect, Frame};
use crate::tuice::{Component, Event, Status};
/// A [`Component`] to handle keyboard shortcuts and assign actions to them.
///
/// Inspired by [Flutter's approach](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts).
pub struct Shortcut {}
impl<Message, B> Component<Message, B> for Shortcut
where
B: Backend,
{
fn draw(&mut self, _bounds: Rect, _frame: &mut Frame<'_, B>) {
todo!()
}
fn on_event(&mut self, _bounds: Rect, _event: Event, _messages: &mut Vec<Message>) -> Status {
Status::Ignored
}
}

View File

@ -14,14 +14,12 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::{
constants::TABLE_GAP_HEIGHT_LIMIT,
tuine::{Event, Status},
tuice::{Component, Event, Status},
};
pub use self::table_column::{TextColumn, TextColumnConstraint};
use self::table_scroll_state::ScrollState as TextTableState;
use super::{Component, ShouldRender};
#[derive(Clone, Debug, Default)]
pub struct StyleSheet {
text: Style,
@ -32,19 +30,21 @@ pub struct StyleSheet {
pub enum TextTableMsg {}
/// A sortable, scrollable table for text data.
pub struct TextTable<'a> {
pub struct TextTable<'a, Message> {
state: TextTableState,
column_widths: Vec<u16>,
columns: Vec<TextColumn>,
show_gap: bool,
show_selected_entry: bool,
data: Vec<Row<'a>>,
rows: Vec<Row<'a>>,
style_sheet: StyleSheet,
sortable: bool,
table_gap: u16,
on_select: Option<Box<dyn Fn(usize) -> Message>>,
on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
}
impl<'a> TextTable<'a> {
impl<'a, Message> TextTable<'a, Message> {
pub fn new<S: Into<Cow<'static, str>>>(columns: Vec<S>) -> Self {
Self {
state: TextTableState::default(),
@ -55,17 +55,27 @@ impl<'a> TextTable<'a> {
.collect(),
show_gap: true,
show_selected_entry: true,
data: Vec::default(),
rows: Vec::default(),
style_sheet: StyleSheet::default(),
sortable: false,
table_gap: 0,
on_select: None,
on_selected_click: None,
}
}
/// Sets the row to display in the table.
///
/// Defaults to displaying no data if not set.
pub fn rows(mut self, rows: Vec<Row<'a>>) -> Self {
self.rows = rows;
self
}
/// Whether to try to show a gap between the table headers and data.
/// Note that if there isn't enough room, the gap will still be hidden.
///
/// Defaults to `true`.
/// Defaults to `true` if not set.
pub fn show_gap(mut self, show_gap: bool) -> Self {
self.show_gap = show_gap;
self
@ -73,7 +83,7 @@ impl<'a> TextTable<'a> {
/// Whether to highlight the selected entry.
///
/// Defaults to `true`.
/// Defaults to `true` if not set.
pub fn show_selected_entry(mut self, show_selected_entry: bool) -> Self {
self.show_selected_entry = show_selected_entry;
self
@ -81,12 +91,31 @@ impl<'a> TextTable<'a> {
/// Whether the table should display as sortable.
///
/// Defaults to `false`.
/// Defaults to `false` if not set.
pub fn sortable(mut self, sortable: bool) -> Self {
self.sortable = sortable;
self
}
/// What to do when selecting an entry. Expects a boxed function that takes in
/// the currently selected index and returns a [`Message`].
///
/// Defaults to `None` if not set.
pub fn on_select(mut self, on_select: Option<Box<dyn Fn(usize) -> Message>>) -> Self {
self.on_select = on_select;
self
}
/// What to do when clicking on an entry that is already selected.
///
/// Defaults to `None` if not set.
pub fn on_selected_click(
mut self, on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
) -> Self {
self.on_selected_click = on_selected_click;
self
}
fn update_column_widths(&mut self, bounds: Rect) {
let total_width = bounds.width;
let mut width_remaining = bounds.width;
@ -136,19 +165,34 @@ impl<'a> TextTable<'a> {
}
}
impl<'a> Component for TextTable<'a> {
type Message = TextTableMsg;
impl<'a, Message, B> From<TextTable<'a, Message>> for Box<dyn Component<Message, B> + 'a>
where
Message: 'a,
B: Backend,
{
fn from(table: TextTable<'a, Message>) -> Self {
Box::new(table)
}
}
type Properties = ();
fn on_event(
&mut self, bounds: Rect, event: Event, messages: &mut Vec<Self::Message>,
) -> Status {
use crate::tuine::MouseBoundIntersect;
impl<'a, Message, B> Component<Message, B> for TextTable<'a, Message>
where
B: Backend,
{
fn on_event(&mut self, bounds: Rect, event: Event, messages: &mut Vec<Message>) -> Status {
use crate::tuice::MouseBoundIntersect;
use crossterm::event::{MouseButton, MouseEventKind};
match event {
Event::Keyboard(_) => Status::Ignored,
Event::Keyboard(key_event) => {
if key_event.modifiers.is_empty() {
match key_event.code {
_ => Status::Ignored,
}
} else {
Status::Ignored
}
}
Event::Mouse(mouse_event) => {
if mouse_event.does_mouse_intersect_bounds(bounds) {
match mouse_event.kind {
@ -156,8 +200,7 @@ impl<'a> Component for TextTable<'a> {
let y = mouse_event.row - bounds.top();
if self.sortable && y == 0 {
// TODO: Do this
Status::Captured
todo!()
} else if y > self.table_gap {
let visual_index = usize::from(y - self.table_gap);
self.state.set_visual_index(visual_index)
@ -165,8 +208,20 @@ impl<'a> Component for TextTable<'a> {
Status::Ignored
}
}
MouseEventKind::ScrollDown => self.state.move_down(1),
MouseEventKind::ScrollUp => self.state.move_up(1),
MouseEventKind::ScrollDown => {
let status = self.state.move_down(1);
if let Some(on_select) = &self.on_select {
messages.push(on_select(self.state.current_index()));
}
status
}
MouseEventKind::ScrollUp => {
let status = self.state.move_up(1);
if let Some(on_select) = &self.on_select {
messages.push(on_select(self.state.current_index()));
}
status
}
_ => Status::Ignored,
}
} else {
@ -176,15 +231,9 @@ impl<'a> Component for TextTable<'a> {
}
}
fn update(&mut self, message: Self::Message) -> ShouldRender {
match message {}
true
}
fn draw<B: Backend>(&mut self, bounds: Rect, frame: &mut Frame<'_, B>) {
fn draw(&mut self, bounds: Rect, frame: &mut Frame<'_, B>) {
self.table_gap = if !self.show_gap
|| (self.data.len() + 2 > bounds.height.into()
|| (self.rows.len() + 2 > bounds.height.into()
&& bounds.height < TABLE_GAP_HEIGHT_LIMIT)
{
0
@ -212,7 +261,7 @@ impl<'a> Component for TextTable<'a> {
.display_start_index(bounds, scrollable_height as usize);
let end = min(self.state.num_items(), start + scrollable_height as usize);
self.data[start..end].to_vec()
self.rows[start..end].to_vec()
};
// Now build up our headers...

View File

@ -1,7 +1,7 @@
use std::cmp::{min, Ordering};
use tui::{layout::Rect, widgets::TableState};
use crate::tuine::Status;
use crate::tuice::Status;
#[derive(Debug, PartialEq, Eq)]
enum ScrollDirection {
@ -197,7 +197,7 @@ impl ScrollState {
mod test {
use tui::layout::Rect;
use crate::tuine::{text_table::table_scroll_state::ScrollDirection, Status};
use crate::tuice::{text_table::table_scroll_state::ScrollDirection, Status};
use super::ScrollState;

View File

@ -9,6 +9,7 @@ pub enum Status {
}
/// An [`Event`] represents some sort of user interface event.
#[derive(Clone, Copy, Debug)]
pub enum Event {
/// A keyboard event
Keyboard(KeyEvent),

View File

@ -0,0 +1,11 @@
/// Which strategy to use while laying out widgets.
pub enum Length {
/// Fill in remaining space. Equivalent to `Length::FlexRatio(1)`.
Flex,
/// Fill in remaining space, with the value being a ratio.
FlexRatio(u16),
/// Fill in a fixed amount of space.
Fixed(u16),
}

2
src/tuice/layout/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod length;
pub use length::Length;

16
src/tuice/mod.rs Normal file
View File

@ -0,0 +1,16 @@
mod tui_rs;
pub mod component;
pub use component::*;
pub mod event;
pub use event::*;
pub mod application;
pub use application::*;
pub mod runtime;
pub use runtime::RuntimeEvent;
pub mod layout;
pub use layout::*;

58
src/tuice/runtime.rs Normal file
View File

@ -0,0 +1,58 @@
use std::sync::mpsc::Receiver;
use tui::layout::Rect;
use crate::tuice::Status;
use super::{Application, Event};
#[derive(Clone, Copy, Debug)]
pub enum RuntimeEvent<Message> {
UserInterface(Event),
Resize { width: u16, height: u16 },
Custom(Message),
}
pub(crate) fn launch<A: Application + 'static>(
mut application: A, receiver: Receiver<RuntimeEvent<A::Message>>,
) {
let mut user_interface = application.view();
while !application.is_terminated() {
if let Ok(event) = receiver.recv() {
match event {
RuntimeEvent::UserInterface(event) => {
let mut messages = vec![];
let bounds = Rect::default(); // TODO: TEMP
match user_interface.on_event(bounds, event, &mut messages) {
Status::Captured => {}
Status::Ignored => {
application.global_event_handler(event, &mut messages);
}
}
for msg in messages {
debug!("Message: {:?}", msg); // FIXME: Remove this debug line!
application.update(msg);
}
user_interface = application.view();
}
RuntimeEvent::Custom(message) => {
application.update(message);
}
RuntimeEvent::Resize {
width: _,
height: _,
} => {
user_interface = application.view();
}
}
} else {
break;
}
}
application.destroy();
}

View File

@ -1,41 +0,0 @@
pub mod text_table;
pub use text_table::{TextColumn, TextColumnConstraint, TextTable};
pub mod shortcut;
pub use shortcut::Shortcut;
use tui::{backend::Backend, layout::Rect, Frame};
use super::{Event, Status};
pub type ShouldRender = bool;
/// A is an element that displays information and can be interacted with.
#[allow(unused_variables)]
pub trait Component {
/// How to inform a component after some event takes place. Typically some enum.
type Message: 'static;
/// Information passed to the component from its parent.
type Properties;
/// Handles an [`Event`]. Defaults to just ignoring the event.
fn on_event(
&mut self, bounds: Rect, event: Event, messages: &mut Vec<Self::Message>,
) -> Status {
Status::Ignored
}
/// How the component should handle a [`Self::Message`]. Defaults to doing nothing.
fn update(&mut self, message: Self::Message) -> ShouldRender {
false
}
/// How the component should handle an update to its properties. Defaults to doing nothing.
fn change(&mut self, props: Self::Properties) -> ShouldRender {
false
}
/// Draws the component.
fn draw<B: Backend>(&mut self, bounds: Rect, frame: &mut Frame<'_, B>);
}

View File

@ -1,35 +0,0 @@
use super::Component;
/// A [`Component`] to handle keyboard shortcuts and assign actions to them.
///
/// Inspired by [Flutter's approach](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts).
pub struct Shortcut<Msg: 'static> {
_p: std::marker::PhantomData<Msg>,
}
impl<Msg> Component for Shortcut<Msg> {
type Message = Msg;
type Properties = ();
fn on_event(
&mut self, bounds: tui::layout::Rect, event: crate::tuine::Event,
messages: &mut Vec<Self::Message>,
) -> crate::tuine::Status {
crate::tuine::Status::Ignored
}
fn update(&mut self, message: Self::Message) -> super::ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> super::ShouldRender {
false
}
fn draw<B: tui::backend::Backend>(
&mut self, bounds: tui::layout::Rect, frame: &mut tui::Frame<'_, B>,
) {
todo!()
}
}

View File

@ -1,7 +0,0 @@
mod tui_stuff;
pub mod component;
pub use component::*;
pub mod event;
pub use event::*;

View File

@ -3,3 +3,4 @@ pub enum DataUnit {
Byte,
Bit,
}