use std::io::{Read, Write};
use std::sync::{mpsc, Arc, Mutex};
use anyhow::Result;
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
pub type TermId = usize;
pub enum TermEvent {
Output(TermId),
Exited(TermId),
}
pub struct Term {
pub exited: bool,
parser: Arc<Mutex<vt100::Parser>>,
writer: Box<dyn Write + Send>,
master: Box<dyn MasterPty + Send>,
_child: Box<dyn Child + Send + Sync>,
rows: u16,
cols: u16,
view_offset: usize,
scrollback_limit: usize,
}
pub fn spawn(
id: TermId,
rows: u16,
cols: u16,
scrollback: usize,
cwd: Option<std::path::PathBuf>,
session: Option<&str>,
tx: mpsc::Sender<TermEvent>,
) -> Result<Term> {
let rows = rows.max(1);
let cols = cols.max(1);
let pty_system = native_pty_system();
let pair = pty_system.openpty(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})?;
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
let mut cmd = CommandBuilder::new(shell);
if let Some(dir) = cwd.filter(|d| d.is_dir()) {
cmd.cwd(dir);
}
if let Some(name) = session {
cmd.env("MARS_SESSION", name);
}
let child = pair.slave.spawn_command(cmd)?;
drop(pair.slave);
let mut reader = pair.master.try_clone_reader()?;
let writer = pair.master.take_writer()?;
let parser = Arc::new(Mutex::new(vt100::Parser::new(rows, cols, scrollback)));
let reader_parser = parser.clone();
std::thread::spawn(move || {
let mut buf = [0u8; 8192];
loop {
match reader.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
if let Ok(mut p) = reader_parser.lock() {
p.process(&buf[..n]);
}
if tx.send(TermEvent::Output(id)).is_err() {
break;
}
}
}
}
let _ = tx.send(TermEvent::Exited(id));
});
Ok(Term {
exited: false,
parser,
writer,
master: pair.master,
_child: child,
rows,
cols,
view_offset: 0,
scrollback_limit: scrollback,
})
}
impl Drop for Term {
fn drop(&mut self) {
let _ = self._child.kill();
}
}
impl Term {
pub fn send_bytes(&mut self, bytes: &[u8]) {
let _ = self.writer.write_all(bytes);
let _ = self.writer.flush();
}
pub fn resize(&mut self, rows: u16, cols: u16) {
let rows = rows.max(1);
let cols = cols.max(1);
if rows == self.rows && cols == self.cols {
return;
}
self.rows = rows;
self.cols = cols;
let _ = self.master.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
});
if let Ok(mut p) = self.parser.lock() {
p.set_size(rows, cols);
}
}
pub fn screen(&self) -> vt100::Screen {
self.parser.lock().unwrap().screen().clone()
}
pub fn scroll_view(&mut self, delta: i64) {
let requested = (self.view_offset as i64 + delta)
.clamp(0, self.scrollback_limit as i64) as usize;
if let Ok(mut p) = self.parser.lock() {
p.set_scrollback(requested);
self.view_offset = p.screen().scrollback();
} else {
self.view_offset = requested;
}
}
pub fn history_tail(&self, lines: usize) -> String {
let Ok(mut p) = self.parser.lock() else { return String::new() };
let rows = p.screen().size().0 as usize;
let saved = self.view_offset;
let mut pages: Vec<String> = Vec::new();
let (mut off, mut got) = (0usize, 0usize);
loop {
p.set_scrollback(off);
pages.push(p.screen().contents());
got += rows;
if got >= lines || off >= self.scrollback_limit {
break;
}
off += rows;
}
p.set_scrollback(saved); pages.reverse(); let joined = pages.join("\n");
let all: Vec<&str> = joined.lines().collect();
let start = all.len().saturating_sub(lines);
all[start..].join("\n")
}
pub fn scroll_to_live(&mut self) {
if self.view_offset != 0 {
self.scroll_view(-(self.view_offset as i64));
}
}
pub fn view_offset(&self) -> usize {
self.view_offset
}
}