Implement TUI colon command mode

This commit is contained in:
Hermes Agent
2026-08-10 08:28:55 +00:00
parent 639ae0d08e
commit d66d9b0f08
7 changed files with 1502 additions and 89 deletions

View File

@@ -78,6 +78,15 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
}
if app.mode() == Mode::Help {
if let Some(help) = app.command_help() {
frame.render_widget(
Paragraph::new(help)
.block(Block::bordered().title("Command help"))
.wrap(Wrap { trim: false }),
area,
);
return;
}
let lines = help_actions(app.help_context_mode()).map(|spec| {
let bindings = spec
.bindings
@@ -231,6 +240,10 @@ fn main_text(app: &App) -> String {
Mode::Dialog if app.discard_confirmation() => {
"Discard all unsaved edits? Press y to discard, n or Esc to keep editing.".to_owned()
}
Mode::Dialog if app.command_confirmation_message().is_some() => app
.command_confirmation_message()
.unwrap_or_default()
.to_owned(),
Mode::Dialog => "Complete or cancel the active dialog.".to_owned(),
Mode::Command => "Enter a command on the bottom line.".to_owned(),
Mode::Help | Mode::Locked => String::new(),
@@ -418,7 +431,11 @@ fn context_line(app: &App) -> Paragraph<'static> {
fn prompt_line(app: &App) -> Paragraph<'static> {
match app.mode() {
Mode::Command => Paragraph::new(":").style(Style::default().fg(Color::Yellow)),
Mode::Command => Paragraph::new(format!(":{}", app.command_line().display()))
.style(Style::default().fg(Color::Yellow)),
Mode::Dialog if app.command_confirmation_message().is_some() => {
Paragraph::new("confirm> y / n / Esc").style(Style::default().fg(Color::Yellow))
}
Mode::Dialog => Paragraph::new("dialog> ").style(Style::default().fg(Color::Yellow)),
Mode::Editor if app.editor().is_some_and(EntryEditor::is_input_active) => {
Paragraph::new("-- INSERT -- Esc stops input; Tab changes field; C-s saves")
@@ -513,6 +530,51 @@ mod tests {
assert!(!viewer_help.contains("insert entry"));
}
#[test]
fn command_prompt_help_and_confirmation_are_rendered_without_secrets() {
let mut app = App::new();
app.dispatch(crate::action::Action::Command);
for character in "otp validate otpauth://totp/test?secret=NEVER-SHOW".chars() {
app.handle_command_input(
crossterm::event::KeyCode::Char(character),
crossterm::event::KeyModifiers::NONE,
);
}
let prompt = render(120, 24, &app);
assert!(prompt.contains("hidden secret-bearing arguments"));
assert!(!prompt.contains("NEVER-SHOW"));
app.dispatch(crate::action::Action::Cancel);
app.dispatch(crate::action::Action::Command);
for character in "help git".chars() {
app.handle_command_input(
crossterm::event::KeyCode::Char(character),
crossterm::event::KeyModifiers::NONE,
);
}
app.handle_command_input(
crossterm::event::KeyCode::Enter,
crossterm::event::KeyModifiers::NONE,
);
assert!(render(120, 30, &app).contains("Usage: git"));
app.dispatch(crate::action::Action::Cancel);
app.dispatch(crate::action::Action::Command);
for character in "remove old/entry".chars() {
app.handle_command_input(
crossterm::event::KeyCode::Char(character),
crossterm::event::KeyModifiers::NONE,
);
}
app.handle_command_input(
crossterm::event::KeyCode::Enter,
crossterm::event::KeyModifiers::NONE,
);
let confirmation = render(120, 24, &app);
assert!(confirmation.contains("Confirm remove"));
assert!(confirmation.contains("confirm> y / n / Esc"));
}
#[test]
fn hierarchy_selection_and_storage_indicators_have_stable_rendering() {
let mut app = App::new();