readline_async/
lib.rs

1use crossterm::{QueueableCommand, cursor};
2use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers};
3use crossterm::style::Print;
4use crossterm::terminal::{Clear, ClearType};
5
6use futures::{select, StreamExt};
7use futures::future::FutureExt;
8use futures::channel::mpsc;
9
10use std::io::Write;
11use thiserror::Error;
12
13#[derive(Error, Debug)]
14pub enum Error {
15    #[error("interrupted")]
16    Interrupted,
17    #[error("end of file")]
18    Eof,
19    #[error("io error: {0}")]
20    IoError(#[from] std::io::Error),
21}
22
23pub struct Editor {
24    history: Vec<String>,
25    history_receiver: mpsc::UnboundedReceiver<String>,
26    
27    events: EventStream,
28}
29
30impl Editor {
31    /// Construct a new editor.
32    ///
33    /// Returns the editor as well as a [tokio::mpsc]
34    pub fn new() -> (Self, mpsc::UnboundedSender<String>) {
35        let (tx, rx) = mpsc::unbounded();
36        let editor = Self {
37            history: vec![],
38            history_receiver: rx,
39
40            events: EventStream::new(),
41        };
42        (editor, tx)
43    }
44
45    /// Ask the user for one new line.
46    ///
47    /// In case of error, the partially entered string is still returned.
48    pub async fn readline(&mut self) -> (String, Result<(), Error>) {
49        let mut buffer = String::new();
50        // TODO keep old screen contents above?
51        // alternatively another screen so we can restore
52        if let Err(e) = self.output(&buffer) {
53            return (buffer, Err(e));
54        }
55
56        loop {
57            let mut event = self.events.next().fuse();
58
59            select! {
60                line = self.history_receiver.next() => match line {
61                    Some(line) => self.history.push(line),
62                    // TODO move into simpler "bye-bye" branch if sender is dropped?
63                    None => continue,
64                },
65                // TODO refactor
66                maybe_event = event => match maybe_event {
67                    Some(Ok(Event::Key(key_event))) => {
68                        if key_event == KeyCode::Enter.into() {
69                            return (buffer, Ok(()));
70                        } else if key_event == KeyCode::Backspace.into() {
71                            let _ = buffer.pop();
72                        } else if let KeyEvent { code: KeyCode::Char(c), modifiers } = key_event {
73                            if c == 'c' && modifiers == KeyModifiers::CONTROL {
74                                return (buffer, Err(Error::Interrupted));
75                            } else if c == 'd' && modifiers == KeyModifiers::CONTROL {
76                                return (buffer, Err(Error::Eof));
77                            } else {
78                                buffer.push(c);
79                            }
80                        } else {
81                            continue;
82                        }
83                    }
84                    // mouse events etc.
85                    Some(Ok(_)) => continue,
86                    Some(Err(e)) => return (buffer, Err(e.into())),
87                    // TODO when is this case reached?
88                    None => return (buffer, Ok(())),
89                }
90            }
91
92            if let Err(e) = self.output(&buffer) {
93                return (buffer, Err(e));
94            }
95        }
96    }
97
98    fn output(&self, buffer: &str) -> Result<(), Error> {
99        // TODO we only need to clear when appending a char
100        // maybe this can be moved in its entirety to the line = .. select branch
101        let mut stdout = std::io::stdout();
102        stdout.queue(Clear(ClearType::All))?.queue(cursor::MoveTo(0, 0))?;
103        // NOTE top left corner is (1, 1). beware of off by one
104        let (_cols, rows) = crossterm::terminal::size()?;
105        let max_history = rows as usize - 2;
106        let total_history = self.history.len();
107        let _to_show = max_history.min(total_history);
108        // TODO handle wrapping
109        // TODO handle long history
110        for line in &self.history {
111            // TODO always \r when printing \n (or tell callers to split their \n's)
112            stdout.queue(Print(line))?.queue(Print("\n\r"))?;
113        }
114        stdout.queue(Print(">> "))?.queue(Print(&buffer))?;
115        stdout.flush()?;
116
117        Ok(())
118    }
119}
120
121pub fn enable_raw_mode() -> Result<(), std::io::Error> {
122    crossterm::terminal::enable_raw_mode()
123}
124
125pub fn disable_raw_mode() -> Result<(), std::io::Error> {
126    crossterm::terminal::disable_raw_mode()
127}