diff options
Diffstat (limited to 'src/state/history/mod.rs')
-rw-r--r-- | src/state/history/mod.rs | 714 |
1 files changed, 714 insertions, 0 deletions
diff --git a/src/state/history/mod.rs b/src/state/history/mod.rs new file mode 100644 index 0000000..acdbb44 --- /dev/null +++ b/src/state/history/mod.rs @@ -0,0 +1,714 @@ +use async_std::io::{ReadExt as _, WriteExt as _}; +use futures_lite::future::FutureExt as _; +use pty_process::Command as _; +use std::error::Error as _; +use std::os::unix::process::ExitStatusExt as _; + +mod builtins; + +pub struct History { + size: (u16, u16), + entries: Vec<async_std::sync::Arc<async_std::sync::Mutex<Entry>>>, + scroll_pos: usize, +} + +impl History { + pub fn new() -> Self { + Self { + size: (24, 80), + entries: vec![], + scroll_pos: 0, + } + } + + // render always happens on the main task + #[allow(clippy::future_not_send)] + pub async fn render( + &self, + out: &mut impl textmode::Textmode, + repl_lines: usize, + focus: Option<usize>, + scrolling: bool, + offset: time::UtcOffset, + ) -> anyhow::Result<()> { + let mut used_lines = repl_lines; + let mut cursor = None; + for (idx, 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( + (usize::from(self.size.0) - 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(()) + } + + // render always happens on the main task + #[allow(clippy::future_not_send)] + 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, + ast: &crate::parse::Commands, + event_w: async_std::channel::Sender<crate::event::Event>, + ) -> anyhow::Result<usize> { + let (input_w, input_r) = async_std::channel::unbounded(); + let (resize_w, resize_r) = async_std::channel::unbounded(); + + let entry = async_std::sync::Arc::new(async_std::sync::Mutex::new( + Entry::new(Ok(ast.clone()), self.size, input_w, resize_w), + )); + + run_commands( + ast.clone(), + async_std::sync::Arc::clone(&entry), + input_r, + resize_r, + event_w, + ); + + self.entries.push(entry); + Ok(self.entries.len() - 1) + } + + pub async fn parse_error( + &mut self, + e: crate::parse::Error, + event_w: async_std::channel::Sender<crate::event::Event>, + ) { + // XXX would be great to not have to do this + let (input_w, input_r) = async_std::channel::unbounded(); + let (resize_w, resize_r) = async_std::channel::unbounded(); + input_w.close(); + input_r.close(); + resize_w.close(); + resize_r.close(); + + let mut entry = Entry::new(Err(e), self.size, input_w, resize_w); + let status = async_std::process::ExitStatus::from_raw(1 << 8); + entry.exit_info = Some(ExitInfo::new(status)); + self.entries.push(async_std::sync::Arc::new( + async_std::sync::Mutex::new(entry), + )); + event_w + .send(crate::event::Event::ProcessExit) + .await + .unwrap(); + } + + pub async fn entry( + &self, + idx: usize, + ) -> async_std::sync::MutexGuardArc<Entry> { + 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<usize>, + 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<usize>, + 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 > usize::from(self.size.0) { + break; + } + iter.add(idx, entry); + } + iter + } +} + +struct VisibleEntries { + entries: std::collections::VecDeque<( + usize, + async_std::sync::MutexGuardArc<Entry>, + )>, +} + +impl VisibleEntries { + fn new() -> Self { + Self { + entries: std::collections::VecDeque::new(), + } + } + + fn add( + &mut self, + idx: usize, + entry: async_std::sync::MutexGuardArc<Entry>, + ) { + // 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<Entry>); + + fn next(&mut self) -> Option<Self::Item> { + self.entries.pop_front() + } +} + +impl std::iter::DoubleEndedIterator for VisibleEntries { + fn next_back(&mut self) -> Option<Self::Item> { + self.entries.pop_back() + } +} + +pub struct Entry { + ast: Result<crate::parse::Commands, crate::parse::Error>, + vt: vt100::Parser, + audible_bell_state: usize, + visual_bell_state: usize, + fullscreen: Option<bool>, + input: async_std::channel::Sender<Vec<u8>>, + resize: async_std::channel::Sender<(u16, u16)>, + start_time: time::OffsetDateTime, + start_instant: std::time::Instant, + exit_info: Option<ExitInfo>, +} + +impl Entry { + fn new( + ast: Result<crate::parse::Commands, crate::parse::Error>, + size: (u16, u16), + input: async_std::channel::Sender<Vec<u8>>, + resize: async_std::channel::Sender<(u16, u16)>, + ) -> Self { + Self { + ast, + 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( + &self, + out: &mut impl textmode::Textmode, + idx: usize, + entry_count: usize, + width: u16, + focused: bool, + scrolling: bool, + offset: time::UtcOffset, + ) { + set_bgcolor(out, idx, 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(); + + set_bgcolor(out, idx, 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(); + + set_bgcolor(out, idx, focused); + out.write_str("$ "); + if self.running() { + out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); + } + out.write_str(self.cmd()); + out.reset_attributes(); + + set_bgcolor(out, idx, focused); + let time = self.exit_info.map_or_else( + || { + format!( + "[{}]", + crate::format::time(self.start_time.to_offset(offset)) + ) + }, + |info| { + format!( + "({}) [{}]", + crate::format::duration( + info.instant - self.start_instant + ), + crate::format::time(self.start_time.to_offset(offset)), + ) + }, + ); + let cur_pos = out.screen().cursor_position(); + out.write_str(&" ".repeat( + usize::from(width) - time.len() - 1 - usize::from(cur_pos.1), + )); + out.write_str(&time); + out.write_str(" "); + out.reset_attributes(); + + match &self.ast { + Ok(_) => { + 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); + } 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); + } + } + } + } + Err(e) => { + out.move_to(out.screen().cursor_position().0 + 1, 0); + out.set_fgcolor(textmode::color::RED); + out.write_str( + &format!("{}", e.error()).replace('\n', "\r\n"), + ); + out.hide_cursor(true); + } + } + out.reset_attributes(); + } + + fn render_fullscreen(&mut self, out: &mut impl textmode::Textmode) { + match &self.ast { + Ok(_) => { + 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; + } + } + Err(e) => { + out.move_to(0, 0); + out.set_fgcolor(textmode::color::RED); + out.write_str( + &format!("{}", e.error()).replace('\n', "\r\n"), + ); + out.hide_cursor(true); + } + } + + out.reset_attributes(); + } + + pub async fn send_input(&self, bytes: Vec<u8>) { + if self.running() { + self.input.send(bytes).await.unwrap(); + } + } + + pub fn cmd(&self) -> &str { + match &self.ast { + Ok(ast) => ast.input_string(), + Err(e) => e.input(), + } + } + + 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 let Err(e) = &self.ast { + return e.error().to_string().lines().count(); + } + + 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, + usize::from(screen.cursor_position().0) + 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_commands( + ast: crate::parse::Commands, + entry: async_std::sync::Arc<async_std::sync::Mutex<Entry>>, + input_r: async_std::channel::Receiver<Vec<u8>>, + resize_r: async_std::channel::Receiver<(u16, u16)>, + event_w: async_std::channel::Sender<crate::event::Event>, +) { + async_std::task::spawn(async move { + let mut status = async_std::process::ExitStatus::from_raw(0 << 8); + for pipeline in ast.pipelines() { + let (pipeline_status, done) = run_pipeline( + pipeline, + async_std::sync::Arc::clone(&entry), + input_r.clone(), + resize_r.clone(), + event_w.clone(), + ) + .await; + status = pipeline_status; + if done { + break; + } + } + entry.lock_arc().await.exit_info = Some(ExitInfo::new(status)); + event_w + .send(crate::event::Event::ProcessExit) + .await + .unwrap(); + }); +} + +async fn run_pipeline( + pipeline: &crate::parse::Pipeline, + entry: async_std::sync::Arc<async_std::sync::Mutex<Entry>>, + input_r: async_std::channel::Receiver<Vec<u8>>, + resize_r: async_std::channel::Receiver<(u16, u16)>, + event_w: async_std::channel::Sender<crate::event::Event>, +) -> (async_std::process::ExitStatus, bool) { + // for now + assert_eq!(pipeline.exes().len(), 1); + + let mut status = async_std::process::ExitStatus::from_raw(0 << 8); + for exe in pipeline.exes() { + status = run_exe( + exe, + async_std::sync::Arc::clone(&entry), + input_r.clone(), + resize_r.clone(), + event_w.clone(), + ) + .await; + + // i'm not sure what exactly the expected behavior here is - + // in zsh, SIGINT kills the whole command line while SIGTERM + // doesn't, but i don't know what the precise logic is or how + // other signals are handled + if status.signal() == Some(signal_hook::consts::signal::SIGINT) { + return (status, true); + } + } + (status, false) +} + +async fn run_exe( + exe: &crate::parse::Exe, + entry: async_std::sync::Arc<async_std::sync::Mutex<Entry>>, + input_r: async_std::channel::Receiver<Vec<u8>>, + resize_r: async_std::channel::Receiver<(u16, u16)>, + event_w: async_std::channel::Sender<crate::event::Event>, +) -> async_std::process::ExitStatus { + if builtins::is(exe.exe()) { + let code: i32 = builtins::run(exe.exe(), exe.args()).into(); + return async_std::process::ExitStatus::from_raw(code << 8); + } + + let mut process = async_std::process::Command::new(exe.exe()); + process.args(exe.args()); + let size = entry.lock_arc().await.vt.screen().size(); + let child = + process.spawn_pty(Some(&pty_process::Size::new(size.0, size.1))); + let child = match child { + Ok(child) => child, + Err(e) => { + let mut entry = entry.lock_arc().await; + entry.vt.process( + format!( + "\x1b[31mnbsh: failed to run {}: {}", + exe.exe(), + e.source().unwrap() + ) + .as_bytes(), + ); + return async_std::process::ExitStatus::from_raw(1 << 8); + } + }; + loop { + enum Res { + Read(Result<usize, std::io::Error>), + Write(Result<Vec<u8>, async_std::channel::RecvError>), + Resize(Result<(u16, u16), async_std::channel::RecvError>), + Exit( + Result<async_std::process::ExitStatus, async_std::io::Error>, + ), + } + 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) }; + let exit = async { Res::Exit(child.status_no_drop().await) }; + match read.race(write).race(resize).or(exit).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) { + panic!("pty read failed: {:?}", e); + } + } + }, + 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); + } + }, + Res::Exit(res) => match res { + Ok(status) => { + return status; + } + Err(e) => { + panic!("failed to get exit status: {}", e); + } + }, + } + } +} + +fn set_bgcolor(out: &mut impl textmode::Textmode, idx: usize, focus: bool) { + if focus { + out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b)); + } else if idx % 2 == 0 { + out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00)); + } else { + out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20)); + } +} |