From 359a4e7fa72911e47e2f9daa1e1a04ecaf84afbc Mon Sep 17 00:00:00 2001 From: Hayden Stainsby Date: Sat, 10 Jun 2023 15:45:35 +0200 Subject: [PATCH] feat(console): help view modal (#432) --- tokio-console/src/input.rs | 20 +++++++++ tokio-console/src/view/async_ops.rs | 22 +++++++++- tokio-console/src/view/controls.rs | 19 ++++++-- tokio-console/src/view/help.rs | 67 +++++++++++++++++++++++++++++ tokio-console/src/view/mod.rs | 31 +++++++++++-- tokio-console/src/view/resource.rs | 9 +++- tokio-console/src/view/table.rs | 19 ++++++-- tokio-console/src/view/task.rs | 9 +++- 8 files changed, 184 insertions(+), 12 deletions(-) create mode 100644 tokio-console/src/view/help.rs diff --git a/tokio-console/src/input.rs b/tokio-console/src/input.rs index 278991e55..088d5b771 100644 --- a/tokio-console/src/input.rs +++ b/tokio-console/src/input.rs @@ -33,3 +33,23 @@ pub(crate) fn is_space(input: &Event) -> bool { }) ) } + +pub(crate) fn is_help_toggle(event: &Event) -> bool { + matches!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('?'), + .. + }) + ) +} + +pub(crate) fn is_esc(event: &Event) -> bool { + matches!( + event, + Event::Key(KeyEvent { + code: KeyCode::Esc, + .. + }) + ) +} diff --git a/tokio-console/src/view/async_ops.rs b/tokio-console/src/view/async_ops.rs index 000319cc3..380799b8d 100644 --- a/tokio-console/src/view/async_ops.rs +++ b/tokio-console/src/view/async_ops.rs @@ -7,6 +7,7 @@ use crate::{ }, view::{ self, bold, + controls::Controls, table::{TableList, TableListState}, DUR_LEN, DUR_TABLE_PRECISION, }, @@ -194,6 +195,24 @@ impl TableList<9> for AsyncOpsTable { table_list_state.len() ))]); + let layout = layout::Layout::default() + .direction(layout::Direction::Vertical) + .margin(0); + + let controls = Controls::new(view_controls(), &area, styles); + let chunks = layout + .constraints( + [ + layout::Constraint::Length(controls.height()), + layout::Constraint::Max(area.height), + ] + .as_ref(), + ) + .split(area); + + let controls_area = chunks[0]; + let async_ops_area = chunks[1]; + let attributes_width = layout::Constraint::Percentage(100); let widths = &[ id_width.constraint(), @@ -214,7 +233,8 @@ impl TableList<9> for AsyncOpsTable { .highlight_symbol(view::TABLE_HIGHLIGHT_SYMBOL) .highlight_style(Style::default().add_modifier(style::Modifier::BOLD)); - frame.render_stateful_widget(table, area, &mut table_list_state.table_state); + frame.render_stateful_widget(table, async_ops_area, &mut table_list_state.table_state); + frame.render_widget(controls.into_widget(), controls_area); table_list_state .sorted_items diff --git a/tokio-console/src/view/controls.rs b/tokio-console/src/view/controls.rs index 81a90a7e2..bf9972532 100644 --- a/tokio-console/src/view/controls.rs +++ b/tokio-console/src/view/controls.rs @@ -38,8 +38,8 @@ impl Controls { styles: &view::Styles, ) -> Self { let mut spans_controls = Vec::with_capacity(view_controls.len() + UNIVERSAL_CONTROLS.len()); - spans_controls.extend(view_controls.iter().map(|c| c.to_spans(styles))); - spans_controls.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles))); + spans_controls.extend(view_controls.iter().map(|c| c.to_spans(styles, 0))); + spans_controls.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles, 0))); let mut lines = vec![Spans::from(vec![Span::from("controls: ")])]; let mut current_line = lines.last_mut().expect("This vector is never empty"); @@ -101,6 +101,18 @@ impl Controls { } } +pub(crate) fn controls_paragraph<'a>( + view_controls: &[ControlDisplay], + styles: &view::Styles, +) -> Paragraph<'a> { + let mut spans = Vec::with_capacity(1 + view_controls.len() + UNIVERSAL_CONTROLS.len()); + spans.push(Spans::from(vec![Span::raw("controls:")])); + spans.extend(view_controls.iter().map(|c| c.to_spans(styles, 2))); + spans.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles, 2))); + + Paragraph::new(spans) +} + /// Construct span to display a control. /// /// A control is made up of an action and one or more keys that will trigger @@ -125,9 +137,10 @@ pub(crate) struct KeyDisplay { } impl ControlDisplay { - pub(crate) fn to_spans(&self, styles: &view::Styles) -> Spans<'static> { + pub(crate) fn to_spans(&self, styles: &view::Styles, indent: usize) -> Spans<'static> { let mut spans = Vec::new(); + spans.push(Span::from(" ".repeat(indent))); spans.push(Span::from(self.action)); spans.push(Span::from(" = ")); for (idx, key_display) in self.keys.iter().enumerate() { diff --git a/tokio-console/src/view/help.rs b/tokio-console/src/view/help.rs new file mode 100644 index 000000000..de249db61 --- /dev/null +++ b/tokio-console/src/view/help.rs @@ -0,0 +1,67 @@ +use ratatui::{ + layout::{self, Constraint, Direction, Layout}, + widgets::{Clear, Paragraph}, +}; + +use crate::{state::State, view}; + +pub(crate) trait HelpText { + fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static>; +} + +/// Simple view for help popup +pub(crate) struct HelpView<'a> { + help_text: Option>, +} + +impl<'a> HelpView<'a> { + pub(super) fn new(help_text: Paragraph<'a>) -> Self { + HelpView { + help_text: Some(help_text), + } + } + + pub(crate) fn render( + &mut self, + styles: &view::Styles, + frame: &mut ratatui::terminal::Frame, + _area: layout::Rect, + _state: &mut State, + ) { + let r = frame.size(); + let content = self + .help_text + .take() + .expect("help_text should be initialized"); + + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(20), + Constraint::Min(15), + Constraint::Percentage(20), + ] + .as_ref(), + ) + .split(r); + + let popup_area = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(20), + Constraint::Percentage(60), + Constraint::Percentage(20), + ] + .as_ref(), + ) + .split(popup_layout[1])[1]; + + let display_text = content.block(styles.border_block().title("Help")); + + // Clear the help block area and render the popup + frame.render_widget(Clear, popup_area); + frame.render_widget(display_text, popup_area); + } +} diff --git a/tokio-console/src/view/mod.rs b/tokio-console/src/view/mod.rs index ca9e87fcc..72221ac73 100644 --- a/tokio-console/src/view/mod.rs +++ b/tokio-console/src/view/mod.rs @@ -1,4 +1,7 @@ -use crate::view::{resources::ResourcesTable, table::TableListState, tasks::TasksTable}; +use crate::view::help::HelpView; +use crate::view::{ + help::HelpText, resources::ResourcesTable, table::TableListState, tasks::TasksTable, +}; use crate::{input, state::State}; use ratatui::{ layout, @@ -10,6 +13,7 @@ use std::{borrow::Cow, cmp}; mod async_ops; mod controls; mod durations; +mod help; mod mini_histogram; mod percentiles; mod resource; @@ -43,6 +47,7 @@ pub struct View { tasks_list: TableListState, resources_list: TableListState, state: ViewState, + show_help_modal: bool, pub(crate) styles: Styles, } @@ -96,6 +101,7 @@ impl View { state: ViewState::TasksList, tasks_list: TableListState::::default(), resources_list: TableListState::::default(), + show_help_modal: false, styles, } } @@ -104,6 +110,11 @@ impl View { use ViewState::*; let mut update_kind = UpdateKind::Other; + if self.should_toggle_help_modal(&event) { + self.show_help_modal = !self.show_help_modal; + return update_kind; + } + if matches!(event, key!(Char('t'))) { self.state = TasksList; return update_kind; @@ -180,32 +191,46 @@ impl View { update_kind } + /// The help modal should toggle on the `?` key and should exit on `Esc` + fn should_toggle_help_modal(&mut self, event: &crossterm::event::Event) -> bool { + input::is_help_toggle(event) || (self.show_help_modal && input::is_esc(event)) + } + pub(crate) fn render( &mut self, frame: &mut ratatui::terminal::Frame, area: layout::Rect, state: &mut State, ) { - match self.state { + let help_text: &dyn HelpText = match self.state { ViewState::TasksList => { self.tasks_list.render(&self.styles, frame, area, state, ()); + &self.tasks_list } ViewState::ResourcesList => { self.resources_list .render(&self.styles, frame, area, state, ()); + &self.resources_list } ViewState::TaskInstance(ref mut view) => { let now = state .last_updated_at() .expect("task view implies we've received an update"); view.render(&self.styles, frame, area, now); + view } ViewState::ResourceInstance(ref mut view) => { view.render(&self.styles, frame, area, state); + view } - } + }; state.retain_active(); + + if self.show_help_modal { + let mut help_view = HelpView::new(help_text.render_help_content(&self.styles)); + help_view.render(&self.styles, frame, area, state); + } } pub(crate) fn current_view(&self) -> &ViewState { diff --git a/tokio-console/src/view/resource.rs b/tokio-console/src/view/resource.rs index 05bc7f68b..25f3c45e0 100644 --- a/tokio-console/src/view/resource.rs +++ b/tokio-console/src/view/resource.rs @@ -6,7 +6,8 @@ use crate::{ self, async_ops::{self, AsyncOpsTable, AsyncOpsTableCtx}, bold, - controls::{ControlDisplay, Controls, KeyDisplay}, + controls::{controls_paragraph, ControlDisplay, Controls, KeyDisplay}, + help::HelpText, TableListState, }, }; @@ -116,6 +117,12 @@ impl ResourceView { } } +impl HelpText for ResourceView { + fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> { + controls_paragraph(view_controls(), styles) + } +} + fn view_controls() -> &'static [ControlDisplay] { static VIEW_CONTROLS: OnceCell> = OnceCell::new(); diff --git a/tokio-console/src/view/table.rs b/tokio-console/src/view/table.rs index 1f36a25b8..10fdd16dc 100644 --- a/tokio-console/src/view/table.rs +++ b/tokio-console/src/view/table.rs @@ -2,13 +2,17 @@ use crate::{ input, state, view::{ self, - controls::{ControlDisplay, KeyDisplay}, + controls::{controls_paragraph, ControlDisplay, KeyDisplay}, + help::HelpText, }, }; +use ratatui::{ + layout, + widgets::{Paragraph, TableState}, +}; +use std::convert::TryFrom; -use ratatui::{layout, widgets::TableState}; use std::cell::RefCell; -use std::convert::TryFrom; use std::rc::Weak; pub(crate) trait TableList { @@ -195,6 +199,15 @@ where } } +impl HelpText for TableListState +where + T: TableList, +{ + fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> { + controls_paragraph(view_controls(), styles) + } +} + pub(crate) const fn view_controls() -> &'static [ControlDisplay] { &[ ControlDisplay { diff --git a/tokio-console/src/view/task.rs b/tokio-console/src/view/task.rs index c3c50fae5..368fc8abc 100644 --- a/tokio-console/src/view/task.rs +++ b/tokio-console/src/view/task.rs @@ -4,8 +4,9 @@ use crate::{ util::Percentage, view::{ self, bold, - controls::{ControlDisplay, Controls, KeyDisplay}, + controls::{controls_paragraph, ControlDisplay, Controls, KeyDisplay}, durations::Durations, + help::HelpText, }, }; use ratatui::{ @@ -253,6 +254,12 @@ impl TaskView { } } +impl HelpText for TaskView { + fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> { + controls_paragraph(view_controls(), styles) + } +} + const fn view_controls() -> &'static [ControlDisplay] { &[ControlDisplay { action: "return to task list",