termit 0.7.0

Terminal UI over crossterm
Documentation
use super::events::{AppEvent, Event};
use super::Resizer;
use crate::input::parser::*;
use crate::mmap::MmapVec;
use ansitok::ElementKind;
use futures_lite::io::{self};
use futures_lite::{ready, stream, Stream, StreamExt};
use log::*;
use std::{iter::once, pin::Pin, task::Poll};

use crate::Control;

/// Terminal input stream decodes ANSI sequences and content and includes window resize events
///
/// It will not do much of resizing without a control FD/Handle that can report it.
///
/// The `raw_mode` param will treat \r \n slightly differently.
pub fn terminal_input_stream<'a, A: AppEvent>(
    control: impl Control + 'a,
    reader: impl io::AsyncRead + 'a,
    raw_mode: bool,
) -> impl Stream<Item = io::Result<Event<A>>> + 'a {
    let ansi = terminal_ansi_input_stream(reader, raw_mode);
    Resizer::new(Box::pin(ansi), Box::pin(control))
}

/// Terminal input stream decodes ANSI sequences and content available with the `parser` feature.
pub fn terminal_ansi_input_stream<'a, A: AppEvent>(
    reader: impl io::AsyncRead + 'a,
    raw_mode: bool,
) -> impl Stream<Item = io::Result<Event<A>>> + 'a {
    AnsiStream::terminal(Box::pin(reader), 8 * 1024)
        .flat_map(move |item| {
            let mut err = None;
            let mut items = None;
            match ansi_input_item_stream(item, raw_mode) {
                Ok(i) => items = Some(stream::iter(i)),
                Err(e) => err = Some(Err(e)),
            }
            stream::iter(err).chain(stream::iter(items).flatten().map(Result::Ok))
        })
        .chain(stream::once(Ok(Event::InputClosed)))
}

fn ansi_input_item_stream<A: AppEvent>(
    item: io::Result<AnsiData>,
    raw_mode: bool,
) -> io::Result<impl IntoIterator<Item = Event<A>>> {
    trace!("ansi item {item:?}");

    let item = item?;

    let mut items: Vec<Event<A>> = vec![];

    let unknown = |e| {
        warn!("Failed to parse input: {}", e);
        Event::Unknown(item.data.to_vec())
    };

    match item.kind {
        Some(ansitok::ElementKind::Text) => {
            for key in std::str::from_utf8(item.data.as_slice())
                .expect("utf-8 already in parser")
                .chars()
                .map(|c| parse_key(c, raw_mode))
            {
                items.push(
                    key.unwrap_or_else(|e| Some(unknown(e)))
                        .ok_or("missing key - not likely")
                        .unwrap_or_else(unknown),
                );
            }
        }
        Some(ansitok::ElementKind::Sgr) => match item.data.as_slice() {
            [0x1b, b'[', b'>', ..] => {
                let e = parse_csi_sgr_mouse(&item.data)
                    .unwrap_or_else(|e| Some(unknown(e)))
                    .ok_or("missing SGR data")
                    .unwrap_or_else(unknown);
                trace!("sgr mouse {e:?}");
                items.push(e);
            }
            _ => {
                items.push(unknown("unrecognized SGR sequence"));
            }
        },
        Some(ansitok::ElementKind::Csi) => {
            let e = parse_csi(&item.data, raw_mode)
                .unwrap_or_else(|e| Some(unknown(e)))
                .ok_or("missing CSI data")
                .unwrap_or_else(unknown);
            trace!("csi {e:?}");
            items.push(e);
        }
        Some(ansitok::ElementKind::Esc) => {
            let e = parse_esc(&item.data, raw_mode)
                .unwrap_or_else(|e| Some(unknown(e)))
                .ok_or("missing ESC data")
                .unwrap_or_else(unknown);
            trace!("esc {e:?}");
            items.push(e);
        }
        Some(ansitok::ElementKind::Osc) => items.push(unknown("unsupported OSC")),
        None => items.push(unknown("invalid data, most likely non-utf-8")),
    }

    Ok(items)
}

struct AnsiStream<T> {
    reader: Pin<Box<T>>,
    buffer: MmapVec<u8>,
    start: usize,
    end: usize,
    /// parsing device sequences, that means input from the user + device specific information
    /// in other words not the application output
    terminal: bool,
}

#[derive(Debug, Clone, PartialEq)]
struct AnsiData {
    kind: Option<ansitok::ElementKind>,
    data: Vec<u8>,
}

impl<T> AnsiStream<T> {
    pub fn application(reader: Pin<Box<T>>, capacity: usize) -> Self {
        Self {
            reader,
            buffer: MmapVec::zeroed(capacity),
            start: 0,
            end: 0,
            terminal: false,
        }
    }
    pub fn terminal(reader: Pin<Box<T>>, capacity: usize) -> Self {
        Self {
            reader,
            buffer: MmapVec::zeroed(capacity),
            start: 0,
            end: 0,
            terminal: true,
        }
    }
    pub fn buffer(&self) -> &[u8] {
        &self.buffer[self.start..self.end]
    }
}

impl<T> Stream for AnsiStream<T>
where
    T: io::AsyncRead,
{
    type Item = io::Result<AnsiData>;

    fn poll_next(
        mut self: Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Option<Self::Item>> {
        // we block if we're stuck with input

        // fix buffer
        if self.start == self.end {
            trace!(
                "start {} == end {} => empty buffer reset",
                self.start,
                self.end
            );
            self.start = 0;
            self.end = 0;
        }
        if self.end == self.buffer.len() && self.start != 0 {
            trace!(
                "start {}, end {} => buffer is full, but we've got space, shifting",
                self.start,
                self.end
            );
            let range = self.start..self.end;
            self.end = range.len();
            self.start = 0;
            self.buffer.copy_within(range, 0);
        }

        // fill buffer
        if self.end != self.buffer.len() {
            let length = self.end - self.start;
            let range = self.end..;
            let Self { reader, buffer, .. } = &mut *self;
            let buffer = &mut buffer[range];

            // we'll block only if we have to
            self.end += if length == 0 {
                trace!("filling buffer - blocking");
                // fill the buffer as it's empty - blocking
                let read = ready!(reader.as_mut().poll_read(cx, buffer))?;
                trace!("filled buffer with {read} bytes");
                read
            } else if length < 1024 {
                trace!("filling buffer - chancing it");
                // opportunistic non-blocking read to refil the buffer.
                // this helps avoiding escape sequences split in half by the parser
                let read = match reader.as_mut().poll_read(cx, buffer) {
                    std::task::Poll::Ready(read) => read,
                    std::task::Poll::Pending => Ok(0),
                }?;
                trace!("filled buffer with {read} bytes");
                read
            } else {
                // enough in buffer
                0
            }
        }

        // parse buffer
        let buffer = self.buffer();
        let text = match std::str::from_utf8(buffer) {
            Ok(str) => str,
            Err(error) => {
                let (valid, _) = buffer.split_at(error.valid_up_to());
                if valid.is_empty() {
                    //first byte is not utf-8
                    //take all non-utf-8 bytes
                    let data = once(&buffer[0])
                        .chain(
                            buffer
                                .iter()
                                .skip(1)
                                .take_while(|b| utf8_char_width(**b) == 0),
                        )
                        .cloned()
                        .collect();
                    let data = AnsiData { kind: None, data };
                    assert!(!buffer.is_empty());
                    assert!(!data.data.is_empty());
                    self.start += data.data.len();
                    return Poll::Ready(Some(Ok(data)));
                } else {
                    std::str::from_utf8(valid).expect("already checked utf-8")
                }
            }
        };

        if self.terminal {
            // ansitok doesn't know which side of the terminal it's parsing. Here we help it a bit
            match text.as_bytes() {
                [0x1b, b'O', _param, ..] => {
                    // ansitok doesn't recognize this one (F1-4, up, down, left, right, home, end)
                    let data = AnsiData {
                        kind: Some(ElementKind::Esc),
                        data: buffer[0..3].to_vec(),
                    };
                    self.start += data.data.len();
                    return Poll::Ready(Some(Ok(data)));
                }
                _ => {
                    // carry on with ansitok
                }
            }
        }

        match ansitok::parse_ansi(text).next() {
            Some(element) => {
                assert!(element.start() == 0);
                trace!("{element:?}");
                let data = buffer[0..element.end()].to_vec();
                self.start += data.len();
                let data = AnsiData {
                    kind: Some(element.kind()),
                    data,
                };
                Poll::Ready(Some(Ok(data)))
            }
            None => {
                if self.end != self.start && self.end == self.buffer.len() {
                    // That's a special case, there is something, but probably an incomplete sequence.
                    // There is no space left and no correct way out of it.
                    // It's a pat situation. We're stuck, let's error here on the safe side
                    // rather than fooling the user that all is well.
                    Poll::Ready(Some(Err(io::Error::new(
                        io::ErrorKind::OutOfMemory,
                        "The buffer is full, but input is incomplete",
                    ))))
                } else {
                    Poll::Ready(None)
                }
            }
        }
    }
}

/// Given a first byte, determines how many bytes are in this UTF-8 character.
/// TODO: use core::str::utf8_char_width; when stable
#[inline]
const fn utf8_char_width(b: u8) -> usize {
    // https://tools.ietf.org/html/rfc3629
    const UTF8_CHAR_WIDTH: &[u8; 256] = &[
        // 1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B
        0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // D
        3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // E
        4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F
    ];
    UTF8_CHAR_WIDTH[b as usize] as usize
}