use std::io::{self, Read, Write};
use serde::{Deserialize, Serialize};
use crate::{
ControlError, Direction, LayoutKind, PaneId, PaneSnapshot, SessionId, TearPane, TearSession,
TearWindow, WindowId,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Request {
ListSessions,
GetSession(SessionId),
GetWindow(WindowId),
GetPane(PaneId),
NewSession {
name: String,
shell: String,
#[serde(default)]
source: Option<crate::session::SessionSource>,
#[serde(default)]
size_cells: Option<(u16, u16)>,
},
RenameSession {
id: SessionId,
new_name: String,
},
KillSession(SessionId),
NewWindow {
session: SessionId,
name: String,
shell: String,
},
KillWindow(WindowId),
SelectWindow(WindowId),
SplitPane {
origin: PaneId,
direction: Direction,
shell: String,
},
KillPane(PaneId),
SelectPane(PaneId),
ResizePane {
id: PaneId,
direction: Direction,
delta_cells: i16,
},
ApplyLayout {
window: WindowId,
kind: LayoutKind,
},
SendKeys {
id: PaneId,
bytes: Vec<u8>,
},
PaneSnapshot(PaneId),
Subscribe(PaneId),
PaneResizeAbsolute {
id: PaneId,
cols: u16,
rows: u16,
},
GetConfig,
ReloadConfig,
SetConfig(String),
SetSpawnEnv(crate::SpawnEnv),
StartPaneRecording(PaneId),
StopPaneRecording(PaneId),
ExportPaneRecording(PaneId),
PaneRecordingStatus(PaneId),
PaneBlocksList {
pane: PaneId,
since_index: u64,
limit: u32,
},
PaneBlockAt {
pane: PaneId,
index: u64,
},
PaneBlocksStatus(PaneId),
PaneSubscriberCount(PaneId),
SetInputPolicy {
id: PaneId,
policy: crate::pane::InputPolicy,
},
SubscribeConfigChange,
Authenticate(String),
IdentifyClient(u64),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Response {
Sessions(Vec<TearSession>),
Session(TearSession),
Window {
session: SessionId,
window: TearWindow,
},
Pane(TearPane),
SessionId(SessionId),
WindowId(WindowId),
PaneId(PaneId),
PaneSnapshot(PaneSnapshot),
PaneBytes(Vec<u8>),
PaneClosed(PaneId),
ConfigYaml(String),
CastJson(String),
RecordingStatus {
enabled: bool,
events: u32,
},
Blocks(Vec<crate::block::Block>),
Block(crate::block::Block),
BlocksStatus {
total: u32,
in_progress: bool,
},
SubscriberCount(u32),
ConfigChanged(String),
Ok,
Err(WireError),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum WireError {
NoSuchSession(SessionId),
NoSuchWindow(WindowId),
NoSuchPane(PaneId),
Transport(String),
Rejected(String),
Internal(String),
}
impl From<ControlError> for WireError {
fn from(e: ControlError) -> Self {
match e {
ControlError::NoSuchSession(id) => WireError::NoSuchSession(id),
ControlError::NoSuchWindow(id) => WireError::NoSuchWindow(id),
ControlError::NoSuchPane(id) => WireError::NoSuchPane(id),
ControlError::Transport(s) => WireError::Transport(s),
ControlError::Rejected(s) => WireError::Rejected(s),
ControlError::Internal(e) => WireError::Internal(e.to_string()),
}
}
}
impl From<WireError> for ControlError {
fn from(e: WireError) -> Self {
match e {
WireError::NoSuchSession(id) => ControlError::NoSuchSession(id),
WireError::NoSuchWindow(id) => ControlError::NoSuchWindow(id),
WireError::NoSuchPane(id) => ControlError::NoSuchPane(id),
WireError::Transport(s) => ControlError::Transport(s),
WireError::Rejected(s) => ControlError::Rejected(s),
WireError::Internal(s) => ControlError::Internal(anyhow::anyhow!(s)),
}
}
}
pub const MAX_FRAME_BYTES: usize = 16 * 1024 * 1024;
#[must_use]
pub fn default_socket_path() -> std::path::PathBuf {
if let Some(dir) = std::env::var_os("XDG_RUNTIME_DIR") {
let mut p = std::path::PathBuf::from(dir);
p.push("tear.sock");
return p;
}
if let Some(home) = std::env::var_os("HOME") {
let mut p = std::path::PathBuf::from(home);
p.push(".local");
p.push("share");
p.push("tear");
p.push("tear.sock");
return p;
}
std::path::PathBuf::from("/tmp/tear.sock")
}
pub fn write_msg<W: Write, T: Serialize>(w: &mut W, msg: &T) -> io::Result<()> {
let mut bytes = Vec::new();
ciborium::ser::into_writer(msg, &mut bytes)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
let len = u32::try_from(bytes.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "frame too large"))?;
w.write_all(&len.to_be_bytes())?;
w.write_all(&bytes)?;
w.flush()?;
Ok(())
}
pub fn read_msg<R: Read, T: for<'de> Deserialize<'de>>(r: &mut R) -> io::Result<T> {
let mut len_buf = [0u8; 4];
r.read_exact(&mut len_buf)?;
let len = u32::from_be_bytes(len_buf) as usize;
if len > MAX_FRAME_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("frame size {len} exceeds MAX_FRAME_BYTES {MAX_FRAME_BYTES}"),
));
}
let mut buf = vec![0u8; len];
r.read_exact(&mut buf)?;
ciborium::de::from_reader(&buf[..])
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn roundtrip_list_sessions_request() {
let mut buf = Vec::new();
write_msg(&mut buf, &Request::ListSessions).unwrap();
let mut cur = Cursor::new(&buf);
let got: Request = read_msg(&mut cur).unwrap();
assert!(matches!(got, Request::ListSessions));
}
#[test]
fn roundtrip_send_keys_request() {
let pane = PaneId::from_seed("pane");
let req = Request::SendKeys {
id: pane,
bytes: vec![1, 2, 3, 4],
};
let mut buf = Vec::new();
write_msg(&mut buf, &req).unwrap();
let mut cur = Cursor::new(&buf);
let got: Request = read_msg(&mut cur).unwrap();
match got {
Request::SendKeys { id, bytes } => {
assert_eq!(id, pane);
assert_eq!(bytes, vec![1, 2, 3, 4]);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn roundtrip_apply_layout_request() {
let window = WindowId::from_seed("win");
let req = Request::ApplyLayout {
window,
kind: LayoutKind::MainVertical,
};
let mut buf = Vec::new();
write_msg(&mut buf, &req).unwrap();
let mut cur = Cursor::new(&buf);
let got: Request = read_msg(&mut cur).unwrap();
match got {
Request::ApplyLayout { window: w, kind } => {
assert_eq!(w, window);
assert_eq!(kind, LayoutKind::MainVertical);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn wire_error_roundtrip_through_control_error() {
let pane = PaneId::from_seed("pane");
let ce = ControlError::NoSuchPane(pane);
let we: WireError = ce.into();
let ce2: ControlError = we.into();
assert!(matches!(ce2, ControlError::NoSuchPane(p) if p == pane));
}
#[test]
fn frame_size_cap_enforced() {
let len: u32 = 32 * 1024 * 1024;
let mut buf = Vec::new();
buf.extend_from_slice(&len.to_be_bytes());
let mut cur = Cursor::new(&buf);
let err = read_msg::<_, Request>(&mut cur).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn default_socket_path_resolves() {
let p = default_socket_path();
assert!(p.to_string_lossy().ends_with("tear.sock"));
}
#[test]
fn roundtrip_pane_resize_absolute_request() {
let pane = PaneId::from_seed("resize-pane");
let req = Request::PaneResizeAbsolute {
id: pane,
cols: 132,
rows: 50,
};
let mut buf = Vec::new();
write_msg(&mut buf, &req).unwrap();
let mut cur = Cursor::new(buf);
let got: Request = read_msg(&mut cur).unwrap();
match got {
Request::PaneResizeAbsolute { id, cols, rows } => {
assert_eq!(id, pane);
assert_eq!(cols, 132);
assert_eq!(rows, 50);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn roundtrip_set_spawn_env_request() {
let env = crate::SpawnEnv::from_overrides(vec![
("TERM".to_owned(), "xterm-ghostty".to_owned()),
("COLORTERM".to_owned(), "truecolor".to_owned()),
])
.with_cwd(Some("/work/dir".to_owned()));
let req = Request::SetSpawnEnv(env.clone());
let mut buf = Vec::new();
write_msg(&mut buf, &req).unwrap();
let mut cur = Cursor::new(buf);
let got: Request = read_msg(&mut cur).unwrap();
match got {
Request::SetSpawnEnv(decoded) => assert_eq!(decoded, env),
_ => panic!("wrong variant"),
}
}
#[test]
fn roundtrip_subscribe_request() {
let pane = PaneId::from_seed("sub-pane");
let req = Request::Subscribe(pane);
let mut buf = Vec::new();
write_msg(&mut buf, &req).unwrap();
let mut cur = Cursor::new(buf);
let got: Request = read_msg(&mut cur).unwrap();
assert!(matches!(got, Request::Subscribe(p) if p == pane));
}
#[test]
fn roundtrip_pane_bytes_response() {
let resp = Response::PaneBytes(b"hello\xff\x00 mixed bytes".to_vec());
let mut buf = Vec::new();
write_msg(&mut buf, &resp).unwrap();
let mut cur = Cursor::new(buf);
let got: Response = read_msg(&mut cur).unwrap();
match got {
Response::PaneBytes(b) => {
assert_eq!(b, b"hello\xff\x00 mixed bytes");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn roundtrip_pane_closed_response() {
let pane = PaneId::from_seed("closed-pane");
let resp = Response::PaneClosed(pane);
let mut buf = Vec::new();
write_msg(&mut buf, &resp).unwrap();
let mut cur = Cursor::new(buf);
let got: Response = read_msg(&mut cur).unwrap();
assert!(matches!(got, Response::PaneClosed(p) if p == pane));
}
#[test]
fn every_wire_error_variant_roundtrips() {
let sid = SessionId::from_seed("s");
let wid = WindowId::from_seed("w");
let pid = PaneId::from_seed("p");
let cases: Vec<ControlError> = vec![
ControlError::NoSuchSession(sid),
ControlError::NoSuchWindow(wid),
ControlError::NoSuchPane(pid),
ControlError::Transport("bad pipe".into()),
ControlError::Rejected("not allowed".into()),
ControlError::Internal(anyhow::anyhow!("boom")),
];
for orig in cases {
let we: WireError = (orig).into();
let ce2: ControlError = we.into();
assert_eq!(
std::mem::discriminant(&ce2_to_kind_marker(&ce2)),
std::mem::discriminant(&ce2_to_kind_marker(&ce2)),
"discriminant preserved"
);
}
}
enum Kind {
S,
W,
P,
T,
R,
I,
}
fn ce2_to_kind_marker(e: &ControlError) -> Kind {
match e {
ControlError::NoSuchSession(_) => Kind::S,
ControlError::NoSuchWindow(_) => Kind::W,
ControlError::NoSuchPane(_) => Kind::P,
ControlError::Transport(_) => Kind::T,
ControlError::Rejected(_) => Kind::R,
ControlError::Internal(_) => Kind::I,
}
}
#[test]
fn truncated_frame_errors_cleanly() {
let mut buf = Vec::new();
let len: u32 = 100;
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(&[1, 2, 3, 4]);
let mut cur = Cursor::new(buf);
let err = read_msg::<_, Request>(&mut cur).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
}
}