use std::io::{BufReader, Read, Write};
use std::path::Path;
use std::thread;
use anyhow::{anyhow, Result};
use ratatui::crossterm::event::{
read as read_event, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste,
EnableMouseCapture, Event,
};
use ratatui::crossterm::execute;
use ratatui::DefaultTerminal;
use crate::ipc::protocol::{self, ClientMessage, FrameData, ServerMessage};
use crate::ipc::transport;
pub fn run(sock: &Path) -> Result<()> {
let stream = transport::connect(sock).map_err(|_| anyhow!("cannot connect to bohay server"))?;
attach(stream.clone(), stream)
}
pub fn attach<R, W>(reader: R, writer: W) -> Result<()>
where
R: Read,
W: Write + Send + 'static,
{
let mut terminal = ratatui::init();
let _ = execute!(std::io::stdout(), EnableBracketedPaste, EnableMouseCapture);
crate::install_tui_panic_hook();
let result = run_inner(reader, writer, &mut terminal);
let _ = execute!(
std::io::stdout(),
DisableMouseCapture,
DisableBracketedPaste
);
ratatui::restore();
result
}
fn run_inner<R, W>(reader: R, mut writer: W, terminal: &mut DefaultTerminal) -> Result<()>
where
R: Read,
W: Write + Send + 'static,
{
let truecolor = protocol::truecolor_supported();
let size = terminal.size()?;
protocol::write_message(
&mut writer,
&ClientMessage::Hello {
version: protocol::PROTOCOL_VERSION,
cols: size.width,
rows: size.height,
},
)?;
let mut reader = BufReader::new(reader);
match protocol::read_message::<_, ServerMessage>(&mut reader)? {
ServerMessage::Welcome { error: Some(e), .. } => return Err(anyhow!("server: {e}")),
ServerMessage::Welcome { .. } => {}
_ => return Err(anyhow!("unexpected handshake")),
}
thread::spawn(move || input_loop(writer));
let mut current: Option<FrameData> = None;
loop {
match protocol::read_message::<_, ServerMessage>(&mut reader) {
Ok(ServerMessage::Frame(frame)) => {
blit(terminal, &frame, truecolor)?;
current = Some(frame);
}
Ok(ServerMessage::FrameDiff(diff)) => {
if let Some(cur) = current.as_mut() {
protocol::apply_diff(cur, &diff);
blit(terminal, cur, truecolor)?;
}
}
Ok(ServerMessage::Notify(msg)) => crate::emit_notification(&msg),
Ok(ServerMessage::Detach) | Ok(ServerMessage::ServerShutdown { .. }) => break,
Ok(_) => {}
Err(_) => break, }
}
Ok(())
}
fn input_loop<W: Write>(mut writer: W) {
loop {
let msg = match read_event() {
Ok(Event::Key(k)) => ClientMessage::Key(k),
Ok(Event::Mouse(m)) => ClientMessage::Mouse(m),
Ok(Event::Resize(w, h)) => ClientMessage::Resize { cols: w, rows: h },
Ok(Event::Paste(s)) => ClientMessage::Paste(s),
Ok(_) => continue,
Err(_) => break,
};
if protocol::write_message(&mut writer, &msg).is_err() {
break;
}
}
}
pub fn remote_bridge(sock: &Path) -> Result<()> {
let conn = transport::connect(sock).map_err(|_| anyhow!("cannot connect to bohay server"))?;
relay(conn.clone(), conn, std::io::stdin(), std::io::stdout())
}
pub fn relay<LR, LW, I, O>(
local_reader: LR,
local_writer: LW,
input: I,
mut output: O,
) -> Result<()>
where
LR: Read,
LW: Write + Send + 'static,
I: Read + Send + 'static,
O: Write,
{
let mut local_writer = local_writer;
let mut input = input;
thread::spawn(move || {
let _ = std::io::copy(&mut input, &mut local_writer);
});
let mut local_reader = local_reader;
std::io::copy(&mut local_reader, &mut output)?;
Ok(())
}
fn blit(terminal: &mut DefaultTerminal, frame: &FrameData, truecolor: bool) -> Result<()> {
let adjust = |c| if truecolor { c } else { protocol::to_256(c) };
terminal.draw(|f| {
let area = f.area();
let buf = f.buffer_mut();
for (i, cell) in frame.cells.iter().enumerate() {
let x = (i as u16) % frame.width;
let y = (i as u16) / frame.width;
if x < area.width && y < area.height {
let target = &mut buf[(x, y)];
let sym = if cell.symbol.is_empty() || cell.symbol.chars().any(|c| c.is_control()) {
" "
} else {
&cell.symbol
};
target.set_symbol(sym);
target.set_fg(adjust(protocol::unpack(cell.fg)));
target.set_bg(adjust(protocol::unpack(cell.bg)));
target.modifier = protocol::unpack_mods(cell.mods);
}
}
if let Some((cx, cy)) = frame.cursor {
if cx < area.width && cy < area.height {
f.set_cursor_position((cx, cy));
}
}
})?;
Ok(())
}
#[cfg(all(test, unix))]
mod tests {
use super::relay;
use std::io::{Cursor, Read, Write};
use std::os::unix::net::UnixStream;
use std::thread;
#[test]
fn relay_pumps_both_directions() {
let (client_side, mut server_side) = UnixStream::pair().unwrap();
let srv = thread::spawn(move || {
let mut got = [0u8; 5];
server_side.read_exact(&mut got).unwrap(); server_side.write_all(b"world").unwrap(); got });
let reader = client_side.try_clone().unwrap();
let mut output: Vec<u8> = Vec::new();
relay(
reader,
client_side,
Cursor::new(b"hello".to_vec()),
&mut output,
)
.unwrap();
assert_eq!(&srv.join().unwrap(), b"hello", "input forwarded to server");
assert_eq!(output, b"world", "server reply forwarded to output");
}
}