diff options
author | Jesse Luehrs <doy@tozt.net> | 2019-06-08 18:11:38 -0400 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2019-06-08 18:11:38 -0400 |
commit | 59abbab62e936a46ceea7bb7ebac5ca9ef4ba2da (patch) | |
tree | f9051679b288203a4c05cff097c6738dfda94fa3 /src | |
parent | 9168c7fca9043d48bc65eaca6837a537d220a1ed (diff) | |
download | nbsh-old-59abbab62e936a46ceea7bb7ebac5ca9ef4ba2da.tar.gz nbsh-old-59abbab62e936a46ceea7bb7ebac5ca9ef4ba2da.zip |
basic readline implementation
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/readline.rs | 169 | ||||
-rw-r--r-- | src/repl.rs | 53 |
3 files changed, 228 insertions, 1 deletions
diff --git a/src/main.rs b/src/main.rs index e7a11a9..105450f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,8 @@ +mod readline; +mod repl; + fn main() { - println!("Hello, world!"); + let _screen = crossterm::RawScreen::into_raw_mode().unwrap(); + + repl::repl(); } diff --git a/src/readline.rs b/src/readline.rs new file mode 100644 index 0000000..73b5195 --- /dev/null +++ b/src/readline.rs @@ -0,0 +1,169 @@ +use std::io::Write; + +#[derive(Debug)] +pub enum Error { + EOF, + IOError(std::io::Error), +} + +pub struct Readline { + reader: Option<KeyReader>, + buffer: String, + prompt: String, + wrote_prompt: bool, + echo: bool, +} + +impl Readline { + fn process_event( + &mut self, + event: crossterm::InputEvent, + ) -> std::result::Result<futures::Async<String>, Error> { + match event { + crossterm::InputEvent::Keyboard(e) => { + return self.process_keyboard_event(e) + } + _ => {} + } + return Ok(futures::Async::NotReady); + } + + fn process_keyboard_event( + &mut self, + event: crossterm::KeyEvent, + ) -> std::result::Result<futures::Async<String>, Error> { + match event { + crossterm::KeyEvent::Char(c) => { + self.echo(c).map_err(|e| Error::IOError(e))?; + + if c == '\n' { + return Ok(futures::Async::Ready(self.buffer.clone())); + } + self.buffer.push(c); + } + crossterm::KeyEvent::Ctrl(c) => { + if c == 'd' { + if self.buffer.is_empty() { + self.echo('\n').map_err(|e| Error::IOError(e))?; + return Err(Error::EOF); + } + } + if c == 'c' { + self.buffer = String::new(); + self.echo('\n').map_err(|e| Error::IOError(e))?; + self.prompt().map_err(|e| Error::IOError(e))?; + } + } + _ => {} + } + return Ok(futures::Async::NotReady); + } + + fn write(&self, buf: &[u8]) -> std::io::Result<()> { + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + stdout.write(buf)?; + stdout.flush() + } + + fn prompt(&self) -> std::io::Result<()> { + self.write(self.prompt.as_bytes()) + } + + fn echo(&self, c: char) -> std::io::Result<()> { + if c == '\n' { + self.write(b"\r\n")?; + return Ok(()); + } + + if !self.echo { + return Ok(()); + } + + let mut buf = [0u8; 4]; + self.write(c.encode_utf8(&mut buf[..]).as_bytes()) + } +} + +impl futures::future::Future for Readline { + type Item = String; + type Error = Error; + + fn poll(&mut self) -> futures::Poll<Self::Item, Self::Error> { + if !self.wrote_prompt { + self.prompt().map_err(|e| Error::IOError(e))?; + self.wrote_prompt = true; + } + + let reader = self.reader.get_or_insert_with(|| { + KeyReader::new(tokio::prelude::task::current()) + }); + if let Some(event) = reader.poll() { + self.process_event(event) + } else { + Ok(futures::Async::NotReady) + } + } +} + +pub fn readline(prompt: &str, echo: bool) -> Readline { + Readline { + reader: None, + buffer: String::new(), + prompt: prompt.to_string(), + wrote_prompt: false, + echo, + } +} + +struct KeyReader { + events: std::sync::mpsc::Receiver<crossterm::InputEvent>, + quit: std::sync::mpsc::Sender<()>, +} + +impl KeyReader { + fn new(task: tokio::prelude::task::Task) -> Self { + let reader = crossterm::input().read_sync(); + let (events_tx, events_rx) = std::sync::mpsc::channel(); + let (quit_tx, quit_rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + for event in reader { + let newline = event + == crossterm::InputEvent::Keyboard( + crossterm::KeyEvent::Char('\n'), + ); + events_tx.send(event).unwrap(); + task.notify(); + if newline { + break; + } + if let Ok(_) = quit_rx.try_recv() { + break; + } + } + }); + + KeyReader { + events: events_rx, + quit: quit_tx, + } + } + + fn poll(&self) -> Option<crossterm::InputEvent> { + if let Ok(event) = self.events.try_recv() { + return Some(event); + } + None + } +} + +impl Drop for KeyReader { + fn drop(&mut self) { + // don't care if it fails to send, this can happen if the thread + // terminates due to seeing a newline before the keyreader goes out of + // scope + match self.quit.send(()) { + _ => {} + } + } +} diff --git a/src/repl.rs b/src/repl.rs new file mode 100644 index 0000000..c44174a --- /dev/null +++ b/src/repl.rs @@ -0,0 +1,53 @@ +use futures::future::Future; +use std::io::Write; + +#[derive(Debug)] +enum Error { + ReadError(crate::readline::Error), + // EvalError(std::io::Error), + PrintError(std::io::Error), + // LoopError, +} + +pub fn repl() { + tokio::run(tokio::prelude::future::lazy(|| { + let mut done = false; + while !done { + let res = read() + .and_then(move |line| eval(&line)) + .and_then(move |out| print(&out)) + .wait(); + match res { + Ok(_) => {} + Err(Error::ReadError(crate::readline::Error::EOF)) => { + done = true; + } + Err(e) => { + let stderr = std::io::stderr(); + let mut stderr = stderr.lock(); + write!(stderr, "error: {:?}", e).unwrap(); + stderr.flush().unwrap(); + done = true; + } + } + } + futures::future::ok(()) + })); +} + +fn read() -> impl futures::future::Future<Item = String, Error = Error> { + crate::readline::readline("$ ", true).map_err(|e| Error::ReadError(e)) +} + +fn eval(line: &str) -> Result<String, Error> { + Ok(format!("got line '{}'\r\n", line)) +} + +fn print(out: &str) -> Result<(), Error> { + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + stdout + .write(out.as_bytes()) + .map_err(|e| Error::PrintError(e))?; + stdout.flush().map_err(|e| Error::PrintError(e)) +} |