use crossterm::{
cursor,
event::{self, Event, KeyCode, KeyModifiers},
execute, queue,
terminal::{self, ClearType},
};
use glyphs::{style, Color};
use std::io::{stdout, Write};
const ACCENT: Color = Color::Rgb { r: 212, g: 92, b: 235 }; const CURSOR_COLOR: Color = Color::Rgb { r: 255, g: 135, b: 175 }; const PLACEHOLDER: Color = Color::Rgb { r: 102, g: 102, b: 102 }; const TEXT_COLOR: Color = Color::Rgb { r: 255, g: 255, b: 255 };
pub fn input(prompt: impl Into<String>) -> Input {
Input::new(prompt)
}
pub struct Input {
prompt: String,
placeholder: String,
default: String,
password: bool,
char_limit: usize,
header: Option<String>,
}
impl Input {
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
placeholder: String::new(),
default: String::new(),
password: false,
char_limit: 0,
header: None,
}
}
#[must_use]
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
#[must_use]
pub fn default(mut self, default: impl Into<String>) -> Self {
self.default = default.into();
self
}
#[must_use]
pub fn password(mut self) -> Self {
self.password = true;
self
}
#[must_use]
pub fn char_limit(mut self, limit: usize) -> Self {
self.char_limit = limit;
self
}
#[must_use]
pub fn header(mut self, header: impl Into<String>) -> Self {
self.header = Some(header.into());
self
}
fn render(&self, value: &str, cursor_pos: usize) -> String {
let mut buffer = String::new();
if let Some(ref header) = self.header {
buffer.push_str(&format!("{}\r\n", style(header).fg(ACCENT).bold()));
}
buffer.push_str(&format!(" {} ", style(&self.prompt).fg(CURSOR_COLOR)));
if value.is_empty() && !self.placeholder.is_empty() {
buffer.push_str(&format!("{}", style(&self.placeholder).fg(PLACEHOLDER).dim()));
} else if self.password {
let dots: String = "●".repeat(value.len());
buffer.push_str(&format!("{}", style(&dots).fg(TEXT_COLOR)));
} else {
let (before, after) = value.split_at(cursor_pos.min(value.len()));
buffer.push_str(&format!("{}", style(before).fg(TEXT_COLOR)));
buffer.push_str(&format!("{}", style("│").fg(CURSOR_COLOR).bold()));
buffer.push_str(&format!("{}", style(after).fg(TEXT_COLOR)));
}
if value.is_empty() || cursor_pos >= value.len() {
if !value.is_empty() {
} else if self.placeholder.is_empty() {
buffer.push_str(&format!("{}", style("│").fg(CURSOR_COLOR).bold()));
}
}
buffer.push_str("\r\n");
buffer
}
pub fn run(self) -> String {
let mut value = self.default.clone();
let mut cursor_pos = value.len();
let mut stdout = stdout();
terminal::enable_raw_mode().expect("Failed to enable raw mode");
execute!(stdout, cursor::Hide).ok();
let has_header = self.header.is_some();
let total_lines = if has_header { 2 } else { 1 };
let buffer = self.render(&value, cursor_pos);
write!(stdout, "{}", buffer).ok();
stdout.flush().ok();
loop {
if let Ok(Event::Key(key)) = event::read() {
match key.code {
KeyCode::Enter => break,
KeyCode::Esc => {
value.clear();
break;
}
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
value.clear();
break;
}
KeyCode::Backspace => {
if cursor_pos > 0 {
cursor_pos -= 1;
value.remove(cursor_pos);
}
}
KeyCode::Delete => {
if cursor_pos < value.len() {
value.remove(cursor_pos);
}
}
KeyCode::Left => {
if cursor_pos > 0 {
cursor_pos -= 1;
}
}
KeyCode::Right => {
if cursor_pos < value.len() {
cursor_pos += 1;
}
}
KeyCode::Home => cursor_pos = 0,
KeyCode::End => cursor_pos = value.len(),
KeyCode::Char(c) => {
if self.char_limit == 0 || value.len() < self.char_limit {
value.insert(cursor_pos, c);
cursor_pos += 1;
}
}
_ => continue,
}
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
for _ in 0..total_lines {
queue!(stdout, cursor::MoveToColumn(0), terminal::Clear(ClearType::CurrentLine), cursor::MoveDown(1)).ok();
}
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
let buffer = self.render(&value, cursor_pos);
write!(stdout, "{}", buffer).ok();
stdout.flush().ok();
}
}
execute!(stdout, cursor::Show).ok();
terminal::disable_raw_mode().expect("Failed to disable raw mode");
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
for _ in 0..total_lines {
queue!(stdout, cursor::MoveToColumn(0), terminal::Clear(ClearType::CurrentLine), cursor::MoveDown(1)).ok();
}
queue!(stdout, cursor::MoveUp(total_lines as u16)).ok();
stdout.flush().ok();
if !value.is_empty() {
let display_value = if self.password {
"●".repeat(value.len())
} else {
value.clone()
};
println!(
" {} {} {}",
style("✓").fg(Color::Green).bold(),
style(&self.prompt).fg(PLACEHOLDER),
style(&display_value).fg(TEXT_COLOR)
);
} else {
println!();
}
value
}
}