use crate::error::{GwmError, Result};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[cfg(unix)]
use libc;
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
use std::io::{Read, Write};
use std::path::Path;
use std::sync::mpsc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PtyKind {
LazyGit,
Terminal,
Review,
Exec,
}
pub struct PtyOverlay {
pub kind: PtyKind,
master: Box<dyn portable_pty::MasterPty + Send>,
child: Box<dyn portable_pty::Child + Send + Sync>,
writer: Box<dyn Write + Send>,
pub parser: tui_term::vt100::Parser,
rx: mpsc::Receiver<Vec<u8>>,
pub cols: u16,
pub rows: u16,
pub diff_file: Option<tempfile::NamedTempFile>,
pub finished: bool,
reaped: bool,
#[cfg_attr(not(unix), allow(dead_code))]
spawn_pid: Option<u32>,
signalled: bool,
}
impl std::fmt::Debug for PtyOverlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PtyOverlay")
.field("kind", &self.kind)
.field("cols", &self.cols)
.field("rows", &self.rows)
.finish_non_exhaustive()
}
}
impl PtyOverlay {
pub fn spawn(kind: PtyKind, argv: &[&str], cwd: &Path, cols: u16, rows: u16) -> Result<Self> {
let Some((bin, args)) = argv.split_first() else {
return Err(GwmError::Other("empty argv for PTY overlay".into()));
};
let pty_system = native_pty_system();
let pair = pty_system
.openpty(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.map_err(|e| GwmError::Other(e.to_string()))?;
let mut cmd = CommandBuilder::new(bin);
for arg in args {
cmd.arg(*arg);
}
cmd.cwd(cwd);
cmd.env("TERM", "xterm-256color");
let child = pair
.slave
.spawn_command(cmd)
.map_err(|e| GwmError::Other(e.to_string()))?;
let spawn_pid = child.process_id();
drop(pair.slave);
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(128);
let mut reader = pair
.master
.try_clone_reader()
.map_err(|e| GwmError::Other(e.to_string()))?;
std::thread::spawn(move || {
let mut buf = [0u8; 4096];
loop {
match reader.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
if tx.send(buf[..n].to_vec()).is_err() {
break;
}
}
}
}
});
let writer = pair.master.take_writer().map_err(|e| GwmError::Other(e.to_string()))?;
let parser = tui_term::vt100::Parser::new(rows, cols, 0);
Ok(Self {
kind,
master: pair.master,
child,
writer,
parser,
rx,
cols,
rows,
diff_file: None,
finished: false,
reaped: false,
spawn_pid,
signalled: false,
})
}
pub fn poll_bytes(&mut self) {
for _ in 0..64 {
match self.rx.try_recv() {
Ok(bytes) => self.parser.process(&bytes),
Err(_) => break,
}
}
}
pub fn write_key(&mut self, key: KeyEvent) -> std::io::Result<()> {
let bytes = key_to_bytes(key);
if !bytes.is_empty() {
self.writer.write_all(&bytes)?;
self.writer.flush()?;
}
Ok(())
}
pub fn resize(&mut self, cols: u16, rows: u16) {
self.cols = cols;
self.rows = rows;
self.parser.screen_mut().set_size(rows, cols);
let _ = self.master.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
});
}
pub fn is_alive(&mut self) -> bool {
match self.child.try_wait() {
Ok(None) => true,
_ => {
self.reaped = true;
false
}
}
}
fn signal_group(&mut self) {
if self.signalled {
return;
}
self.signalled = true;
#[cfg(unix)]
if let Some(pid) = self.spawn_pid {
unsafe { libc::kill(-(pid as libc::pid_t), libc::SIGKILL) };
}
}
pub fn mark_finished(&mut self) {
self.signal_group();
self.finished = true;
}
pub fn kill(&mut self) {
self.signal_group();
if self.reaped {
return;
}
let _ = self.child.kill();
for _ in 0..100 {
match self.child.try_wait() {
Ok(Some(_)) => {
self.reaped = true;
return;
}
_ => {
for _ in 0..128 {
if self.rx.try_recv().is_err() {
break;
}
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
}
let _ = self.child.wait();
self.reaped = true;
}
pub fn is_reaped(&self) -> bool {
self.reaped
}
pub fn group_signalled(&self) -> bool {
self.signalled
}
pub fn try_wait_after_kill(&mut self) -> Option<portable_pty::ExitStatus> {
self.child.try_wait().ok().flatten()
}
}
pub fn key_to_bytes(key: KeyEvent) -> Vec<u8> {
match key.code {
KeyCode::Char(c) => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
let c = c.to_ascii_lowercase();
if c.is_ascii_lowercase() {
return vec![(c as u8) - b'a' + 1];
}
return vec![];
}
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
let bytes = s.as_bytes().to_vec();
if key.modifiers.contains(KeyModifiers::ALT) {
let mut out = vec![27u8];
out.extend_from_slice(&bytes);
return out;
}
bytes
}
KeyCode::Enter => vec![b'\r'],
KeyCode::Backspace => vec![127],
KeyCode::Esc => vec![27],
KeyCode::Tab => vec![b'\t'],
KeyCode::BackTab => vec![27, b'[', b'Z'],
KeyCode::Up => vec![27, b'[', b'A'],
KeyCode::Down => vec![27, b'[', b'B'],
KeyCode::Right => vec![27, b'[', b'C'],
KeyCode::Left => vec![27, b'[', b'D'],
KeyCode::Home => vec![27, b'[', b'H'],
KeyCode::End => vec![27, b'[', b'F'],
KeyCode::Delete => vec![27, b'[', b'3', b'~'],
KeyCode::Insert => vec![27, b'[', b'2', b'~'],
KeyCode::PageUp => vec![27, b'[', b'5', b'~'],
KeyCode::PageDown => vec![27, b'[', b'6', b'~'],
KeyCode::F(n) => f_key_bytes(n),
_ => vec![],
}
}
fn f_key_bytes(n: u8) -> Vec<u8> {
match n {
1 => vec![27, b'O', b'P'],
2 => vec![27, b'O', b'Q'],
3 => vec![27, b'O', b'R'],
4 => vec![27, b'O', b'S'],
5 => vec![27, b'[', b'1', b'5', b'~'],
6 => vec![27, b'[', b'1', b'7', b'~'],
7 => vec![27, b'[', b'1', b'8', b'~'],
8 => vec![27, b'[', b'1', b'9', b'~'],
9 => vec![27, b'[', b'2', b'0', b'~'],
10 => vec![27, b'[', b'2', b'1', b'~'],
11 => vec![27, b'[', b'2', b'3', b'~'],
12 => vec![27, b'[', b'2', b'4', b'~'],
_ => vec![],
}
}