Skip to content

Commit a8a90e9

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 <bhargav.voleti93@gmail.com>
1 parent ee71e22 commit a8a90e9

File tree

13 files changed

+445
-98
lines changed

13 files changed

+445
-98
lines changed

console-subscriber/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub struct ConsoleLayer {
7979
///
8080
/// 16 is probably a reasonable number of waker ops; it's a bit generous if
8181
/// there's only one async runtime library in use, but if there are multiple,
82-
/// they might all have their own sets of waker ops.
82+
/// they might all have their own sets fof waker ops.
8383
waker_callsites: Callsites<16>,
8484

8585
/// Set of callsites for spans representing resources

tokio-console/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// TODO(hds): Remove when upgrading to Clap 4.x
2+
#![allow(clippy::almost_swapped)]
3+
14
use crate::view::Palette;
25
use clap::{ArgGroup, IntoApp, Parser as Clap, Subcommand, ValueHint};
36
use clap_complete::Shell;

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: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub(crate) use crate::view::table::view_controls;
12
use crate::{
23
state::{
34
async_ops::{AsyncOp, SortBy},
@@ -6,7 +7,8 @@ use crate::{
67
},
78
view::{
89
self, bold,
9-
table::{self, TableList, TableListState},
10+
controls::Controls,
11+
table::{TableList, TableListState},
1012
DUR_LEN, DUR_TABLE_PRECISION,
1113
},
1214
};
@@ -197,11 +199,11 @@ impl TableList<9> for AsyncOpsTable {
197199
.direction(layout::Direction::Vertical)
198200
.margin(0);
199201

200-
let controls = table::Controls::for_area(&area, styles);
202+
let controls = Controls::new(view_controls(), &area, styles);
201203
let chunks = layout
202204
.constraints(
203205
[
204-
layout::Constraint::Length(controls.height),
206+
layout::Constraint::Length(controls.height()),
205207
layout::Constraint::Max(area.height),
206208
]
207209
.as_ref(),
@@ -232,7 +234,7 @@ impl TableList<9> for AsyncOpsTable {
232234
.highlight_style(Style::default().add_modifier(style::Modifier::BOLD));
233235

234236
frame.render_stateful_widget(table, async_ops_area, &mut table_list_state.table_state);
235-
frame.render_widget(controls.paragraph, controls_area);
237+
frame.render_widget(controls.into_widget(), controls_area);
236238

237239
table_list_state
238240
.sorted_items

tokio-console/src/view/controls.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use crate::view::{self, bold};
2+
3+
use ratatui::{
4+
layout,
5+
text::{Span, Spans, Text},
6+
widgets::{Paragraph, Widget},
7+
};
8+
9+
/// Construct a widget to display the controls available to the user in the
10+
/// current view.
11+
pub(crate) struct Controls {
12+
paragraph: Paragraph<'static>,
13+
height: u16,
14+
}
15+
16+
impl Controls {
17+
pub(in crate::view) fn new(
18+
view_controls: &'static [ControlDisplay],
19+
area: &layout::Rect,
20+
styles: &view::Styles,
21+
) -> Self {
22+
let universal_controls = universal_controls();
23+
24+
let mut spans_controls = Vec::with_capacity(view_controls.len() + universal_controls.len());
25+
26+
spans_controls.extend(view_controls.iter().map(|c| c.to_spans(styles, 0)));
27+
spans_controls.extend(universal_controls.iter().map(|c| c.to_spans(styles, 0)));
28+
29+
let mut lines = vec![Spans::from(vec![Span::from("controls: ")])];
30+
let mut current_line = lines.last_mut().expect("This vector is never empty");
31+
let separator = Span::from(", ");
32+
33+
let controls_count: usize = spans_controls.len();
34+
for (idx, spans) in spans_controls.into_iter().enumerate() {
35+
// If this is the first item on this line - or first item on the
36+
// first line, then always include it - even if it goes beyond the
37+
// line width, not much we can do anyway.
38+
if idx == 0 || current_line.width() == 0 {
39+
current_line.0.extend(spans.0);
40+
continue;
41+
}
42+
43+
// Include the width of our separator in the current item if we
44+
// aren't placing the last item. This is the separator after the
45+
// new element.
46+
let needed_trailing_separator_width = if idx == controls_count + 1 {
47+
separator.width()
48+
} else {
49+
0
50+
};
51+
52+
let total_width = current_line.width()
53+
+ separator.width()
54+
+ spans.width()
55+
+ needed_trailing_separator_width;
56+
57+
// If the current item fits on this line, append it.
58+
// Otherwise, append only the separator - we accounted for its
59+
// width in the previous loop iteration - and then create a new
60+
// line for the current item.
61+
if total_width <= area.width as usize {
62+
current_line.0.push(separator.clone());
63+
current_line.0.extend(spans.0);
64+
} else {
65+
current_line.0.push(separator.clone());
66+
lines.push(spans);
67+
current_line = lines.last_mut().expect("This vector is never empty");
68+
}
69+
}
70+
71+
let height = lines.len() as u16;
72+
let text = Text::from(lines);
73+
74+
Self {
75+
paragraph: Paragraph::new(text),
76+
height,
77+
}
78+
}
79+
80+
pub(crate) fn height(&self) -> u16 {
81+
self.height
82+
}
83+
84+
pub(crate) fn into_widget(self) -> impl Widget {
85+
self.paragraph
86+
}
87+
}
88+
89+
pub(crate) fn controls_paragraph<'a>(
90+
view_controls: &[ControlDisplay],
91+
styles: &view::Styles,
92+
) -> Paragraph<'a> {
93+
let universal_controls = universal_controls();
94+
95+
let mut spans = Vec::with_capacity(1 + view_controls.len() + universal_controls.len());
96+
spans.push(Spans::from(vec![Span::raw("controls:")]));
97+
spans.extend(view_controls.iter().map(|c| c.to_spans(styles, 2)));
98+
spans.extend(universal_controls.iter().map(|c| c.to_spans(styles, 2)));
99+
100+
Paragraph::new(spans)
101+
}
102+
103+
/// Construct span to display a control.
104+
///
105+
/// A control is made up of an action and one or more keys that will trigger
106+
/// that action.
107+
#[derive(Clone)]
108+
pub(crate) struct ControlDisplay {
109+
pub(crate) action: &'static str,
110+
pub(crate) keys: &'static [KeyDisplay],
111+
}
112+
113+
/// A key or keys which will be displayed to the user as part of spans
114+
/// constructed by `ControlDisplay`.
115+
///
116+
/// The `base` description of the key should be ASCII only, more advanced
117+
/// descriptions can be supplied for that key in the `utf8` field. This
118+
/// allows the application to pick the best one to display at runtime
119+
/// based on the termainal being used.
120+
#[derive(Clone)]
121+
pub(crate) struct KeyDisplay {
122+
pub(crate) base: &'static str,
123+
pub(crate) utf8: Option<&'static str>,
124+
}
125+
126+
impl ControlDisplay {
127+
pub(crate) fn to_spans(&self, styles: &view::Styles, indent: usize) -> Spans<'static> {
128+
let mut spans = Vec::new();
129+
130+
spans.push(Span::from(" ".repeat(indent)));
131+
spans.push(Span::from(self.action));
132+
spans.push(Span::from(" = "));
133+
for (idx, key_display) in self.keys.iter().enumerate() {
134+
if idx > 0 {
135+
spans.push(Span::from(" or "))
136+
}
137+
138+
spans.push(bold(match key_display.utf8 {
139+
Some(utf8) => styles.if_utf8(utf8, key_display.base),
140+
None => key_display.base,
141+
}));
142+
}
143+
144+
Spans::from(spans)
145+
}
146+
147+
// pub(crate) fn into_paragraph()
148+
}
149+
150+
/// Returns a list of controls which are available in all views.
151+
const fn universal_controls() -> &'static [ControlDisplay] {
152+
&[
153+
ControlDisplay {
154+
action: "toggle pause",
155+
keys: &[KeyDisplay {
156+
base: "space",
157+
utf8: None,
158+
}],
159+
},
160+
ControlDisplay {
161+
action: "toggle help",
162+
keys: &[KeyDisplay {
163+
base: "?",
164+
utf8: None,
165+
}],
166+
},
167+
ControlDisplay {
168+
action: "quit",
169+
keys: &[KeyDisplay {
170+
base: "q",
171+
utf8: None,
172+
}],
173+
},
174+
]
175+
}

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+
}

0 commit comments

Comments
 (0)