rusty_repl 0.3.0

REPL library with customisable prompts and clean terminal management.
Documentation
//! Input handling module for REPL environments.
//!
//! This module integrates [`reedline`](https://docs.rs/reedline) into your REPL or
//! interactive CLI, providing an [`InputHandler`] abstraction that:
//!
//! - wraps a [`Reedline`] line editor instance,
//! - uses configuration from [`ReplConfig`],
//! - manages user input and control signals (`CtrlC`, `CtrlD`),
//! - and dispatches each completed line to a user-supplied callback.
//!
//! The design hides the boilerplate of setting up a `reedline` loop while still
//! allowing customization through `ReplConfig` (for example, prompts and keyword
//! highlighting styles).
//!
//! # Example
//! ```ignore
//! use reedline::DefaultPrompt;
//! use nu_ansi_term::Color;
//! use crate::repl::{input::InputHandler, config::ReplConfig, style::KeywordStyle};
//!
//! // Define a keyword highlighting style
//! let kw_style = KeywordStyle {
//!     keywords: vec!["help".into(), "exit".into()],
//!     foreground: Color::Green,
//! };
//!
//! // Build a configuration using the builder-style API
//! let config = ReplConfig::new("Rusty REPL")
//!     .with_prompt(DefaultPrompt::default())
//!     .with_kw_style(kw_style);
//!
//! // Create and run the input handler
//! let mut handler = InputHandler::new(Arc::new(config));
//! handler.run(|line| {
//!     println!("You typed: {line}");
//!     line.trim() == "exit" // returning true breaks the loop
//! }).unwrap();
//! ```

use crate::{
    CleanPrompt,
    repl::{config::ReplConfig, highlighter::KeywordHighlighter},
};
use reedline::{Prompt, Reedline, Signal};
use std::{
    io::{Result, Write, stdout},
    sync::Arc,
};

/// Handles user input in a REPL environment using [`reedline`].
///
/// [`InputHandler`] owns a [`Reedline`] editor instance and the prompt to be used
/// for rendering the REPL interface. It reads user input, interprets control
/// signals, and invokes a user-defined closure for each entered line.
///
/// The handler automatically retrieves configuration values—such as prompt and
/// keyword highlighting—from a shared [`ReplConfig`].
pub struct InputHandler {
    line_editor: Reedline,
    prompt: Arc<dyn Prompt>,
}

impl InputHandler {
    /// Creates a new [`InputHandler`] using the provided [`ReplConfig`].
    ///
    /// This initializes the internal [`Reedline`] editor, applying an optional
    /// [`KeywordHighlighter`] if `kw_style` is configured. If no prompt is defined
    /// in the config, a [`CleanPrompt`] is used by default.
    ///
    /// # Example
    /// ```
    /// # use std::sync::Arc;
    /// # use crate::repl::{InputHandler, config::ReplConfig};
    /// let cfg = Arc::new(ReplConfig::default());
    /// let mut handler = InputHandler::new(cfg);
    /// ```
    pub fn new(config: Arc<ReplConfig>) -> Self {
        let default_prompt = CleanPrompt::default();

        let prompt = config
            .prompt()
            .clone()
            .unwrap_or_else(|| Arc::new(default_prompt));

        let line_editor = match &config.kw_style() {
            Some(style) => Reedline::create().with_highlighter(Box::new(KeywordHighlighter::new(
                &style.keywords,
                style.foreground,
            ))),
            None => Reedline::create(),
        };

        Self {
            line_editor,
            prompt,
        }
    }

    /// Runs the REPL input loop.
    ///
    /// This function continuously reads user input lines using the configured prompt,
    /// and passes each line to a user-supplied closure.
    ///
    /// # Arguments
    ///
    /// * `func` — A closure that processes each input line.
    ///   Returning `true` exits the loop, while returning `false` continues it.
    ///
    /// # Behavior
    ///
    /// - `CtrlC` or `CtrlD` terminates the loop and prints `Aborted!`.
    /// - Each successful line read is passed to the callback.
    /// - Standard output is flushed after every iteration.
    ///
    /// # Errors
    ///
    /// Returns an [`std::io::Error`] if flushing stdout fails.
    pub fn run<F>(&mut self, func: F) -> Result<()>
    where
        F: Fn(String) -> bool,
    {
        loop {
            // match self.line_editor.read_line(prompt) {
            match self.line_editor.read_line(self.prompt.as_ref()) {
                Ok(Signal::Success(buffer)) => {
                    if func(buffer) {
                        break;
                    }
                }
                Ok(Signal::CtrlC) | Ok(Signal::CtrlD) => {
                    println!("\nAborted!");
                    break;
                }
                Err(err) => eprintln!("Input error: {err}"),
            }

            stdout().flush()?;
        }

        Ok(())
    }
}

impl Drop for InputHandler {
    /// Ensures stdout is flushed when the `InputHandler` is dropped.
    ///
    /// Any I/O errors during cleanup are reported to stderr.
    fn drop(&mut self) {
        if let Err(err) = stdout().flush() {
            eprintln!("Failed to flush stdout during cleanup: {err}");
        }
    }
}