From 40a943a422623f0533fbac40285164709b86b5a0 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Mon, 13 Dec 2021 02:53:20 -0500 Subject: more rearranging --- src/history.rs | 574 -------------------------------------------------- src/main.rs | 2 - src/readline.rs | 161 -------------- src/state.rs | 436 -------------------------------------- src/state/history.rs | 574 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/state/mod.rs | 439 ++++++++++++++++++++++++++++++++++++++ src/state/readline.rs | 161 ++++++++++++++ 7 files changed, 1174 insertions(+), 1173 deletions(-) delete mode 100644 src/history.rs delete mode 100644 src/readline.rs delete mode 100644 src/state.rs create mode 100644 src/state/history.rs create mode 100644 src/state/mod.rs create mode 100644 src/state/readline.rs diff --git a/src/history.rs b/src/history.rs deleted file mode 100644 index 048c1c7..0000000 --- a/src/history.rs +++ /dev/null @@ -1,574 +0,0 @@ -use async_std::io::{ReadExt as _, WriteExt as _}; -use futures_lite::future::FutureExt as _; -use pty_process::Command as _; -use std::os::unix::process::ExitStatusExt as _; - -pub struct History { - size: (u16, u16), - entries: Vec>, - scroll_pos: usize, -} - -impl History { - pub fn new() -> Self { - Self { - size: (24, 80), - entries: vec![], - scroll_pos: 0, - } - } - - pub async fn render( - &self, - out: &mut impl textmode::Textmode, - repl_lines: usize, - focus: Option, - scrolling: bool, - offset: time::UtcOffset, - ) -> anyhow::Result<()> { - let mut used_lines = repl_lines; - let mut cursor = None; - for (idx, mut entry) in - self.visible(repl_lines, focus, scrolling).await.rev() - { - let focused = focus.map_or(false, |focus| idx == focus); - used_lines += entry.lines(self.size.1, focused && !scrolling); - out.move_to( - (self.size.0 as usize - used_lines).try_into().unwrap(), - 0, - ); - entry.render( - out, - idx, - self.entry_count(), - self.size.1, - focused, - scrolling, - offset, - ); - if focused && !scrolling { - cursor = Some(( - out.screen().cursor_position(), - out.screen().hide_cursor(), - )); - } - } - if let Some((pos, hide)) = cursor { - out.move_to(pos.0, pos.1); - out.hide_cursor(hide); - } - Ok(()) - } - - pub async fn render_fullscreen( - &self, - out: &mut impl textmode::Textmode, - idx: usize, - ) { - let mut entry = self.entries[idx].lock_arc().await; - entry.render_fullscreen(out); - } - - pub async fn resize(&mut self, size: (u16, u16)) { - self.size = size; - for entry in &self.entries { - let entry = entry.lock_arc().await; - if entry.running() { - entry.resize.send(size).await.unwrap(); - } - } - } - - pub async fn run( - &mut self, - cmd: &str, - event_w: async_std::channel::Sender, - ) -> anyhow::Result { - let (exe, args) = crate::parse::cmd(cmd); - let (input_w, input_r) = async_std::channel::unbounded(); - let (resize_w, resize_r) = async_std::channel::unbounded(); - let entry = - crate::util::mutex(Entry::new(cmd, self.size, input_w, resize_w)); - if crate::builtins::is(&exe) { - let code: i32 = crate::builtins::run(&exe, &args).into(); - entry.lock_arc().await.exit_info = Some(ExitInfo::new( - async_std::process::ExitStatus::from_raw(code << 8), - )); - event_w - .send(crate::event::Event::ProcessExit) - .await - .unwrap(); - } else { - let mut process = async_std::process::Command::new(&exe); - process.args(&args); - let child = process - .spawn_pty(Some(&pty_process::Size::new( - self.size.0, - self.size.1, - ))) - .unwrap(); - run_process( - child, - async_std::sync::Arc::clone(&entry), - input_r, - resize_r, - event_w, - ); - } - self.entries.push(entry); - Ok(self.entries.len() - 1) - } - - pub async fn entry( - &self, - idx: usize, - ) -> async_std::sync::MutexGuardArc { - self.entries[idx].lock_arc().await - } - - pub fn entry_count(&self) -> usize { - self.entries.len() - } - - pub async fn make_focus_visible( - &mut self, - repl_lines: usize, - focus: Option, - scrolling: bool, - ) { - if self.entries.is_empty() || focus.is_none() { - return; - } - let focus = focus.unwrap(); - - let mut done = false; - while focus - < self - .visible(repl_lines, Some(focus), scrolling) - .await - .map(|(idx, _)| idx) - .next() - .unwrap() - { - self.scroll_pos += 1; - done = true; - } - if done { - return; - } - - while focus - > self - .visible(repl_lines, Some(focus), scrolling) - .await - .map(|(idx, _)| idx) - .last() - .unwrap() - { - self.scroll_pos -= 1; - } - } - - async fn visible( - &self, - repl_lines: usize, - focus: Option, - scrolling: bool, - ) -> VisibleEntries { - let mut iter = VisibleEntries::new(); - if self.entries.is_empty() { - return iter; - } - - let mut used_lines = repl_lines; - for (idx, entry) in - self.entries.iter().enumerate().rev().skip(self.scroll_pos) - { - let entry = entry.lock_arc().await; - let focused = focus.map_or(false, |focus| idx == focus); - used_lines += entry.lines(self.size.1, focused && !scrolling); - if used_lines > self.size.0 as usize { - break; - } - iter.add(idx, entry); - } - iter - } -} - -struct VisibleEntries { - entries: std::collections::VecDeque<( - usize, - async_std::sync::MutexGuardArc, - )>, -} - -impl VisibleEntries { - fn new() -> Self { - Self { - entries: std::collections::VecDeque::new(), - } - } - - fn add( - &mut self, - idx: usize, - entry: async_std::sync::MutexGuardArc, - ) { - // push_front because we are adding them in reverse order - self.entries.push_front((idx, entry)); - } -} - -impl std::iter::Iterator for VisibleEntries { - type Item = (usize, async_std::sync::MutexGuardArc); - - fn next(&mut self) -> Option { - self.entries.pop_front() - } -} - -impl std::iter::DoubleEndedIterator for VisibleEntries { - fn next_back(&mut self) -> Option { - self.entries.pop_back() - } -} - -pub struct Entry { - cmd: String, - vt: vt100::Parser, - audible_bell_state: usize, - visual_bell_state: usize, - fullscreen: Option, - input: async_std::channel::Sender>, - resize: async_std::channel::Sender<(u16, u16)>, - start_time: time::OffsetDateTime, - start_instant: std::time::Instant, - exit_info: Option, -} - -impl Entry { - fn new( - cmd: &str, - size: (u16, u16), - input: async_std::channel::Sender>, - resize: async_std::channel::Sender<(u16, u16)>, - ) -> Self { - Self { - cmd: cmd.into(), - vt: vt100::Parser::new(size.0, size.1, 0), - audible_bell_state: 0, - visual_bell_state: 0, - input, - resize, - fullscreen: None, - start_time: time::OffsetDateTime::now_utc(), - start_instant: std::time::Instant::now(), - exit_info: None, - } - } - - fn render( - &mut self, - out: &mut impl textmode::Textmode, - idx: usize, - entry_count: usize, - width: u16, - focused: bool, - scrolling: bool, - offset: time::UtcOffset, - ) { - self.set_bgcolor(out, focused); - out.set_fgcolor(textmode::color::YELLOW); - let entry_count_width = format!("{}", entry_count + 1).len(); - let idx_str = format!("{}", idx + 1); - out.write_str(&" ".repeat(entry_count_width - idx_str.len())); - out.write_str(&idx_str); - out.write_str(" "); - out.reset_attributes(); - - self.set_bgcolor(out, focused); - if let Some(info) = self.exit_info { - if info.status.signal().is_some() { - out.set_fgcolor(textmode::color::MAGENTA); - } else if info.status.success() { - out.set_fgcolor(textmode::color::DARKGREY); - } else { - out.set_fgcolor(textmode::color::RED); - } - out.write_str(&crate::format::exit_status(info.status)); - } else { - out.write_str(" "); - } - out.reset_attributes(); - - self.set_bgcolor(out, focused); - out.write_str("$ "); - if self.running() { - out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); - } - out.write_str(&self.cmd); - out.reset_attributes(); - - self.set_bgcolor(out, focused); - let time = if let Some(info) = self.exit_info { - format!( - "({}) [{}]", - crate::format::duration(info.instant - self.start_instant), - crate::format::time(self.start_time.to_offset(offset)), - ) - } else { - format!( - "[{}]", - crate::format::time(self.start_time.to_offset(offset)) - ) - }; - let cur_pos = out.screen().cursor_position(); - out.write_str( - &" ".repeat(width as usize - time.len() - 1 - cur_pos.1 as usize), - ); - out.write_str(&time); - out.write_str(" "); - out.reset_attributes(); - - if self.binary() { - let msg = "This appears to be binary data. Fullscreen this entry to view anyway."; - let len: u16 = msg.len().try_into().unwrap(); - out.move_to( - out.screen().cursor_position().0 + 1, - (width - len) / 2, - ); - out.set_fgcolor(textmode::color::RED); - out.write_str(msg); - out.hide_cursor(true); - out.reset_attributes(); - } else { - let last_row = self.output_lines(width, focused && !scrolling); - if last_row > 5 { - out.write(b"\r\n"); - out.set_fgcolor(textmode::color::BLUE); - out.write_str("..."); - out.reset_attributes(); - } - let mut out_row = out.screen().cursor_position().0 + 1; - let screen = self.vt.screen(); - let pos = screen.cursor_position(); - let mut wrapped = false; - let mut cursor_found = None; - for (idx, row) in screen - .rows_formatted(0, width) - .enumerate() - .take(last_row) - .skip(last_row.saturating_sub(5)) - { - let idx: u16 = idx.try_into().unwrap(); - out.reset_attributes(); - if !wrapped { - out.move_to(out_row, 0); - } - out.write(&row); - wrapped = screen.row_wrapped(idx); - if pos.0 == idx { - cursor_found = Some(out_row); - } - out_row += 1; - } - if focused && !scrolling { - if let Some(row) = cursor_found { - out.hide_cursor(screen.hide_cursor()); - out.move_to(row, pos.1); - } else { - out.hide_cursor(true); - } - } - } - out.reset_attributes(); - } - - fn render_fullscreen(&mut self, out: &mut impl textmode::Textmode) { - let screen = self.vt.screen(); - let new_audible_bell_state = screen.audible_bell_count(); - let new_visual_bell_state = screen.visual_bell_count(); - - out.write(&screen.state_formatted()); - - if self.audible_bell_state != new_audible_bell_state { - out.write(b"\x07"); - self.audible_bell_state = new_audible_bell_state; - } - - if self.visual_bell_state != new_visual_bell_state { - out.write(b"\x1bg"); - self.visual_bell_state = new_visual_bell_state; - } - - out.reset_attributes(); - } - - fn set_bgcolor(&self, out: &mut impl textmode::Textmode, focus: bool) { - if focus { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 64)); - } else { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - } - } - - pub async fn send_input(&self, bytes: Vec) { - if self.running() { - self.input.send(bytes).await.unwrap(); - } - } - - pub fn cmd(&self) -> String { - self.cmd.clone() - } - - pub fn toggle_fullscreen(&mut self) { - if let Some(fullscreen) = self.fullscreen { - self.fullscreen = Some(!fullscreen); - } else { - self.fullscreen = Some(!self.vt.screen().alternate_screen()); - } - } - - pub fn set_fullscreen(&mut self, fullscreen: bool) { - self.fullscreen = Some(fullscreen); - } - - pub fn running(&self) -> bool { - self.exit_info.is_none() - } - - pub fn binary(&self) -> bool { - self.vt.screen().errors() > 5 - } - - pub fn lines(&self, width: u16, focused: bool) -> usize { - let lines = self.output_lines(width, focused); - 1 + std::cmp::min(6, lines) - } - - pub fn output_lines(&self, width: u16, focused: bool) -> usize { - if self.binary() { - return 1; - } - - let screen = self.vt.screen(); - let mut last_row = 0; - for (idx, row) in screen.rows(0, width).enumerate() { - if !row.is_empty() { - last_row = idx + 1; - } - } - if focused && self.running() { - last_row = std::cmp::max( - last_row, - screen.cursor_position().0 as usize + 1, - ); - } - last_row - } - - pub fn should_fullscreen(&self) -> bool { - self.fullscreen - .unwrap_or_else(|| self.vt.screen().alternate_screen()) - } -} - -#[derive(Copy, Clone)] -struct ExitInfo { - status: async_std::process::ExitStatus, - instant: std::time::Instant, -} - -impl ExitInfo { - fn new(status: async_std::process::ExitStatus) -> Self { - Self { - status, - instant: std::time::Instant::now(), - } - } -} - -fn run_process( - mut child: pty_process::async_std::Child, - entry: crate::util::Mutex, - input_r: async_std::channel::Receiver>, - resize_r: async_std::channel::Receiver<(u16, u16)>, - event_w: async_std::channel::Sender, -) { - async_std::task::spawn(async move { - loop { - enum Res { - Read(Result), - Write(Result, async_std::channel::RecvError>), - Resize(Result<(u16, u16), async_std::channel::RecvError>), - } - let mut buf = [0_u8; 4096]; - let mut pty = child.pty(); - let read = async { Res::Read(pty.read(&mut buf).await) }; - let write = async { Res::Write(input_r.recv().await) }; - let resize = async { Res::Resize(resize_r.recv().await) }; - match read.race(write).race(resize).await { - Res::Read(res) => match res { - Ok(bytes) => { - let mut entry = entry.lock_arc().await; - let pre_alternate_screen = - entry.vt.screen().alternate_screen(); - entry.vt.process(&buf[..bytes]); - let post_alternate_screen = - entry.vt.screen().alternate_screen(); - if entry.fullscreen.is_none() - && pre_alternate_screen != post_alternate_screen - { - event_w - .send(crate::event::Event::ProcessAlternateScreen) - .await - .unwrap(); - } - event_w - .send(crate::event::Event::ProcessOutput) - .await - .unwrap(); - } - Err(e) => { - if e.raw_os_error() != Some(libc::EIO) { - eprintln!("pty read failed: {:?}", e); - } - // XXX not sure if this is safe - are we sure - // the child exited? - entry.lock_arc().await.exit_info = Some( - ExitInfo::new(child.status().await.unwrap()), - ); - event_w - .send(crate::event::Event::ProcessExit) - .await - .unwrap(); - break; - } - }, - Res::Write(res) => match res { - Ok(bytes) => { - pty.write(&bytes).await.unwrap(); - } - Err(e) => { - panic!("failed to read from input channel: {}", e); - } - }, - Res::Resize(res) => match res { - Ok(size) => { - child - .resize_pty(&pty_process::Size::new( - size.0, size.1, - )) - .unwrap(); - entry.lock_arc().await.vt.set_size(size.0, size.1); - } - Err(e) => { - panic!("failed to read from resize channel: {}", e); - } - }, - } - } - }); -} diff --git a/src/main.rs b/src/main.rs index 24e59ee..22af8ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,7 @@ mod builtins; mod env; mod event; mod format; -mod history; mod parse; -mod readline; mod state; mod util; diff --git a/src/readline.rs b/src/readline.rs deleted file mode 100644 index 585d6b6..0000000 --- a/src/readline.rs +++ /dev/null @@ -1,161 +0,0 @@ -use unicode_width::{UnicodeWidthChar as _, UnicodeWidthStr as _}; - -pub struct Readline { - size: (u16, u16), - input_line: String, - pos: usize, -} - -impl Readline { - pub fn new() -> Self { - Self { - size: (24, 80), - input_line: "".into(), - pos: 0, - } - } - - pub async fn render( - &self, - out: &mut impl textmode::Textmode, - entry_count: usize, - focus: bool, - offset: time::UtcOffset, - ) -> anyhow::Result<()> { - let pwd = crate::env::pwd()?; - let user = crate::env::user()?; - let hostname = crate::env::hostname()?; - let time = crate::env::time(offset)?; - let prompt_char = crate::env::prompt_char()?; - - let id = format!("{}@{}", user, hostname); - let idlen: u16 = id.len().try_into().unwrap(); - let timelen: u16 = time.len().try_into().unwrap(); - - out.move_to(self.size.0 - 2, 0); - if focus { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 64)); - } else { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - } - out.write(b"\x1b[K"); - out.set_fgcolor(textmode::color::YELLOW); - out.write_str(&format!("{}", entry_count + 1)); - out.reset_attributes(); - if focus { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 64)); - } else { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - } - out.write_str(" ("); - out.write_str(&pwd); - out.write_str(")"); - out.move_to(self.size.0 - 2, self.size.1 - 4 - idlen - timelen); - out.write_str(&id); - out.write_str(" ["); - out.write_str(&time); - out.write_str("]"); - - out.move_to(self.size.0 - 1, 0); - out.reset_attributes(); - out.write_str(&prompt_char); - out.write_str(" "); - out.reset_attributes(); - out.write(b"\x1b[K"); - out.write_str(&self.input_line); - out.reset_attributes(); - out.move_to(self.size.0 - 1, 2 + self.pos_width()); - if focus { - out.hide_cursor(false); - } - Ok(()) - } - - pub async fn resize(&mut self, size: (u16, u16)) { - self.size = size; - } - - pub fn lines(&self) -> usize { - 2 // XXX handle wrapping - } - - pub fn input(&self) -> String { - self.input_line.clone() - } - - pub fn add_input(&mut self, s: &str) { - self.input_line.insert_str(self.byte_pos(), s); - self.pos += s.chars().count(); - } - - pub fn set_input(&mut self, s: &str) { - self.input_line = s.to_string(); - self.pos = s.chars().count(); - } - - pub fn backspace(&mut self) { - while self.pos > 0 { - self.pos -= 1; - let width = - self.input_line.remove(self.byte_pos()).width().unwrap_or(0); - if width > 0 { - break; - } - } - } - - pub fn clear_input(&mut self) { - self.input_line.clear(); - self.pos = 0; - } - - pub fn clear_backwards(&mut self) { - self.input_line = self.input_line.chars().skip(self.pos).collect(); - self.pos = 0; - } - - pub fn cursor_left(&mut self) { - if self.pos == 0 { - return; - } - self.pos -= 1; - while let Some(c) = self.input_line.chars().nth(self.pos) { - if c.width().unwrap_or(0) == 0 { - self.pos -= 1; - } else { - break; - } - } - } - - pub fn cursor_right(&mut self) { - if self.pos == self.input_line.chars().count() { - return; - } - self.pos += 1; - while let Some(c) = self.input_line.chars().nth(self.pos) { - if c.width().unwrap_or(0) == 0 { - self.pos += 1; - } else { - break; - } - } - } - - fn pos_width(&self) -> u16 { - self.input_line - .chars() - .take(self.pos) - .collect::() - .width() - .try_into() - .unwrap() - } - - fn byte_pos(&self) -> usize { - self.input_line - .char_indices() - .nth(self.pos) - .map_or(self.input_line.len(), |(i, _)| i) - } -} diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index cdc3cdb..0000000 --- a/src/state.rs +++ /dev/null @@ -1,436 +0,0 @@ -#[derive(Copy, Clone, Debug)] -enum Focus { - Readline, - History(usize), - Scrolling(Option), -} - -#[derive(Copy, Clone, Debug)] -enum Scene { - Readline, - Fullscreen, -} - -pub enum Action { - Refresh, - HardRefresh, - Resize(u16, u16), - Quit, -} - -pub struct State { - readline: crate::readline::Readline, - history: crate::history::History, - focus: Focus, - scene: Scene, - escape: bool, - hide_readline: bool, - offset: time::UtcOffset, -} - -impl State { - pub fn new(offset: time::UtcOffset) -> Self { - Self { - readline: crate::readline::Readline::new(), - history: crate::history::History::new(), - focus: Focus::Readline, - scene: Scene::Readline, - escape: false, - hide_readline: false, - offset, - } - } - - pub async fn render( - &self, - out: &mut impl textmode::Textmode, - ) -> anyhow::Result<()> { - out.clear(); - 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.history.entry_count(), - 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.history.entry_count(), - 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.history.entry_count(), - 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: crate::event::Event, - event_w: &async_std::channel::Sender, - ) -> Option { - match event { - crate::event::Event::Key(key) => { - return if self.escape { - self.escape = false; - self.handle_key_escape(key).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).await - } - } - }; - } - crate::event::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)); - } - crate::event::Event::ProcessOutput => { - // 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; - } - crate::event::Event::ProcessAlternateScreen => { - self.scene = self.default_scene(self.focus, None).await; - } - crate::event::Event::ProcessExit => { - if let Some(idx) = self.focus_idx() { - let entry = self.history.entry(idx).await; - if !entry.running() { - self.set_focus( - if self.hide_readline { - Focus::Readline - } else { - Focus::Scrolling(Some(idx)) - }, - Some(entry), - ) - .await; - } - } - } - crate::event::Event::ClockTimer => {} - }; - Some(Action::Refresh) - } - - async fn handle_key_escape( - &mut self, - key: textmode::Key, - ) -> Option { - 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') => { - 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(' ') => { - 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('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('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, - ) -> Option { - 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 cmd = self.readline.input(); - self.readline.clear_input(); - let idx = - self.history.run(&cmd, event_w.clone()).await.unwrap(); - self.set_focus(Focus::History(idx), None).await; - self.hide_readline = true; - } - 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 - .entry(idx) - .await - .send_input(key.into_bytes()) - .await; - } - - async fn default_scene( - &self, - focus: Focus, - entry: Option>, - ) -> 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>, - ) { - 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 { - match self.focus { - Focus::History(idx) => Some(idx), - Focus::Readline => None, - Focus::Scrolling(idx) => idx, - } - } - - fn scroll_up(&self, idx: Option) -> Option { - 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) -> Option { - 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) - } -} diff --git a/src/state/history.rs b/src/state/history.rs new file mode 100644 index 0000000..048c1c7 --- /dev/null +++ b/src/state/history.rs @@ -0,0 +1,574 @@ +use async_std::io::{ReadExt as _, WriteExt as _}; +use futures_lite::future::FutureExt as _; +use pty_process::Command as _; +use std::os::unix::process::ExitStatusExt as _; + +pub struct History { + size: (u16, u16), + entries: Vec>, + scroll_pos: usize, +} + +impl History { + pub fn new() -> Self { + Self { + size: (24, 80), + entries: vec![], + scroll_pos: 0, + } + } + + pub async fn render( + &self, + out: &mut impl textmode::Textmode, + repl_lines: usize, + focus: Option, + scrolling: bool, + offset: time::UtcOffset, + ) -> anyhow::Result<()> { + let mut used_lines = repl_lines; + let mut cursor = None; + for (idx, mut entry) in + self.visible(repl_lines, focus, scrolling).await.rev() + { + let focused = focus.map_or(false, |focus| idx == focus); + used_lines += entry.lines(self.size.1, focused && !scrolling); + out.move_to( + (self.size.0 as usize - used_lines).try_into().unwrap(), + 0, + ); + entry.render( + out, + idx, + self.entry_count(), + self.size.1, + focused, + scrolling, + offset, + ); + if focused && !scrolling { + cursor = Some(( + out.screen().cursor_position(), + out.screen().hide_cursor(), + )); + } + } + if let Some((pos, hide)) = cursor { + out.move_to(pos.0, pos.1); + out.hide_cursor(hide); + } + Ok(()) + } + + pub async fn render_fullscreen( + &self, + out: &mut impl textmode::Textmode, + idx: usize, + ) { + let mut entry = self.entries[idx].lock_arc().await; + entry.render_fullscreen(out); + } + + pub async fn resize(&mut self, size: (u16, u16)) { + self.size = size; + for entry in &self.entries { + let entry = entry.lock_arc().await; + if entry.running() { + entry.resize.send(size).await.unwrap(); + } + } + } + + pub async fn run( + &mut self, + cmd: &str, + event_w: async_std::channel::Sender, + ) -> anyhow::Result { + let (exe, args) = crate::parse::cmd(cmd); + let (input_w, input_r) = async_std::channel::unbounded(); + let (resize_w, resize_r) = async_std::channel::unbounded(); + let entry = + crate::util::mutex(Entry::new(cmd, self.size, input_w, resize_w)); + if crate::builtins::is(&exe) { + let code: i32 = crate::builtins::run(&exe, &args).into(); + entry.lock_arc().await.exit_info = Some(ExitInfo::new( + async_std::process::ExitStatus::from_raw(code << 8), + )); + event_w + .send(crate::event::Event::ProcessExit) + .await + .unwrap(); + } else { + let mut process = async_std::process::Command::new(&exe); + process.args(&args); + let child = process + .spawn_pty(Some(&pty_process::Size::new( + self.size.0, + self.size.1, + ))) + .unwrap(); + run_process( + child, + async_std::sync::Arc::clone(&entry), + input_r, + resize_r, + event_w, + ); + } + self.entries.push(entry); + Ok(self.entries.len() - 1) + } + + pub async fn entry( + &self, + idx: usize, + ) -> async_std::sync::MutexGuardArc { + self.entries[idx].lock_arc().await + } + + pub fn entry_count(&self) -> usize { + self.entries.len() + } + + pub async fn make_focus_visible( + &mut self, + repl_lines: usize, + focus: Option, + scrolling: bool, + ) { + if self.entries.is_empty() || focus.is_none() { + return; + } + let focus = focus.unwrap(); + + let mut done = false; + while focus + < self + .visible(repl_lines, Some(focus), scrolling) + .await + .map(|(idx, _)| idx) + .next() + .unwrap() + { + self.scroll_pos += 1; + done = true; + } + if done { + return; + } + + while focus + > self + .visible(repl_lines, Some(focus), scrolling) + .await + .map(|(idx, _)| idx) + .last() + .unwrap() + { + self.scroll_pos -= 1; + } + } + + async fn visible( + &self, + repl_lines: usize, + focus: Option, + scrolling: bool, + ) -> VisibleEntries { + let mut iter = VisibleEntries::new(); + if self.entries.is_empty() { + return iter; + } + + let mut used_lines = repl_lines; + for (idx, entry) in + self.entries.iter().enumerate().rev().skip(self.scroll_pos) + { + let entry = entry.lock_arc().await; + let focused = focus.map_or(false, |focus| idx == focus); + used_lines += entry.lines(self.size.1, focused && !scrolling); + if used_lines > self.size.0 as usize { + break; + } + iter.add(idx, entry); + } + iter + } +} + +struct VisibleEntries { + entries: std::collections::VecDeque<( + usize, + async_std::sync::MutexGuardArc, + )>, +} + +impl VisibleEntries { + fn new() -> Self { + Self { + entries: std::collections::VecDeque::new(), + } + } + + fn add( + &mut self, + idx: usize, + entry: async_std::sync::MutexGuardArc, + ) { + // push_front because we are adding them in reverse order + self.entries.push_front((idx, entry)); + } +} + +impl std::iter::Iterator for VisibleEntries { + type Item = (usize, async_std::sync::MutexGuardArc); + + fn next(&mut self) -> Option { + self.entries.pop_front() + } +} + +impl std::iter::DoubleEndedIterator for VisibleEntries { + fn next_back(&mut self) -> Option { + self.entries.pop_back() + } +} + +pub struct Entry { + cmd: String, + vt: vt100::Parser, + audible_bell_state: usize, + visual_bell_state: usize, + fullscreen: Option, + input: async_std::channel::Sender>, + resize: async_std::channel::Sender<(u16, u16)>, + start_time: time::OffsetDateTime, + start_instant: std::time::Instant, + exit_info: Option, +} + +impl Entry { + fn new( + cmd: &str, + size: (u16, u16), + input: async_std::channel::Sender>, + resize: async_std::channel::Sender<(u16, u16)>, + ) -> Self { + Self { + cmd: cmd.into(), + vt: vt100::Parser::new(size.0, size.1, 0), + audible_bell_state: 0, + visual_bell_state: 0, + input, + resize, + fullscreen: None, + start_time: time::OffsetDateTime::now_utc(), + start_instant: std::time::Instant::now(), + exit_info: None, + } + } + + fn render( + &mut self, + out: &mut impl textmode::Textmode, + idx: usize, + entry_count: usize, + width: u16, + focused: bool, + scrolling: bool, + offset: time::UtcOffset, + ) { + self.set_bgcolor(out, focused); + out.set_fgcolor(textmode::color::YELLOW); + let entry_count_width = format!("{}", entry_count + 1).len(); + let idx_str = format!("{}", idx + 1); + out.write_str(&" ".repeat(entry_count_width - idx_str.len())); + out.write_str(&idx_str); + out.write_str(" "); + out.reset_attributes(); + + self.set_bgcolor(out, focused); + if let Some(info) = self.exit_info { + if info.status.signal().is_some() { + out.set_fgcolor(textmode::color::MAGENTA); + } else if info.status.success() { + out.set_fgcolor(textmode::color::DARKGREY); + } else { + out.set_fgcolor(textmode::color::RED); + } + out.write_str(&crate::format::exit_status(info.status)); + } else { + out.write_str(" "); + } + out.reset_attributes(); + + self.set_bgcolor(out, focused); + out.write_str("$ "); + if self.running() { + out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); + } + out.write_str(&self.cmd); + out.reset_attributes(); + + self.set_bgcolor(out, focused); + let time = if let Some(info) = self.exit_info { + format!( + "({}) [{}]", + crate::format::duration(info.instant - self.start_instant), + crate::format::time(self.start_time.to_offset(offset)), + ) + } else { + format!( + "[{}]", + crate::format::time(self.start_time.to_offset(offset)) + ) + }; + let cur_pos = out.screen().cursor_position(); + out.write_str( + &" ".repeat(width as usize - time.len() - 1 - cur_pos.1 as usize), + ); + out.write_str(&time); + out.write_str(" "); + out.reset_attributes(); + + if self.binary() { + let msg = "This appears to be binary data. Fullscreen this entry to view anyway."; + let len: u16 = msg.len().try_into().unwrap(); + out.move_to( + out.screen().cursor_position().0 + 1, + (width - len) / 2, + ); + out.set_fgcolor(textmode::color::RED); + out.write_str(msg); + out.hide_cursor(true); + out.reset_attributes(); + } else { + let last_row = self.output_lines(width, focused && !scrolling); + if last_row > 5 { + out.write(b"\r\n"); + out.set_fgcolor(textmode::color::BLUE); + out.write_str("..."); + out.reset_attributes(); + } + let mut out_row = out.screen().cursor_position().0 + 1; + let screen = self.vt.screen(); + let pos = screen.cursor_position(); + let mut wrapped = false; + let mut cursor_found = None; + for (idx, row) in screen + .rows_formatted(0, width) + .enumerate() + .take(last_row) + .skip(last_row.saturating_sub(5)) + { + let idx: u16 = idx.try_into().unwrap(); + out.reset_attributes(); + if !wrapped { + out.move_to(out_row, 0); + } + out.write(&row); + wrapped = screen.row_wrapped(idx); + if pos.0 == idx { + cursor_found = Some(out_row); + } + out_row += 1; + } + if focused && !scrolling { + if let Some(row) = cursor_found { + out.hide_cursor(screen.hide_cursor()); + out.move_to(row, pos.1); + } else { + out.hide_cursor(true); + } + } + } + out.reset_attributes(); + } + + fn render_fullscreen(&mut self, out: &mut impl textmode::Textmode) { + let screen = self.vt.screen(); + let new_audible_bell_state = screen.audible_bell_count(); + let new_visual_bell_state = screen.visual_bell_count(); + + out.write(&screen.state_formatted()); + + if self.audible_bell_state != new_audible_bell_state { + out.write(b"\x07"); + self.audible_bell_state = new_audible_bell_state; + } + + if self.visual_bell_state != new_visual_bell_state { + out.write(b"\x1bg"); + self.visual_bell_state = new_visual_bell_state; + } + + out.reset_attributes(); + } + + fn set_bgcolor(&self, out: &mut impl textmode::Textmode, focus: bool) { + if focus { + out.set_bgcolor(textmode::Color::Rgb(32, 32, 64)); + } else { + out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); + } + } + + pub async fn send_input(&self, bytes: Vec) { + if self.running() { + self.input.send(bytes).await.unwrap(); + } + } + + pub fn cmd(&self) -> String { + self.cmd.clone() + } + + pub fn toggle_fullscreen(&mut self) { + if let Some(fullscreen) = self.fullscreen { + self.fullscreen = Some(!fullscreen); + } else { + self.fullscreen = Some(!self.vt.screen().alternate_screen()); + } + } + + pub fn set_fullscreen(&mut self, fullscreen: bool) { + self.fullscreen = Some(fullscreen); + } + + pub fn running(&self) -> bool { + self.exit_info.is_none() + } + + pub fn binary(&self) -> bool { + self.vt.screen().errors() > 5 + } + + pub fn lines(&self, width: u16, focused: bool) -> usize { + let lines = self.output_lines(width, focused); + 1 + std::cmp::min(6, lines) + } + + pub fn output_lines(&self, width: u16, focused: bool) -> usize { + if self.binary() { + return 1; + } + + let screen = self.vt.screen(); + let mut last_row = 0; + for (idx, row) in screen.rows(0, width).enumerate() { + if !row.is_empty() { + last_row = idx + 1; + } + } + if focused && self.running() { + last_row = std::cmp::max( + last_row, + screen.cursor_position().0 as usize + 1, + ); + } + last_row + } + + pub fn should_fullscreen(&self) -> bool { + self.fullscreen + .unwrap_or_else(|| self.vt.screen().alternate_screen()) + } +} + +#[derive(Copy, Clone)] +struct ExitInfo { + status: async_std::process::ExitStatus, + instant: std::time::Instant, +} + +impl ExitInfo { + fn new(status: async_std::process::ExitStatus) -> Self { + Self { + status, + instant: std::time::Instant::now(), + } + } +} + +fn run_process( + mut child: pty_process::async_std::Child, + entry: crate::util::Mutex, + input_r: async_std::channel::Receiver>, + resize_r: async_std::channel::Receiver<(u16, u16)>, + event_w: async_std::channel::Sender, +) { + async_std::task::spawn(async move { + loop { + enum Res { + Read(Result), + Write(Result, async_std::channel::RecvError>), + Resize(Result<(u16, u16), async_std::channel::RecvError>), + } + let mut buf = [0_u8; 4096]; + let mut pty = child.pty(); + let read = async { Res::Read(pty.read(&mut buf).await) }; + let write = async { Res::Write(input_r.recv().await) }; + let resize = async { Res::Resize(resize_r.recv().await) }; + match read.race(write).race(resize).await { + Res::Read(res) => match res { + Ok(bytes) => { + let mut entry = entry.lock_arc().await; + let pre_alternate_screen = + entry.vt.screen().alternate_screen(); + entry.vt.process(&buf[..bytes]); + let post_alternate_screen = + entry.vt.screen().alternate_screen(); + if entry.fullscreen.is_none() + && pre_alternate_screen != post_alternate_screen + { + event_w + .send(crate::event::Event::ProcessAlternateScreen) + .await + .unwrap(); + } + event_w + .send(crate::event::Event::ProcessOutput) + .await + .unwrap(); + } + Err(e) => { + if e.raw_os_error() != Some(libc::EIO) { + eprintln!("pty read failed: {:?}", e); + } + // XXX not sure if this is safe - are we sure + // the child exited? + entry.lock_arc().await.exit_info = Some( + ExitInfo::new(child.status().await.unwrap()), + ); + event_w + .send(crate::event::Event::ProcessExit) + .await + .unwrap(); + break; + } + }, + Res::Write(res) => match res { + Ok(bytes) => { + pty.write(&bytes).await.unwrap(); + } + Err(e) => { + panic!("failed to read from input channel: {}", e); + } + }, + Res::Resize(res) => match res { + Ok(size) => { + child + .resize_pty(&pty_process::Size::new( + size.0, size.1, + )) + .unwrap(); + entry.lock_arc().await.vt.set_size(size.0, size.1); + } + Err(e) => { + panic!("failed to read from resize channel: {}", e); + } + }, + } + } + }); +} diff --git a/src/state/mod.rs b/src/state/mod.rs new file mode 100644 index 0000000..2fcaf3f --- /dev/null +++ b/src/state/mod.rs @@ -0,0 +1,439 @@ +mod history; +mod readline; + +#[derive(Copy, Clone, Debug)] +enum Focus { + Readline, + History(usize), + Scrolling(Option), +} + +#[derive(Copy, Clone, Debug)] +enum Scene { + Readline, + Fullscreen, +} + +pub enum Action { + Refresh, + HardRefresh, + Resize(u16, u16), + Quit, +} + +pub struct State { + readline: readline::Readline, + history: history::History, + focus: Focus, + scene: Scene, + escape: bool, + hide_readline: bool, + offset: time::UtcOffset, +} + +impl State { + pub fn new(offset: time::UtcOffset) -> Self { + Self { + readline: readline::Readline::new(), + history: history::History::new(), + focus: Focus::Readline, + scene: Scene::Readline, + escape: false, + hide_readline: false, + offset, + } + } + + pub async fn render( + &self, + out: &mut impl textmode::Textmode, + ) -> anyhow::Result<()> { + out.clear(); + 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.history.entry_count(), + 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.history.entry_count(), + 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.history.entry_count(), + 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: crate::event::Event, + event_w: &async_std::channel::Sender, + ) -> Option { + match event { + crate::event::Event::Key(key) => { + return if self.escape { + self.escape = false; + self.handle_key_escape(key).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).await + } + } + }; + } + crate::event::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)); + } + crate::event::Event::ProcessOutput => { + // 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; + } + crate::event::Event::ProcessAlternateScreen => { + self.scene = self.default_scene(self.focus, None).await; + } + crate::event::Event::ProcessExit => { + if let Some(idx) = self.focus_idx() { + let entry = self.history.entry(idx).await; + if !entry.running() { + self.set_focus( + if self.hide_readline { + Focus::Readline + } else { + Focus::Scrolling(Some(idx)) + }, + Some(entry), + ) + .await; + } + } + } + crate::event::Event::ClockTimer => {} + }; + Some(Action::Refresh) + } + + async fn handle_key_escape( + &mut self, + key: textmode::Key, + ) -> Option { + 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') => { + 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(' ') => { + 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('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('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, + ) -> Option { + 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 cmd = self.readline.input(); + self.readline.clear_input(); + let idx = + self.history.run(&cmd, event_w.clone()).await.unwrap(); + self.set_focus(Focus::History(idx), None).await; + self.hide_readline = true; + } + 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 + .entry(idx) + .await + .send_input(key.into_bytes()) + .await; + } + + async fn default_scene( + &self, + focus: Focus, + entry: Option>, + ) -> 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>, + ) { + 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 { + match self.focus { + Focus::History(idx) => Some(idx), + Focus::Readline => None, + Focus::Scrolling(idx) => idx, + } + } + + fn scroll_up(&self, idx: Option) -> Option { + 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) -> Option { + 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) + } +} diff --git a/src/state/readline.rs b/src/state/readline.rs new file mode 100644 index 0000000..585d6b6 --- /dev/null +++ b/src/state/readline.rs @@ -0,0 +1,161 @@ +use unicode_width::{UnicodeWidthChar as _, UnicodeWidthStr as _}; + +pub struct Readline { + size: (u16, u16), + input_line: String, + pos: usize, +} + +impl Readline { + pub fn new() -> Self { + Self { + size: (24, 80), + input_line: "".into(), + pos: 0, + } + } + + pub async fn render( + &self, + out: &mut impl textmode::Textmode, + entry_count: usize, + focus: bool, + offset: time::UtcOffset, + ) -> anyhow::Result<()> { + let pwd = crate::env::pwd()?; + let user = crate::env::user()?; + let hostname = crate::env::hostname()?; + let time = crate::env::time(offset)?; + let prompt_char = crate::env::prompt_char()?; + + let id = format!("{}@{}", user, hostname); + let idlen: u16 = id.len().try_into().unwrap(); + let timelen: u16 = time.len().try_into().unwrap(); + + out.move_to(self.size.0 - 2, 0); + if focus { + out.set_bgcolor(textmode::Color::Rgb(32, 32, 64)); + } else { + out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); + } + out.write(b"\x1b[K"); + out.set_fgcolor(textmode::color::YELLOW); + out.write_str(&format!("{}", entry_count + 1)); + out.reset_attributes(); + if focus { + out.set_bgcolor(textmode::Color::Rgb(32, 32, 64)); + } else { + out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); + } + out.write_str(" ("); + out.write_str(&pwd); + out.write_str(")"); + out.move_to(self.size.0 - 2, self.size.1 - 4 - idlen - timelen); + out.write_str(&id); + out.write_str(" ["); + out.write_str(&time); + out.write_str("]"); + + out.move_to(self.size.0 - 1, 0); + out.reset_attributes(); + out.write_str(&prompt_char); + out.write_str(" "); + out.reset_attributes(); + out.write(b"\x1b[K"); + out.write_str(&self.input_line); + out.reset_attributes(); + out.move_to(self.size.0 - 1, 2 + self.pos_width()); + if focus { + out.hide_cursor(false); + } + Ok(()) + } + + pub async fn resize(&mut self, size: (u16, u16)) { + self.size = size; + } + + pub fn lines(&self) -> usize { + 2 // XXX handle wrapping + } + + pub fn input(&self) -> String { + self.input_line.clone() + } + + pub fn add_input(&mut self, s: &str) { + self.input_line.insert_str(self.byte_pos(), s); + self.pos += s.chars().count(); + } + + pub fn set_input(&mut self, s: &str) { + self.input_line = s.to_string(); + self.pos = s.chars().count(); + } + + pub fn backspace(&mut self) { + while self.pos > 0 { + self.pos -= 1; + let width = + self.input_line.remove(self.byte_pos()).width().unwrap_or(0); + if width > 0 { + break; + } + } + } + + pub fn clear_input(&mut self) { + self.input_line.clear(); + self.pos = 0; + } + + pub fn clear_backwards(&mut self) { + self.input_line = self.input_line.chars().skip(self.pos).collect(); + self.pos = 0; + } + + pub fn cursor_left(&mut self) { + if self.pos == 0 { + return; + } + self.pos -= 1; + while let Some(c) = self.input_line.chars().nth(self.pos) { + if c.width().unwrap_or(0) == 0 { + self.pos -= 1; + } else { + break; + } + } + } + + pub fn cursor_right(&mut self) { + if self.pos == self.input_line.chars().count() { + return; + } + self.pos += 1; + while let Some(c) = self.input_line.chars().nth(self.pos) { + if c.width().unwrap_or(0) == 0 { + self.pos += 1; + } else { + break; + } + } + } + + fn pos_width(&self) -> u16 { + self.input_line + .chars() + .take(self.pos) + .collect::() + .width() + .try_into() + .unwrap() + } + + fn byte_pos(&self) -> usize { + self.input_line + .char_indices() + .nth(self.pos) + .map_or(self.input_line.len(), |(i, _)| i) + } +} -- cgit v1.2.3-54-g00ecf