diff --git a/tokio-console/src/view/async_ops.rs b/tokio-console/src/view/async_ops.rs index d59eaf88b..000319cc3 100644 --- a/tokio-console/src/view/async_ops.rs +++ b/tokio-console/src/view/async_ops.rs @@ -1,3 +1,4 @@ +pub(crate) use crate::view::table::view_controls; use crate::{ state::{ async_ops::{AsyncOp, SortBy}, @@ -6,7 +7,7 @@ use crate::{ }, view::{ self, bold, - table::{self, TableList, TableListState}, + table::{TableList, TableListState}, DUR_LEN, DUR_TABLE_PRECISION, }, }; @@ -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(), @@ -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 diff --git a/tokio-console/src/view/controls.rs b/tokio-console/src/view/controls.rs new file mode 100644 index 000000000..81a90a7e2 --- /dev/null +++ b/tokio-console/src/view/controls.rs @@ -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) + } +} diff --git a/tokio-console/src/view/mod.rs b/tokio-console/src/view/mod.rs index 47c345285..ca9e87fcc 100644 --- a/tokio-console/src/view/mod.rs +++ b/tokio-console/src/view/mod.rs @@ -8,6 +8,7 @@ use ratatui::{ use std::{borrow::Cow, cmp}; mod async_ops; +mod controls; mod durations; mod mini_histogram; mod percentiles; diff --git a/tokio-console/src/view/resource.rs b/tokio-console/src/view/resource.rs index 0a5f830d9..05bc7f68b 100644 --- a/tokio-console/src/view/resource.rs +++ b/tokio-console/src/view/resource.rs @@ -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}; @@ -42,6 +45,7 @@ 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() @@ -49,7 +53,7 @@ impl ResourceView { .constraints( [ // controls - layout::Constraint::Length(1), + layout::Constraint::Length(controls.height()), // resource stats layout::Constraint::Length(8), // async ops @@ -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())]), @@ -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 { @@ -119,3 +115,18 @@ impl ResourceView { self.initial_render = false; } } + +fn view_controls() -> &'static [ControlDisplay] { + static VIEW_CONTROLS: OnceCell> = 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() + }) +} diff --git a/tokio-console/src/view/resources.rs b/tokio-console/src/view/resources.rs index 4248de3d5..124ba22d5 100644 --- a/tokio-console/src/view/resources.rs +++ b/tokio-console/src/view/resources.rs @@ -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, }, }; @@ -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) @@ -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(), @@ -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 diff --git a/tokio-console/src/view/table.rs b/tokio-console/src/view/table.rs index 7f1a7548e..1f36a25b8 100644 --- a/tokio-console/src/view/table.rs +++ b/tokio-console/src/view/table.rs @@ -1,15 +1,14 @@ use crate::{ input, state, - view::{self, bold}, + view::{ + self, + controls::{ControlDisplay, KeyDisplay}, + }, }; -use ratatui::{ - layout, - text::{self, Span, Spans, Text}, - widgets::{Paragraph, TableState, Wrap}, -}; -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 { @@ -45,11 +44,6 @@ pub(crate) struct TableListState, const N: usize> { last_key_event: Option, } -pub(crate) struct Controls { - pub(crate) paragraph: Paragraph<'static>, - pub(crate) height: u16, -} - impl, const N: usize> TableListState { pub(in crate::view) fn len(&self) -> usize { self.sorted_items.len() @@ -201,52 +195,61 @@ where } } -impl Controls { - pub(in crate::view) fn for_area(area: &layout::Rect, styles: &view::Styles) -> Self { - let text = Text::from(Spans::from(vec![ - Span::raw("controls: "), - bold(styles.if_utf8("\u{2190}\u{2192}", "left, right")), - Span::raw(" or "), - bold("h, l"), - text::Span::raw(" = select column (sort), "), - bold(styles.if_utf8("\u{2191}\u{2193}", "up, down")), - Span::raw(" or "), - bold("k, j"), - text::Span::raw(" = scroll, "), - bold(styles.if_utf8("\u{21B5}", "enter")), - text::Span::raw(" = view details, "), - bold("i"), - text::Span::raw(" = invert sort (highest/lowest), "), - bold("q"), - text::Span::raw(" = quit "), - bold("gg"), - text::Span::raw(" = scroll to top, "), - bold("G"), - text::Span::raw(" = scroll to bottom"), - ])); - - // how many lines do we need to display the controls? - let mut height = 1; - - // if the area is narrower than the width of the controls text, we need - // to wrap the text across multiple lines. - let width = text.width() as u16; - if area.width < width { - height = width / area.width; - - // if the text's width is not neatly divisible by the area's width - // (and it almost never will be), round up for the remaining text. - if width % area.width > 0 { - height += 1 - }; - } - - Self { - // TODO(eliza): it would be nice if we could wrap this on commas, - // specifically, rather than whitespace...but that seems like a - // bunch of additional work... - paragraph: Paragraph::new(text).wrap(Wrap { trim: true }), - height, - } - } +pub(crate) const fn view_controls() -> &'static [ControlDisplay] { + &[ + ControlDisplay { + action: "select column (sort)", + keys: &[ + KeyDisplay { + base: "left, right", + utf8: Some("\u{2190}\u{2192}"), + }, + KeyDisplay { + base: "h, l", + utf8: None, + }, + ], + }, + ControlDisplay { + action: "scroll", + keys: &[ + KeyDisplay { + base: "up, down", + utf8: Some("\u{2191}\u{2193}"), + }, + KeyDisplay { + base: "k, j", + utf8: None, + }, + ], + }, + ControlDisplay { + action: "view details", + keys: &[KeyDisplay { + base: "enter", + utf8: Some("\u{21B5}"), + }], + }, + ControlDisplay { + action: "invert sort (highest/lowest)", + keys: &[KeyDisplay { + base: "i", + utf8: None, + }], + }, + ControlDisplay { + action: "scroll to top", + keys: &[KeyDisplay { + base: "gg", + utf8: None, + }], + }, + ControlDisplay { + action: "scroll to bottom", + keys: &[KeyDisplay { + base: "G", + utf8: None, + }], + }, + ] } diff --git a/tokio-console/src/view/task.rs b/tokio-console/src/view/task.rs index d9368c0c7..c3c50fae5 100644 --- a/tokio-console/src/view/task.rs +++ b/tokio-console/src/view/task.rs @@ -2,12 +2,16 @@ use crate::{ input, state::{tasks::Task, DetailsRef}, util::Percentage, - view::{self, bold, durations::Durations}, + view::{ + self, bold, + controls::{ControlDisplay, Controls, KeyDisplay}, + durations::Durations, + }, }; use ratatui::{ layout::{self, Layout}, text::{Span, Spans, Text}, - widgets::{Block, List, ListItem, Paragraph}, + widgets::{List, ListItem, Paragraph}, }; use std::{ cell::RefCell, @@ -49,6 +53,8 @@ impl TaskView { .as_ref() .filter(|details| details.span_id() == task.span_id()); + let controls = Controls::new(view_controls(), &area, styles); + let warnings: Vec<_> = task .warnings() .iter() @@ -74,7 +80,7 @@ impl TaskView { .constraints( [ // controls - layout::Constraint::Length(1), + layout::Constraint::Length(controls.height()), // task stats layout::Constraint::Length(10), // poll duration @@ -94,7 +100,7 @@ impl TaskView { .constraints( [ // controls - layout::Constraint::Length(1), + layout::Constraint::Length(controls.height()), // warnings (add 2 for top and bottom borders) layout::Constraint::Length(warnings.len() as u16 + 2), // task stats @@ -131,14 +137,6 @@ impl TaskView { ) .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"), - ]); - // Just preallocate capacity for ID, name, target, total, busy, and idle. let mut overview = Vec::with_capacity(8); overview.push(Spans::from(vec![ @@ -246,7 +244,7 @@ impl TaskView { let fields_widget = Paragraph::new(fields).block(styles.border_block().title("Fields")); - frame.render_widget(Block::default().title(controls), controls_area); + frame.render_widget(controls.into_widget(), controls_area); frame.render_widget(task_widget, stats_area[0]); frame.render_widget(wakers_widget, stats_area[1]); frame.render_widget(poll_durations_widget, poll_dur_area); @@ -254,3 +252,13 @@ impl TaskView { frame.render_widget(fields_widget, fields_area); } } + +const fn view_controls() -> &'static [ControlDisplay] { + &[ControlDisplay { + action: "return to task list", + keys: &[KeyDisplay { + base: "esc", + utf8: Some("\u{238B} esc"), + }], + }] +} diff --git a/tokio-console/src/view/tasks.rs b/tokio-console/src/view/tasks.rs index 078ebca0b..7d15934ac 100644 --- a/tokio-console/src/view/tasks.rs +++ b/tokio-console/src/view/tasks.rs @@ -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, }, }; @@ -212,13 +213,13 @@ impl TableList<12> for TasksTable { .direction(layout::Direction::Vertical) .margin(0); - let controls = table::Controls::for_area(&area, styles); + let controls = Controls::new(view_controls(), &area, styles); let (controls_area, tasks_area, warnings_area) = if warnings.is_empty() { let chunks = layout .constraints( [ - layout::Constraint::Length(controls.height), + layout::Constraint::Length(controls.height()), layout::Constraint::Max(area.height), ] .as_ref(), @@ -230,7 +231,7 @@ impl TableList<12> for TasksTable { let chunks = layout .constraints( [ - layout::Constraint::Length(controls.height), + layout::Constraint::Length(controls.height()), layout::Constraint::Length(warnings_height), layout::Constraint::Max(area.height), ] @@ -269,7 +270,7 @@ impl TableList<12> for TasksTable { .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); if let Some(area) = warnings_area { let block = styles