infoterm 0.1.1

ncurses-compatible terminfo parsing library
Documentation
//! Character escaping for capability strings.

use std::fmt::{self, Display, Formatter, Write};
use std::ops::Range;
use std::iter::{FlatMap, FusedIterator};
use std::slice;

fn escape_byte(output: &mut [u8; 4], byte: u8) -> Range<u8> {
    #[inline]
    fn backslash(byte: u8) -> ([u8; 4], u8) {
        ([b'\\', byte, 0, 0], 2)
    }

    #[inline]
    fn ctrl(byte: u8) -> ([u8; 4], u8) {
        ([b'^', byte, 0, 0], 2)
    }

    #[inline]
    fn octal(byte: u8) -> ([u8; 4], u8) {
        ([b'\\', (byte >> 6) + b'0', (byte >> 3 & 7) + b'0', (byte & 7) + b'0'], 4)
    }

    #[inline]
    fn bare(byte: u8) -> ([u8; 4], u8) {
        ([byte, 0, 0, 0], 1)
    }

    let (data, len) = match byte {
        0x07            => backslash(b'a'), // bell
        0x08            => backslash(b'b'), // backspace
        // Infocmp prefers \E, but \e works with `echo`
        0x1B            => backslash(b'e'), // escape
        0x0C            => backslash(b'f'), // form feed
        b'\n'           => backslash(b'n'), // \l becomes \n
        b'\r'           => backslash(b'r'),
        // Allowed but not needed
        // b' '            => backslash(b's'),
        b'\t'           => backslash(b't'),
        b'^'            => backslash(b'^'),
        b'\\'           => backslash(b'\\'),
        b','            => backslash(b','),
        // Allowed but not needed
        // b':'            => backslash(b':'),
        b'\0'           => backslash(b'0'),
        x if x < 32     => ctrl(x + b'@'),  // 0x01 -> ^A
        x if x > 126    => octal(x),        // \nnn
        x               => bare(x),
    };

    *output = data;
    0..len
}

#[derive(Clone)]
struct EscapeIter {
    data: [u8; 4],
    alive: Range<u8>
}

impl EscapeIter {
    #[inline]
    fn new(&byte: &u8) -> Self {
        let mut data = [0; 4];
        let alive = escape_byte(&mut data, byte);
        Self { data, alive }

    }

    #[inline]
    fn _next(&mut self) -> Option<u8> {
        self.alive.next().map(|i| self.data[usize::from(i)])
    }

    #[inline]
    fn _next_back(&mut self) -> Option<u8> {
        self.alive.next_back().map(|i| self.data[usize::from(i)])
    }

    #[inline]
    fn _len(&self) -> usize {
        usize::from(self.alive.end - self.alive.start)
    }
}

impl Iterator for EscapeIter {
    type Item = u8;

    #[inline]
    fn next(&mut self) -> Option<u8> {
        self._next()
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        let n = self._len();
        (n, Some(n))
    }

    #[inline]
    fn count(self) -> usize {
        self._len()
    }

    #[inline]
    fn last(mut self) -> Option<u8> {
        self._next_back()
    }
}

impl DoubleEndedIterator for EscapeIter {
    #[inline]
    fn next_back(&mut self) -> Option<Self::Item> {
        self._next_back()
    }
}

impl ExactSizeIterator for EscapeIter {
    #[inline]
    fn len(&self) -> usize {
        self._len()
    }
}

impl FusedIterator for EscapeIter {}

/// An iterator over the escaped version of a byte slice.
///
/// This `struct` is created by the [`escape_terminfo`] function.
/// See its documentation for more information.
#[derive(Clone)]
pub struct EscapeTerminfo<'a> {
    inner: FlatMap<slice::Iter<'a, u8>, EscapeIter, fn(&u8) -> EscapeIter>
}

impl<'a> Iterator for EscapeTerminfo<'a> {
    type Item = u8;

    #[inline]
    fn next(&mut self) -> Option<u8> {
        self.inner.next()
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.inner.size_hint()
    }

    #[inline]
    fn fold<Acc, Fold>(self, init: Acc, fold: Fold) -> Acc
    where
        Fold: FnMut(Acc, Self::Item) -> Acc,
    {
        self.inner.fold(init, fold)
    }

    #[inline]
    fn last(mut self) -> Option<u8> {
        self.next_back()
    }
}

impl<'a> DoubleEndedIterator for EscapeTerminfo<'a> {
    fn next_back(&mut self) -> Option<u8> {
        self.inner.next_back()
    }
}

impl<'a> FusedIterator for EscapeTerminfo<'a> {}

impl<'a> Display for EscapeTerminfo<'a> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        self.clone().try_for_each(|b| f.write_char(b as char))
    }
}

/// Returns an iterator that produces an escaped version of the given slice.
///
/// The escaped format is compatible with terminfo source format specified by X/Open curses.\
/// The following replacements are made:
///
/// Byte                                  | Replacement
/// --------------------------------------|----------------
/// Bell                                  | `\a`
/// Backspace                             | `\b`
/// Escape                                | `\e`
/// Form feed                             | `\f`
/// Line feed                             | `\n`
/// Carriage return                       | `\r`
/// Tab                                   | `\t`
/// Caret (^)                             | `\^`
/// Backslash (\\)                        | `\\`
/// Comma (,)                             | `\,`
/// Null                                  | `\0`
/// Other control characters              | `^X` ([unctrl])
/// Characters outside of the ASCII range | `\nnn` (octal)
///
/// [unctrl]: https://man.archlinux.org/man/curs_util.3x.en#unctrl
pub fn escape_terminfo(slice: &[u8]) -> EscapeTerminfo {
    EscapeTerminfo { inner: slice.iter().flat_map(EscapeIter::new) }
}