summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2021-11-09 21:23:19 -0500
committerJesse Luehrs <doy@tozt.net>2021-11-09 21:23:19 -0500
commitfbc4db18ecea9755199b945368c02f32c0481c95 (patch)
treedfe91e5f538aeb6642e74af09323c288933cb00b /src
parent66cdb4e85efaa2e9abf270fc875239b6790c4a40 (diff)
downloadnbsh-fbc4db18ecea9755199b945368c02f32c0481c95.tar.gz
nbsh-fbc4db18ecea9755199b945368c02f32c0481c95.zip
basic command running and display
Diffstat (limited to 'src')
-rw-r--r--src/history.rs126
-rw-r--r--src/main.rs13
-rw-r--r--src/nbsh.rs106
-rw-r--r--src/repl.rs45
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(())
+ }
+}