use crate::shell::prelude::*; mod entry; pub use entry::Entry; mod pty; 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, } } 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, mut entry) in self.visible(repl_lines, focus, scrolling).await.rev() { let focused = focus.map_or(false, |focus| idx == focus); used_lines += entry.lines(self.entry_count(), 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, 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(()) } 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 send_input(&mut self, idx: usize, input: Vec) { self.entry(idx).await.send_input(input).await; } pub async fn resize(&mut self, size: (u16, u16)) { self.size = size; for entry in &self.entries { entry.lock_arc().await.resize(size).await; } } pub async fn run( &mut self, cmdline: &str, env: &Env, 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 = crate::mutex::new(Entry::new( cmdline.to_string(), env.clone(), self.size, input_w, resize_w, )); run_commands( cmdline.to_string(), crate::mutex::clone(&entry), env.clone(), input_r, resize_r, event_w, ); self.entries.push(entry); Ok(self.entries.len() - 1) } pub async fn entry(&self, idx: usize) -> crate::mutex::Guard { 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.entry_count(), focused && !scrolling); if used_lines > usize::from(self.size.0) { break; } iter.add(idx, entry); } iter } } struct VisibleEntries { entries: std::collections::VecDeque<(usize, crate::mutex::Guard)>, } impl VisibleEntries { fn new() -> Self { Self { entries: std::collections::VecDeque::new(), } } fn add(&mut self, idx: usize, entry: crate::mutex::Guard) { // 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, crate::mutex::Guard); 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() } } fn run_commands( cmdline: String, entry: crate::mutex::Mutex, mut env: Env, 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 pty = match pty::Pty::new( entry.lock_arc().await.size(), &entry, input_r, resize_r, event_w.clone(), ) { Ok(pty) => pty, Err(e) => { let mut entry = entry.lock_arc().await; entry.process( format!("nbsh: failed to allocate pty: {}\r\n", e) .as_bytes(), ); env.set_status(async_std::process::ExitStatus::from_raw( 1 << 8, )); entry.finish(env, event_w).await; return; } }; let status = match spawn_commands(&cmdline, &pty, &mut env, event_w.clone()) .await { Ok(status) => status, Err(e) => { let mut entry = entry.lock_arc().await; entry.process( format!( "nbsh: failed to spawn {}: {}\r\n", cmdline, e ) .as_bytes(), ); env.set_status(async_std::process::ExitStatus::from_raw( 1 << 8, )); entry.finish(env, event_w).await; return; } }; env.set_status(status); entry.lock_arc().await.finish(env, event_w).await; pty.close().await; }); } async fn spawn_commands( cmdline: &str, pty: &pty::Pty, env: &mut Env, event_w: async_std::channel::Sender, ) -> anyhow::Result { let mut cmd = pty_process::Command::new(std::env::current_exe()?); cmd.args(&["-c", cmdline]); env.apply(&mut cmd); let (from_r, from_w) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?; // Safety: dup2 is an async-signal-safe function unsafe { cmd.pre_exec(move || { nix::unistd::dup2(from_w, 3)?; Ok(()) }); } let child = pty.spawn(cmd)?; nix::unistd::close(from_w)?; let (read_w, read_r) = async_std::channel::unbounded(); let new_read = move || { let read_w = read_w.clone(); async_std::task::spawn(async move { let event = blocking::unblock(move || { // Safety: from_r was just opened above and is only // referenced in this closure, which takes ownership of it // at the start and returns ownership of it at the end let fh = unsafe { std::fs::File::from_raw_fd(from_r) }; let event = bincode::deserialize_from(&fh); let _ = fh.into_raw_fd(); event }) .await; if read_w.is_closed() { // we should never drop read_r while there are still valid // things to read assert!(event.is_err()); } else { read_w.send(event).await.unwrap(); } }); }; new_read(); let mut read_done = false; let mut exit_done = None; loop { enum Res { Read(bincode::Result), Exit(std::io::Result), } let read_r = read_r.clone(); let read = async move { Res::Read(read_r.recv().await.unwrap()) }; let exit = async { Res::Exit(if exit_done.is_none() { child.status_no_drop().await } else { std::future::pending().await }) }; match read.or(exit).await { Res::Read(Ok(event)) => match event { crate::runner::Event::RunPipeline(idx, span) => { event_w .send(Event::ChildRunPipeline(idx, span)) .await .unwrap(); new_read(); } crate::runner::Event::Suspend(idx) => { event_w.send(Event::ChildSuspend(idx)).await.unwrap(); new_read(); } crate::runner::Event::Exit(new_env) => { *env = new_env; read_done = true; } }, Res::Read(Err(e)) => { if let bincode::ErrorKind::Io(io_e) = &*e { if io_e.kind() == std::io::ErrorKind::UnexpectedEof { read_done = true; } else { anyhow::bail!(e); } } else { anyhow::bail!(e); } } Res::Exit(Ok(status)) => { exit_done = Some(status); } Res::Exit(Err(e)) => { anyhow::bail!(e); } } if let (true, Some(status)) = (read_done, exit_done) { nix::unistd::close(from_r)?; // nix::sys::signal::Signal is repr(i32) #[allow(clippy::as_conversions)] return Ok(status); } } }