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 _; use textmode::Textmode as _; pub struct History { size: (u16, u16), entries: Vec>, } impl History { pub fn new() -> Self { Self { size: (24, 80), entries: vec![], } } pub async fn handle_key(&self, key: textmode::Key, idx: usize) { let entry = self.entries[idx].lock_arc().await; if entry.running() { entry.input.send(key.into_bytes()).await.unwrap(); } } pub async fn render( &self, out: &mut textmode::Output, repl_lines: usize, focus: Option, ) -> anyhow::Result<()> { let mut used_lines = repl_lines; let mut pos = None; for (idx, entry) in self.entries.iter().enumerate().rev() { let mut entry = entry.lock_arc().await; let focused = focus.map_or(false, |focus| idx == focus); let last_row = entry.lines(self.size.1, focused); used_lines += 1 + std::cmp::min(6, last_row); if used_lines > self.size.0 as usize { break; } if focused && used_lines == 1 && entry.running() { used_lines = 2; } out.move_to( (self.size.0 as usize - used_lines).try_into().unwrap(), 0, ); entry.render(out, self.size.1, focused); if focused { pos = Some(out.screen().cursor_position()); } } if let Some(pos) = pos { out.move_to(pos.0, pos.1); } Ok(()) } pub async fn render_fullscreen( &self, out: &mut textmode::Output, 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, action_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(HistoryEntry::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), )); action_w .send(crate::action::Action::UpdateFocus( crate::state::Focus::Readline, )) .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, action_w, ); } self.entries.push(entry); Ok(self.entries.len() - 1) } pub async fn toggle_fullscreen(&mut self, idx: usize) { self.entries[idx].lock_arc().await.toggle_fullscreen(); } pub async fn should_fullscreen(&self, idx: usize) -> bool { self.entries[idx].lock_arc().await.should_fullscreen() } pub fn entry_count(&self) -> usize { self.entries.len() } } struct HistoryEntry { 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: chrono::DateTime, start_instant: std::time::Instant, exit_info: Option, } impl HistoryEntry { 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: chrono::Local::now(), start_instant: std::time::Instant::now(), exit_info: None, } } fn render( &mut self, out: &mut textmode::Output, width: u16, focused: bool, ) { out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); 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 focused { out.set_fgcolor(textmode::color::BLACK); out.set_bgcolor(textmode::color::CYAN); } else { out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); } out.write_str("$ "); out.reset_attributes(); out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); if self.running() { out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); } out.write_str(&self.cmd); out.reset_attributes(); out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); let time = if let Some(info) = self.exit_info { format!( "[{} ({:6})]", self.start_time.time().format("%H:%M:%S"), crate::format::duration(info.instant - self.start_instant) ) } else { format!("[{}]", self.start_time.time().format("%H:%M:%S")) }; 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(); let last_row = self.lines(width, focused); if last_row > 5 { out.write(b"\r\n"); out.set_fgcolor(textmode::color::BLUE); out.write(b"..."); 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.write(b"\x1b[m"); if !wrapped { out.write(format!("\x1b[{}H", out_row + 1).as_bytes()); } out.write(&row); wrapped = screen.row_wrapped(idx); if pos.0 == idx { cursor_found = Some(out_row); } out_row += 1; } if focused { if let Some(row) = cursor_found { if screen.hide_cursor() { out.write(b"\x1b[?25l"); } else { out.write(b"\x1b[?25h"); out.move_to(row, pos.1); } } else { out.write(b"\x1b[?25l"); } } out.reset_attributes(); } fn render_fullscreen(&mut self, out: &mut textmode::Output) { 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 toggle_fullscreen(&mut self) { if let Some(fullscreen) = self.fullscreen { self.fullscreen = Some(!fullscreen); } else { self.fullscreen = Some(!self.vt.screen().alternate_screen()); } } fn running(&self) -> bool { self.exit_info.is_none() } fn lines(&self, width: u16, focused: bool) -> usize { 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 } 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)>, action_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 { action_w .send(crate::action::Action::CheckUpdateScene) .await .unwrap(); } action_w .send(crate::action::Action::Render) .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()), ); action_w .send(crate::action::Action::UpdateFocus( crate::state::Focus::Readline, )) .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); } }, } } }); }