External panelize: command history with Up/Down arrow navigation

Added command history to ExternalPanelizeDialog:
- history: Vec<String> stores previously-run commands (deduped)
- history_idx: Option<usize> tracks position during navigation
- Up arrow: navigate to older entries (backward through history)
- Down arrow: navigate to newer entries (forward), exit to live input
- Typing any character exits history mode
- Successful command runs push to history (dedup consecutive)
- Render hint shows '↑↓ history' indicator

Self-contained — no changes to Input widget required.
1433 tests pass, zero warnings.
This commit is contained in:
2026-07-08 19:43:43 +03:00
parent 1b1e137961
commit 30f696e0fa
@@ -46,6 +46,10 @@ pub struct ExternalPanelizeDialog {
last_error: Option<String>,
/// Last stderr captured from the command (for the status line).
last_stderr: Option<String>,
/// Previous successfully-run commands, most recent last.
history: Vec<String>,
/// Current position in history when navigating (None = live input).
history_idx: Option<usize>,
}
impl ExternalPanelizeDialog {
@@ -59,6 +63,8 @@ impl ExternalPanelizeDialog {
command,
last_error: None,
last_stderr: None,
history: Vec::new(),
history_idx: None,
}
}
@@ -85,13 +91,35 @@ impl ExternalPanelizeDialog {
match key {
Key::ESCAPE => ExternalPanelizeOutcome::Cancel,
Key::ENTER => self.run_command(),
// Up/Down arrow — navigate command history.
k if k.code == 0x2191 || k.code == 0x2193 => {
self.navigate_history(k.code == 0x2191);
ExternalPanelizeOutcome::Running
}
_ => {
let _ = self.command.handle_key(key);
self.history_idx = None; // exit history mode on typing
ExternalPanelizeOutcome::Running
}
}
}
/// Navigate command history. `up` = true means go backward (older
/// entries), false = go forward (newer).
fn navigate_history(&mut self, up: bool) {
if self.history.is_empty() {
return;
}
let new_idx = match self.history_idx {
Some(i) if up => i.saturating_sub(1),
Some(i) => (i + 1).min(self.history.len() - 1),
None if up => self.history.len() - 1,
None => return, // at live input, down does nothing
};
self.history_idx = Some(new_idx);
self.command.set_text(self.history[new_idx].clone());
}
/// Run the current command (if non-empty) and translate the result
/// into an [`ExternalPanelizeOutcome`].
fn run_command(&mut self) -> ExternalPanelizeOutcome {
@@ -113,6 +141,10 @@ impl ExternalPanelizeDialog {
self.last_error = Some("command produced no paths".to_string());
ExternalPanelizeOutcome::Empty
} else {
// Push to history on success, dedup consecutive entries.
if self.history.last() != Some(&cmd) {
self.history.push(cmd);
}
self.last_error = None;
ExternalPanelizeOutcome::Apply(paths)
}
@@ -143,6 +175,11 @@ impl ExternalPanelizeDialog {
.split(inner);
let hint = Line::from(vec![
Span::styled("↑↓", Style::default().fg(theme.info)),
Span::styled(
" history ",
Style::default().fg(theme.hidden),
),
Span::styled("Enter", Style::default().fg(theme.warning)),
Span::styled(
format!(" {} ", crate::locale::t("dialog_action_select")),