diff options
author | Jesse Luehrs <doy@tozt.net> | 2021-11-09 21:23:19 -0500 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2021-11-09 21:23:19 -0500 |
commit | fbc4db18ecea9755199b945368c02f32c0481c95 (patch) | |
tree | dfe91e5f538aeb6642e74af09323c288933cb00b /src | |
parent | 66cdb4e85efaa2e9abf270fc875239b6790c4a40 (diff) | |
download | nbsh-fbc4db18ecea9755199b945368c02f32c0481c95.tar.gz nbsh-fbc4db18ecea9755199b945368c02f32c0481c95.zip |
basic command running and display
Diffstat (limited to 'src')
-rw-r--r-- | src/history.rs | 126 | ||||
-rw-r--r-- | src/main.rs | 13 | ||||
-rw-r--r-- | src/nbsh.rs | 106 | ||||
-rw-r--r-- | src/repl.rs | 45 |
4 files changed, 287 insertions, 3 deletions
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(()) + } +} |