minitt-util 0.2.4

CLI Utilities extracted from Mini-TT language's type-checker
Documentation
use std::fmt::{Display, Error, Formatter};
use std::io::{stdin, stdout, Write};
use std::path::PathBuf;

use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::Validator;
use rustyline::{CompletionType, Config, Context, Editor, Helper};

pub struct MiniHelper {
    pub all_cmd: Vec<String>,
    pub file_completer: FilenameCompleter,
}

impl Completer for MiniHelper {
    type Candidate = Pair;

    fn complete(
        &self,
        line: &str,
        pos: usize,
        ctx: &Context<'_>,
    ) -> Result<(usize, Vec<Self::Candidate>), ReadlineError> {
        if line.starts_with(":load ") || line.starts_with(":l ") {
            return self.file_completer.complete(line, pos, ctx);
        }
        let start = line
            .chars()
            .enumerate()
            .find(|(_, i)| !i.is_whitespace())
            .map(|(i, _)| i)
            .unwrap_or(0);
        let subs = if pos > start {
            &line[start..pos]
        } else {
            &line[start..]
        };
        let base = self
            .all_cmd
            .iter()
            .filter(|s| s.starts_with(subs))
            .map(|s| s.clone())
            .map(|str| Pair {
                display: str.clone(),
                replacement: str,
            })
            .collect();
        Ok((start, base))
    }
}

impl Hinter for MiniHelper {
    fn hint(&self, line: &str, pos: usize, _: &Context<'_>) -> Option<String> {
        if line.len() < 2 {
            return None;
        }
        self.all_cmd
            .iter()
            .filter(|cmd| cmd.starts_with(line))
            .cloned()
            .map(|cmd| cmd[pos..].to_string())
            .next()
    }
}

impl Highlighter for MiniHelper {}

impl Helper for MiniHelper {}

impl Validator for MiniHelper {}

#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum ReplEnvType {
    Plain,
    Rich,
}

impl Display for ReplEnvType {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
        match self {
            ReplEnvType::Plain => f.write_str("PLAIN"),
            ReplEnvType::Rich => f.write_str("RICH"),
        }
    }
}

pub fn create_editor(all_cmd: &[&str]) -> Editor<MiniHelper> {
    let all_cmd: Vec<_> = all_cmd.iter().map(|s| s.to_string()).collect();
    let mut r = Editor::with_config(
        Config::builder()
            .history_ignore_space(true)
            .completion_type(CompletionType::List)
            .build(),
    );
    r.set_helper(Some(MiniHelper {
        all_cmd,
        file_completer: FilenameCompleter::new(),
    }));
    r
}

pub fn repl_plain<TCS>(
    mut tcs: TCS,
    prompt: &str,
    welcome_message: impl FnOnce(ReplEnvType) -> (),
    work: impl Fn(TCS, ReplEnvType, &str) -> Option<TCS>,
) {
    welcome_message(ReplEnvType::Plain);
    let stdin = stdin();
    loop {
        print!("{}", prompt);
        stdout().flush().expect("Cannot flush stdout!");
        let mut line = String::new();
        stdin.read_line(&mut line).expect("Cannot read from stdin!");
        if let Some(ok) = work(tcs, ReplEnvType::Plain, line.trim()) {
            tcs = ok;
        } else {
            break;
        };
    }
}

pub fn repl_rich<TCS>(
    mut tcs: TCS,
    prompt: &str,
    create_editor: impl FnOnce() -> Editor<MiniHelper>,
    history: Option<PathBuf>,
    welcome_message: impl FnOnce(ReplEnvType) -> (),
    work: impl Fn(TCS, ReplEnvType, &str) -> Option<TCS>,
) {
    let mut r = create_editor();
    if let Some(history) = &history {
        if let Err(err) = r.load_history(history) {
            eprintln!("Failed to load REPL history: {}", err)
        }
    }
    welcome_message(ReplEnvType::Rich);
    loop {
        match r.readline(prompt) {
            Ok(line) => {
                let line = line.trim();
                r.add_history_entry(line);
                if let Some(ok) = work(tcs, ReplEnvType::Rich, line) {
                    tcs = ok;
                } else {
                    break;
                };
            }
            Err(ReadlineError::Interrupted) => {}
            Err(ReadlineError::Eof) => {
                println!("Interrupted");
                break;
            }
            Err(err) => {
                println!("Error: {:?}", err);
                break;
            }
        };
    }
    if let Some(history) = &history {
        if let Err(err) = r.save_history(history) {
            eprintln!("Failed to save REPL history: {}", err)
        }
    }
}

pub fn repl<TCS>(
    tcs: TCS,
    prompt: &str,
    repl_kind: ReplEnvType,
    create_editor: impl FnOnce() -> Editor<MiniHelper>,
    history: impl FnOnce() -> Option<PathBuf>,
    welcome_message: impl FnOnce(ReplEnvType) -> (),
    work: impl Fn(TCS, ReplEnvType, &str) -> Option<TCS>,
) {
    use ReplEnvType::*;
    match repl_kind {
        Plain => repl_plain(tcs, prompt, welcome_message, work),
        Rich => repl_rich(tcs, prompt, create_editor, history(), welcome_message, work),
    };
}