Skip to content

Commit ea00694

Browse files
authored
refac(console): generalize controls widget (#427)
Each view in tokio-console has a widget up the top that lists the available controls for that view. There was a common implementation of this for table based views (tasks, resources, and async_ops) and separate implementations for the task and resource views. The resource view included two controls widgets, one for the Resource details at the top of the view, and another above the table of Async Ops at the bottom of the view. This change centralises the logic for the creation of this controls widget. This change is mostly a precursor to also displaying the controls in the help view (at which point we can revisit whether the entire list needs to be shown at the top of the screen). Controls (an action and the key or keys used to invoke it) are defined in structs so that their definition can be separated from the display logic (which includes whether or not UTF-8 is supported). This allows the problem of the text in the controls widget wrapping in the middle of a control definition to be fixed. Previously a subset of the controls would have wrapped like this: ```text controls: select column (sort) = ←→ or h, l, scroll = ↑↓ or k, j, view details = ↵, invert sort (highest/lowest) = i, ``` Notice how "view details = ↵," was split across multiple lines. The same list of controls will now wrap at a full control definition. ```text controls: select column (sort) = ←→ or h, l, scroll = ↑↓ or k, j, view details = ↵, invert sort (highest/lowest) = i, ``` Additionally, the list of controls on the Resource view has been consolidated up the top of the screen. Universal controls, those that are available in all views, are also defined centrally. As well as the quit action, using the space bar to pause has been added to that list. This was previously somewhat of an undocumented feature.
1 parent ee0b8e2 commit ea00694

File tree

8 files changed

+268
-116
lines changed

8 files changed

+268
-116
lines changed

tokio-console/src/view/async_ops.rs

+3-21
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,7 @@ use crate::{
67
},
78
view::{
89
self, bold,
9-
table::{self, TableList, TableListState},
10+
table::{TableList, TableListState},
1011
DUR_LEN, DUR_TABLE_PRECISION,
1112
},
1213
};
@@ -193,24 +194,6 @@ impl TableList<9> for AsyncOpsTable {
193194
table_list_state.len()
194195
))]);
195196

196-
let layout = layout::Layout::default()
197-
.direction(layout::Direction::Vertical)
198-
.margin(0);
199-
200-
let controls = table::Controls::for_area(&area, styles);
201-
let chunks = layout
202-
.constraints(
203-
[
204-
layout::Constraint::Length(controls.height),
205-
layout::Constraint::Max(area.height),
206-
]
207-
.as_ref(),
208-
)
209-
.split(area);
210-
211-
let controls_area = chunks[0];
212-
let async_ops_area = chunks[1];
213-
214197
let attributes_width = layout::Constraint::Percentage(100);
215198
let widths = &[
216199
id_width.constraint(),
@@ -231,8 +214,7 @@ impl TableList<9> for AsyncOpsTable {
231214
.highlight_symbol(view::TABLE_HIGHLIGHT_SYMBOL)
232215
.highlight_style(Style::default().add_modifier(style::Modifier::BOLD));
233216

234-
frame.render_stateful_widget(table, async_ops_area, &mut table_list_state.table_state);
235-
frame.render_widget(controls.paragraph, controls_area);
217+
frame.render_stateful_widget(table, area, &mut table_list_state.table_state);
236218

237219
table_list_state
238220
.sorted_items

tokio-console/src/view/controls.rs

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

tokio-console/src/view/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use ratatui::{
88
use std::{borrow::Cow, cmp};
99

1010
mod async_ops;
11+
mod controls;
1112
mod durations;
1213
mod mini_histogram;
1314
mod percentiles;

tokio-console/src/view/resource.rs

+24-13
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ use crate::{
44
state::State,
55
view::{
66
self,
7-
async_ops::{AsyncOpsTable, AsyncOpsTableCtx},
8-
bold, TableListState,
7+
async_ops::{self, AsyncOpsTable, AsyncOpsTableCtx},
8+
bold,
9+
controls::{ControlDisplay, Controls, KeyDisplay},
10+
TableListState,
911
},
1012
};
13+
use once_cell::sync::OnceCell;
1114
use ratatui::{
1215
layout::{self, Layout},
1316
text::{Span, Spans, Text},
14-
widgets::{Block, Paragraph},
17+
widgets::Paragraph,
1518
};
1619
use std::{cell::RefCell, rc::Rc};
1720

@@ -42,14 +45,15 @@ impl ResourceView {
4245
state: &mut State,
4346
) {
4447
let resource = &*self.resource.borrow();
48+
let controls = Controls::new(view_controls(), &area, styles);
4549

4650
let (controls_area, stats_area, async_ops_area) = {
4751
let chunks = Layout::default()
4852
.direction(layout::Direction::Vertical)
4953
.constraints(
5054
[
5155
// controls
52-
layout::Constraint::Length(1),
56+
layout::Constraint::Length(controls.height()),
5357
// resource stats
5458
layout::Constraint::Length(8),
5559
// async ops
@@ -72,14 +76,6 @@ impl ResourceView {
7276
)
7377
.split(stats_area);
7478

75-
let controls = Spans::from(vec![
76-
Span::raw("controls: "),
77-
bold(styles.if_utf8("\u{238B} esc", "esc")),
78-
Span::raw(" = return to task list, "),
79-
bold("q"),
80-
Span::raw(" = quit"),
81-
]);
82-
8379
let overview = vec![
8480
Spans::from(vec![bold("ID: "), Span::raw(resource.id_str())]),
8581
Spans::from(vec![bold("Parent ID: "), Span::raw(resource.parent())]),
@@ -107,7 +103,7 @@ impl ResourceView {
107103
Paragraph::new(overview).block(styles.border_block().title("Resource"));
108104
let fields_widget = Paragraph::new(fields).block(styles.border_block().title("Attributes"));
109105

110-
frame.render_widget(Block::default().title(controls), controls_area);
106+
frame.render_widget(controls.into_widget(), controls_area);
111107
frame.render_widget(resource_widget, stats_area[0]);
112108
frame.render_widget(fields_widget, stats_area[1]);
113109
let ctx = AsyncOpsTableCtx {
@@ -119,3 +115,18 @@ impl ResourceView {
119115
self.initial_render = false;
120116
}
121117
}
118+
119+
fn view_controls() -> &'static [ControlDisplay] {
120+
static VIEW_CONTROLS: OnceCell<Vec<ControlDisplay>> = OnceCell::new();
121+
122+
VIEW_CONTROLS.get_or_init(|| {
123+
let resource_controls = &[ControlDisplay {
124+
action: "return to task list",
125+
keys: &[KeyDisplay {
126+
base: "esc",
127+
utf8: Some("\u{238B} esc"),
128+
}],
129+
}];
130+
[resource_controls, async_ops::view_controls()].concat()
131+
})
132+
}

tokio-console/src/view/resources.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use crate::{
55
},
66
view::{
77
self, bold,
8-
table::{self, TableList, TableListState},
8+
controls::Controls,
9+
table::{view_controls, TableList, TableListState},
910
DUR_LEN, DUR_TABLE_PRECISION,
1011
},
1112
};
@@ -163,7 +164,7 @@ impl TableList<9> for ResourcesTable {
163164
table_list_state.len()
164165
))]);
165166

166-
let controls = table::Controls::for_area(&area, styles);
167+
let controls = Controls::new(view_controls(), &area, styles);
167168

168169
let layout = layout::Layout::default()
169170
.direction(layout::Direction::Vertical)
@@ -172,7 +173,7 @@ impl TableList<9> for ResourcesTable {
172173
let chunks = layout
173174
.constraints(
174175
[
175-
layout::Constraint::Length(controls.height),
176+
layout::Constraint::Length(controls.height()),
176177
layout::Constraint::Max(area.height),
177178
]
178179
.as_ref(),
@@ -202,7 +203,7 @@ impl TableList<9> for ResourcesTable {
202203
.highlight_style(Style::default().add_modifier(style::Modifier::BOLD));
203204

204205
frame.render_stateful_widget(table, tasks_area, &mut table_list_state.table_state);
205-
frame.render_widget(controls.paragraph, controls_area);
206+
frame.render_widget(controls.into_widget(), controls_area);
206207

207208
table_list_state
208209
.sorted_items

0 commit comments

Comments
 (0)