diff options
author | Jesse Luehrs <doy@tozt.net> | 2022-01-05 07:18:29 -0500 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2022-01-05 07:18:29 -0500 |
commit | a30174620d6b64f838989a634c265a353b2ab117 (patch) | |
tree | 023ff89d0a7b61550b17eb49702722c81c38499c /src/shell/mod.rs | |
parent | 404ae6202e24c7bfc5625edb3ac064df4ecd105f (diff) | |
download | nbsh-a30174620d6b64f838989a634c265a353b2ab117.tar.gz nbsh-a30174620d6b64f838989a634c265a353b2ab117.zip |
a bunch more reorganization
Diffstat (limited to 'src/shell/mod.rs')
-rw-r--r-- | src/shell/mod.rs | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/src/shell/mod.rs b/src/shell/mod.rs new file mode 100644 index 0000000..a19ae09 --- /dev/null +++ b/src/shell/mod.rs @@ -0,0 +1,591 @@ +use crate::shell::prelude::*; + +use async_std::stream::StreamExt as _; +use textmode::Textmode as _; + +mod event; +mod history; +mod prelude; +mod readline; + +pub async fn run() -> anyhow::Result<i32> { + 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 (event_w, event_r) = async_std::channel::unbounded(); + + { + // nix::sys::signal::Signal is repr(i32) + #[allow(clippy::as_conversions)] + let signals = signal_hook_async_std::Signals::new(&[ + nix::sys::signal::Signal::SIGWINCH as i32, + ])?; + let event_w = event_w.clone(); + async_std::task::spawn(async move { + // nix::sys::signal::Signal is repr(i32) + #[allow(clippy::as_conversions)] + let mut signals = async_std::stream::once( + nix::sys::signal::Signal::SIGWINCH as i32, + ) + .chain(signals); + while signals.next().await.is_some() { + event_w + .send(Event::Resize( + terminal_size::terminal_size().map_or( + (24, 80), + |( + terminal_size::Width(w), + terminal_size::Height(h), + )| { (h, w) }, + ), + )) + .await + .unwrap(); + } + }); + } + + { + let event_w = event_w.clone(); + async_std::task::spawn(async move { + while let Some(key) = input.read_key().await.unwrap() { + event_w.send(Event::Key(key)).await.unwrap(); + } + }); + } + + // redraw the clock every second + { + let event_w = event_w.clone(); + async_std::task::spawn(async move { + let first_sleep = 1_000_000_000_u64.saturating_sub( + time::OffsetDateTime::now_utc().nanosecond().into(), + ); + async_std::task::sleep(std::time::Duration::from_nanos( + first_sleep, + )) + .await; + let mut interval = async_std::stream::interval( + std::time::Duration::from_secs(1), + ); + event_w.send(Event::ClockTimer).await.unwrap(); + while interval.next().await.is_some() { + event_w.send(Event::ClockTimer).await.unwrap(); + } + }); + } + + let mut shell = Shell::new(crate::info::get_offset()); + let event_reader = event::Reader::new(event_r); + while let Some(event) = event_reader.recv().await { + match shell.handle_event(event, &event_w).await { + Some(Action::Refresh) => { + shell.render(&mut output).await?; + output.refresh().await?; + } + Some(Action::HardRefresh) => { + shell.render(&mut output).await?; + output.hard_refresh().await?; + } + Some(Action::Resize(rows, cols)) => { + output.set_size(rows, cols); + shell.render(&mut output).await?; + output.hard_refresh().await?; + } + Some(Action::Quit) => break, + None => {} + } + } + + Ok(0) +} + +#[derive(Copy, Clone, Debug)] +enum Focus { + Readline, + History(usize), + Scrolling(Option<usize>), +} + +#[derive(Copy, Clone, Debug)] +enum Scene { + Readline, + Fullscreen, +} + +pub enum Action { + Refresh, + HardRefresh, + Resize(u16, u16), + Quit, +} + +pub struct Shell { + readline: readline::Readline, + history: history::History, + env: crate::Env, + focus: Focus, + scene: Scene, + escape: bool, + hide_readline: bool, + offset: time::UtcOffset, +} + +impl Shell { + pub fn new(offset: time::UtcOffset) -> Self { + Self { + readline: readline::Readline::new(), + history: history::History::new(), + env: crate::Env::new(), + focus: Focus::Readline, + scene: Scene::Readline, + escape: false, + hide_readline: false, + offset, + } + } + + // render always happens on the main task + #[allow(clippy::future_not_send)] + pub async fn render( + &self, + out: &mut impl textmode::Textmode, + ) -> anyhow::Result<()> { + out.clear(); + out.write(&vt100::Parser::default().screen().input_mode_formatted()); + match self.scene { + Scene::Readline => match self.focus { + Focus::Readline => { + self.history + .render( + out, + self.readline.lines(), + None, + false, + self.offset, + ) + .await?; + self.readline + .render(out, &self.env, true, self.offset) + .await?; + } + Focus::History(idx) => { + if self.hide_readline { + self.history + .render(out, 0, Some(idx), false, self.offset) + .await?; + } else { + self.history + .render( + out, + self.readline.lines(), + Some(idx), + false, + self.offset, + ) + .await?; + let pos = out.screen().cursor_position(); + self.readline + .render(out, &self.env, false, self.offset) + .await?; + out.move_to(pos.0, pos.1); + } + } + Focus::Scrolling(idx) => { + self.history + .render( + out, + self.readline.lines(), + idx, + true, + self.offset, + ) + .await?; + self.readline + .render(out, &self.env, idx.is_none(), self.offset) + .await?; + out.hide_cursor(true); + } + }, + Scene::Fullscreen => { + if let Focus::History(idx) = self.focus { + self.history.render_fullscreen(out, idx).await; + } else { + unreachable!(); + } + } + } + Ok(()) + } + + pub async fn handle_event( + &mut self, + event: Event, + event_w: &async_std::channel::Sender<Event>, + ) -> Option<Action> { + match event { + Event::Key(key) => { + return if self.escape { + self.escape = false; + self.handle_key_escape(key, event_w.clone()).await + } else if key == textmode::Key::Ctrl(b'e') { + self.escape = true; + None + } else { + match self.focus { + Focus::Readline => { + self.handle_key_readline(key, event_w.clone()) + .await + } + Focus::History(idx) => { + self.handle_key_history(key, idx).await; + None + } + Focus::Scrolling(_) => { + self.handle_key_escape(key, event_w.clone()).await + } + } + }; + } + Event::Resize(new_size) => { + self.readline.resize(new_size).await; + self.history.resize(new_size).await; + return Some(Action::Resize(new_size.0, new_size.1)); + } + Event::PtyOutput => { + // the number of visible lines may have changed, so make sure + // the focus is still visible + self.history + .make_focus_visible( + self.readline.lines(), + self.focus_idx(), + matches!(self.focus, Focus::Scrolling(_)), + ) + .await; + self.scene = self.default_scene(self.focus, None).await; + } + Event::PtyClose => { + if let Some(idx) = self.focus_idx() { + let entry = self.history.entry(idx).await; + if !entry.running() { + if self.hide_readline { + let idx = self.env.idx(); + self.env = entry.env().clone(); + self.env.set_idx(idx); + } + self.set_focus( + if self.hide_readline { + Focus::Readline + } else { + Focus::Scrolling(Some(idx)) + }, + Some(entry), + ) + .await; + } + } + } + Event::ChildSuspend(idx) => { + if self.focus_idx() == Some(idx) { + self.set_focus(Focus::Readline, None).await; + } + } + Event::ClockTimer => {} + }; + Some(Action::Refresh) + } + + async fn handle_key_escape( + &mut self, + key: textmode::Key, + event_w: async_std::channel::Sender<Event>, + ) -> Option<Action> { + match key { + textmode::Key::Ctrl(b'd') => { + return Some(Action::Quit); + } + textmode::Key::Ctrl(b'e') => { + self.set_focus(Focus::Scrolling(self.focus_idx()), None) + .await; + } + textmode::Key::Ctrl(b'l') => { + return Some(Action::HardRefresh); + } + textmode::Key::Ctrl(b'm') => { + if let Some(idx) = self.focus_idx() { + self.readline.clear_input(); + let entry = self.history.entry(idx).await; + let input = entry.cmd(); + let idx = match self.parse(input) { + Ok(ast) => { + let idx = self + .history + .run(ast, &self.env, event_w.clone()) + .await + .unwrap(); + self.set_focus(Focus::History(idx), Some(entry)) + .await; + self.hide_readline = true; + idx + } + Err(e) => self + .history + .parse_error(e, &self.env, event_w.clone()) + .await + .unwrap(), + }; + self.env.set_idx(idx + 1); + } else { + self.set_focus(Focus::Readline, None).await; + } + } + textmode::Key::Char(' ') => { + let idx = self.focus_idx(); + let (focus, entry) = if let Some(idx) = idx { + let entry = self.history.entry(idx).await; + (entry.running(), Some(entry)) + } else { + (true, None) + }; + if focus { + self.set_focus( + idx.map_or(Focus::Readline, |idx| { + Focus::History(idx) + }), + entry, + ) + .await; + } + } + textmode::Key::Char('e') => { + if let Focus::History(idx) = self.focus { + self.handle_key_history(textmode::Key::Ctrl(b'e'), idx) + .await; + } + } + textmode::Key::Char('f') => { + if let Some(idx) = self.focus_idx() { + let mut entry = self.history.entry(idx).await; + let mut focus = Focus::History(idx); + if let Focus::Scrolling(_) = self.focus { + entry.set_fullscreen(true); + } else { + entry.toggle_fullscreen(); + if !entry.should_fullscreen() && !entry.running() { + focus = Focus::Scrolling(Some(idx)); + } + } + self.set_focus(focus, Some(entry)).await; + } + } + textmode::Key::Char('i') => { + if let Some(idx) = self.focus_idx() { + let entry = self.history.entry(idx).await; + self.readline.set_input(entry.cmd()); + self.set_focus(Focus::Readline, Some(entry)).await; + } + } + textmode::Key::Char('j') | textmode::Key::Down => { + self.set_focus( + Focus::Scrolling(self.scroll_down(self.focus_idx())), + None, + ) + .await; + } + textmode::Key::Char('k') | textmode::Key::Up => { + self.set_focus( + Focus::Scrolling(self.scroll_up(self.focus_idx())), + None, + ) + .await; + } + textmode::Key::Char('n') => { + self.set_focus(self.next_running().await, None).await; + } + textmode::Key::Char('p') => { + self.set_focus(self.prev_running().await, None).await; + } + textmode::Key::Char('r') => { + self.set_focus(Focus::Readline, None).await; + } + _ => { + return None; + } + } + Some(Action::Refresh) + } + + async fn handle_key_readline( + &mut self, + key: textmode::Key, + event_w: async_std::channel::Sender<Event>, + ) -> Option<Action> { + match key { + textmode::Key::Char(c) => { + self.readline.add_input(&c.to_string()); + } + textmode::Key::Ctrl(b'c') => self.readline.clear_input(), + textmode::Key::Ctrl(b'd') => { + return Some(Action::Quit); + } + textmode::Key::Ctrl(b'l') => { + return Some(Action::HardRefresh); + } + textmode::Key::Ctrl(b'm') => { + let input = self.readline.input(); + if !input.is_empty() { + let idx = match self.parse(input) { + Ok(ast) => { + let idx = self + .history + .run(ast, &self.env, event_w.clone()) + .await + .unwrap(); + self.set_focus(Focus::History(idx), None).await; + self.hide_readline = true; + idx + } + Err(e) => self + .history + .parse_error(e, &self.env, event_w.clone()) + .await + .unwrap(), + }; + self.env.set_idx(idx + 1); + self.readline.clear_input(); + } + } + textmode::Key::Ctrl(b'u') => self.readline.clear_backwards(), + textmode::Key::Backspace => self.readline.backspace(), + textmode::Key::Left => self.readline.cursor_left(), + textmode::Key::Right => self.readline.cursor_right(), + textmode::Key::Up => { + let entry_count = self.history.entry_count(); + if entry_count > 0 { + self.set_focus( + Focus::Scrolling(Some(entry_count - 1)), + None, + ) + .await; + } + } + _ => return None, + } + Some(Action::Refresh) + } + + async fn handle_key_history(&mut self, key: textmode::Key, idx: usize) { + self.history.send_input(idx, key.into_bytes()).await; + } + + async fn default_scene( + &self, + focus: Focus, + entry: Option<async_std::sync::MutexGuardArc<history::Entry>>, + ) -> Scene { + match focus { + Focus::Readline | Focus::Scrolling(_) => Scene::Readline, + Focus::History(idx) => { + let fullscreen = if let Some(entry) = entry { + entry.should_fullscreen() + } else { + self.history.entry(idx).await.should_fullscreen() + }; + if fullscreen { + Scene::Fullscreen + } else { + Scene::Readline + } + } + } + } + + async fn set_focus( + &mut self, + new_focus: Focus, + entry: Option<async_std::sync::MutexGuardArc<history::Entry>>, + ) { + self.focus = new_focus; + self.hide_readline = false; + self.scene = self.default_scene(new_focus, entry).await; + // passing entry into default_scene above consumes it, which means + // that the mutex lock will be dropped before we call into + // make_focus_visible, which is important because otherwise we might + // get a deadlock depending on what is visible + self.history + .make_focus_visible( + self.readline.lines(), + self.focus_idx(), + matches!(self.focus, Focus::Scrolling(_)), + ) + .await; + } + + fn focus_idx(&self) -> Option<usize> { + match self.focus { + Focus::History(idx) => Some(idx), + Focus::Readline => None, + Focus::Scrolling(idx) => idx, + } + } + + fn scroll_up(&self, idx: Option<usize>) -> Option<usize> { + idx.map_or_else( + || { + let count = self.history.entry_count(); + if count == 0 { + None + } else { + Some(count - 1) + } + }, + |idx| Some(idx.saturating_sub(1)), + ) + } + + fn scroll_down(&self, idx: Option<usize>) -> Option<usize> { + idx.and_then(|idx| { + if idx >= self.history.entry_count() - 1 { + None + } else { + Some(idx + 1) + } + }) + } + + async fn next_running(&self) -> Focus { + let count = self.history.entry_count(); + let cur = self.focus_idx().unwrap_or(count); + for idx in ((cur + 1)..count).chain(0..cur) { + if self.history.entry(idx).await.running() { + return Focus::History(idx); + } + } + self.focus_idx().map_or(Focus::Readline, Focus::History) + } + + async fn prev_running(&self) -> Focus { + let count = self.history.entry_count(); + let cur = self.focus_idx().unwrap_or(count); + for idx in ((cur + 1)..count).chain(0..cur).rev() { + if self.history.entry(idx).await.running() { + return Focus::History(idx); + } + } + self.focus_idx().map_or(Focus::Readline, Focus::History) + } + + fn parse( + &self, + cmd: &str, + ) -> Result<crate::parse::Commands, crate::parse::Error> { + let ast = crate::parse::Commands::parse(cmd)?; + // todo: interpolate + Ok(ast) + } +} |