From fbc4db18ecea9755199b945368c02f32c0481c95 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Tue, 9 Nov 2021 21:23:19 -0500 Subject: basic command running and display --- Cargo.lock | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 +++ src/history.rs | 126 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 13 +++- src/nbsh.rs | 106 ++++++++++++++++++++++++++++++++ src/repl.rs | 45 ++++++++++++++ 6 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 src/history.rs create mode 100644 src/nbsh.rs create mode 100644 src/repl.rs diff --git a/Cargo.lock b/Cargo.lock index 29524a8..1797f47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "async-channel" version = "1.6.1" @@ -86,6 +92,23 @@ dependencies = [ "event-listener", ] +[[package]] +name = "async-process" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b21b63ab5a0db0369deb913540af2892750e42d949faacc7a61495ac418a1692" +dependencies = [ + "async-io", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi", +] + [[package]] name = "async-std" version = "1.10.0" @@ -131,6 +154,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + [[package]] name = "blocking" version = "1.0.2" @@ -353,6 +382,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "js-sys" version = "0.3.55" @@ -379,9 +414,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.106" +version = "0.2.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673" +checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" [[package]] name = "log" @@ -399,13 +434,40 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + [[package]] name = "nbsh" version = "0.1.0" dependencies = [ "anyhow", + "async-process", "async-std", "futures", + "libc", + "pty-process", + "textmode", + "vt100", +] + +[[package]] +name = "nix" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e06129fb611568ef4e868c14b326274959aa70ff7776e9d55323531c374945" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", ] [[package]] @@ -476,6 +538,19 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "pty-process" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e989070d8241a051075bad757baf7b27518ce532bca7ce1c4599f5c35dfb18" +dependencies = [ + "async-io", + "async-process", + "libc", + "nix", + "thiserror", +] + [[package]] name = "quote" version = "1.0.10" @@ -485,6 +560,25 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "signal-hook" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.5" @@ -512,12 +606,69 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textmode" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a744371e8b61790d5df2f0d64568d3423fcea761dd9d069eb4199228394b010" +dependencies = [ + "blocking", + "futures-lite", + "itoa", + "nix", + "terminal_size", + "thiserror", + "vt100", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "value-bag" version = "1.0.0-alpha.8" @@ -534,6 +685,39 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "vt100" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6552f8bf8f321d38f509ddf5c5c092df487e2d0b5d89f0025fc010372693779e" +dependencies = [ + "itoa", + "log", + "unicode-width", + "vte", +] + +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index facee8c..adb439d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,12 @@ edition = "2021" [dependencies] anyhow = "1.0.45" +async-process = "1.2.0" async-std = "1.10.0" futures = "0.3.17" +libc = "0.2.107" +pty-process = { version = "0.1.0", features = ["backend-async-std"] } +textmode = { version = "0.1.0", features = ["async"] } +vt100 = "0.12.0" + +[features] diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..339e53d --- /dev/null +++ b/src/history.rs @@ -0,0 +1,126 @@ +use async_std::io::ReadExt as _; +use pty_process::Command as _; +use textmode::Textmode as _; + +#[derive(Default)] +pub struct History { + entries: Vec>>, +} + +impl History { + pub fn new() -> Self { + Self::default() + } + + pub async fn run( + &mut self, + cmd: &str, + render: async_std::channel::Sender<()>, + ) -> anyhow::Result<()> { + let (exe, args) = parse_cmd(cmd); + let mut process = async_process::Command::new(&exe); + process.args(&args); + let child = process + .spawn_pty(Some(&pty_process::Size::new(24, 80))) + .unwrap(); + let entry = async_std::sync::Arc::new(async_std::sync::Mutex::new( + HistoryEntry::new(cmd), + )); + let task_entry = async_std::sync::Arc::clone(&entry); + let task_render = render.clone(); + async_std::task::spawn(async move { + loop { + let mut buf = [0_u8; 4096]; + match child.pty().read(&mut buf).await { + Ok(bytes) => { + task_entry.lock_arc().await.vt.process(&buf[..bytes]); + } + Err(e) => { + if e.raw_os_error() != Some(libc::EIO) { + eprintln!("pty read failed: {:?}", e); + } + task_entry.lock_arc().await.running = false; + task_render.send(()).await.unwrap(); + break; + } + } + task_render.send(()).await.unwrap(); + } + }); + self.entries.push(entry); + render.send(()).await.unwrap(); + Ok(()) + } + + pub async fn render( + &self, + out: &mut textmode::Output, + repl_lines: usize, + ) -> anyhow::Result<()> { + let mut used_lines = repl_lines; + for entry in self.entries.iter().rev() { + let entry = entry.lock_arc().await; + let screen = entry.vt.screen(); + let mut last_row = 0; + for (idx, row) in screen.rows(0, 80).enumerate() { + if !row.is_empty() { + last_row = idx + 1; + } + } + used_lines += 1 + std::cmp::min(6, last_row); + if used_lines > 24 { + break; + } + out.move_to((24 - used_lines).try_into().unwrap(), 0); + out.write_str("$ "); + if entry.running { + out.set_bgcolor(vt100::Color::Rgb(16, 64, 16)); + } + out.write_str(&entry.cmd); + out.reset_attributes(); + out.write(b"\r\n"); + if last_row > 5 { + out.set_bgcolor(textmode::color::RED); + out.write(b"..."); + out.reset_attributes(); + out.write(b"\r\n"); + } + for row in screen + .rows_formatted(0, 80) + .take(last_row) + .skip(last_row.saturating_sub(5)) + { + out.write(&row); + out.write(b"\r\n"); + } + out.reset_attributes(); + } + Ok(()) + } +} + +struct HistoryEntry { + cmd: String, + vt: vt100::Parser, + running: bool, // option end time + // start time +} + +impl HistoryEntry { + fn new(cmd: &str) -> Self { + Self { + cmd: cmd.into(), + vt: vt100::Parser::new(24, 80, 0), + running: true, + } + } +} + +fn parse_cmd(full_cmd: &str) -> (String, Vec) { + let mut parts = full_cmd.split(' '); + let cmd = parts.next().unwrap(); + ( + cmd.to_string(), + parts.map(std::string::ToString::to_string).collect(), + ) +} diff --git a/src/main.rs b/src/main.rs index f1fce9f..0d1bb69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,15 @@ -use async_std::io::WriteExt as _; +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![allow(clippy::missing_const_for_fn)] +#![allow(clippy::unused_self)] + +mod history; +mod nbsh; +mod repl; async fn async_main() -> anyhow::Result<()> { - async_std::io::stdout().write_all(b"hello world\n").await?; - Ok(()) + let nbsh = nbsh::Nbsh::new(); + nbsh.run().await } fn main() { diff --git a/src/nbsh.rs b/src/nbsh.rs new file mode 100644 index 0000000..772ff5c --- /dev/null +++ b/src/nbsh.rs @@ -0,0 +1,106 @@ +use textmode::Textmode as _; + +pub struct Nbsh { + repl: crate::repl::Repl, + history: crate::history::History, +} + +impl Nbsh { + pub fn new() -> Self { + Self { + repl: crate::repl::Repl::new(), + history: crate::history::History::new(), + } + } + + pub async fn run(self) -> anyhow::Result<()> { + let mut input = textmode::Input::new().await?; + let mut output = textmode::Output::new().await?; + + // avoid the guards getting stuck in a task that doesn't run to + // completion + let _input_guard = input.take_raw_guard(); + let _output_guard = output.take_screen_guard(); + + let (run_w, run_r) = async_std::channel::unbounded(); + let (render_w, render_r) = async_std::channel::unbounded(); + + self.render(&mut output).await.unwrap(); + + let locked_self = + async_std::sync::Arc::new(async_std::sync::Mutex::new(self)); + + let readline_self = std::sync::Arc::clone(&locked_self); + let readline_render = render_w.clone(); + let readline_task = async_std::task::spawn(async move { + loop { + let key = input.read_key().await.unwrap(); + let mut self_ = readline_self.lock_arc().await; + let (last, cmd) = self_.handle_key(key); + if last { + break; + } + if let Some(cmd) = cmd { + run_w.send(cmd).await.unwrap(); + } + readline_render.send(()).await.unwrap(); + } + }); + + let history_self = std::sync::Arc::clone(&locked_self); + let history_render = render_w.clone(); + async_std::task::spawn(async move { + while let Ok(cmd) = run_r.recv().await { + let mut self_ = history_self.lock_arc().await; + self_ + .history + .run(&cmd, history_render.clone()) + .await + .unwrap(); + } + }); + + let render_self = std::sync::Arc::clone(&locked_self); + async_std::task::spawn(async move { + while let Ok(()) = render_r.recv().await { + while let Ok(()) = render_r.try_recv() {} + let self_ = render_self.lock_arc().await; + self_.render(&mut output).await.unwrap(); + } + }); + + readline_task.await; + + Ok(()) + } + + fn handle_key( + &mut self, + key: Option, + ) -> (bool, Option) { + let mut cmd = None; + match key { + Some(textmode::Key::String(s)) => self.repl.add_input(&s), + Some(textmode::Key::Char(c)) => { + self.repl.add_input(&c.to_string()); + } + Some(textmode::Key::Ctrl(b'c')) => self.repl.clear_input(), + Some(textmode::Key::Ctrl(b'd')) | None => return (true, None), + Some(textmode::Key::Ctrl(b'm')) => { + cmd = Some(self.repl.input()); + self.repl.clear_input(); + } + Some(textmode::Key::Backspace) => self.repl.backspace(), + _ => {} + } + (false, cmd) + } + + async fn render(&self, out: &mut textmode::Output) -> anyhow::Result<()> { + out.clear(); + self.history.render(out, self.repl.lines()).await?; + self.repl.render(out).await?; + out.refresh().await?; + Ok(()) + } +} diff --git a/src/repl.rs b/src/repl.rs new file mode 100644 index 0000000..258718e --- /dev/null +++ b/src/repl.rs @@ -0,0 +1,45 @@ +use textmode::Textmode as _; + +pub struct Repl { + prompt: String, + input_line: String, +} + +impl Repl { + pub fn new() -> Self { + Self { + prompt: "$ ".into(), + input_line: "".into(), + } + } + + pub fn input(&self) -> String { + self.input_line.clone() + } + + pub fn add_input(&mut self, s: &str) { + self.input_line.push_str(s); + } + + pub fn backspace(&mut self) { + self.input_line.pop(); + } + + pub fn clear_input(&mut self) { + self.input_line.clear(); + } + + pub fn lines(&self) -> usize { + 1 // XXX handle wrapping, multiline prompts + } + + pub async fn render( + &self, + out: &mut textmode::Output, + ) -> anyhow::Result<()> { + out.move_to(23, 0); + out.write_str(&self.prompt); + out.write_str(&self.input_line); + Ok(()) + } +} -- cgit v1.2.3-54-g00ecf