diff options
Diffstat (limited to 'src/tui.rs')
-rw-r--r-- | src/tui.rs | 233 |
1 files changed, 228 insertions, 5 deletions
@@ -1,15 +1,238 @@ -use snafu::ResultExt as _; +use futures::future::Future as _; +use futures::stream::Stream as _; +use snafu::{OptionExt as _, ResultExt as _}; +use std::io::Write as _; #[derive(Debug, snafu::Snafu)] pub enum Error { - #[snafu(display("error from state: {}", source))] - State { source: crate::state::Error }, + #[snafu(display("invalid command index: {}", idx))] + InvalidCommandIndex { idx: usize }, + + #[snafu(display("error during read: {}", source))] + Read { source: crate::readline::Error }, + + #[snafu(display("error during eval: {}", source))] + Eval { source: crate::eval::Error }, + + #[snafu(display("error during print: {}", source))] + Print { source: std::io::Error }, + + #[snafu(display("eof"))] + EOF, } +pub type Result<T> = std::result::Result<T, Error>; + pub fn tui() { - let state = crate::state::State::new().context(State); - match state { + match Tui::new() { Ok(state) => tokio::run(state), Err(e) => eprintln!("failed to create state: {}", e), } } + +pub struct Tui { + idx: usize, + readline: Option<crate::readline::Readline>, + commands: std::collections::HashMap<usize, Command>, +} + +impl Tui { + pub fn new() -> Result<Self> { + Ok(Self { + idx: 0, + readline: Some(Self::read()?), + commands: std::collections::HashMap::new(), + }) + } + + fn read() -> Result<crate::readline::Readline> { + crate::readline::readline("$ ", true).context(Read) + } + + fn eval( + &mut self, + idx: usize, + line: &str, + ) -> std::result::Result<(), Error> { + 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), + } + Ok(()) + } + + fn command_start( + &mut self, + idx: usize, + cmd: &str, + args: &[String], + ) -> Result<()> { + let command = self + .commands + .get_mut(&idx) + .context(InvalidCommandIndex { idx })?; + let cmd = cmd.to_string(); + let args = args.to_vec(); + eprint!("running '{} {:?}'\r\n", cmd, args); + command.cmd = Some(cmd); + command.args = Some(args); + Ok(()) + } + + fn command_output(&mut self, idx: usize, output: &[u8]) -> Result<()> { + let command = self + .commands + .get_mut(&idx) + .context(InvalidCommandIndex { idx })?; + command.output.append(&mut output.to_vec()); + + let stdout = std::io::stdout(); + let mut stdout = stdout.lock(); + stdout.write(output).context(Print)?; + stdout.flush().context(Print)?; + + Ok(()) + } + + fn command_exit( + &mut self, + idx: usize, + status: std::process::ExitStatus, + ) -> Result<()> { + let command = self + .commands + .get_mut(&idx) + .context(InvalidCommandIndex { idx })?; + command.status = Some(status); + eprint!("command exited: {}\r\n", status); + Ok(()) + } + + fn poll_with_errors(&mut self) -> futures::Poll<(), Error> { + loop { + let mut did_work = false; + + if self.readline.is_none() && self.commands.is_empty() { + self.idx += 1; + self.readline = Some(Self::read()?) + } + + 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 { + source: + crate::eval::Error::Parser { + source: + crate::parser::Error::CommandRequired, + .. + }, + }) => {} + Err(e) => return Err(e), + } + did_work = true; + } + Ok(futures::Async::NotReady) => { + self.readline.replace(r); + } + Err(crate::readline::Error::EOF) => { + return Err(Error::EOF) + } + Err(e) => return Err(e).context(Read), + } + } + + for idx in self.commands.keys().cloned().collect::<Vec<usize>>() { + match self + .commands + .get_mut(&idx) + .unwrap() + .future + .poll() + .context(Eval)? + { + futures::Async::Ready(Some(event)) => match event { + crate::eval::CommandEvent::CommandStart( + cmd, + args, + ) => { + self.command_start(idx, &cmd, &args)?; + did_work = true; + } + crate::eval::CommandEvent::Output(out) => { + self.command_output(idx, &out)?; + did_work = true; + } + crate::eval::CommandEvent::CommandExit(status) => { + self.command_exit(idx, status)?; + did_work = true; + } + }, + futures::Async::Ready(None) => { + self.commands + .remove(&idx) + .context(InvalidCommandIndex { idx })?; + did_work = true; + } + futures::Async::NotReady => {} + } + } + + if !did_work { + return Ok(futures::Async::NotReady); + } + } + } +} + +impl futures::future::Future for Tui { + type Item = (); + type Error = (); + + fn poll(&mut self) -> futures::Poll<Self::Item, Self::Error> { + loop { + match self.poll_with_errors() { + Ok(a) => return Ok(a), + Err(Error::EOF) => return Ok(futures::Async::Ready(())), + Err(e) => { + eprint!("error polling state: {}\r\n", e); + } + } + } + } +} + +struct Command { + future: crate::eval::Eval, + cmd: Option<String>, + args: Option<Vec<String>>, + output: Vec<u8>, + status: Option<std::process::ExitStatus>, +} + +impl Command { + fn new(future: crate::eval::Eval) -> Self { + Self { + future, + cmd: None, + args: None, + output: vec![], + status: None, + } + } +} |