From cb0abb3e3bd27835dd6c5df2f23e328ca6e7ca1a Mon Sep 17 00:00:00 2001 From: Simonas Kareiva Date: Thu, 21 May 2026 19:26:30 +0300 Subject: [PATCH] Move help to a separate dialog box --- src/tui.rs | 125 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 107 insertions(+), 18 deletions(-) diff --git a/src/tui.rs b/src/tui.rs index be57b95..31bafc5 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -7,7 +7,7 @@ use crossterm::terminal::{ use ratatui::backend::CrosstermBackend; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Color, Style}; -use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; +use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap}; use ratatui::{Frame, Terminal}; use std::io::{stdout, Stdout}; use std::time::{Duration, Instant}; @@ -324,6 +324,7 @@ fn main_loop( let (mut screen, mut log) = initial_state(prog, ®istry); let mut focus = default_focus(&screen); let mut editor = Editor::new(); + let mut help_open = false; // Resume case: if the bottom of the log is a LevelPrompt, animate it // from scratch so resumed sessions feel the same as fresh ones. @@ -336,7 +337,15 @@ fn main_loop( loop { terminal.draw(|frame| { - render(frame, &screen, focus, &editor, &log, typewriter.as_ref()); + render( + frame, + &screen, + focus, + &editor, + &log, + typewriter.as_ref(), + help_open, + ); })?; let poll_timeout = typewriter @@ -357,6 +366,7 @@ fn main_loop( &mut focus, &mut editor, &mut log, + &mut help_open, key, prog, ®istry, @@ -403,17 +413,32 @@ fn step( focus: &mut Focus, editor: &mut Editor, log: &mut HistoryLog, + help_open: &mut bool, key: KeyEvent, prog: &mut Progress, registry: &[Box], ) -> Result { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - // Global: Ctrl-Q quits from anywhere. + // Global: Ctrl-Q quits from anywhere, including the help dialog. if ctrl && key.code == KeyCode::Char('q') { return Ok(true); } + // Help: Ctrl-H opens (idempotent). + if ctrl && key.code == KeyCode::Char('h') { + *help_open = true; + return Ok(false); + } + + // While help is open, only Esc closes it; everything else is swallowed. + if *help_open { + if key.code == KeyCode::Esc { + *help_open = false; + } + return Ok(false); + } + // Global: Ctrl-X grades while in Game. if ctrl && key.code == KeyCode::Char('x') && matches!(screen, Screen::Game) { grade(editor.text(), prog, registry, log)?; @@ -582,6 +607,7 @@ fn render( editor: &Editor, log: &HistoryLog, typewriter: Option<&Typewriter>, + help_open: bool, ) { // Outer layout: two-column area on top, 1-row status bar across the // full width below. @@ -609,29 +635,98 @@ fn render( render_editor_inactive(frame, right); } - render_status_bar(frame, status, screen, focus); + render_status_bar(frame, status, screen, focus, help_open); + + // Help dialog overlay — drawn last so it sits on top of everything. + if help_open { + let area = centered_rect(70, 60, frame.size()); + frame.render_widget(Clear, area); + frame.render_widget(help_widget(), area); + } } -fn render_status_bar(frame: &mut Frame, area: Rect, screen: &Screen, focus: Focus) { +/// Center a rectangle of `pct_x` × `pct_y` percent inside `area`. +fn centered_rect(pct_x: u16, pct_y: u16, area: Rect) -> Rect { + let vertical = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage((100 - pct_y) / 2), + Constraint::Percentage(pct_y), + Constraint::Percentage((100 - pct_y) / 2), + ] + .as_ref(), + ) + .split(area); + + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage((100 - pct_x) / 2), + Constraint::Percentage(pct_x), + Constraint::Percentage((100 - pct_x) / 2), + ] + .as_ref(), + ) + .split(vertical[1])[1] +} + +fn help_widget<'a>() -> Paragraph<'a> { + let body = "\n\ + ⌨ Controls\n\ + \n\ + [Tab] swap focus between log and editor\n\ + [Ctrl-X] grade your YAML\n\ + [Ctrl-H] open this help\n\ + [Esc] close help · or focus log when editing\n\ + [↑/↓] scroll the log (when log is focused)\n\ + [PgUp/PgDn] scroll faster\n\ + [r] reset progress\n\ + [q] / [Ctrl-Q] quit\n\ + \n\ + Press [Esc] to close." + .to_string(); + Paragraph::new(body) + .block( + Block::default() + .borders(Borders::ALL) + .title(" 📜 Controls "), + ) + .wrap(Wrap { trim: false }) +} + +fn render_status_bar( + frame: &mut Frame, + area: Rect, + screen: &Screen, + focus: Focus, + help_open: bool, +) { let style = Style::default().fg(Color::White).bg(Color::DarkGray); - let p = Paragraph::new(status_text(screen, focus)).style(style); + let p = Paragraph::new(status_text(screen, focus, help_open)).style(style); frame.render_widget(p, area); } -fn status_text(screen: &Screen, focus: Focus) -> String { +fn status_text(screen: &Screen, focus: Focus, help_open: bool) -> String { + if help_open { + return " [Esc] close help · [Ctrl-Q] quit".into(); + } match (screen, focus) { (Screen::Welcome, _) => { - " [Enter] begin · [↑/↓] scroll · [PgUp/PgDn] page · [q] quit".into() + " [Enter] begin · [↑/↓] scroll · [PgUp/PgDn] page · [Ctrl-H] help · [q] quit".into() } (Screen::Game, Focus::Editor) => { - " [Ctrl-X] grade · [Tab] focus log · [Esc] focus log · [Ctrl-Q] quit".into() + " [Ctrl-X] grade · [Tab] focus log · [Esc] focus log · [Ctrl-H] help · [Ctrl-Q] quit" + .into() } (Screen::Game, Focus::Game) => { - " [Ctrl-X] grade · [Tab] focus editor · [↑/↓] scroll · [r] reset · [q] quit".into() + " [Ctrl-X] grade · [Tab] focus editor · [↑/↓] scroll · [r] reset · [Ctrl-H] help · [q] quit" + .into() } (Screen::ResetConfirm, _) => " [y] confirm wipe · any other key cancels".into(), (Screen::Completed, _) => { - " [↑/↓] scroll · [PgUp/PgDn] page · [r] reset · [q] quit".into() + " [↑/↓] scroll · [PgUp/PgDn] page · [r] reset · [Ctrl-H] help · [q] quit".into() } } } @@ -723,13 +818,7 @@ fn entry_lines(entry: &LogEntry) -> Vec { " Score ≥ 95 % → 🥇 Gold · ≥ 80 % → 🥈 Silver · ≥ 70 % → 🥉 Bronze.".to_string(), " Below 70 % means retry — refine your YAML and press Ctrl-X again.".to_string(), String::new(), - " ⌨ Controls".to_string(), - " [Tab] swap focus between log and editor".to_string(), - " [Ctrl-X] grade your YAML".to_string(), - " [↑/↓] scroll the log (when log is focused)".to_string(), - " [PgUp/PgDn] scroll faster".to_string(), - " [r] reset progress".to_string(), - " [q] / [Ctrl-Q] quit".to_string(), + " Press [Ctrl-H] any time for the full controls.".to_string(), ], LogEntry::LevelPrompt { level_id,