diff --git a/src/help_window.rs b/src/help_window.rs index b50a52c..67c4c60 100644 --- a/src/help_window.rs +++ b/src/help_window.rs @@ -13,45 +13,103 @@ impl HelpWindow { HelpWindow } - pub fn render(&self, frame: &mut impl FrameWrapperInterface, area: Rect) { - // Clear the background area first. - frame.render_widget(Clear, area); + pub fn render(&self, frame: &mut impl FrameWrapperInterface) { + let frame_rect = frame.size(); - let block = Block::default() - .title("Help") - .borders(Borders::ALL) - .style(Style::default().fg(Color::White).bg(Color::Black)); - - let inner_area = block.inner(area); + if frame_rect.height < 3 { + frame.render_widget(Clear, frame_rect); + return; + } let help_text = vec![ - "Navigation:", - "s - Start/unpause the test", - "Esc - Pause the test", - "q - Quit", "", - "Configuration:", - "--duration - Set test duration", - "--numbers - Include numbers in the test", - "--uppercase - Include uppercase letters", - "Run 'donkeytype help' for more options", + " Navigation:", + " 's' - Start/resume the test", + " - Pause the test", + " 'q' - Quit", + " '?' - Toggle this window", + "", + " Configuration:", + " --duration - Set test duration", + " --numbers - Include numbers in the test", + " --uppercase - Include uppercase letters", + "", + " Run 'donkeytype help' in your terminal to get more information ", + "", ]; + let longest_help_msg_len = help_text.iter().map(|s| s.len()).max().unwrap(); + let help_text_lines_count = help_text.len(); + + // check if there is enough space vertically to display the help message + if frame_rect.height <= help_text_lines_count as u16 { + let paragraph = + Paragraph::new( "Terminal window is too short to display the help window\nresize the terminal or press \"?\" to return to the test") + .style(Style::default().fg(Color::Red).bg(Color::Black)); + + frame.render_widget(Clear, frame_rect); + frame.render_widget(paragraph, frame_rect); + + return; + } + + // check if there is enough space horizontally to display the help message + if frame_rect.width - 2 <= longest_help_msg_len as u16 { + let paragraph = Paragraph::new( + "Terminal window is too narrow\nto display the help window\nresize the terminal\nor press the \"?\" key\nto return to the test", + ) + .style(Style::default().fg(Color::Red).bg(Color::Black)); + + frame.render_widget(Clear, frame_rect); + frame.render_widget(paragraph, frame_rect); + + return; + } + + // Create a clear overlay to dim the background + frame.render_widget( + Paragraph::new("") + .style(Style::default().bg(Color::Black).fg(Color::DarkGray)) + .block(Block::default()), + frame_rect, + ); + + let area = Self::get_centered_rect( + longest_help_msg_len.try_into().unwrap(), + help_text_lines_count.try_into().unwrap(), + frame.size(), + ); + + // Clear the background area first. + frame.render_widget(Clear, area); + + let block = Block::default().title(" Help ").borders(Borders::ALL); + + let inner_area = block.inner(area); + // Create constraints dynamically based on help_text length - let constraints = vec![Constraint::Length(1); help_text.len()]; + let constraints = vec![Constraint::Length(1); help_text_lines_count]; let chunks = Layout::default() .direction(Direction::Vertical) .constraints(constraints) .split(inner_area); - // Render block frame.render_widget(block, area); // Render text paragraphs for (i, &text) in help_text.iter().enumerate() { - let paragraph = Paragraph::new(text).style(Style::default().fg(Color::White)); + let paragraph = Paragraph::new(text); frame.render_widget(paragraph, chunks[i]); } } + + fn get_centered_rect(window_width: u16, window_height: u16, r: Rect) -> Rect { + let x = r.x + (r.width.saturating_sub(window_width + 2)) / 2; + let y = if r.height > window_height + 4 { 3 } else { 0 }; + + Rect::new(x, y, window_width + 2, window_height + 1) + } } + +// TODO: add tests diff --git a/src/runner.rs b/src/runner.rs index 703cdba..3325300 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -21,7 +21,6 @@ use crate::expected_input::ExpectedInputInterface; use crate::help_window::HelpWindow; use crate::helpers::split_by_char_index; use crate::test_results::{Stats, TestResults}; -use ratatui::widgets::Block; use ratatui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, @@ -135,31 +134,36 @@ impl Runner { if let Event::Key(key) = event::read().context("Unable to read event")? { if key.kind == KeyEventKind::Press { match self.input_mode { - InputMode::Normal => match key.code { - KeyCode::Char('s') => { - // Hide help window if it's shown. - self.show_help = false; - - start_time = if self.is_started { - start_time + pause_time.elapsed() - } else { - Instant::now() - }; - self.is_started = true; - self.input_mode = InputMode::Editing; - } - KeyCode::Char('q') => { - // todo return canceled test error and handle it in main - return Ok(TestResults::new( - Stats::default(), - self.config.clone(), - false, - )); - } - KeyCode::Char('?') => { - self.show_help = !self.show_help; - } - _ => {} + InputMode::Normal => match self.show_help { + true => match key.code { + KeyCode::Char('?') => { + self.show_help = false; + } + _ => {} + }, + false => match key.code { + KeyCode::Char('s') => { + start_time = if self.is_started { + start_time + pause_time.elapsed() + } else { + Instant::now() + }; + self.is_started = true; + self.input_mode = InputMode::Editing; + } + KeyCode::Char('q') => { + // TODO: return canceled test error and handle it in main + return Ok(TestResults::new( + Stats::default(), + self.config.clone(), + false, + )); + } + KeyCode::Char('?') => { + self.show_help = true; + } + _ => {} + }, }, InputMode::Editing => match key.code { // Crossterm returns `ctrl+w` or ``ctrl+h` when `ctrl+backspace` is pressed @@ -246,18 +250,7 @@ impl Runner { // Then render help window on top if needed if self.show_help { - // Create a clear overlay to dim the background - let full_area = frame.size(); - frame.render_widget( - Paragraph::new("") - .style(Style::default().bg(Color::Black).fg(Color::White)) - .block(Block::default()), - full_area, - ); - - // Render the help window in the center - let help_area = centered_rect(60, 60, frame.size()); - self.help_window.render(frame, help_area); + self.help_window.render(frame) } } @@ -343,12 +336,12 @@ impl Runner { let help_message = match self.input_mode { InputMode::Normal => { if self.is_started { - "press 's' to unpause the test, 'q' to quit, '?' for help" + "press 's' to resume the test, 'q' to quit, '?' for help" } else { "press 's' to start the test, 'q' to quit, '?' for help" } } - InputMode::Editing => "press 'Esc' to pause the test", + InputMode::Editing => "press '' to pause the test", }; self.print_block_of_text( frame, @@ -654,8 +647,8 @@ mod test { vec![ vec![ ("30 seconds left", Color::Yellow), - (" ", Color::Reset), - ("press 'Esc' to pause the test", Color::Yellow), + (" ", Color::Reset), + ("press '' to pause the test", Color::Yellow), ], vec![ ("foobar", Color::Green), @@ -669,7 +662,8 @@ mod test { ); test_runner(&mut runner, buffer, |frame, runner| { - runner.render(frame, 30); + let duration = 30; + runner.render(frame, duration); }); } @@ -790,23 +784,3 @@ mod test { runner.move_cursor(&mut frame, area, input_current_line_len, current_line_index) } } - -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(popup_layout[1])[1] -}