diff --git a/crates/turborepo-ui/src/tui/app.rs b/crates/turborepo-ui/src/tui/app.rs index c7fa7f32f290f..7705e3bf010f5 100644 --- a/crates/turborepo-ui/src/tui/app.rs +++ b/crates/turborepo-ui/src/tui/app.rs @@ -8,7 +8,7 @@ use std::{ use ratatui::{ backend::{Backend, CrosstermBackend}, layout::{Constraint, Layout}, - widgets::TableState, + widgets::{Clear, TableState}, Frame, Terminal, }; use tokio::{ @@ -17,6 +17,8 @@ use tokio::{ }; use tracing::{debug, trace}; +use crate::tui::popup::{popup, popup_area}; + pub const FRAMERATE: Duration = Duration::from_millis(3); const RESIZE_DEBOUNCE_DELAY: Duration = Duration::from_millis(10); @@ -53,6 +55,7 @@ pub struct App { selected_task_index: usize, is_task_selection_pinned: bool, has_sidebar: bool, + showing_help_popup: bool, done: bool, } @@ -97,6 +100,7 @@ impl App { task_list_scroll: TableState::default().with_selected(selected_task_index), selected_task_index, has_sidebar: true, + showing_help_popup: false, is_task_selection_pinned: has_user_interacted, } } @@ -118,6 +122,7 @@ impl App { Ok(InputOptions { focus: &self.section_focus, has_selection, + is_help_popup_open: self.showing_help_popup, }) } @@ -790,6 +795,9 @@ fn update( Event::ToggleSidebar => { app.has_sidebar = !app.has_sidebar; } + Event::ToggleHelpPopup => { + app.showing_help_popup = !app.showing_help_popup; + } Event::Input { bytes } => { app.forward_input(&bytes)?; } @@ -862,6 +870,13 @@ fn view(app: &mut App, f: &mut Frame) { f.render_stateful_widget(&table_to_render, table, &mut app.task_list_scroll); f.render_widget(&pane_to_render, pane); + + if app.showing_help_popup { + let area = popup_area(*f.buffer_mut().area()); + let area = area.intersection(*f.buffer_mut().area()); + f.render_widget(Clear, area); // Clears background underneath popup + f.render_widget(popup(area), area); + } } #[cfg(test)] diff --git a/crates/turborepo-ui/src/tui/event.rs b/crates/turborepo-ui/src/tui/event.rs index 446f110d8f176..4259938c07c2e 100644 --- a/crates/turborepo-ui/src/tui/event.rs +++ b/crates/turborepo-ui/src/tui/event.rs @@ -51,6 +51,7 @@ pub enum Event { cols: u16, }, ToggleSidebar, + ToggleHelpPopup, SearchEnter, SearchExit { restore_scroll: bool, diff --git a/crates/turborepo-ui/src/tui/input.rs b/crates/turborepo-ui/src/tui/input.rs index bbcc03370f542..2a0b16b425b83 100644 --- a/crates/turborepo-ui/src/tui/input.rs +++ b/crates/turborepo-ui/src/tui/input.rs @@ -12,6 +12,7 @@ use super::{ pub struct InputOptions<'a> { pub focus: &'a LayoutSections, pub has_selection: bool, + pub is_help_popup_open: bool, } pub fn start_crossterm_stream(tx: mpsc::Sender) -> Option> { @@ -80,6 +81,7 @@ fn translate_key_event(options: InputOptions, key_event: KeyEvent) -> Option { Some(Event::SearchEnter) } + KeyCode::Esc if options.is_help_popup_open => Some(Event::ToggleHelpPopup), KeyCode::Esc if matches!(options.focus, LayoutSections::Search { .. }) => { Some(Event::SearchExit { restore_scroll: true, @@ -112,6 +114,7 @@ fn translate_key_event(options: InputOptions, key_event: KeyEvent) -> Option { Some(Event::ScrollDown) } + KeyCode::Char('m') => Some(Event::ToggleHelpPopup), KeyCode::Up | KeyCode::Char('k') => Some(Event::Up), KeyCode::Down | KeyCode::Char('j') => Some(Event::Down), KeyCode::Enter | KeyCode::Char('i') => Some(Event::EnterInteractive), @@ -443,6 +446,7 @@ mod test { InputOptions { focus: search(), has_selection: false, + is_help_popup_open: false, } } diff --git a/crates/turborepo-ui/src/tui/mod.rs b/crates/turborepo-ui/src/tui/mod.rs index 5e0995829d507..dc0b2a82091a3 100644 --- a/crates/turborepo-ui/src/tui/mod.rs +++ b/crates/turborepo-ui/src/tui/mod.rs @@ -5,6 +5,7 @@ pub mod event; mod handle; mod input; mod pane; +mod popup; mod search; mod size; mod spinner; diff --git a/crates/turborepo-ui/src/tui/popup.rs b/crates/turborepo-ui/src/tui/popup.rs new file mode 100644 index 0000000000000..d8a0e49187101 --- /dev/null +++ b/crates/turborepo-ui/src/tui/popup.rs @@ -0,0 +1,80 @@ +use std::cmp::min; + +use ratatui::{ + layout::{Constraint, Flex, Layout, Rect}, + text::Line, + widgets::{Block, List, ListItem, Padding}, +}; + +const BIND_LIST: [&str; 11] = [ + "m - Toggle this help popup", + "↑ or j - Select previous task", + "↓ or k - Select next task", + "h - Toggle task list", + "/ - Filter tasks to search term", + "ESC - Clear filter", + "i - Interact with task", + "Ctrl+z - Stop interacting with task", + "c - Copy logs selection (Only when logs are selected)", + "Ctrl+n - Scroll logs up", + "Ctrl+p - Scroll logs down", +]; + +pub fn popup_area(area: Rect) -> Rect { + let screen_width = area.width; + let screen_height = area.height; + + let popup_width = BIND_LIST + .iter() + .map(|s| s.len().saturating_add(4)) + .max() + .unwrap_or(0) as u16; + let popup_height = min((BIND_LIST.len().saturating_add(4)) as u16, screen_height); + + let x = screen_width.saturating_sub(popup_width) / 2; + let y = screen_height.saturating_sub(popup_height) / 2; + + let vertical = Layout::vertical([Constraint::Percentage(100)]).flex(Flex::Center); + let horizontal = Layout::horizontal([Constraint::Percentage(100)]).flex(Flex::Center); + + let [vertical_area] = vertical.areas(Rect { + x, + y, + width: popup_width, + height: popup_height, + }); + + let [area] = horizontal.areas(vertical_area); + + area +} + +pub fn popup(area: Rect) -> List<'static> { + let available_height = area.height.saturating_sub(4) as usize; + + let items: Vec = BIND_LIST + .iter() + .take(available_height) + .map(|item| ListItem::new(Line::from(*item))) + .collect(); + + let title_bottom = if available_height < BIND_LIST.len() { + let binds_not_visible = BIND_LIST.len().saturating_sub(available_height); + + let pluralize = if binds_not_visible > 1 { "s" } else { "" }; + let message = format!( + " {} more bind{}. Make your terminal taller. ", + binds_not_visible, pluralize + ); + Line::from(message) + } else { + Line::from("") + }; + + let outer = Block::bordered() + .title(" Keybinds ") + .title_bottom(title_bottom.to_string()) + .padding(Padding::uniform(1)); + + List::new(items).block(outer) +} diff --git a/crates/turborepo-ui/src/tui/table.rs b/crates/turborepo-ui/src/tui/table.rs index 1757d8657f9ae..3490b36ab6e54 100644 --- a/crates/turborepo-ui/src/tui/table.rs +++ b/crates/turborepo-ui/src/tui/table.rs @@ -21,8 +21,8 @@ pub struct TaskTable<'b> { spinner: SpinnerState, } -const TASK_NAVIGATE_INSTRUCTIONS: &str = "↑ ↓ to navigate"; -const HIDE_INSTRUCTIONS: &str = "h to hide"; +const TASK_NAVIGATE_INSTRUCTIONS: &str = "↑ ↓ - Select"; +const MORE_BINDS_INSTRUCTIONS: &str = "m - More binds"; impl<'b> TaskTable<'b> { /// Construct a new table with all of the planned tasks @@ -122,7 +122,7 @@ impl<'a> StatefulWidget for &'a TaskTable<'a> { ) .footer( vec![Text::styled( - format!("{TASK_NAVIGATE_INSTRUCTIONS}\n{HIDE_INSTRUCTIONS}"), + format!("{TASK_NAVIGATE_INSTRUCTIONS}\n{MORE_BINDS_INSTRUCTIONS}"), Style::default().add_modifier(Modifier::DIM), )] .into_iter()