From 65ec6eb0f5c587139f730afa2291493a8a828c1f Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Fri, 12 Jul 2019 03:35:06 -0400 Subject: allow the tui to manage the overall raw screen --- src/builtins.rs | 6 +---- src/eval.rs | 71 ++++++++++++++++++++++++++++++++++++------------------- src/process.rs | 31 +++++++++++++++--------- src/readline.rs | 54 +++++++++++++++++++++++++++++++++++------- src/repl.rs | 12 +++------- src/tui.rs | 73 +++++++++++++++++++++++++++++++++------------------------ 6 files changed, 158 insertions(+), 89 deletions(-) diff --git a/src/builtins.rs b/src/builtins.rs index 83fa0d1..d1f5b61 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -35,10 +35,6 @@ pub enum Error { pub type Result = std::result::Result; -pub fn exec(cmd: &str, args: &[String]) -> Result { - Builtin::new(cmd, args) -} - pub struct Builtin { cmd: String, args: Vec, @@ -47,7 +43,7 @@ pub struct Builtin { } impl Builtin { - fn new(cmd: &str, args: &[String]) -> Result { + pub fn new(cmd: &str, args: &[String]) -> Result { match cmd { "cd" => Ok(Self { cmd: cmd.to_string(), diff --git a/src/eval.rs b/src/eval.rs index af3de9e..a7e09c4 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -28,9 +28,10 @@ pub enum Error { }, } +#[allow(dead_code)] pub type Result = std::result::Result; -pub fn eval(line: &str) -> Result { +pub fn eval(line: &str) -> Eval { Eval::new(line) } @@ -41,32 +42,28 @@ pub enum CommandEvent { } pub struct Eval { - stream: Box< - dyn futures::stream::Stream - + Send, + line: String, + stream: Option< + Box< + dyn futures::stream::Stream + + Send, + >, >, + manage_screen: bool, } impl Eval { - fn new(line: &str) -> Result { - let (cmd, args) = - crate::parser::parse(line).context(Parser { line })?; - let builtin_stream = crate::builtins::exec(&cmd, &args); - let stream: Box< - dyn futures::stream::Stream - + Send, - > = if let Ok(s) = builtin_stream { - Box::new(s.context(BuiltinExecution { cmd })) - } else { - let process_stream = crate::process::spawn(&cmd, &args); - match process_stream { - Ok(s) => Box::new(s.context(ProcessExecution { cmd })), - Err(e) => { - return Err(e).context(Command { cmd }); - } - } - }; - Ok(Self { stream }) + pub fn new(line: &str) -> Self { + Self { + line: line.to_string(), + stream: None, + manage_screen: true, + } + } + + pub fn set_raw(mut self, raw: bool) -> Self { + self.manage_screen = raw; + self } } @@ -76,6 +73,32 @@ impl futures::stream::Stream for Eval { type Error = Error; fn poll(&mut self) -> futures::Poll, Self::Error> { - self.stream.poll() + if self.stream.is_none() { + let line = self.line.as_ref(); + let (cmd, args) = + crate::parser::parse(line).context(Parser { line })?; + let builtin_stream = crate::builtins::Builtin::new(&cmd, &args); + let stream: Box< + dyn futures::stream::Stream< + Item = CommandEvent, + Error = Error, + > + Send, + > = if let Ok(s) = builtin_stream { + Box::new(s.context(BuiltinExecution { cmd })) + } else { + let process_stream = + crate::process::Process::new(&cmd, &args) + .context(Command { cmd: cmd.clone() })? + .set_raw(self.manage_screen); + Box::new(process_stream.context(ProcessExecution { cmd })) + }; + self.stream = Some(stream); + } + + if let Some(ref mut stream) = &mut self.stream { + stream.poll() + } else { + unreachable!() + } } } diff --git a/src/process.rs b/src/process.rs index 4845bef..9993be7 100644 --- a/src/process.rs +++ b/src/process.rs @@ -39,11 +39,7 @@ pub enum Error { pub type Result = std::result::Result; -pub fn spawn(cmd: &str, args: &[String]) -> Result { - RunningProcess::new(cmd, args) -} - -pub struct RunningProcess { +pub struct Process { pty: tokio_pty_process::AsyncPtyMaster, process: tokio_pty_process::Child, // TODO: tokio::io::Stdin is broken @@ -55,7 +51,8 @@ pub struct RunningProcess { started: bool, output_done: bool, exit_done: bool, - _screen: crossterm::RawScreen, + manage_screen: bool, + raw_screen: Option, } struct Resizer<'a, T> { @@ -75,8 +72,8 @@ impl<'a, T: tokio_pty_process::PtyMaster> futures::future::Future } } -impl RunningProcess { - fn new(cmd: &str, args: &[String]) -> Result { +impl Process { + pub fn new(cmd: &str, args: &[String]) -> Result { let pty = tokio_pty_process::AsyncPtyMaster::open().context(OpenPty)?; @@ -108,18 +105,30 @@ impl RunningProcess { started: false, output_done: false, exit_done: false, - _screen: crossterm::RawScreen::into_raw_mode() - .context(IntoRawMode)?, + manage_screen: true, + raw_screen: None, }) } + + #[allow(dead_code)] + pub fn set_raw(mut self, raw: bool) -> Self { + self.manage_screen = raw; + self + } } #[must_use = "streams do nothing unless polled"] -impl futures::stream::Stream for RunningProcess { +impl futures::stream::Stream for Process { type Item = crate::eval::CommandEvent; type Error = Error; fn poll(&mut self) -> futures::Poll, Self::Error> { + if self.manage_screen && self.raw_screen.is_none() { + self.raw_screen = Some( + crossterm::RawScreen::into_raw_mode().context(IntoRawMode)?, + ); + } + if !self.started { self.started = true; return Ok(futures::Async::Ready(Some( diff --git a/src/readline.rs b/src/readline.rs index f878d50..0efd982 100644 --- a/src/readline.rs +++ b/src/readline.rs @@ -24,19 +24,21 @@ pub enum Error { pub type Result = std::result::Result; -pub fn readline() -> Result { +pub fn readline() -> Readline { Readline::new() } pub struct Readline { reader: Option, state: ReadlineState, - _raw_screen: crossterm::RawScreen, + raw_screen: Option, } struct ReadlineState { prompt: String, echo: bool, + output: bool, + manage_screen: bool, buffer: String, cursor: usize, @@ -44,21 +46,20 @@ struct ReadlineState { } impl Readline { - pub fn new() -> Result { - let screen = - crossterm::RawScreen::into_raw_mode().context(IntoRawMode)?; - - Ok(Self { + pub fn new() -> Self { + Self { reader: None, state: ReadlineState { prompt: String::from("$ "), echo: true, + output: true, + manage_screen: true, buffer: String::new(), cursor: 0, wrote_prompt: false, }, - _raw_screen: screen, - }) + raw_screen: None, + } } #[allow(dead_code)] @@ -73,6 +74,22 @@ impl Readline { self } + #[allow(dead_code)] + pub fn disable_output(mut self, disable: bool) -> Self { + self.state.output = !disable; + self + } + + pub fn set_raw(mut self, raw: bool) -> Self { + self.state.manage_screen = raw; + self + } + + #[allow(dead_code)] + pub fn cursor_pos(&self) -> usize { + self.state.cursor + } + fn with_reader(&mut self, f: F) -> Result where F: FnOnce(&KeyReader, &mut ReadlineState) -> Result, @@ -212,6 +229,10 @@ impl ReadlineState { } fn write(&self, buf: &[u8]) -> std::io::Result<()> { + if !self.output { + return Ok(()); + } + let stdout = std::io::stdout(); let mut stdout = stdout.lock(); stdout.write_all(buf)?; @@ -246,6 +267,15 @@ impl ReadlineState { } } +impl std::fmt::Display for Readline { + fn fmt( + &self, + f: &mut std::fmt::Formatter, + ) -> std::result::Result<(), std::fmt::Error> { + write!(f, "{}{}", self.state.prompt, self.state.buffer) + } +} + #[must_use = "futures do nothing unless polled"] impl futures::future::Future for Readline { type Item = String; @@ -257,6 +287,12 @@ impl futures::future::Future for Readline { self.state.wrote_prompt = true; } + if self.state.manage_screen && self.raw_screen.is_none() { + self.raw_screen = Some( + crossterm::RawScreen::into_raw_mode().context(IntoRawMode)?, + ); + } + self.with_reader(|reader, state| { loop { match reader.events.try_recv() { diff --git a/src/repl.rs b/src/repl.rs index 7f4c10d..4188fbe 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -1,4 +1,4 @@ -use futures::future::{Future as _, IntoFuture as _}; +use futures::future::Future as _; use futures::stream::Stream as _; use snafu::futures01::{FutureExt as _, StreamExt as _}; use snafu::ResultExt as _; @@ -51,20 +51,14 @@ pub fn repl() { } fn read() -> impl futures::future::Future { - crate::readline::readline() - .into_future() - .flatten() - .context(Read) + crate::readline::readline().context(Read) } fn eval( line: &str, ) -> impl futures::stream::Stream { - crate::eval::eval(line) - .into_future() - .flatten_stream() - .context(Eval) + crate::eval::eval(line).context(Eval) } fn print(event: &crate::eval::CommandEvent) -> Result<()> { diff --git a/src/tui.rs b/src/tui.rs index 49f7d6f..17b2eed 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -8,6 +8,12 @@ enum Error { #[snafu(display("invalid command index: {}", idx))] InvalidCommandIndex { idx: usize }, + #[snafu(display( + "failed to put the terminal into raw mode: {}", + source + ))] + IntoRawMode { source: std::io::Error }, + #[snafu(display("error during read: {}", source))] Read { source: crate::readline::Error }, @@ -32,6 +38,7 @@ pub struct Tui { idx: usize, readline: Option, commands: std::collections::HashMap, + raw_screen: Option, } impl Tui { @@ -39,8 +46,8 @@ impl Tui { Self::default() } - fn read() -> Result { - crate::readline::readline().context(Read) + fn read() -> crate::readline::Readline { + crate::readline::Readline::new().set_raw(false) } fn eval( @@ -51,13 +58,8 @@ impl Tui { if self.commands.contains_key(&idx) { return Err(Error::InvalidCommandIndex { idx }); } - let eval = crate::eval::eval(line).context(Eval); - match eval { - Ok(eval) => { - self.commands.insert(idx, Command::new(eval)); - } - Err(e) => return Err(e), - } + let eval = crate::eval::Eval::new(line).set_raw(false); + self.commands.insert(idx, Command::new(eval)); Ok(()) } @@ -124,25 +126,17 @@ impl Tui { Ok(()) } - fn poll_read(&mut self) -> Result { + fn poll_read(&mut self) { if self.readline.is_none() && self.commands.is_empty() { self.idx += 1; - self.readline = Some(Self::read()?) + self.readline = Some(Self::read()) } - Ok(false) } fn poll_eval(&mut self) -> Result { if let Some(mut r) = self.readline.take() { match r.poll() { Ok(futures::Async::Ready(line)) => { - // overlapping RawScreen lifespans don't work properly - // - if readline creates a RawScreen, then eval - // creates a separate one, then readline drops it, the - // screen will go back to cooked even though a - // RawScreen instance is still live. - drop(r); - match self.eval(self.idx, &line) { Ok(()) | Err(Error::Eval { @@ -173,25 +167,36 @@ impl Tui { let mut did_work = false; for idx in self.commands.keys().cloned().collect::>() { - match self - .commands - .get_mut(&idx) - .unwrap() - .future - .poll() - .context(Eval)? - { - futures::Async::Ready(Some(event)) => { + match self.commands.get_mut(&idx).unwrap().future.poll() { + Ok(futures::Async::Ready(Some(event))) => { self.print(idx, event)?; did_work = true; } - futures::Async::Ready(None) => { + Ok(futures::Async::Ready(None)) => { self.commands .remove(&idx) .context(InvalidCommandIndex { idx })?; did_work = true; } - futures::Async::NotReady => {} + Ok(futures::Async::NotReady) => {} + + // Parser and Command errors are always fatal, but execution + // errors might not be + Err(e @ crate::eval::Error::Parser { .. }) => { + self.commands + .remove(&idx) + .context(InvalidCommandIndex { idx })?; + return Err(e).context(Eval); + } + Err(e @ crate::eval::Error::Command { .. }) => { + self.commands + .remove(&idx) + .context(InvalidCommandIndex { idx })?; + return Err(e).context(Eval); + } + Err(e) => { + return Err(e).context(Eval); + } } } @@ -199,10 +204,16 @@ impl Tui { } fn poll_with_errors(&mut self) -> futures::Poll<(), Error> { + if self.raw_screen.is_none() { + self.raw_screen = Some( + crossterm::RawScreen::into_raw_mode().context(IntoRawMode)?, + ); + } + loop { let mut did_work = false; - did_work |= self.poll_read()?; + self.poll_read(); did_work |= self.poll_eval()?; did_work |= self.poll_print()?; -- cgit v1.2.3-54-g00ecf