use crate::color::Color;
use crossterm::{
style::{Print, PrintStyledContent, ResetColor, SetForegroundColor, Stylize},
ExecutableCommand,
};
use std::io::{self, BufRead, BufReader, Write};
#[derive(Debug, Clone)]
pub struct InputConfig {
pub prefix: String,
pub prompt: String,
pub prefix_color: Color,
pub prompt_color: Color,
pub input_text_color: Color,
pub max_chars_per_line: usize,
pub indent_level: usize,
}
impl Default for InputConfig {
fn default() -> Self {
Self {
prefix: String::new(),
prompt: String::from(">> "),
prefix_color: Color::Blue,
prompt_color: Color::White,
input_text_color: Color::White,
max_chars_per_line: 80,
indent_level: 0,
}
}
}
pub fn read_input(cfg: &InputConfig) -> io::Result<String> {
let mut stdout = io::stdout();
if cfg.indent_level > 0 {
let indent = " ".repeat(cfg.indent_level);
stdout.execute(Print(indent))?;
}
if !cfg.prefix.is_empty() {
stdout.execute(PrintStyledContent(
cfg.prefix.clone().with(cfg.prefix_color.into()),
))?;
}
stdout.execute(PrintStyledContent(
cfg.prompt.clone().with(cfg.prompt_color.into()),
))?;
stdout.flush()?;
stdout.execute(SetForegroundColor(cfg.input_text_color.into()))?;
let mut buf = String::new();
let bytes = io::stdin().read_line(&mut buf)?;
stdout.execute(ResetColor)?;
if bytes == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"EOF while reading input",
));
}
if buf.ends_with('\n') {
buf.pop();
if buf.ends_with('\r') {
buf.pop();
}
}
Ok(buf)
}
pub fn read_multiline_input(cfg: &InputConfig, terminator: &str) -> io::Result<String> {
let mut stdout = io::stdout();
if cfg.indent_level > 0 {
let indent = " ".repeat(cfg.indent_level);
stdout.execute(Print(indent.clone()))?;
}
if !cfg.prefix.is_empty() {
stdout.execute(PrintStyledContent(
cfg.prefix.clone().with(cfg.prefix_color.into()),
))?;
}
let header = format!("{} (end with '{}' on new line)\n", cfg.prompt, terminator);
stdout.execute(PrintStyledContent(header.with(cfg.prompt_color.into())))?;
stdout.flush()?;
let stdin = io::stdin();
let reader = BufReader::new(stdin.lock());
let mut lines = Vec::new();
for line in reader.lines() {
let input = line?;
if input.trim() == terminator {
break;
}
lines.push(input);
}
Ok(lines.join("\n"))
}
pub(crate) fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
if current.len() + word.len() + 1 > max_width {
lines.push(current.trim_end().to_string());
current.clear();
}
current.push_str(word);
current.push(' ');
}
if !current.is_empty() {
lines.push(current.trim_end().to_string());
}
lines
}
pub fn read_secret_input(cfg: &InputConfig) -> io::Result<String> {
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
style::{Print, PrintStyledContent},
terminal::{disable_raw_mode, enable_raw_mode},
ExecutableCommand,
};
use std::io::{self, Write};
let mut stdout = io::stdout();
if cfg.indent_level > 0 {
let indent = " ".repeat(cfg.indent_level);
stdout.execute(Print(indent))?;
}
if !cfg.prefix.is_empty() {
stdout.execute(PrintStyledContent(
cfg.prefix.clone().with(cfg.prefix_color.into()),
))?;
}
stdout.execute(PrintStyledContent(
cfg.prompt.clone().with(cfg.prompt_color.into()),
))?;
stdout.flush()?;
enable_raw_mode()?;
let mut input = String::new();
loop {
if let Event::Key(KeyEvent {
code, modifiers, ..
}) = event::read()?
{
if code == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL) {
disable_raw_mode()?;
println!();
return Err(io::Error::new(io::ErrorKind::Interrupted, "Input canceled"));
}
match code {
KeyCode::Enter => break,
KeyCode::Char(c) => input.push(c),
KeyCode::Backspace => {
input.pop();
}
_ => {}
}
}
}
disable_raw_mode()?;
println!();
Ok(input)
}