Skip to content

Commit

Permalink
refac(console): generalize controls widget (#427)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hds authored and hawkw committed Sep 29, 2023
1 parent d308935 commit 925d0b6
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 116 deletions.
24 changes: 3 additions & 21 deletions tokio-console/src/view/async_ops.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) use crate::view::table::view_controls;
use crate::{
state::{
async_ops::{AsyncOp, SortBy},
Expand All @@ -6,7 +7,7 @@ use crate::{
},
view::{
self, bold,
table::{self, TableList, TableListState},
table::{TableList, TableListState},
DUR_LEN, DUR_TABLE_PRECISION,
},
};
Expand Down Expand Up @@ -193,24 +194,6 @@ impl TableList<9> for AsyncOpsTable {
table_list_state.len()
))]);

let layout = layout::Layout::default()
.direction(layout::Direction::Vertical)
.margin(0);

let controls = table::Controls::for_area(&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(),
Expand All @@ -231,8 +214,7 @@ 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, async_ops_area, &mut table_list_state.table_state);
frame.render_widget(controls.paragraph, controls_area);
frame.render_stateful_widget(table, area, &mut table_list_state.table_state);

table_list_state
.sorted_items
Expand Down
145 changes: 145 additions & 0 deletions tokio-console/src/view/controls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use crate::view::{self, bold};

use ratatui::{
layout,
text::{Span, Spans, Text},
widgets::{Paragraph, Widget},
};

/// A list of controls which are available in all views.
const UNIVERSAL_CONTROLS: &[ControlDisplay] = &[
ControlDisplay {
action: "toggle pause",
keys: &[KeyDisplay {
base: "space",
utf8: None,
}],
},
ControlDisplay {
action: "quit",
keys: &[KeyDisplay {
base: "q",
utf8: None,
}],
},
];

/// Construct a widget to display the controls available to the user in the
/// current view.
pub(crate) struct Controls {
paragraph: Paragraph<'static>,
height: u16,
}

impl Controls {
pub(in crate::view) fn new(
view_controls: &'static [ControlDisplay],
area: &layout::Rect,
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)));

let mut lines = vec![Spans::from(vec![Span::from("controls: ")])];
let mut current_line = lines.last_mut().expect("This vector is never empty");
let separator = Span::from(", ");

let controls_count: usize = spans_controls.len();
for (idx, spans) in spans_controls.into_iter().enumerate() {
// If this is the first item on this line - or first item on the
// first line, then always include it - even if it goes beyond the
// line width, not much we can do anyway.
if idx == 0 || current_line.width() == 0 {
current_line.0.extend(spans.0);
continue;
}

// Include the width of our separator in the current item if we
// aren't placing the last item. This is the separator after the
// new element.
let needed_trailing_separator_width = if idx == controls_count + 1 {
separator.width()
} else {
0
};

let total_width = current_line.width()
+ separator.width()
+ spans.width()
+ needed_trailing_separator_width;

// If the current item fits on this line, append it.
// Otherwise, append only the separator - we accounted for its
// width in the previous loop iteration - and then create a new
// line for the current item.
if total_width <= area.width as usize {
current_line.0.push(separator.clone());
current_line.0.extend(spans.0);
} else {
current_line.0.push(separator.clone());
lines.push(spans);
current_line = lines.last_mut().expect("This vector is never empty");
}
}

let height = lines.len() as u16;
let text = Text::from(lines);

Self {
paragraph: Paragraph::new(text),
height,
}
}

pub(crate) fn height(&self) -> u16 {
self.height
}

pub(crate) fn into_widget(self) -> impl Widget {
self.paragraph
}
}

/// Construct span to display a control.
///
/// A control is made up of an action and one or more keys that will trigger
/// that action.
#[derive(Clone)]
pub(crate) struct ControlDisplay {
pub(crate) action: &'static str,
pub(crate) keys: &'static [KeyDisplay],
}

/// A key or keys which will be displayed to the user as part of spans
/// constructed by `ControlDisplay`.
///
/// The `base` description of the key should be ASCII only, more advanced
/// descriptions can be supplied for that key in the `utf8` field. This
/// allows the application to pick the best one to display at runtime
/// based on the termainal being used.
#[derive(Clone)]
pub(crate) struct KeyDisplay {
pub(crate) base: &'static str,
pub(crate) utf8: Option<&'static str>,
}

impl ControlDisplay {
pub(crate) fn to_spans(&self, styles: &view::Styles) -> Spans<'static> {
let mut spans = Vec::new();

spans.push(Span::from(self.action));
spans.push(Span::from(" = "));
for (idx, key_display) in self.keys.iter().enumerate() {
if idx > 0 {
spans.push(Span::from(" or "))
}
spans.push(bold(match key_display.utf8 {
Some(utf8) => styles.if_utf8(utf8, key_display.base),
None => key_display.base,
}));
}

Spans::from(spans)
}
}
1 change: 1 addition & 0 deletions tokio-console/src/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use ratatui::{
use std::{borrow::Cow, cmp};

mod async_ops;
mod controls;
mod durations;
mod mini_histogram;
mod percentiles;
Expand Down
37 changes: 24 additions & 13 deletions tokio-console/src/view/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ use crate::{
state::State,
view::{
self,
async_ops::{AsyncOpsTable, AsyncOpsTableCtx},
bold, TableListState,
async_ops::{self, AsyncOpsTable, AsyncOpsTableCtx},
bold,
controls::{ControlDisplay, Controls, KeyDisplay},
TableListState,
},
};
use once_cell::sync::OnceCell;
use ratatui::{
layout::{self, Layout},
text::{Span, Spans, Text},
widgets::{Block, Paragraph},
widgets::Paragraph,
};
use std::{cell::RefCell, rc::Rc};

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

let (controls_area, stats_area, async_ops_area) = {
let chunks = Layout::default()
.direction(layout::Direction::Vertical)
.constraints(
[
// controls
layout::Constraint::Length(1),
layout::Constraint::Length(controls.height()),
// resource stats
layout::Constraint::Length(8),
// async ops
Expand All @@ -72,14 +76,6 @@ impl ResourceView {
)
.split(stats_area);

let controls = Spans::from(vec![
Span::raw("controls: "),
bold(styles.if_utf8("\u{238B} esc", "esc")),
Span::raw(" = return to task list, "),
bold("q"),
Span::raw(" = quit"),
]);

let overview = vec![
Spans::from(vec![bold("ID: "), Span::raw(resource.id_str())]),
Spans::from(vec![bold("Parent ID: "), Span::raw(resource.parent())]),
Expand Down Expand Up @@ -107,7 +103,7 @@ impl ResourceView {
Paragraph::new(overview).block(styles.border_block().title("Resource"));
let fields_widget = Paragraph::new(fields).block(styles.border_block().title("Attributes"));

frame.render_widget(Block::default().title(controls), controls_area);
frame.render_widget(controls.into_widget(), controls_area);
frame.render_widget(resource_widget, stats_area[0]);
frame.render_widget(fields_widget, stats_area[1]);
let ctx = AsyncOpsTableCtx {
Expand All @@ -119,3 +115,18 @@ impl ResourceView {
self.initial_render = false;
}
}

fn view_controls() -> &'static [ControlDisplay] {
static VIEW_CONTROLS: OnceCell<Vec<ControlDisplay>> = OnceCell::new();

VIEW_CONTROLS.get_or_init(|| {
let resource_controls = &[ControlDisplay {
action: "return to task list",
keys: &[KeyDisplay {
base: "esc",
utf8: Some("\u{238B} esc"),
}],
}];
[resource_controls, async_ops::view_controls()].concat()
})
}
9 changes: 5 additions & 4 deletions tokio-console/src/view/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::{
},
view::{
self, bold,
table::{self, TableList, TableListState},
controls::Controls,
table::{view_controls, TableList, TableListState},
DUR_LEN, DUR_TABLE_PRECISION,
},
};
Expand Down Expand Up @@ -163,7 +164,7 @@ impl TableList<9> for ResourcesTable {
table_list_state.len()
))]);

let controls = table::Controls::for_area(&area, styles);
let controls = Controls::new(view_controls(), &area, styles);

let layout = layout::Layout::default()
.direction(layout::Direction::Vertical)
Expand All @@ -172,7 +173,7 @@ impl TableList<9> for ResourcesTable {
let chunks = layout
.constraints(
[
layout::Constraint::Length(controls.height),
layout::Constraint::Length(controls.height()),
layout::Constraint::Max(area.height),
]
.as_ref(),
Expand Down Expand Up @@ -202,7 +203,7 @@ impl TableList<9> for ResourcesTable {
.highlight_style(Style::default().add_modifier(style::Modifier::BOLD));

frame.render_stateful_widget(table, tasks_area, &mut table_list_state.table_state);
frame.render_widget(controls.paragraph, controls_area);
frame.render_widget(controls.into_widget(), controls_area);

table_list_state
.sorted_items
Expand Down
Loading

0 comments on commit 925d0b6

Please sign in to comment.