readline-async 0.1.0

Async-ready readline alternative
Documentation
use crossterm::{QueueableCommand, cursor};
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers};
use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType};

use futures::{select, StreamExt};
use futures::future::FutureExt;
use futures::channel::mpsc;

use std::io::Write;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("interrupted")]
    Interrupted,
    #[error("end of file")]
    Eof,
    #[error("io error: {0}")]
    IoError(#[from] std::io::Error),
}

pub struct Editor {
    history: Vec<String>,
    history_receiver: mpsc::UnboundedReceiver<String>,
    
    events: EventStream,
}

impl Editor {
    /// Construct a new editor.
    ///
    /// Returns the editor as well as a [tokio::mpsc]
    pub fn new() -> (Self, mpsc::UnboundedSender<String>) {
        let (tx, rx) = mpsc::unbounded();
        let editor = Self {
            history: vec![],
            history_receiver: rx,

            events: EventStream::new(),
        };
        (editor, tx)
    }

    /// Ask the user for one new line.
    ///
    /// In case of error, the partially entered string is still returned.
    pub async fn readline(&mut self) -> (String, Result<(), Error>) {
        let mut buffer = String::new();
        // TODO keep old screen contents above?
        // alternatively another screen so we can restore
        if let Err(e) = self.output(&buffer) {
            return (buffer, Err(e));
        }

        loop {
            let mut event = self.events.next().fuse();

            select! {
                line = self.history_receiver.next() => match line {
                    Some(line) => self.history.push(line),
                    // TODO move into simpler "bye-bye" branch if sender is dropped?
                    None => continue,
                },
                // TODO refactor
                maybe_event = event => match maybe_event {
                    Some(Ok(Event::Key(key_event))) => {
                        if key_event == KeyCode::Enter.into() {
                            return (buffer, Ok(()));
                        } else if key_event == KeyCode::Backspace.into() {
                            let _ = buffer.pop();
                        } else if let KeyEvent { code: KeyCode::Char(c), modifiers } = key_event {
                            if c == 'c' && modifiers == KeyModifiers::CONTROL {
                                return (buffer, Err(Error::Interrupted));
                            } else if c == 'd' && modifiers == KeyModifiers::CONTROL {
                                return (buffer, Err(Error::Eof));
                            } else {
                                buffer.push(c);
                            }
                        } else {
                            continue;
                        }
                    }
                    // mouse events etc.
                    Some(Ok(_)) => continue,
                    Some(Err(e)) => return (buffer, Err(e.into())),
                    // TODO when is this case reached?
                    None => return (buffer, Ok(())),
                }
            }

            if let Err(e) = self.output(&buffer) {
                return (buffer, Err(e));
            }
        }
    }

    fn output(&self, buffer: &str) -> Result<(), Error> {
        // TODO we only need to clear when appending a char
        // maybe this can be moved in its entirety to the line = .. select branch
        let mut stdout = std::io::stdout();
        stdout.queue(Clear(ClearType::All))?.queue(cursor::MoveTo(0, 0))?;
        // NOTE top left corner is (1, 1). beware of off by one
        let (_cols, rows) = crossterm::terminal::size()?;
        let max_history = rows as usize - 2;
        let total_history = self.history.len();
        let _to_show = max_history.min(total_history);
        // TODO handle wrapping
        // TODO handle long history
        for line in &self.history {
            // TODO always \r when printing \n (or tell callers to split their \n's)
            stdout.queue(Print(line))?.queue(Print("\n\r"))?;
        }
        stdout.queue(Print(">> "))?.queue(Print(&buffer))?;
        stdout.flush()?;

        Ok(())
    }
}

pub fn enable_raw_mode() -> Result<(), std::io::Error> {
    crossterm::terminal::enable_raw_mode()
}

pub fn disable_raw_mode() -> Result<(), std::io::Error> {
    crossterm::terminal::disable_raw_mode()
}