summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock188
-rw-r--r--Cargo.toml7
-rw-r--r--src/history.rs126
-rw-r--r--src/main.rs13
-rw-r--r--src/nbsh.rs106
-rw-r--r--src/repl.rs45
6 files changed, 480 insertions, 5 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 29524a8..1797f47 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9,6 +9,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -87,6 +93,23 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -132,6 +155,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -354,6 +383,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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"
@@ -400,12 +435,39 @@ 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]]
@@ -477,6 +539,19 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -486,6 +561,25 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -513,12 +607,69 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -535,6 +686,39 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<async_std::sync::Arc<async_std::sync::Mutex<HistoryEntry>>>,
+}
+
+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<String>) {
+ 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<textmode::Key>,
+ ) -> (bool, Option<String>) {
+ 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(())
+ }
+}