From a875058500cc93f4d18bd5481d94987232bfbc2a Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sat, 25 Dec 2021 15:39:08 -0500 Subject: rearrange some things --- src/builtins.rs | 47 --- src/main.rs | 1 - src/state/history.rs | 712 ----------------------------------------- src/state/history/builtins.rs | 47 +++ src/state/history/mod.rs | 714 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 761 insertions(+), 760 deletions(-) delete mode 100644 src/builtins.rs delete mode 100644 src/state/history.rs create mode 100644 src/state/history/builtins.rs create mode 100644 src/state/history/mod.rs diff --git a/src/builtins.rs b/src/builtins.rs deleted file mode 100644 index e8f87da..0000000 --- a/src/builtins.rs +++ /dev/null @@ -1,47 +0,0 @@ -pub fn is(exe: &str) -> bool { - matches!(exe, "cd") -} - -pub fn run<'a>(exe: &str, args: impl IntoIterator) -> u8 { - match exe { - "cd" => impls::cd( - args.into_iter() - .map(std::convert::AsRef::as_ref) - .next() - .unwrap_or(""), - ), - _ => unreachable!(), - } -} - -mod impls { - pub fn cd(dir: &str) -> u8 { - let dir = if dir.is_empty() { - home() - } else if dir.starts_with('~') { - let path: std::path::PathBuf = dir.into(); - if let std::path::Component::Normal(prefix) = - path.components().next().unwrap() - { - if prefix.to_str() == Some("~") { - home().join(path.strip_prefix(prefix).unwrap()) - } else { - // TODO - return 1; - } - } else { - unreachable!() - } - } else { - dir.into() - }; - match std::env::set_current_dir(dir) { - Ok(()) => 0, - Err(_) => 1, - } - } - - fn home() -> std::path::PathBuf { - std::env::var_os("HOME").unwrap().into() - } -} diff --git a/src/main.rs b/src/main.rs index c698b91..602eb8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,6 @@ #![allow(clippy::too_many_lines)] #![allow(clippy::type_complexity)] -mod builtins; mod env; mod event; mod format; diff --git a/src/state/history.rs b/src/state/history.rs deleted file mode 100644 index f147c37..0000000 --- a/src/state/history.rs +++ /dev/null @@ -1,712 +0,0 @@ -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 _; - -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, - } - } - - // 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, - 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, - ) -> anyhow::Result { - 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, - ) { - // 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 { - 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 > usize::from(self.size.0) { - 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 { - ast: Result, - 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( - ast: Result, - size: (u16, u16), - input: async_std::channel::Sender>, - 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) { - 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>, - 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 { - 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>, - input_r: async_std::channel::Receiver>, - resize_r: async_std::channel::Receiver<(u16, u16)>, - event_w: async_std::channel::Sender, -) -> (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>, - input_r: async_std::channel::Receiver>, - resize_r: async_std::channel::Receiver<(u16, u16)>, - event_w: async_std::channel::Sender, -) -> async_std::process::ExitStatus { - if crate::builtins::is(exe.exe()) { - let code: i32 = crate::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), - Write(Result, async_std::channel::RecvError>), - Resize(Result<(u16, u16), async_std::channel::RecvError>), - Exit( - Result, - ), - } - 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)); - } -} diff --git a/src/state/history/builtins.rs b/src/state/history/builtins.rs new file mode 100644 index 0000000..e8f87da --- /dev/null +++ b/src/state/history/builtins.rs @@ -0,0 +1,47 @@ +pub fn is(exe: &str) -> bool { + matches!(exe, "cd") +} + +pub fn run<'a>(exe: &str, args: impl IntoIterator) -> u8 { + match exe { + "cd" => impls::cd( + args.into_iter() + .map(std::convert::AsRef::as_ref) + .next() + .unwrap_or(""), + ), + _ => unreachable!(), + } +} + +mod impls { + pub fn cd(dir: &str) -> u8 { + let dir = if dir.is_empty() { + home() + } else if dir.starts_with('~') { + let path: std::path::PathBuf = dir.into(); + if let std::path::Component::Normal(prefix) = + path.components().next().unwrap() + { + if prefix.to_str() == Some("~") { + home().join(path.strip_prefix(prefix).unwrap()) + } else { + // TODO + return 1; + } + } else { + unreachable!() + } + } else { + dir.into() + }; + match std::env::set_current_dir(dir) { + Ok(()) => 0, + Err(_) => 1, + } + } + + fn home() -> std::path::PathBuf { + std::env::var_os("HOME").unwrap().into() + } +} 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>>, + 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, + 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, + ) -> anyhow::Result { + 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, + ) { + // 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 { + 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 > usize::from(self.size.0) { + 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 { + ast: Result, + 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( + ast: Result, + size: (u16, u16), + input: async_std::channel::Sender>, + 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) { + 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>, + 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 { + 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>, + input_r: async_std::channel::Receiver>, + resize_r: async_std::channel::Receiver<(u16, u16)>, + event_w: async_std::channel::Sender, +) -> (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>, + input_r: async_std::channel::Receiver>, + resize_r: async_std::channel::Receiver<(u16, u16)>, + event_w: async_std::channel::Sender, +) -> 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), + Write(Result, async_std::channel::RecvError>), + Resize(Result<(u16, u16), async_std::channel::RecvError>), + Exit( + Result, + ), + } + 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)); + } +} -- cgit v1.2.3-54-g00ecf