amen 0.0.3

A CLI menu tool utilizing abbreviations
Documentation
use error::AmenError;
use layout::Layout;
use std::collections::{BTreeSet, HashMap};
use std::io::{Read, Write};
use termion::input::TermRead;
use termion::{color, event, style};
use trie_rs::{Trie, TrieBuilder};

mod abbrev;
pub mod error;
pub mod layout;

use crate::abbrev::AbbrevPhrase;

pub fn run_amen<'a, W: Write, R: Read>(
    input: R,
    output: W,
    phrases: &[&'a str],
    layout: &Layout,
) -> Result<&'a str, AmenError> {
    let abbrev_phrases = abbrev::assign_abbrevs(phrases);
    let trie = collect_abbrevs_to_trie(&abbrev_phrases);

    let pick_phrase = pick_phrase(input, output, abbrev_phrases, trie, layout)
        .map_err(|err| AmenError::Internal(Box::new(err)));

    if let Some(phrase) = pick_phrase? {
        Ok(phrase)
    } else {
        Err(AmenError::NoneSelected)
    }
}

fn collect_abbrevs_to_trie(abbrevs: &HashMap<String, AbbrevPhrase>) -> Trie<u8> {
    let mut trie_builder = TrieBuilder::new();
    for (abbrev, _) in abbrevs.iter() {
        trie_builder.push(abbrev);
    }
    trie_builder.build()
}

type Type = trie_rs::Trie<u8>;

fn pick_phrase<'a, W: Write, R: Read>(
    input: R,
    mut output: W,
    abbrev_phrases: HashMap<String, AbbrevPhrase<'a>>,
    trie: Type,
    layout: &Layout,
) -> Result<Option<&'a str>, std::io::Error> {
    let mut key_iter = input.keys();
    let mut input_abbrev = String::new();

    Ok(loop {
        let predicted_abbrevs: BTreeSet<_> = if input_abbrev.is_empty() {
            abbrev_phrases.keys().map(|s| s.to_string()).collect()
        } else {
            trie.predictive_search(&input_abbrev)
                .into_iter()
                .map(|u8s| std::str::from_utf8(&u8s).unwrap().to_string())
                .collect()
        };
        match predicted_abbrevs.len() {
            0 => {
                input_abbrev.pop();
            }
            1 => {
                let item = abbrev_phrases.get(&*input_abbrev).unwrap();
                write!(output, "{}", layout.clear())?;
                break Some(item.phrase);
            }
            _ => {
                write!(output, "{}", layout.clear())?;

                for abbrev in predicted_abbrevs.iter() {
                    let abbrev_data = &abbrev_phrases.get(&*abbrev).unwrap();
                    write!(output, "{}", layout.goto_text_position(abbrev_data.phrase))?;
                    print_abbrev(&mut output, abbrev_data, &input_abbrev)?;
                }

                output.flush()?;
            }
        };

        let key = key_iter.next().expect("Failed getting next key")?;
        match key {
            event::Key::Char(c) => {
                input_abbrev.push(c);
            }
            event::Key::Esc => {
                if input_abbrev.is_empty() {
                    break None;
                } else {
                    input_abbrev.clear();
                }
            }
            _ => {}
        }
    })
}

fn print_abbrev<T: Write>(
    screen: &mut T,
    abbrev_data: &AbbrevPhrase,
    input_abbrev: &str,
) -> Result<(), std::io::Error> {
    for (i, c) in abbrev_data.phrase.char_indices() {
        let not_already_typed =
            input_abbrev.is_empty() || i > abbrev_data.indices[input_abbrev.len() - 1];

        if abbrev_data.indices.contains(&i) && not_already_typed {
            write!(
                screen,
                "{}{}{}{}{}",
                color::Fg(color::Cyan),
                style::Bold,
                c,
                style::Reset,
                color::Fg(color::Reset)
            )?;
        } else {
            write!(
                screen,
                "{}{}{}",
                color::Fg(color::Magenta),
                c,
                color::Fg(color::Reset),
            )?;
        };
    }
    Ok(())
}