use std::borrow::Cow;
use crate::error::Result;
use reedline::{
Emacs, KeyCode, KeyModifiers, Keybindings, Prompt, PromptEditMode, PromptHistorySearch,
Reedline, ReedlineEvent, Signal, default_emacs_keybindings,
};
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum InputResult {
Submitted(String),
Cancelled,
}
impl InputResult {
pub(crate) fn into_result(self) -> crate::error::Result<String> {
match self {
InputResult::Submitted(text) => Ok(text),
InputResult::Cancelled => Err(crate::error::CruiseError::StepPaused),
}
}
}
struct CruisePrompt;
impl Prompt for CruisePrompt {
fn render_prompt_left(&self) -> Cow<'_, str> {
Cow::Borrowed("")
}
fn render_prompt_right(&self) -> Cow<'_, str> {
Cow::Borrowed("")
}
fn render_prompt_indicator(&self, _prompt_mode: PromptEditMode) -> Cow<'_, str> {
Cow::Borrowed("> ")
}
fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
Cow::Borrowed("... ")
}
fn render_prompt_history_search_indicator(
&self,
_history_search: PromptHistorySearch,
) -> Cow<'_, str> {
Cow::Borrowed("")
}
}
pub(crate) fn prompt_multiline(message: &str) -> Result<InputResult> {
println!("{message}");
let kb = build_keybindings();
let mut editor = build_reedline(kb);
let prompt = CruisePrompt;
loop {
let signal = editor.read_line(&prompt)?;
if let Some(result) = map_signal(signal) {
return Ok(result);
}
}
}
fn build_reedline(kb: Keybindings) -> Reedline {
let edit_mode = Box::new(Emacs::new(kb));
Reedline::create()
.use_kitty_keyboard_enhancement(true)
.with_edit_mode(edit_mode)
}
fn build_keybindings() -> Keybindings {
let mut kb = default_emacs_keybindings();
kb.add_binding(KeyModifiers::NONE, KeyCode::Esc, ReedlineEvent::CtrlC);
kb
}
fn map_signal(signal: Signal) -> Option<InputResult> {
match signal {
Signal::Success(text) if text.trim().is_empty() => None,
Signal::Success(text) => Some(InputResult::Submitted(text)),
Signal::CtrlC | Signal::CtrlD => Some(InputResult::Cancelled),
}
}
#[cfg(test)]
mod tests {
use super::*;
use reedline::{EditCommand, KeyCode, KeyModifiers, ReedlineEvent, Signal};
#[test]
fn test_into_result_submitted_returns_text() {
let result = InputResult::Submitted("add feature X".to_string()).into_result();
assert_eq!(result.unwrap_or_else(|e| panic!("{e:?}")), "add feature X");
}
#[test]
fn test_into_result_submitted_multiline_preserved() {
let multiline = "line1\nline2\nline3".to_string();
let result = InputResult::Submitted(multiline.clone()).into_result();
assert_eq!(result.unwrap_or_else(|e| panic!("{e:?}")), multiline);
}
#[test]
fn test_into_result_submitted_empty_string_returns_ok() {
let result = InputResult::Submitted(String::new()).into_result();
assert_eq!(result.unwrap_or_else(|e| panic!("{e:?}")), "");
}
#[test]
fn test_into_result_cancelled_returns_step_paused_err() {
let result = InputResult::Cancelled.into_result();
assert!(
matches!(result, Err(crate::error::CruiseError::StepPaused)),
"expected Err(StepPaused), got {result:?}"
);
}
#[test]
fn test_map_signal_success_nonempty_returns_submitted() {
let sig = Signal::Success("hello world".to_string());
let result = map_signal(sig);
assert_eq!(
result,
Some(InputResult::Submitted("hello world".to_string()))
);
}
#[test]
fn test_map_signal_success_multiline_returns_submitted() {
let sig = Signal::Success("line1\nline2".to_string());
let result = map_signal(sig);
assert_eq!(
result,
Some(InputResult::Submitted("line1\nline2".to_string()))
);
}
#[test]
fn test_map_signal_success_empty_returns_none() {
let sig = Signal::Success(String::new());
let result = map_signal(sig);
assert_eq!(result, None);
}
#[test]
fn test_map_signal_success_whitespace_only_returns_none() {
let sig = Signal::Success(" \t ".to_string());
let result = map_signal(sig);
assert_eq!(result, None);
}
#[test]
fn test_map_signal_success_blank_multiline_returns_none() {
let sig = Signal::Success(" \n \n ".to_string());
let result = map_signal(sig);
assert_eq!(result, None);
}
#[test]
fn test_map_signal_ctrl_c_returns_cancelled() {
let result = map_signal(Signal::CtrlC);
assert_eq!(result, Some(InputResult::Cancelled));
}
#[test]
fn test_map_signal_ctrl_d_returns_cancelled() {
let result = map_signal(Signal::CtrlD);
assert_eq!(result, Some(InputResult::Cancelled));
}
#[test]
fn test_keybindings_alt_enter_is_insert_newline() {
let kb = build_keybindings();
let binding = kb.find_binding(KeyModifiers::ALT, KeyCode::Enter);
assert_eq!(
binding,
Some(ReedlineEvent::Edit(vec![EditCommand::InsertNewline]))
);
}
#[test]
fn test_keybindings_shift_enter_is_insert_newline() {
let kb = build_keybindings();
let binding = kb.find_binding(KeyModifiers::SHIFT, KeyCode::Enter);
assert_eq!(
binding,
Some(ReedlineEvent::Edit(vec![EditCommand::InsertNewline]))
);
}
#[test]
fn test_keybindings_esc_maps_to_ctrl_c() {
let kb = build_keybindings();
let binding = kb.find_binding(KeyModifiers::NONE, KeyCode::Esc);
assert_eq!(binding, Some(ReedlineEvent::CtrlC));
}
}