pub mod confirm;
pub mod multi_select;
pub mod select;
pub mod text;
use rskit_errors::AppError;
use super::render::Style;
use super::terminal::Terminal;
fn non_interactive_error(prompt: &str) -> AppError {
AppError::invalid_input(
"prompt",
format!("non-interactive mode requires a default for: {prompt}"),
)
}
fn closed_input(prompt: &str) -> AppError {
AppError::invalid_input("prompt", format!("input closed before answering: {prompt}"))
}
fn cancelled(prompt: &str) -> AppError {
AppError::cancelled(format!("prompt cancelled: {prompt}"))
}
fn with_raw_mode<T, R>(
terminal: &mut T,
body: impl FnOnce(&mut T) -> rskit_errors::AppResult<R>,
) -> rskit_errors::AppResult<R>
where
T: Terminal + ?Sized,
{
terminal.begin_interactive()?;
let mut guard = RawModeGuard {
terminal,
armed: true,
};
let result = body(&mut *guard.terminal);
let teardown = guard.disarm();
match result {
Ok(value) => teardown.map(|()| value),
Err(error) => Err(error),
}
}
struct RawModeGuard<'a, T: Terminal + ?Sized> {
terminal: &'a mut T,
armed: bool,
}
impl<T: Terminal + ?Sized> RawModeGuard<'_, T> {
fn disarm(&mut self) -> rskit_errors::AppResult<()> {
let result = self.terminal.end_interactive();
if result.is_ok() {
self.armed = false;
}
result
}
}
impl<T: Terminal + ?Sized> Drop for RawModeGuard<'_, T> {
fn drop(&mut self) {
if self.armed {
let _ = self.terminal.end_interactive();
}
}
}
const fn focus_up(cursor: usize, len: usize) -> usize {
if cursor == 0 { len - 1 } else { cursor - 1 }
}
const fn focus_down(cursor: usize, len: usize) -> usize {
if cursor + 1 >= len { 0 } else { cursor + 1 }
}
fn write_answer(
terminal: &mut (impl Terminal + ?Sized),
style: Style,
hint: Option<&str>,
) -> rskit_errors::AppResult<()> {
let marker = style.glyphs().answer();
let text = hint.map_or_else(
|| format!(" {marker} "),
|hint| format!(" {marker} {hint}: "),
);
terminal.write(&text)?;
terminal.flush()
}
fn notice(
terminal: &mut (impl Terminal + ?Sized),
style: Style,
text: &str,
) -> rskit_errors::AppResult<()> {
let styled = style.palette().warn(text).into_owned();
terminal.write_line(&format!(" {styled}"))
}
fn parse_index(input: &str, len: usize) -> Option<usize> {
let number: usize = input.parse().ok()?;
(1..=len).contains(&number).then(|| number - 1)
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use rskit_errors::{AppError, AppResult};
use super::{focus_down, focus_up, with_raw_mode};
use crate::prompt::terminal::{Capabilities, ScriptedTerminal, Terminal};
struct FlakyTeardown {
fail_remaining: Cell<u32>,
teardown_calls: Cell<u32>,
raw: Cell<bool>,
}
impl FlakyTeardown {
fn failing(times: u32) -> Self {
Self {
fail_remaining: Cell::new(times),
teardown_calls: Cell::new(0),
raw: Cell::new(false),
}
}
}
impl Terminal for FlakyTeardown {
fn capabilities(&self) -> Capabilities {
Capabilities::key_driven()
}
fn read_line(&mut self) -> AppResult<Option<String>> {
Ok(None)
}
fn read_key(&mut self) -> AppResult<crate::prompt::Key> {
Err(AppError::invalid_input("terminal", "no keys scripted"))
}
fn write(&mut self, _text: &str) -> AppResult<()> {
Ok(())
}
fn write_line(&mut self, _text: &str) -> AppResult<()> {
Ok(())
}
fn flush(&mut self) -> AppResult<()> {
Ok(())
}
fn clear_last_lines(&mut self, _count: u16) -> AppResult<()> {
Ok(())
}
fn begin_interactive(&mut self) -> AppResult<()> {
self.raw.set(true);
Ok(())
}
fn end_interactive(&mut self) -> AppResult<()> {
self.teardown_calls.set(self.teardown_calls.get() + 1);
let remaining = self.fail_remaining.get();
if remaining > 0 {
self.fail_remaining.set(remaining - 1);
return Err(AppError::invalid_input("terminal", "teardown failed"));
}
self.raw.set(false);
Ok(())
}
}
#[test]
fn focus_wraps_both_ends() {
assert_eq!(focus_up(0, 3), 2);
assert_eq!(focus_up(2, 3), 1);
assert_eq!(focus_down(2, 3), 0);
assert_eq!(focus_down(0, 3), 1);
}
#[test]
fn raw_mode_restored_on_normal_return() {
let mut term = ScriptedTerminal::key_driven();
let out = with_raw_mode(&mut term, |t| {
assert!(t.is_interactive());
Ok(7)
});
assert_eq!(out.expect("ok"), 7);
assert!(!term.is_interactive());
}
#[test]
fn raw_mode_restored_when_body_panics() {
let mut term = ScriptedTerminal::key_driven();
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
with_raw_mode(&mut term, |_| -> rskit_errors::AppResult<()> {
panic!("body blew up mid-prompt");
})
}));
assert!(unwound.is_err());
assert!(!term.is_interactive());
}
#[test]
fn failed_teardown_is_retried_by_drop_net() {
let mut term = FlakyTeardown::failing(1);
let out = with_raw_mode(&mut term, |t| {
assert!(t.capabilities().is_key_driven());
t.write("x")?;
t.write_line("y")?;
t.flush()?;
t.clear_last_lines(1)?;
assert_eq!(t.read_line()?, None);
assert!(t.read_key().is_err());
Ok(())
});
assert!(out.is_err());
assert_eq!(term.teardown_calls.get(), 2);
assert!(!term.raw.get());
}
}