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, fullscreen: Option, input: async_std::channel::Sender>, resize: async_std::channel::Sender<(u16, u16)>, start_time: time::OffsetDateTime, start_instant: std::time::Instant, } impl Entry { pub fn new( cmdline: String, env: Env, size: (u16, u16), input: async_std::channel::Sender>, resize: async_std::channel::Sender<(u16, u16)>, ) -> Self { let span = (0, cmdline.len()); 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, fullscreen: None, start_time: time::OffsetDateTime::now_utc(), start_instant: std::time::Instant::now(), } } pub fn render( &mut self, out: &mut impl textmode::Textmode, idx: usize, entry_count: usize, size: (u16, u16), focused: bool, scrolling: bool, offset: time::UtcOffset, ) { 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)), ) }, ); self.bell(out); if focused { self.audible_bell = false; self.visual_bell = false; } 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(); if self.audible_bell || self.visual_bell { out.set_bgcolor(textmode::Color::Rgb(64, 16, 16)); } else { set_bgcolor(out, idx, focused); } out.write_str("$ "); 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; let cmd = if self.cmd().len() > max_len { &self.cmd()[..(max_len - 4)] } else { self.cmd() }; if let State::Running(span) = self.state { let span = (span.0.min(cmd.len()), span.1.min(cmd.len())); if !cmd[..span.0].is_empty() { out.write_str(&cmd[..span.0]); } 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); } if !cmd[span.1..].is_empty() { out.write_str(&cmd[span.1..]); } } else { out.write_str(cmd); } if self.cmd().len() > max_len { if let State::Running(span) = self.state { if span.0 < cmd.len() && span.1 > cmd.len() { out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); } } out.write_str(" "); if let State::Running(span) = self.state { if span.1 > cmd.len() { out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); } } out.set_fgcolor(textmode::color::BLUE); out.write_str("..."); } out.reset_attributes(); 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), )); 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, (size.1 - len) / 2, ); out.set_fgcolor(textmode::color::RED); out.write_str(msg); out.hide_cursor(true); } else { let last_row = self.output_lines(focused && !scrolling); let mut max_lines = self.max_lines(entry_count); if last_row > max_lines { out.write(b"\r\n"); out.set_fgcolor(textmode::color::BLUE); out.write_str("..."); out.reset_attributes(); max_lines -= 1; } 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, size.1) .enumerate() .take(last_row) .skip(last_row.saturating_sub(max_lines)) { 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(); } 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) { 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 size(&self) -> (u16, u16) { self.vt.screen().size() } 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 cmd(&self) -> &str { &self.cmdline } pub fn env(&self) -> &Env { &self.env } 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 { matches!(self.state, State::Running(_)) } pub fn binary(&self) -> bool { self.vt.screen().errors() > 5 } pub fn lines(&self, entry_count: usize, focused: bool) -> usize { 1 + std::cmp::min( self.output_lines(focused), self.max_lines(entry_count), ) } fn max_lines(&self, entry_count: usize) -> usize { if self.env.idx() == entry_count - 1 { usize::from(self.size().0) * 2 / 3 } else { 5 } } pub fn output_lines(&self, 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, self.size().1).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()) } pub fn set_span(&mut self, span: (usize, usize)) { if matches!(self.state, State::Running(_)) { self.state = State::Running(span); } } pub async fn finish( &mut self, env: Env, event_w: async_std::channel::Sender, ) { self.state = State::Exited(ExitInfo::new(env.latest_status())); self.env = env; event_w.send(Event::PtyClose).await.unwrap(); } fn exit_info(&self) -> Option<&ExitInfo> { match &self.state { State::Running(..) => None, State::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; } } } 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 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)); } }