chant 0.1.1

Shell glamour - beautiful prompts and output for scripts 🪄
Documentation
//! Text input command with beautiful Charm-style rendering.
//!
//! Uses buffered rendering to prevent flickering.

use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyModifiers},
    execute, queue,
    terminal::{self, ClearType},
};
use glyphs::{style, Color};
use std::io::{stdout, Write};

// Beautiful colors
const ACCENT: Color = Color::Rgb { r: 212, g: 92, b: 235 };    // Purple/Pink
const CURSOR_COLOR: Color = Color::Rgb { r: 255, g: 135, b: 175 }; // Pink
const PLACEHOLDER: Color = Color::Rgb { r: 102, g: 102, b: 102 }; // Gray
const TEXT_COLOR: Color = Color::Rgb { r: 255, g: 255, b: 255 };  // White

/// Create an input prompt.
pub fn input(prompt: impl Into<String>) -> Input {
    Input::new(prompt)
}

/// Text input builder.
pub struct Input {
    prompt: String,
    placeholder: String,
    default: String,
    password: bool,
    char_limit: usize,
    header: Option<String>,
}

impl Input {
    /// Create a new input prompt.
    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,
        }
    }

    /// Set placeholder text.
    #[must_use]
    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
        self.placeholder = placeholder.into();
        self
    }

    /// Set default value.
    #[must_use]
    pub fn default(mut self, default: impl Into<String>) -> Self {
        self.default = default.into();
        self
    }

    /// Enable password mode.
    #[must_use]
    pub fn password(mut self) -> Self {
        self.password = true;
        self
    }

    /// Set character limit.
    #[must_use]
    pub fn char_limit(mut self, limit: usize) -> Self {
        self.char_limit = limit;
        self
    }

    /// Set header text.
    #[must_use]
    pub fn header(mut self, header: impl Into<String>) -> Self {
        self.header = Some(header.into());
        self
    }

    /// Build the render buffer
    fn render(&self, value: &str, cursor_pos: usize) -> String {
        let mut buffer = String::new();

        // Header
        if let Some(ref header) = self.header {
            buffer.push_str(&format!("{}\r\n", style(header).fg(ACCENT).bold()));
        }

        // Prompt with nice styling
        buffer.push_str(&format!(" {} ", style(&self.prompt).fg(CURSOR_COLOR)));

        // Value or placeholder with cursor visualization
        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 {
            // Show text with cursor position
            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 empty or at end, show cursor
        if value.is_empty() || cursor_pos >= value.len() {
            if !value.is_empty() {
                // Cursor already shown in the split
            } else if self.placeholder.is_empty() {
                buffer.push_str(&format!("{}", style("").fg(CURSOR_COLOR).bold()));
            }
        }

        buffer.push_str("\r\n");
        buffer
    }

    /// Run the input prompt.
    pub fn run(self) -> String {
        let mut value = self.default.clone();
        let mut cursor_pos = value.len();
        let mut stdout = stdout();

        // Enable raw mode (no cursor hide for input - we show our own)
        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 };

        // Initial render
        let buffer = self.render(&value, cursor_pos);
        write!(stdout, "{}", buffer).ok();
        stdout.flush().ok();

        loop {
            // Handle input
            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,
                }

                // Move up and clear for re-render
                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();

                // Render new state
                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");
        
        // Clear and show final result
        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
    }
}