feat(hh): help overlay (F1 / /help)
Centered modal listing every command, keybinding, and roster glyph. Opens with F1 (desktop) or the /help command (phone-friendly, since F-keys aren't on the Termux keyboard); any key closes it. Rendered with a Clear overlay so it floats above the panes. Works from chat or drive mode; Ctrl-Q still quits. 9 tests pass; clean build; verified live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
51bc85e078
commit
8e6365a649
|
|
@ -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<CrosstermBackend<std::io::Stdout>>,
|
||||
) {
|
||||
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");
|
||||
|
|
|
|||
82
hh/src/ui.rs
82
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 <user>", "let a member drive the shell (owner)"),
|
||||
kv("/revoke <user>", "take back drive permission (owner)"),
|
||||
kv("/sudo <user>", "delegate VM superuser (real sudo) (owner)"),
|
||||
kv("/unsudo <user>", "revoke VM superuser (owner)"),
|
||||
kv("/send <file>", "offer a file to the room"),
|
||||
kv("/sendd <dir>", "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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user