Skip to main content

rec/replay/
prompt.rs

1//! Interactive prompts for replay.
2//!
3//! Provides step mode, destructive command confirmation, and error
4//! recovery prompts using dialoguer. All styling goes to stderr so
5//! it doesn't interfere with command output.
6
7use std::io::IsTerminal;
8
9use dialoguer::{Confirm, Select};
10
11/// User action for step mode.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum StepAction {
14    /// Execute this command
15    Run,
16    /// Skip this command
17    Skip,
18    /// Abort the entire replay
19    Abort,
20    /// Run all remaining commands without prompting
21    RunAll,
22}
23
24/// User action for error recovery.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ErrorAction {
27    /// Continue to the next command
28    Continue,
29    /// Abort the entire replay
30    Abort,
31    /// Retry the failed command
32    Retry,
33}
34
35/// Prompt the user for a step-mode action.
36///
37/// Shows the command with its index and total count, with a warning
38/// prefix if the command is destructive. Returns the user's chosen
39/// action.
40///
41/// On error or interrupt, returns `StepAction::Abort`.
42#[must_use]
43pub fn prompt_step(command: &str, index: usize, total: usize, is_destructive: bool) -> StepAction {
44    let prefix = if is_destructive {
45        "\x1b[31;1m\u{26a0}\x1b[0m "
46    } else {
47        ""
48    };
49
50    let prompt = format!(
51        "{}[{}/{}] \x1b[1m$ {}\x1b[0m",
52        prefix,
53        index + 1,
54        total,
55        command
56    );
57    eprintln!("{prompt}");
58
59    let items = &["Run", "Skip", "Abort", "Run all remaining"];
60    let selection = Select::new()
61        .with_prompt("Action")
62        .items(items)
63        .default(0)
64        .interact();
65
66    match selection {
67        Ok(0) => StepAction::Run,
68        Ok(1) => StepAction::Skip,
69        Ok(3) => StepAction::RunAll,
70        _ => StepAction::Abort,
71    }
72}
73
74/// Prompt for confirmation before executing a destructive command.
75///
76/// Prints a styled warning block to stderr with the command and
77/// reason for flagging. Returns `true` to execute, `false` to skip.
78///
79/// On error, returns `false` (safe default).
80#[must_use]
81pub fn prompt_destructive(command: &str, reason: &str) -> bool {
82    eprintln!();
83    eprintln!("  \x1b[31;1m\u{26a0} Destructive command detected\x1b[0m");
84    eprintln!("  Command: \x1b[1m{command}\x1b[0m");
85    eprintln!("  Reason:  {reason}");
86    eprintln!();
87
88    Confirm::new()
89        .with_prompt("Execute this command?")
90        .default(false)
91        .interact()
92        .unwrap_or(false)
93}
94
95/// Prompt for error recovery after a command fails.
96///
97/// Prints a styled error block to stderr with the exit code and
98/// command text. Returns the user's chosen recovery action.
99///
100/// On error, returns `ErrorAction::Abort`.
101#[must_use]
102pub fn prompt_error(command: &str, exit_code: Option<i32>) -> ErrorAction {
103    let code_str = exit_code.map_or_else(|| "unknown".to_string(), |c| c.to_string());
104
105    eprintln!();
106    eprintln!("  \x1b[31m\u{2717} Command failed\x1b[0m (exit code: {code_str})");
107    eprintln!("  $ {command}");
108    eprintln!();
109
110    let items = &["Continue", "Abort", "Retry"];
111    let selection = Select::new()
112        .with_prompt("What would you like to do?")
113        .items(items)
114        .default(0)
115        .interact();
116
117    match selection {
118        Ok(0) => ErrorAction::Continue,
119        Ok(2) => ErrorAction::Retry,
120        _ => ErrorAction::Abort,
121    }
122}
123
124/// Check if stdin is an interactive terminal.
125///
126/// Used by the replay engine to decide whether interactive prompts
127/// are available. In non-interactive mode (piped input), prompts
128/// should be skipped or the operation should error.
129#[must_use]
130pub fn is_interactive() -> bool {
131    std::io::stdin().is_terminal()
132}