use std::io::{Read, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
use tokio::sync::mpsc;
use tui_term::widget::PseudoTerminal;
use crate::events::AppMsg;
const READ_BUF_SIZE: usize = 8192;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttachMode {
Preview,
Focused,
}
impl AttachMode {
fn tmux_attach_args(self) -> &'static [&'static str] {
match self {
AttachMode::Preview => &["attach", "-f", "read-only"],
AttachMode::Focused => &["attach"],
}
}
}
const MIN_ROWS: u16 = 4;
const MIN_COLS: u16 = 20;
pub struct EmbedTerminal {
session: String,
parser: vt100::Parser,
master: Box<dyn MasterPty + Send>,
writer: Option<Box<dyn Write + Send>>,
child: Box<dyn Child + Send + Sync>,
stop: Arc<AtomicBool>,
rows: u16,
cols: u16,
mode: AttachMode,
default_colors: crate::terminal_query::DefaultColors,
color_query_scanner: crate::terminal_query::QueryScanner,
}
impl EmbedTerminal {
#[allow(clippy::too_many_arguments)]
pub fn spawn(
socket: Option<&str>,
session: &str,
rows: u16,
cols: u16,
mode: AttachMode,
initial_snapshot: Option<&[u8]>,
default_colors: crate::terminal_query::DefaultColors,
evt_tx: mpsc::UnboundedSender<AppMsg>,
) -> std::io::Result<Self> {
let rows = rows.max(MIN_ROWS);
let cols = cols.max(MIN_COLS);
let pty = native_pty_system();
let pair = pty
.openpty(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
})
.map_err(io_err("openpty"))?;
let mut cmd = CommandBuilder::new("tmux");
if let Some(sock) = socket {
cmd.arg("-L");
cmd.arg(sock);
}
for a in mode.tmux_attach_args() {
cmd.arg(a);
}
cmd.arg("-t");
cmd.arg(session);
cmd.env("TERM", "xterm-256color");
reset_window_size(socket, session);
let child = pair
.slave
.spawn_command(cmd)
.map_err(io_err("spawn tmux"))?;
drop(pair.slave);
let mut reader = pair
.master
.try_clone_reader()
.map_err(io_err("clone reader"))?;
let writer = pair.master.take_writer().map_err(io_err("take writer"))?;
let stop = Arc::new(AtomicBool::new(false));
let stop_reader = stop.clone();
let session_owned = session.to_string();
let evt_tx_reader = evt_tx;
thread::Builder::new()
.name(format!("bosun-embed-{}", session))
.spawn(move || {
let mut buf = [0u8; READ_BUF_SIZE];
loop {
if stop_reader.load(Ordering::Relaxed) {
break;
}
match reader.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let chunk = buf[..n].to_vec();
if evt_tx_reader
.send(AppMsg::EmbedBytes {
session: session_owned.clone(),
bytes: chunk,
})
.is_err()
{
break;
}
}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
})
.map_err(io_err("spawn reader"))?;
let mut parser = vt100::Parser::new(rows, cols, 0);
if let Some(snap) = initial_snapshot {
parser.process(snap);
}
Ok(Self {
session: session.to_string(),
parser,
master: pair.master,
writer: Some(writer),
child,
stop,
rows,
cols,
mode,
default_colors,
color_query_scanner: crate::terminal_query::QueryScanner::default(),
})
}
pub fn write(&mut self, bytes: &[u8]) -> std::io::Result<()> {
self.write_raw(bytes)
}
pub fn mode(&self) -> AttachMode {
self.mode
}
pub fn wants_mouse(&self) -> bool {
!matches!(
self.parser.screen().mouse_protocol_mode(),
vt100::MouseProtocolMode::None
)
}
pub fn application_cursor(&self) -> bool {
self.parser.screen().application_cursor()
}
pub fn session(&self) -> &str {
&self.session
}
pub fn feed(&mut self, bytes: &[u8]) {
for (kind, term) in self.color_query_scanner.scan(bytes) {
let reply = self.default_colors.response(kind, term);
if let Err(e) = self.write_raw(&reply) {
tracing::warn!("embed color-query reply failed: {}", e);
}
}
self.parser.process(bytes);
}
fn write_raw(&mut self, bytes: &[u8]) -> std::io::Result<()> {
if let Some(w) = self.writer.as_mut() {
w.write_all(bytes)?;
w.flush()?;
}
Ok(())
}
pub fn resize(&mut self, rows: u16, cols: u16) {
let rows = rows.max(MIN_ROWS);
let cols = cols.max(MIN_COLS);
if rows == self.rows && cols == self.cols {
return;
}
self.rows = rows;
self.cols = cols;
self.parser.screen_mut().set_size(rows, cols);
let _ = self.master.resize(PtySize {
rows,
cols,
pixel_width: 0,
pixel_height: 0,
});
}
pub fn render(&self, buf: &mut Buffer, area: Rect) {
let widget = PseudoTerminal::new(self.parser.screen());
widget.render(area, buf);
}
}
impl Drop for EmbedTerminal {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
let _ = self.child.kill();
}
}
fn io_err<E: std::fmt::Display>(what: &'static str) -> impl FnOnce(E) -> std::io::Error {
move |e| std::io::Error::other(format!("{what}: {e}"))
}
fn reset_window_size(socket: Option<&str>, session: &str) {
let mut cmd = std::process::Command::new("tmux");
if let Some(s) = socket {
cmd.arg("-L").arg(s);
}
cmd.args(["set-option", "-t", session, "window-size", "latest"]);
if let Err(e) = cmd.status() {
tracing::debug!("tmux set window-size latest on {}: {}", session, e);
}
}