use std::io::{self, IsTerminal};
use std::path::PathBuf;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crate::error::ForgeError;
use crate::ui::{
RawModeGuard, RenderOptions, StatusKind, confirm_key_help, overwrite_key_help, status_line,
stdout_line,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PromptOutcome<T> {
Confirmed(T),
Cancelled,
Interrupted,
Unavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OverwriteChoice {
All,
KeepExisting,
Custom,
}
pub(crate) fn confirm(message: &str, allow_stdin: bool) -> Result<PromptOutcome<()>, ForgeError> {
if !io::stdin().is_terminal() && !allow_stdin {
return Ok(PromptOutcome::Unavailable);
}
let _progress = crate::ui::progress::pause();
let options = RenderOptions::stdout();
stdout_line(&format!(
"{} {}",
status_line(StatusKind::Hint, message, &options),
confirm_key_help(&options)
));
let answer = if io::stdin().is_terminal() {
read_confirmation_key()?
} else {
read_user_line()?
};
let answer = answer.trim();
if answer.eq_ignore_ascii_case("y") {
Ok(PromptOutcome::Confirmed(()))
} else if answer.eq_ignore_ascii_case("interrupt") {
Ok(PromptOutcome::Interrupted)
} else {
Ok(PromptOutcome::Cancelled)
}
}
pub(crate) fn choose_overwrite(
message: &str,
) -> Result<PromptOutcome<OverwriteChoice>, ForgeError> {
if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
return Ok(PromptOutcome::Unavailable);
}
let _progress = crate::ui::progress::pause();
let options = RenderOptions::stdout();
stdout_line(&format!(
"{} {}",
status_line(StatusKind::Hint, message, &options),
overwrite_key_help(&options)
));
let mut raw_mode = RawModeGuard::acquire().map_err(|error| {
ForgeError::Command(format!("failed to read overwrite choice: {error}"))
})?;
let outcome = loop {
let event = event::read().map_err(|error| {
ForgeError::Command(format!("failed to read overwrite choice: {error}"))
})?;
let Event::Key(key) = event else {
continue;
};
if let Some(outcome) = overwrite_key_event(key) {
break outcome;
}
};
raw_mode.restore().map_err(|error| {
ForgeError::Command(format!("failed to restore terminal input mode: {error}"))
})?;
Ok(outcome)
}
pub(crate) fn read_user_line() -> Result<String, ForgeError> {
let mut line = String::new();
let bytes = io::stdin()
.read_line(&mut line)
.map_err(|source| ForgeError::Io {
path: PathBuf::from("stdin"),
source,
})?;
if bytes == 0 {
return Err(ForgeError::Io {
path: PathBuf::from("stdin"),
source: io::Error::new(io::ErrorKind::UnexpectedEof, "stdin closed"),
});
}
Ok(line)
}
pub(crate) fn wait_for_enter_or_interrupt() -> Result<(), ForgeError> {
if !io::stdin().is_terminal() {
read_user_line().map(|_| ())
} else {
let mut raw_mode = RawModeGuard::acquire().map_err(|source| ForgeError::Io {
path: PathBuf::from("stdin"),
source,
})?;
let result = loop {
let event = event::read().map_err(|source| ForgeError::Io {
path: PathBuf::from("stdin"),
source,
})?;
let Event::Key(key) = event else {
continue;
};
if key.kind != KeyEventKind::Press {
continue;
}
if key.code == KeyCode::Enter {
break Ok(());
}
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
break Err(ForgeError::Command(
"operation interrupted by Ctrl-C".to_string(),
));
}
};
raw_mode.restore().map_err(|source| ForgeError::Io {
path: PathBuf::from("stdin"),
source,
})?;
result
}
}
fn read_confirmation_key() -> Result<String, ForgeError> {
let mut raw_mode = RawModeGuard::acquire().map_err(|source| ForgeError::Io {
path: PathBuf::from("stdin"),
source,
})?;
let answer = loop {
let event = event::read().map_err(|source| ForgeError::Io {
path: PathBuf::from("stdin"),
source,
})?;
let Event::Key(key) = event else {
continue;
};
if key.kind != KeyEventKind::Press {
continue;
}
break confirmation_key(key);
};
raw_mode.restore().map_err(|source| ForgeError::Io {
path: PathBuf::from("stdin"),
source,
})?;
Ok(answer)
}
fn confirmation_key(key: KeyEvent) -> String {
match key.code {
KeyCode::Char('y' | 'Y') => "y".to_string(),
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
"interrupt".to_string()
}
_ => String::new(),
}
}
fn overwrite_key(key: KeyEvent) -> Option<PromptOutcome<OverwriteChoice>> {
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
return Some(PromptOutcome::Interrupted);
}
match key.code {
KeyCode::Char('y' | 'Y') => Some(PromptOutcome::Confirmed(OverwriteChoice::All)),
KeyCode::Char('n' | 'N') | KeyCode::Esc => {
Some(PromptOutcome::Confirmed(OverwriteChoice::KeepExisting))
}
KeyCode::Char('c' | 'C') => Some(PromptOutcome::Confirmed(OverwriteChoice::Custom)),
_ => None,
}
}
fn overwrite_key_event(key: KeyEvent) -> Option<PromptOutcome<OverwriteChoice>> {
(key.kind == KeyEventKind::Press)
.then(|| overwrite_key(key))
.flatten()
}
#[cfg(test)]
mod tests {
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crate::ui::input::{
OverwriteChoice, PromptOutcome, confirmation_key, overwrite_key, overwrite_key_event,
};
#[test]
fn confirmation_keys_have_stable_outcomes() {
assert_eq!(
confirmation_key(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)),
"y"
);
assert_eq!(
confirmation_key(KeyEvent::new(KeyCode::Char('N'), KeyModifiers::NONE)),
""
);
assert_eq!(
confirmation_key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
"interrupt"
);
}
#[test]
fn overwrite_keys_have_stable_outcomes() {
for (key, expected) in [
(
KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::NONE),
PromptOutcome::Confirmed(OverwriteChoice::All),
),
(
KeyEvent::new(KeyCode::Char('N'), KeyModifiers::NONE),
PromptOutcome::Confirmed(OverwriteChoice::KeepExisting),
),
(
KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE),
PromptOutcome::Confirmed(OverwriteChoice::Custom),
),
(
KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
PromptOutcome::Confirmed(OverwriteChoice::KeepExisting),
),
(
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
PromptOutcome::Interrupted,
),
] {
assert_eq!(overwrite_key(key), Some(expected));
}
assert_eq!(
overwrite_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
None
);
for kind in [KeyEventKind::Repeat, KeyEventKind::Release] {
assert_eq!(
overwrite_key_event(KeyEvent::new_with_kind(
KeyCode::Char('Y'),
KeyModifiers::NONE,
kind,
)),
None
);
}
}
}