Implement TUI search and mutation workflows

This commit is contained in:
Hermes Agent
2026-08-10 10:11:34 +00:00
parent c86ea9eb0e
commit ce3e4a9f79
13 changed files with 1195 additions and 93 deletions

View File

@@ -170,6 +170,9 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
))
},
),
Mode::Browser if app.grep_view().is_some() => Paragraph::new(grep_lines(
app.grep_view().expect("checked decrypted search results"),
)),
_ => Paragraph::new(main_text(app)),
};
frame.render_widget(
@@ -252,10 +255,6 @@ 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(),
@@ -404,6 +403,37 @@ fn editor_lines(editor: &EntryEditor) -> Vec<Line<'_>> {
.collect()
}
fn grep_lines(view: &crate::search::GrepView) -> Vec<Line<'_>> {
if view.entries().is_empty() {
return vec![Line::from("No decrypted entries matched.")];
}
let mut lines = Vec::new();
for (index, entry) in view.entries().iter().enumerate() {
let selected = view.selected_index() == Some(index);
lines.push(Line::styled(
format!("{} {}", if selected { ">" } else { " " }, entry.path()),
if selected {
Style::default().bg(Color::Blue).fg(Color::White)
} else {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
},
));
for matched in entry.lines() {
let contents = std::str::from_utf8(matched.contents().expose())
.unwrap_or("[non-UTF-8 matched line]");
let prefix = if view.includes_line_numbers() {
format!("{}: ", matched.number())
} else {
String::new()
};
lines.push(Line::raw(format!(" {prefix}{contents}")));
}
}
lines
}
fn mode_title(mode: Mode) -> &'static str {
match mode {
Mode::Browser => "Browser",
@@ -437,6 +467,9 @@ fn context_line(app: &App) -> Paragraph<'static> {
if app.mode() == Mode::Dialog && app.workflow().is_some() {
return Paragraph::new("Tab/Shift-Tab focus Space toggle Ctrl-S submit Esc cancel");
}
if app.grep_view().is_some() {
return Paragraph::new("j/k result Enter open entry Esc close decrypted results");
}
let text = context_actions(app.mode())
.map(|spec| format!("{} {}", spec.bindings[0].display, spec.label))
.collect::<Vec<_>>()
@@ -448,9 +481,6 @@ fn prompt_line(app: &App) -> Paragraph<'static> {
match app.mode() {
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 if app.workflow().is_some() => {
Paragraph::new("form> ").style(Style::default().fg(Color::Yellow))
}
@@ -589,8 +619,9 @@ mod tests {
crossterm::event::KeyModifiers::NONE,
);
let confirmation = render(120, 24, &app);
assert!(confirmation.contains("Confirm remove"));
assert!(confirmation.contains("confirm> y / n / Esc"));
assert!(confirmation.contains("Remove entry or folder"));
assert!(confirmation.contains("Confirm permanent removal: no"));
assert!(confirmation.contains("Ctrl-S submit"));
}
#[test]