Skip to content

Commit cebf692

Browse files
hdsbIgBV
andcommitted
feat(console): help view modal
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 <[email protected]>
1 parent ea00694 commit cebf692

File tree

9 files changed

+185
-13
lines changed

9 files changed

+185
-13
lines changed

console-subscriber/src/stats.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ impl TaskStats {
217217

218218
self.wakes.fetch_add(1, Release);
219219
if self_wake {
220-
self.self_wakes.fetch_add(1, Release);
220+
self.wakes.fetch_add(1, Release);
221221
}
222222

223223
self.make_dirty();

tokio-console/src/input.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,23 @@ pub(crate) fn is_space(input: &Event) -> bool {
3333
})
3434
)
3535
}
36+
37+
pub(crate) fn is_help_toggle(event: &Event) -> bool {
38+
matches!(
39+
event,
40+
Event::Key(KeyEvent {
41+
code: KeyCode::Char('?'),
42+
..
43+
})
44+
)
45+
}
46+
47+
pub(crate) fn is_esc(event: &Event) -> bool {
48+
matches!(
49+
event,
50+
Event::Key(KeyEvent {
51+
code: KeyCode::Esc,
52+
..
53+
})
54+
)
55+
}

tokio-console/src/view/async_ops.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
},
88
view::{
99
self, bold,
10+
controls::Controls,
1011
table::{TableList, TableListState},
1112
DUR_LEN, DUR_TABLE_PRECISION,
1213
},
@@ -194,6 +195,24 @@ impl TableList<9> for AsyncOpsTable {
194195
table_list_state.len()
195196
))]);
196197

198+
let layout = layout::Layout::default()
199+
.direction(layout::Direction::Vertical)
200+
.margin(0);
201+
202+
let controls = Controls::new(view_controls(), &area, styles);
203+
let chunks = layout
204+
.constraints(
205+
[
206+
layout::Constraint::Length(controls.height()),
207+
layout::Constraint::Max(area.height),
208+
]
209+
.as_ref(),
210+
)
211+
.split(area);
212+
213+
let controls_area = chunks[0];
214+
let async_ops_area = chunks[1];
215+
197216
let attributes_width = layout::Constraint::Percentage(100);
198217
let widths = &[
199218
id_width.constraint(),
@@ -214,7 +233,8 @@ impl TableList<9> for AsyncOpsTable {
214233
.highlight_symbol(view::TABLE_HIGHLIGHT_SYMBOL)
215234
.highlight_style(Style::default().add_modifier(style::Modifier::BOLD));
216235

217-
frame.render_stateful_widget(table, area, &mut table_list_state.table_state);
236+
frame.render_stateful_widget(table, async_ops_area, &mut table_list_state.table_state);
237+
frame.render_widget(controls.into_widget(), controls_area);
218238

219239
table_list_state
220240
.sorted_items

tokio-console/src/view/controls.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ impl Controls {
3838
styles: &view::Styles,
3939
) -> Self {
4040
let mut spans_controls = Vec::with_capacity(view_controls.len() + UNIVERSAL_CONTROLS.len());
41-
spans_controls.extend(view_controls.iter().map(|c| c.to_spans(styles)));
42-
spans_controls.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles)));
41+
spans_controls.extend(view_controls.iter().map(|c| c.to_spans(styles, 0)));
42+
spans_controls.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles, 0)));
4343

4444
let mut lines = vec![Spans::from(vec![Span::from("controls: ")])];
4545
let mut current_line = lines.last_mut().expect("This vector is never empty");
@@ -101,6 +101,18 @@ impl Controls {
101101
}
102102
}
103103

104+
pub(crate) fn controls_paragraph<'a>(
105+
view_controls: &[ControlDisplay],
106+
styles: &view::Styles,
107+
) -> Paragraph<'a> {
108+
let mut spans = Vec::with_capacity(1 + view_controls.len() + UNIVERSAL_CONTROLS.len());
109+
spans.push(Spans::from(vec![Span::raw("controls:")]));
110+
spans.extend(view_controls.iter().map(|c| c.to_spans(styles, 2)));
111+
spans.extend(UNIVERSAL_CONTROLS.iter().map(|c| c.to_spans(styles, 2)));
112+
113+
Paragraph::new(spans)
114+
}
115+
104116
/// Construct span to display a control.
105117
///
106118
/// A control is made up of an action and one or more keys that will trigger
@@ -125,9 +137,10 @@ pub(crate) struct KeyDisplay {
125137
}
126138

127139
impl ControlDisplay {
128-
pub(crate) fn to_spans(&self, styles: &view::Styles) -> Spans<'static> {
140+
pub(crate) fn to_spans(&self, styles: &view::Styles, indent: usize) -> Spans<'static> {
129141
let mut spans = Vec::new();
130142

143+
spans.push(Span::from(" ".repeat(indent)));
131144
spans.push(Span::from(self.action));
132145
spans.push(Span::from(" = "));
133146
for (idx, key_display) in self.keys.iter().enumerate() {

tokio-console/src/view/help.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use ratatui::{
2+
layout::{self, Constraint, Direction, Layout},
3+
widgets::{Clear, Paragraph},
4+
};
5+
6+
use crate::{state::State, view};
7+
8+
pub(crate) trait HelpText {
9+
fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static>;
10+
}
11+
12+
/// Simple view for help popup
13+
pub(crate) struct HelpView<'a> {
14+
help_text: Option<Paragraph<'a>>,
15+
}
16+
17+
impl<'a> HelpView<'a> {
18+
pub(super) fn new(help_text: Paragraph<'a>) -> Self {
19+
HelpView {
20+
help_text: Some(help_text),
21+
}
22+
}
23+
24+
pub(crate) fn render<B: ratatui::backend::Backend>(
25+
&mut self,
26+
styles: &view::Styles,
27+
frame: &mut ratatui::terminal::Frame<B>,
28+
_area: layout::Rect,
29+
_state: &mut State,
30+
) {
31+
let r = frame.size();
32+
let content = self
33+
.help_text
34+
.take()
35+
.expect("help_text should be initialized");
36+
37+
let popup_layout = Layout::default()
38+
.direction(Direction::Vertical)
39+
.constraints(
40+
[
41+
Constraint::Percentage(20),
42+
Constraint::Min(15),
43+
Constraint::Percentage(20),
44+
]
45+
.as_ref(),
46+
)
47+
.split(r);
48+
49+
let popup_area = Layout::default()
50+
.direction(Direction::Horizontal)
51+
.constraints(
52+
[
53+
Constraint::Percentage(20),
54+
Constraint::Percentage(60),
55+
Constraint::Percentage(20),
56+
]
57+
.as_ref(),
58+
)
59+
.split(popup_layout[1])[1];
60+
61+
let display_text = content.block(styles.border_block().title("Help"));
62+
63+
// Clear the help block area and render the popup
64+
frame.render_widget(Clear, popup_area);
65+
frame.render_widget(display_text, popup_area);
66+
}
67+
}

tokio-console/src/view/mod.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::view::{resources::ResourcesTable, table::TableListState, tasks::TasksTable};
1+
use crate::view::help::HelpView;
2+
use crate::view::{
3+
help::HelpText, resources::ResourcesTable, table::TableListState, tasks::TasksTable,
4+
};
25
use crate::{input, state::State};
36
use ratatui::{
47
layout,
@@ -10,6 +13,7 @@ use std::{borrow::Cow, cmp};
1013
mod async_ops;
1114
mod controls;
1215
mod durations;
16+
mod help;
1317
mod mini_histogram;
1418
mod percentiles;
1519
mod resource;
@@ -43,6 +47,7 @@ pub struct View {
4347
tasks_list: TableListState<TasksTable, 12>,
4448
resources_list: TableListState<ResourcesTable, 9>,
4549
state: ViewState,
50+
show_help_modal: bool,
4651
pub(crate) styles: Styles,
4752
}
4853

@@ -96,6 +101,7 @@ impl View {
96101
state: ViewState::TasksList,
97102
tasks_list: TableListState::<TasksTable, 12>::default(),
98103
resources_list: TableListState::<ResourcesTable, 9>::default(),
104+
show_help_modal: false,
99105
styles,
100106
}
101107
}
@@ -104,6 +110,11 @@ impl View {
104110
use ViewState::*;
105111
let mut update_kind = UpdateKind::Other;
106112

113+
if self.should_toggle_help_modal(&event) {
114+
self.show_help_modal = !self.show_help_modal;
115+
return update_kind;
116+
}
117+
107118
if matches!(event, key!(Char('t'))) {
108119
self.state = TasksList;
109120
return update_kind;
@@ -180,32 +191,46 @@ impl View {
180191
update_kind
181192
}
182193

194+
/// The help modal should toggle on the `?` key and should exit on `Esc`
195+
fn should_toggle_help_modal(&mut self, event: &crossterm::event::Event) -> bool {
196+
input::is_help_toggle(event) || (self.show_help_modal && input::is_esc(event))
197+
}
198+
183199
pub(crate) fn render<B: ratatui::backend::Backend>(
184200
&mut self,
185201
frame: &mut ratatui::terminal::Frame<B>,
186202
area: layout::Rect,
187203
state: &mut State,
188204
) {
189-
match self.state {
205+
let help_text: &dyn HelpText = match self.state {
190206
ViewState::TasksList => {
191207
self.tasks_list.render(&self.styles, frame, area, state, ());
208+
&self.tasks_list
192209
}
193210
ViewState::ResourcesList => {
194211
self.resources_list
195212
.render(&self.styles, frame, area, state, ());
213+
&self.resources_list
196214
}
197215
ViewState::TaskInstance(ref mut view) => {
198216
let now = state
199217
.last_updated_at()
200218
.expect("task view implies we've received an update");
201219
view.render(&self.styles, frame, area, now);
220+
view
202221
}
203222
ViewState::ResourceInstance(ref mut view) => {
204223
view.render(&self.styles, frame, area, state);
224+
view
205225
}
206-
}
226+
};
207227

208228
state.retain_active();
229+
230+
if self.show_help_modal {
231+
let mut help_view = HelpView::new(help_text.render_help_content(&self.styles));
232+
help_view.render(&self.styles, frame, area, state);
233+
}
209234
}
210235

211236
pub(crate) fn current_view(&self) -> &ViewState {

tokio-console/src/view/resource.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use crate::{
66
self,
77
async_ops::{self, AsyncOpsTable, AsyncOpsTableCtx},
88
bold,
9-
controls::{ControlDisplay, Controls, KeyDisplay},
9+
controls::{controls_paragraph, ControlDisplay, Controls, KeyDisplay},
10+
help::HelpText,
1011
TableListState,
1112
},
1213
};
@@ -116,6 +117,12 @@ impl ResourceView {
116117
}
117118
}
118119

120+
impl HelpText for ResourceView {
121+
fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> {
122+
controls_paragraph(view_controls(), styles)
123+
}
124+
}
125+
119126
fn view_controls() -> &'static [ControlDisplay] {
120127
static VIEW_CONTROLS: OnceCell<Vec<ControlDisplay>> = OnceCell::new();
121128

tokio-console/src/view/table.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ use crate::{
22
input, state,
33
view::{
44
self,
5-
controls::{ControlDisplay, KeyDisplay},
5+
controls::{controls_paragraph, ControlDisplay, KeyDisplay},
6+
help::HelpText,
67
},
78
};
9+
use ratatui::{
10+
layout,
11+
widgets::{Paragraph, TableState},
12+
};
13+
use std::convert::TryFrom;
814

9-
use ratatui::{layout, widgets::TableState};
1015
use std::cell::RefCell;
11-
use std::convert::TryFrom;
1216
use std::rc::Weak;
1317

1418
pub(crate) trait TableList<const N: usize> {
@@ -195,6 +199,15 @@ where
195199
}
196200
}
197201

202+
impl<T, const N: usize> HelpText for TableListState<T, N>
203+
where
204+
T: TableList<N>,
205+
{
206+
fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> {
207+
controls_paragraph(view_controls(), styles)
208+
}
209+
}
210+
198211
pub(crate) const fn view_controls() -> &'static [ControlDisplay] {
199212
&[
200213
ControlDisplay {

tokio-console/src/view/task.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use crate::{
44
util::Percentage,
55
view::{
66
self, bold,
7-
controls::{ControlDisplay, Controls, KeyDisplay},
7+
controls::{controls_paragraph, ControlDisplay, Controls, KeyDisplay},
88
durations::Durations,
9+
help::HelpText,
910
},
1011
};
1112
use ratatui::{
@@ -253,6 +254,12 @@ impl TaskView {
253254
}
254255
}
255256

257+
impl HelpText for TaskView {
258+
fn render_help_content(&self, styles: &view::Styles) -> Paragraph<'static> {
259+
controls_paragraph(view_controls(), styles)
260+
}
261+
}
262+
256263
const fn view_controls() -> &'static [ControlDisplay] {
257264
&[ControlDisplay {
258265
action: "return to task list",

0 commit comments

Comments
 (0)