diff options
Diffstat (limited to 'src/shell/history/entry.rs')
-rw-r--r-- | src/shell/history/entry.rs | 344 |
1 files changed, 190 insertions, 154 deletions
diff --git a/src/shell/history/entry.rs b/src/shell/history/entry.rs index a45d99d..0491bf7 100644 --- a/src/shell/history/entry.rs +++ b/src/shell/history/entry.rs @@ -1,25 +1,13 @@ use crate::shell::prelude::*; -enum State { - Running((usize, usize)), - Exited(ExitInfo), -} - pub struct Entry { cmdline: String, env: Env, - state: State, - vt: vt100::Parser, - audible_bell_state: usize, - visual_bell_state: usize, - audible_bell: bool, - visual_bell: bool, - real_bell_pending: bool, + pty: super::pty::Pty, 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, + start_time: time::OffsetDateTime, + state: State, } impl Entry { @@ -27,39 +15,37 @@ impl Entry { cmdline: String, env: Env, size: (u16, u16), - input: async_std::channel::Sender<Vec<u8>>, - resize: async_std::channel::Sender<(u16, u16)>, - ) -> Self { - let span = (0, cmdline.len()); - Self { + event_w: crate::shell::event::Writer, + ) -> Result<Self> { + let start_instant = std::time::Instant::now(); + let start_time = time::OffsetDateTime::now_utc(); + + let (pty, pts) = super::pty::Pty::new(size, event_w.clone()).unwrap(); + let (child, fh) = Self::spawn_command(&cmdline, &env, &pts)?; + tokio::spawn(Self::task(child, fh, env.idx(), event_w)); + Ok(Self { cmdline, env, - state: State::Running(span), - vt: vt100::Parser::new(size.0, size.1, 0), - audible_bell_state: 0, - visual_bell_state: 0, - audible_bell: false, - visual_bell: false, - real_bell_pending: false, - input, - resize, + pty, fullscreen: None, - start_time: time::OffsetDateTime::now_utc(), - start_instant: std::time::Instant::now(), - } + start_instant, + start_time, + state: State::Running((0, 0)), + }) } pub fn render( - &mut self, + &self, out: &mut impl textmode::Textmode, - idx: usize, entry_count: usize, - size: (u16, u16), + vt: &mut super::pty::Vt, focused: bool, scrolling: bool, offset: time::UtcOffset, ) { - let time = self.exit_info().map_or_else( + let idx = self.env.idx(); + let size = out.screen().size(); + let time = self.state.exit_info().map_or_else( || { format!( "[{}]", @@ -77,13 +63,11 @@ impl Entry { }, ); - self.bell(out); - if focused { - self.audible_bell = false; - self.visual_bell = false; + if vt.bell(focused) { + out.write(b"\x07"); } - set_bgcolor(out, idx, focused); + Self::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); @@ -92,8 +76,8 @@ impl Entry { out.write_str(" "); out.reset_attributes(); - set_bgcolor(out, idx, focused); - if let Some(info) = self.exit_info() { + Self::set_bgcolor(out, idx, focused); + if let Some(info) = self.state.exit_info() { if info.status.signal().is_some() { out.set_fgcolor(textmode::color::MAGENTA); } else if info.status.success() { @@ -107,13 +91,13 @@ impl Entry { } out.reset_attributes(); - if self.audible_bell || self.visual_bell { + if vt.is_bell() { out.set_bgcolor(textmode::Color::Rgb(64, 16, 16)); } else { - set_bgcolor(out, idx, focused); + Self::set_bgcolor(out, idx, focused); } out.write_str("$ "); - set_bgcolor(out, idx, focused); + Self::set_bgcolor(out, idx, focused); let start = usize::from(out.screen().cursor_position().1); let end = usize::from(size.1) - time.len() - 2; let max_len = end - start; @@ -130,7 +114,7 @@ impl Entry { if !cmd[span.0..span.1].is_empty() { out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); out.write_str(&cmd[span.0..span.1]); - set_bgcolor(out, idx, focused); + Self::set_bgcolor(out, idx, focused); } if !cmd[span.1..].is_empty() { out.write_str(&cmd[span.1..]); @@ -155,7 +139,7 @@ impl Entry { } out.reset_attributes(); - set_bgcolor(out, idx, focused); + Self::set_bgcolor(out, idx, focused); let cur_pos = out.screen().cursor_position(); out.write_str(&" ".repeat( usize::from(size.1) - time.len() - 1 - usize::from(cur_pos.1), @@ -164,7 +148,7 @@ impl Entry { out.write_str(" "); out.reset_attributes(); - if self.binary() { + if vt.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( @@ -175,7 +159,8 @@ impl Entry { out.write_str(msg); out.hide_cursor(true); } else { - let last_row = self.output_lines(focused && !scrolling); + let last_row = + vt.output_lines(focused && !scrolling, self.state.running()); let mut max_lines = self.max_lines(entry_count); if last_row > max_lines { out.write(b"\r\n"); @@ -185,7 +170,7 @@ impl Entry { max_lines -= 1; } let mut out_row = out.screen().cursor_position().0 + 1; - let screen = self.vt.screen(); + let screen = vt.screen(); let pos = screen.cursor_position(); let mut wrapped = false; let mut cursor_found = None; @@ -216,66 +201,41 @@ impl Entry { } } } - out.reset_attributes(); - } - pub fn render_fullscreen(&mut self, out: &mut impl textmode::Textmode) { - out.write(&self.vt.screen().state_formatted()); - self.bell(out); - self.audible_bell = false; - self.visual_bell = false; out.reset_attributes(); } - pub async fn send_input(&self, bytes: Vec<u8>) { - if self.running() { - self.input.send(bytes).await.unwrap(); - } - } - - pub async fn resize(&mut self, size: (u16, u16)) { - if self.running() { - self.resize.send(size).await.unwrap(); - self.vt.set_size(size.0, size.1); - } + pub fn render_fullscreen(&self, out: &mut impl textmode::Textmode) { + self.pty.with_vt_mut(|vt| { + out.write(&vt.screen().state_formatted()); + if vt.bell(true) { + out.write(b"\x07"); + } + out.reset_attributes(); + }); } - pub fn size(&self) -> (u16, u16) { - self.vt.screen().size() + pub fn input(&self, bytes: Vec<u8>) { + self.pty.input(bytes); } - pub fn process(&mut self, input: &[u8]) { - self.vt.process(input); - let screen = self.vt.screen(); - - let new_audible_bell_state = screen.audible_bell_count(); - if new_audible_bell_state != self.audible_bell_state { - self.audible_bell = true; - self.real_bell_pending = true; - self.audible_bell_state = new_audible_bell_state; - } - - let new_visual_bell_state = screen.visual_bell_count(); - if new_visual_bell_state != self.visual_bell_state { - self.visual_bell = true; - self.real_bell_pending = true; - self.visual_bell_state = new_visual_bell_state; - } + pub fn resize(&self, size: (u16, u16)) { + self.pty.resize(size); } pub fn cmd(&self) -> &str { &self.cmdline } - pub fn env(&self) -> &Env { - &self.env + pub fn start_time(&self) -> time::OffsetDateTime { + self.start_time } 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()); + self.fullscreen = Some(!self.pty.fullscreen()); } } @@ -284,110 +244,186 @@ impl Entry { } pub fn running(&self) -> bool { - matches!(self.state, State::Running(_)) + self.state.running() } - pub fn binary(&self) -> bool { - self.vt.screen().errors() > 5 + pub fn exited(&mut self, exit_info: ExitInfo) { + self.state = State::Exited(exit_info); } pub fn lines(&self, entry_count: usize, focused: bool) -> usize { + let running = self.running(); 1 + std::cmp::min( - self.output_lines(focused), + self.pty.with_vt(|vt| vt.output_lines(focused, running)), self.max_lines(entry_count), ) } + pub fn should_fullscreen(&self) -> bool { + self.fullscreen.unwrap_or_else(|| self.pty.fullscreen()) + } + + pub fn lock_vt(&self) -> std::sync::MutexGuard<super::pty::Vt> { + self.pty.lock_vt() + } + + pub fn set_span(&mut self, new_span: (usize, usize)) { + if let State::Running(ref mut span) = self.state { + *span = new_span; + } + } + fn max_lines(&self, entry_count: usize) -> usize { if self.env.idx() == entry_count - 1 { - usize::from(self.size().0) * 2 / 3 + 15 } else { 5 } } - pub fn output_lines(&self, focused: bool) -> usize { - if self.binary() { - return 1; + 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)); } + } - let screen = self.vt.screen(); - let mut last_row = 0; - for (idx, row) in screen.rows(0, self.size().1).enumerate() { - if !row.is_empty() { - last_row = idx + 1; - } + fn spawn_command( + cmdline: &str, + env: &Env, + pts: &pty_process::Pts, + ) -> Result<(tokio::process::Child, std::fs::File)> { + let mut cmd = pty_process::Command::new(crate::info::current_exe()?); + cmd.args(&["-c", cmdline, "--status-fd", "3"]); + env.apply(&mut cmd); + let (from_r, from_w) = + nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?; + // Safety: from_r was just opened above and is not used anywhere else + let fh = unsafe { std::fs::File::from_raw_fd(from_r) }; + // Safety: dup2 is an async-signal-safe function + unsafe { + cmd.pre_exec(move || { + nix::unistd::dup2(from_w, 3)?; + Ok(()) + }); } - if focused && self.running() { - last_row = std::cmp::max( - last_row, - usize::from(screen.cursor_position().0) + 1, - ); - } - last_row + let child = cmd.spawn(pts)?; + nix::unistd::close(from_w)?; + Ok((child, fh)) } - pub fn should_fullscreen(&self) -> bool { - self.fullscreen - .unwrap_or_else(|| self.vt.screen().alternate_screen()) - } + async fn task( + mut child: tokio::process::Child, + fh: std::fs::File, + idx: usize, + event_w: crate::shell::event::Writer, + ) { + enum Res { + Read(crate::runner::Event), + Exit(std::io::Result<std::process::ExitStatus>), + } - pub fn set_span(&mut self, span: (usize, usize)) { - if matches!(self.state, State::Running(_)) { - self.state = State::Running(span); + let (read_w, read_r) = tokio::sync::mpsc::unbounded_channel(); + tokio::task::spawn_blocking(move || loop { + let event = bincode::deserialize_from(&fh); + match event { + Ok(event) => { + read_w.send(event).unwrap(); + } + Err(e) => { + match &*e { + bincode::ErrorKind::Io(io_e) => { + assert!( + io_e.kind() + == std::io::ErrorKind::UnexpectedEof + ); + } + e => { + panic!("{}", e); + } + } + break; + } + } + }); + + let mut stream: futures_util::stream::SelectAll<_> = [ + tokio_stream::wrappers::UnboundedReceiverStream::new(read_r) + .map(Res::Read) + .boxed(), + futures_util::stream::once(child.wait()) + .map(Res::Exit) + .boxed(), + ] + .into_iter() + .collect(); + let mut exit_status = None; + let mut new_env = None; + while let Some(res) = stream.next().await { + match res { + Res::Read(event) => match event { + crate::runner::Event::RunPipeline(new_span) => { + // we could just update the span in place here, but we + // do this as an event so that we can also trigger a + // refresh + event_w.send(Event::ChildRunPipeline(idx, new_span)); + } + crate::runner::Event::Suspend => { + event_w.send(Event::ChildSuspend(idx)); + } + crate::runner::Event::Exit(env) => { + new_env = Some(env); + } + }, + Res::Exit(status) => { + exit_status = Some(status.unwrap()); + } + } } + event_w.send(Event::ChildExit( + idx, + ExitInfo::new(exit_status.unwrap()), + new_env, + )); } +} - pub async fn finish( - &mut self, - env: Env, - event_w: async_std::channel::Sender<Event>, - ) { - self.state = State::Exited(ExitInfo::new(env.latest_status())); - self.env = env; - event_w.send(Event::PtyClose).await.unwrap(); - } +enum State { + Running((usize, usize)), + Exited(ExitInfo), +} +impl State { fn exit_info(&self) -> Option<&ExitInfo> { - match &self.state { - State::Running(..) => None, - State::Exited(exit_info) => Some(exit_info), + match self { + Self::Running(_) => None, + Self::Exited(exit_info) => Some(exit_info), } } - fn bell(&mut self, out: &mut impl textmode::Textmode) { - if self.real_bell_pending { - if self.audible_bell { - out.write(b"\x07"); - } - if self.visual_bell { - out.write(b"\x1bg"); - } - self.real_bell_pending = false; - } + fn running(&self) -> bool { + self.exit_info().is_none() } } -struct ExitInfo { - status: async_std::process::ExitStatus, +#[derive(Debug)] +pub struct ExitInfo { + status: std::process::ExitStatus, instant: std::time::Instant, } impl ExitInfo { - fn new(status: async_std::process::ExitStatus) -> Self { + fn new(status: std::process::ExitStatus) -> Self { Self { status, instant: std::time::Instant::now(), } } } - -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)); - } -} |