From 0827779f77aaf9c36ebe13cfd8657cb4bbddfa35 Mon Sep 17 00:00:00 2001 From: Hayden Stainsby Date: Tue, 6 Jun 2023 18:01:02 +0200 Subject: [PATCH] feat(console): help view modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a help modal which is available on every view. The help help modal can be accessed by pressing `?` and overlays the current view. To leave the help modal, the user can press `?` or `Esc`. This PR is based on #243 originally authored by @bIgBV. The previous PR has been dormant for around a year. Currently the help modal only displays a vertical list of controls. This is the same information that is available in the controls widget on each view. Here is an example of the tasks view with the help view modal active: ```text connection: http://localhost:6669/ (CONNECTED) views: t = tasks, r = resources controls: select column (sort) = ←→ or h, l, scroll = ↑↓ or k, j, view details = ↵, invert sort (highest/lowest) = i, scroll to top = gg, scroll to bottom╭Help──────────────────────────────────────────╮t = q ╭Warnings───────│controls: │───────────────╮ │⚠ 1 tasks have │ select column (sort) = ←→ or h, l │ │ ╰───────────────│ scroll = ↑↓ or k, j │───────────────╯ ╭Tasks (12) ▶ Ru│ view details = ↵ │───────────────╮ │Warn ID State│ invert sort (highest/lowest) = i │t Location│ │ 19 ▶ │ scroll to top = gg │::task console-│ │ 22 ⏸ │ scroll to bottom = G │::task console-│ │⚠ 1 23 ⏸ │ toggle pause = space │::task console-│ │ 24 ⏸ │ toggle help = ? │::task console-│ │ 25 ⏸ │ quit = q │::task console-│ │ 74 ⏹ │ │::task console-│ │ 75 ⏸ │ │::task console-│ │ 77 ⏸ │ │::task console-│ │ 78 ⏸ ╰──────────────────────────────────────────────╯::task console-│ │ 79 ⏹ wait 11s 4ms 56µs 11s 2 tokio::task console-│ ╰──────────────────────────────────────────────────────────────────────────────╯ ``` However, the idea is that the help modal can provide contextual information depending on the view and the state of the application being observed. This will allow us to provide more details about any lints which are currently triggering and also to reduce the height of the current controls widget to just one line (perhaps optionally) as the full list of controls can be accessed from the help view. Co-authored-by: bIgBV --- 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",