use rskit_errors::AppResult;
use super::{cancelled, closed_input, non_interactive_error, with_raw_mode};
use crate::prompt::key::Key;
use crate::prompt::mode::PromptMode;
use crate::prompt::render::{self, Style};
use crate::prompt::terminal::Terminal;
use crate::prompt::validate::Validator;
pub fn run(
terminal: &mut (impl Terminal + ?Sized),
style: Style,
mode: PromptMode,
prompt: &str,
default: Option<&str>,
validator: Option<&dyn Validator>,
) -> AppResult<String> {
if !mode.is_interactive() {
let value = default.ok_or_else(|| non_interactive_error(prompt))?;
if let Some(v) = validator {
v.validate(value).map_err(|reason| {
rskit_errors::AppError::invalid_input(
"prompt",
format!("default for {prompt} is invalid: {reason}"),
)
})?;
}
return Ok(value.to_string());
}
if terminal.capabilities().is_key_driven() {
key_driven(terminal, style, prompt, default, validator)
} else {
line_driven(terminal, style, prompt, default, validator)
}
}
fn accept(
value: &str,
default: Option<&str>,
validator: Option<&dyn Validator>,
) -> Result<Option<String>, String> {
let resolved = if value.is_empty() {
default
} else {
Some(value)
};
let Some(resolved) = resolved else {
return Ok(None);
};
if let Some(v) = validator {
v.validate(resolved)?;
}
Ok(Some(resolved.to_string()))
}
fn key_driven(
terminal: &mut (impl Terminal + ?Sized),
style: Style,
prompt: &str,
default: Option<&str>,
validator: Option<&dyn Validator>,
) -> AppResult<String> {
terminal.write_line(&heading(style, prompt, default))?;
with_raw_mode(terminal, |terminal| {
let mut buffer = String::new();
let mut error: Option<String> = None;
let mut drawn = draw(terminal, style, &buffer, error.as_deref())?;
loop {
match terminal.read_key()? {
Key::Enter => match accept(buffer.trim(), default, validator) {
Ok(Some(value)) => return Ok(value),
Ok(None) => error = Some("a value is required".to_string()),
Err(reason) => error = Some(reason),
},
Key::Escape | Key::Interrupt => return Err(cancelled(prompt)),
Key::Backspace => {
buffer.pop();
error = None;
}
Key::Space => {
buffer.push(' ');
error = None;
}
Key::Char(c) => {
buffer.push(c);
error = None;
}
_ => continue,
}
terminal.clear_last_lines(drawn)?;
drawn = draw(terminal, style, &buffer, error.as_deref())?;
}
})
}
fn draw(
terminal: &mut (impl Terminal + ?Sized),
style: Style,
buffer: &str,
error: Option<&str>,
) -> AppResult<u16> {
terminal.write_line(&format!(
" {} {buffer}",
style.palette().dim(style.glyphs().answer())
))?;
let mut lines = 1;
if let Some(message) = error {
terminal.write_line(&format!(" {}", style.palette().warn(message)))?;
lines += 1;
}
terminal.flush()?;
Ok(lines)
}
fn line_driven(
terminal: &mut (impl Terminal + ?Sized),
style: Style,
prompt: &str,
default: Option<&str>,
validator: Option<&dyn Validator>,
) -> AppResult<String> {
loop {
terminal.write(&format!("{}: ", heading(style, prompt, default)))?;
terminal.flush()?;
let Some(line) = terminal.read_line()? else {
return Err(closed_input(prompt));
};
match accept(line.trim(), default, validator) {
Ok(Some(value)) => return Ok(value),
Ok(None) => {
let notice = style.palette().warn("a value is required").into_owned();
terminal.write_line(&format!(" {notice}"))?;
}
Err(reason) => {
let notice = style.palette().warn(&reason).into_owned();
terminal.write_line(&format!(" {notice}"))?;
}
}
}
}
fn heading(style: Style, prompt: &str, default: Option<&str>) -> String {
let base = render::heading(style, prompt);
match default {
Some(value) => format!("{base} [{value}]"),
None => base,
}
}