| use std::io::{self, IsTerminal}; |
|
|
| use anyhow::Result; |
| use colored::Colorize; |
| use crossterm::execute; |
| use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; |
| use rustyline::DefaultEditor; |
| use tracing::debug; |
|
|
| |
| |
| |
| |
| |
| |
| fn strip_bracketed_paste(s: &str) -> String { |
| s.replace("\x1b[200~", "").replace("\x1b[201~", "") |
| } |
|
|
| |
| pub struct InputBuilder { |
| pub(crate) message: String, |
| pub(crate) allow_empty: bool, |
| pub(crate) default: Option<String>, |
| pub(crate) default_display: Option<String>, |
| } |
|
|
| impl InputBuilder { |
| |
| pub fn allow_empty(mut self, allow: bool) -> Self { |
| self.allow_empty = allow; |
| self |
| } |
|
|
| |
| pub fn with_default<T>(mut self, default: T) -> Self |
| where |
| T: std::fmt::Display + AsRef<str>, |
| { |
| self.default = Some(default.as_ref().to_string()); |
| self.default_display = Some(default.to_string()); |
| self |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub fn prompt(self) -> Result<Option<String>> { |
| |
| |
| if !std::io::stdin().is_terminal() { |
| return Ok(None); |
| } |
|
|
| |
| |
| |
| |
| |
| let _guard = AlternateScreenGuard::enter(); |
|
|
| let mut rl = DefaultEditor::new()?; |
|
|
| |
| |
| let prompt_str = if cfg!(windows) { |
| format!("? {}: ", self.message) |
| } else { |
| format!("{} {}: ", "?".yellow().bold(), self.message.bold()) |
| }; |
|
|
| let initial = self.default.as_deref().unwrap_or(""); |
|
|
| loop { |
| let readline = rl.readline_with_initial(&prompt_str, (initial, "")); |
| debug!(output = ?readline, "Readline input"); |
| let line = match readline { |
| Ok(s) => s, |
| Err(rustyline::error::ReadlineError::Eof) |
| | Err(rustyline::error::ReadlineError::Interrupted) => return Ok(None), |
| Err(e) => return Err(e.into()), |
| }; |
|
|
| let value = strip_bracketed_paste(&line); |
| let trimmed = value.trim(); |
|
|
| if trimmed.is_empty() { |
| if let Some(ref default_val) = self.default { |
| return Ok(Some(default_val.clone())); |
| } |
| if self.allow_empty { |
| return Ok(Some(String::new())); |
| } |
| continue; |
| } |
|
|
| return Ok(Some(trimmed.to_string())); |
| } |
| } |
| } |
|
|
| |
| |
| |
| struct AlternateScreenGuard; |
|
|
| impl AlternateScreenGuard { |
| fn enter() -> Option<Self> { |
| execute!(io::stdout(), EnterAlternateScreen).ok()?; |
| Some(Self) |
| } |
| } |
|
|
| impl Drop for AlternateScreenGuard { |
| fn drop(&mut self) { |
| let _ = execute!(io::stdout(), LeaveAlternateScreen); |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use pretty_assertions::assert_eq; |
|
|
| use super::*; |
| use crate::ForgeWidget; |
|
|
| #[test] |
| fn test_input_builder_creates() { |
| let builder = ForgeWidget::input("Enter name"); |
| assert_eq!(builder.message, "Enter name"); |
| assert_eq!(builder.allow_empty, false); |
| } |
|
|
| #[test] |
| fn test_input_builder_with_default() { |
| let builder = ForgeWidget::input("Enter key").with_default("mykey"); |
| assert_eq!(builder.default, Some("mykey".to_string())); |
| } |
|
|
| #[test] |
| fn test_input_builder_allow_empty() { |
| let builder = ForgeWidget::input("Enter").allow_empty(true); |
| assert_eq!(builder.allow_empty, true); |
| } |
|
|
| #[test] |
| fn test_strip_bracketed_paste() { |
| let fixture = "\x1b[200~myapikey\x1b[201~"; |
| let actual = strip_bracketed_paste(fixture); |
| let expected = "myapikey"; |
| assert_eq!(actual, expected); |
|
|
| let fixture = "myapikey"; |
| let actual = strip_bracketed_paste(fixture); |
| let expected = "myapikey"; |
| assert_eq!(actual, expected); |
|
|
| let fixture = "\x1b[200~myapikey"; |
| let actual = strip_bracketed_paste(fixture); |
| let expected = "myapikey"; |
| assert_eq!(actual, expected); |
| } |
| } |
|
|