copyrat 0.8.3

A tmux plugin for copy-pasting within tmux panes.
Documentation
use crate::{Error, Result};

/// Catalog of available alphabets.
///
/// # Note
///
/// Keep in mind letters 'n' and 'y' are systematically removed at runtime to
/// prevent conflict with navigation and yank/copy keys.
const ALPHABETS: [(&str, &str); 21] = [
    // ("abcd", "abcd"),
    ("qwerty", "asdfqwerzxcvjklmiuopghtybn"),
    ("qwerty-homerow", "asdfjklgh"),
    ("qwerty-left-hand", "asdfqwerzcxv"),
    ("qwerty-right-hand", "jkluiopmyhn"),
    ("azerty", "qsdfazerwxcvjklmuiopghtybn"),
    ("azerty-homerow", "qsdfjkmgh"),
    ("azerty-left-hand", "qsdfazerwxcv"),
    ("azerty-right-hand", "jklmuiophyn"),
    ("qwertz", "asdfqweryxcvjkluiopmghtzbn"),
    ("qwertz-homerow", "asdfghjkl"),
    ("qwertz-left-hand", "asdfqweryxcv"),
    ("qwertz-right-hand", "jkluiopmhzn"),
    ("dvorak", "aoeuqjkxpyhtnsgcrlmwvzfidb"),
    ("dvorak-homerow", "aoeuhtnsid"),
    ("dvorak-left-hand", "aoeupqjkyix"),
    ("dvorak-right-hand", "htnsgcrlmwvz"),
    ("colemak", "arstqwfpzxcvneioluymdhgjbk"),
    ("colemak-homerow", "arstneiodh"),
    ("colemak-left-hand", "arstqwfpzxcv"),
    ("colemak-right-hand", "neioluymjhk"),
    (
        "longest",
        "aoeuqjkxpyhtnsgcrlmwvzfidb-;,~<>'@!#$%^&*~1234567890",
    ),
];

/// Parse a name string into `Alphabet`, used during CLI parsing.
///
/// # Note
///
/// Letters 'n' and 'N' are systematically removed to prevent conflict with
/// navigation keys (arrows and 'n' 'N'). Letters 'y' and 'Y' are also removed
/// to prevent conflict with yank/copy.
pub fn parse_alphabet(src: &str) -> Result<Alphabet> {
    let alphabet_pair = ALPHABETS.iter().find(|&(name, _letters)| name == &src);

    match alphabet_pair {
        Some((_name, letters)) => {
            let letters = letters.replace(&['n', 'N', 'y', 'Y'][..], "");
            Ok(Alphabet(letters))
        }
        None => Err(Error::UnknownAlphabet),
    }
}

/// Type-safe string alphabet (newtype).
#[derive(Debug, Clone)]
pub struct Alphabet(pub String);

impl Alphabet {
    /// Create `n` hints from the Alphabet.
    ///
    /// An Alphabet of `m` letters can produce at most `m^2` hints. In case
    /// this limit is exceeded, this function will generate the `n` hints from
    /// an Alphabet which has more letters (50). This will ensure 2500 hints
    /// can be generated, which should cover all use cases (I think even
    /// easymotion has less).
    ///
    /// If more hints are needed, unfortunately, this will keep producing
    /// empty (`""`) hints.
    ///
    /// ```text
    /// // The algorithm works as follows:
    /// //                                  --- lead ----
    /// // initial state                 |  a   b   c   d
    ///
    /// // along as we need more hints, and still have capacity, do the following
    ///
    /// //                                  --- lead ----  --- gen --- -------------- prev ---------------
    /// // pick d, generate da db dc dd  |  a   b   c  (d) da db dc dd
    /// // pick c, generate ca cb cc cd  |  a   b  (c) (d) ca cb cc cd da db dc dd
    /// // pick b, generate ba bb bc bd  |  a  (b) (c) (d) ba bb bc bd ca cb cc cd da db dc dd
    /// // pick a, generate aa ab ac ad  | (a) (b) (c) (d) aa ab ac ad ba bb bc bd ca cb cc cd da db dc dd
    /// ```
    pub fn make_hints(&self, n: usize) -> Vec<String> {
        // Shortcut if we have enough letters in the Alphabet.
        if self.0.len() >= n {
            return self.0.chars().take(n).map(|c| c.to_string()).collect();
        }

        // Use the "longest" alphabet if the current alphabet cannot produce as
        // many hints as asked.
        let letters: Vec<char> = if self.0.len().pow(2) >= n {
            self.0.chars().collect()
        } else {
            let alt_alphabet = parse_alphabet("longest").unwrap();
            alt_alphabet.0.chars().collect()
        };

        let mut lead = letters.clone();
        let mut prev: Vec<String> = Vec::new();

        loop {
            if lead.len() + prev.len() >= n {
                break;
            }

            if lead.is_empty() {
                break;
            }
            let prefix = lead.pop().unwrap();

            // generate characters pairs
            let gen: Vec<String> = letters
                .iter()
                .take(n - lead.len() - prev.len())
                .map(|c| format!("{prefix}{c}"))
                .collect();

            // Insert gen in front of prev
            prev.splice(..0, gen);
        }

        // Finalize by concatenating the lead and prev components, filling
        // with "" as necessary.
        let lead: Vec<String> = lead.iter().map(|c| c.to_string()).collect();

        let filler: Vec<String> = std::iter::repeat_n("", n - lead.len() - prev.len())
            .map(|s| s.to_string())
            .collect();

        [lead, prev, filler].concat()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn simple_hints() {
        let alphabet = Alphabet("abcd".to_string());
        let hints = alphabet.make_hints(3);
        assert_eq!(hints, ["a", "b", "c"]);
    }

    #[test]
    fn composed_hints() {
        let alphabet = Alphabet("abcd".to_string());
        let hints = alphabet.make_hints(6);
        assert_eq!(hints, ["a", "b", "c", "da", "db", "dc"]);
    }

    #[test]
    fn composed_hints_multiple() {
        let alphabet = Alphabet("abcd".to_string());
        let hints = alphabet.make_hints(8);
        assert_eq!(hints, ["a", "b", "ca", "cb", "da", "db", "dc", "dd"]);
    }

    #[test]
    fn composed_hints_max_2() {
        let alphabet = Alphabet("ab".to_string());
        let hints = alphabet.make_hints(4);
        assert_eq!(hints, ["aa", "ab", "ba", "bb"]);
    }

    #[test]
    fn composed_hints_max_4() {
        let alphabet = Alphabet("abcd".to_string());
        let hints = alphabet.make_hints(13);
        assert_eq!(
            hints,
            ["a", "ba", "bb", "bc", "bd", "ca", "cb", "cc", "cd", "da", "db", "dc", "dd"]
        );
    }

    #[test]
    fn hints_with_longest_alphabet() {
        let alphabet = Alphabet("ab".to_string());
        let hints = alphabet.make_hints(2500);
        assert_eq!(hints.len(), 2500);
        assert_eq!(&hints[..3], ["aa", "ao", "ae"]);
        assert_eq!(&hints[2497..], ["08", "09", "00"]);
    }

    #[test]
    fn hints_exceed_longest_alphabet() {
        let alphabet = Alphabet("ab".to_string());
        let hints = alphabet.make_hints(10000);
        // 2500 unique hints are produced from the longest alphabet
        // The 7500 last ones come from the filler ("" empty hints).
        assert_eq!(hints.len(), 10000);
        assert!(&hints[2500..].iter().all(|s| s.is_empty()));
    }
}