summaryrefslogtreecommitdiffstats
path: root/src/shell/mod.rs
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2022-01-05 07:18:29 -0500
committerJesse Luehrs <doy@tozt.net>2022-01-05 07:18:29 -0500
commita30174620d6b64f838989a634c265a353b2ab117 (patch)
tree023ff89d0a7b61550b17eb49702722c81c38499c /src/shell/mod.rs
parent404ae6202e24c7bfc5625edb3ac064df4ecd105f (diff)
downloadnbsh-a30174620d6b64f838989a634c265a353b2ab117.tar.gz
nbsh-a30174620d6b64f838989a634c265a353b2ab117.zip
a bunch more reorganization
Diffstat (limited to 'src/shell/mod.rs')
-rw-r--r--src/shell/mod.rs591
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)
+ }
+}