diff --git a/hh/src/app.rs b/hh/src/app.rs index 5f7dfb2..cf07c23 100644 --- a/hh/src/app.rs +++ b/hh/src/app.rs @@ -95,6 +95,8 @@ pub struct App { pub chat_scroll: usize, /// Sandbox terminal scrollback: rows scrolled up from the bottom. pub sbx_scroll: usize, + /// Whether the help overlay is showing. + pub show_help: bool, } impl App { @@ -115,6 +117,7 @@ impl App { transfers: HashMap::new(), chat_scroll: 0, sbx_scroll: 0, + show_help: false, } } @@ -405,7 +408,11 @@ pub async fn run(session: Session, theme: Theme) -> Result<()> { if k.modifiers.contains(KeyModifiers::CONTROL) && matches!(k.code, KeyCode::Char('q')) { break Ok(()); } - if k.code == KeyCode::F(2) { + if app.show_help { + app.show_help = false; // any key dismisses the overlay + } else if k.code == KeyCode::F(1) { + app.show_help = true; // F1 from any mode + } else if k.code == KeyCode::F(2) { if app.sandbox.is_none() { } else if app.can_drive() { app.driving = !app.driving; @@ -592,7 +599,9 @@ fn handle_command( term: &Terminal>, ) { let room = &session.room; - if line == "/drive" { + if line == "/help" || line == "/?" { + app.show_help = true; + } else if line == "/drive" { // Mobile-friendly alternative to F2 (no function key needed). if app.sandbox.is_none() { app.sys("no sandbox running — /sbx launch first"); diff --git a/hh/src/ui.rs b/hh/src/ui.rs index 4bb8c9b..d4487fc 100644 --- a/hh/src/ui.rs +++ b/hh/src/ui.rs @@ -2,10 +2,10 @@ use crate::app::{App, ChatLine}; use crate::theme::Theme; -use ratatui::layout::{Constraint, Layout, Position}; +use ratatui::layout::{Constraint, Layout, Position, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, List, ListItem, Paragraph, Wrap}; +use ratatui::widgets::{Block, Clear, List, ListItem, Paragraph, Wrap}; use ratatui::Frame; pub fn draw(f: &mut Frame, app: &App, theme: &Theme) { @@ -35,6 +35,84 @@ pub fn draw(f: &mut Frame, app: &App, theme: &Theme) { draw_sandbox(f, area, app, theme); } draw_input(f, rows[2], app, theme); + + if app.show_help { + draw_help(f, f.area(), theme); + } +} + +fn centered(percent_x: u16, percent_y: u16, area: Rect) -> Rect { + let vy = (100u16.saturating_sub(percent_y)) / 2; + let vx = (100u16.saturating_sub(percent_x)) / 2; + let col = Layout::vertical([ + Constraint::Percentage(vy), + Constraint::Percentage(percent_y), + Constraint::Percentage(vy), + ]) + .split(area)[1]; + Layout::horizontal([ + Constraint::Percentage(vx), + Constraint::Percentage(percent_x), + Constraint::Percentage(vx), + ]) + .split(col)[1] +} + +fn draw_help(f: &mut Frame, area: Rect, theme: &Theme) { + let acc = Style::default().fg(theme.accent).add_modifier(Modifier::BOLD); + let key = Style::default().fg(theme.title); + let dim = Style::default().fg(theme.system); + let kv = |k: &str, v: &str| { + Line::from(vec![ + Span::styled(format!(" {k:<26}"), key), + Span::styled(v.to_string(), dim), + ]) + }; + let head = |s: &str| Line::from(Span::styled(s.to_string(), acc)); + let lines = vec![ + head("⛧ COMMANDS (type in the input bar)"), + kv("/sbx launch [backend]", "summon a sandbox: local | docker | multipass"), + kv("/sbx stop", "tear down the sandbox (purges the VM)"), + kv("/drive", "type into the shared shell (Esc releases)"), + kv("/grant ", "let a member drive the shell (owner)"), + kv("/revoke ", "take back drive permission (owner)"), + kv("/sudo ", "delegate VM superuser (real sudo) (owner)"), + kv("/unsudo ", "revoke VM superuser (owner)"), + kv("/send ", "offer a file to the room"), + kv("/sendd ", "offer a directory (sent as a tar)"), + kv("/accept · /reject", "respond to an incoming file offer"), + kv("/help", "show / hide this menu"), + Line::from(""), + head("⛧ KEYS"), + kv("Enter", "send chat message"), + kv("F1 · /help", "toggle this help (any key closes it)"), + kv("F2 · /drive", "take the shell · Esc releases it"), + kv("Ctrl-C (while driving)", "interrupt the running command"), + kv("PgUp / PgDn", "scroll chat · Home/End = oldest/live"), + kv("Up / Down", "scroll the sandbox terminal (when not driving)"), + kv("Ctrl-Q", "quit hack-house"), + Line::from(""), + head("⛧ ROSTER GLYPHS"), + kv("⛧ owner ⚡ sudoer", "◆ may drive • member"), + Line::from(""), + Line::from(Span::styled( + " malware bless · press any key to close", + Style::default().fg(theme.dim).add_modifier(Modifier::ITALIC), + )), + ]; + let w = centered(78, 90, area); + f.render_widget(Clear, w); + let help = Paragraph::new(lines) + .block( + Block::bordered() + .border_style(Style::default().fg(theme.accent)) + .title(Span::styled( + " ⛧ hack-house — help ⛧ ", + Style::default().fg(theme.title).add_modifier(Modifier::BOLD), + )), + ) + .wrap(Wrap { trim: false }); + f.render_widget(help, w); } fn draw_sandbox(f: &mut Frame, area: ratatui::layout::Rect, app: &App, theme: &Theme) {