1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10pub const PROTOCOL_VERSION: u32 = 1;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub struct TermSize {
16 pub cols: u16,
17 pub rows: u16,
18}
19
20impl TermSize {
21 pub fn new(cols: u16, rows: u16) -> Self {
22 Self { cols, rows }
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ClientHello {
29 pub protocol_version: u32,
31 pub client_version: String,
33 pub term_size: TermSize,
35 pub env: HashMap<String, Option<String>>,
38}
39
40impl ClientHello {
41 pub fn new(term_size: TermSize) -> Self {
43 let mut env = HashMap::new();
44
45 for key in &["TERM", "COLORTERM", "LANG", "LC_ALL"] {
47 env.insert(key.to_string(), std::env::var(key).ok());
48 }
49
50 Self {
51 protocol_version: PROTOCOL_VERSION,
52 client_version: env!("CARGO_PKG_VERSION").to_string(),
53 term_size,
54 env,
55 }
56 }
57
58 pub fn term(&self) -> Option<&str> {
60 self.env.get("TERM").and_then(|v| v.as_deref())
61 }
62
63 pub fn supports_truecolor(&self) -> bool {
65 self.env
66 .get("COLORTERM")
67 .and_then(|v| v.as_deref())
68 .map(|v| v == "truecolor" || v == "24bit")
69 .unwrap_or(false)
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ServerHello {
76 pub protocol_version: u32,
78 pub server_version: String,
80 pub session_id: String,
82}
83
84impl ServerHello {
85 pub fn new(session_id: String) -> Self {
86 Self {
87 protocol_version: PROTOCOL_VERSION,
88 server_version: env!("CARGO_PKG_VERSION").to_string(),
89 session_id,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct VersionMismatch {
97 pub server_version: String,
98 pub client_version: String,
99 pub action: String,
101 pub message: String,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(tag = "type", rename_all = "snake_case")]
107pub enum ClientControl {
108 Hello(ClientHello),
110 Resize { cols: u16, rows: u16 },
112 Ping,
114 Detach,
116 Quit,
118 OpenFiles {
120 files: Vec<FileRequest>,
121 #[serde(default)]
122 wait: bool,
123 },
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct FileRequest {
129 pub path: String,
130 pub line: Option<usize>,
131 pub column: Option<usize>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub end_line: Option<usize>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub end_column: Option<usize>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub message: Option<String>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(tag = "type", rename_all = "snake_case")]
143pub enum ServerControl {
144 Hello(ServerHello),
146 VersionMismatch(VersionMismatch),
148 Pong,
150 SetTitle { title: String },
152 Bell,
154 Quit { reason: String },
156 Error { message: String },
158 WaitComplete,
160 SetClipboard {
163 text: String,
164 use_osc52: bool,
166 use_system_clipboard: bool,
168 },
169 SuspendClient,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(untagged)]
181pub enum ControlMessage {
182 Client(ClientControl),
183 Server(ServerControl),
184}
185
186pub fn read_control_message<R: std::io::BufRead>(reader: &mut R) -> std::io::Result<String> {
188 let mut line = String::new();
189 reader.read_line(&mut line)?;
190 Ok(line)
191}
192
193pub fn write_control_message<W: std::io::Write>(
195 writer: &mut W,
196 msg: &impl Serialize,
197) -> std::io::Result<()> {
198 let json = serde_json::to_string(msg).map_err(|e| std::io::Error::other(e.to_string()))?;
199 writeln!(writer, "{}", json)?;
200 writer.flush()
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn test_client_hello_captures_protocol_version() {
209 let hello = ClientHello::new(TermSize::new(80, 24));
210 assert_eq!(hello.protocol_version, PROTOCOL_VERSION);
211 }
212
213 #[test]
214 fn test_client_hello_roundtrip() {
215 let hello = ClientHello::new(TermSize::new(120, 40));
216 let json = serde_json::to_string(&hello).unwrap();
217 let parsed: ClientHello = serde_json::from_str(&json).unwrap();
218 assert_eq!(parsed.term_size.cols, 120);
219 assert_eq!(parsed.term_size.rows, 40);
220 }
221
222 #[test]
223 fn test_control_messages_use_snake_case_tags() {
224 let resize = ClientControl::Resize {
225 cols: 100,
226 rows: 50,
227 };
228 let json = serde_json::to_string(&resize).unwrap();
229 assert!(json.contains("\"type\":\"resize\""));
231 }
232
233 #[test]
234 fn test_server_hello_includes_session_id() {
235 let hello = ServerHello::new("my-session".to_string());
236 assert_eq!(hello.session_id, "my-session");
237 assert_eq!(hello.protocol_version, PROTOCOL_VERSION);
238 }
239
240 #[test]
241 fn test_version_mismatch_roundtrip() {
242 let mismatch = VersionMismatch {
243 server_version: "1.0.0".to_string(),
244 client_version: "2.0.0".to_string(),
245 action: "upgrade_server".to_string(),
246 message: "Version mismatch".to_string(),
247 };
248 let msg = ServerControl::VersionMismatch(mismatch);
249 let json = serde_json::to_string(&msg).unwrap();
250 let parsed: ServerControl = serde_json::from_str(&json).unwrap();
251
252 match parsed {
253 ServerControl::VersionMismatch(m) => {
254 assert_eq!(m.server_version, "1.0.0");
255 assert_eq!(m.client_version, "2.0.0");
256 }
257 _ => panic!("Expected VersionMismatch"),
258 }
259 }
260
261 #[test]
262 fn test_truecolor_detection() {
263 let mut hello = ClientHello::new(TermSize::new(80, 24));
264
265 hello.env.remove("COLORTERM");
267 assert!(!hello.supports_truecolor());
268
269 hello
271 .env
272 .insert("COLORTERM".to_string(), Some("truecolor".to_string()));
273 assert!(hello.supports_truecolor());
274
275 hello
277 .env
278 .insert("COLORTERM".to_string(), Some("24bit".to_string()));
279 assert!(hello.supports_truecolor());
280 }
281
282 #[test]
283 fn test_all_client_control_variants_serialize() {
284 let variants: Vec<ClientControl> = vec![
285 ClientControl::Hello(ClientHello::new(TermSize::new(80, 24))),
286 ClientControl::Resize {
287 cols: 100,
288 rows: 50,
289 },
290 ClientControl::Ping,
291 ClientControl::Detach,
292 ClientControl::Quit,
293 ClientControl::OpenFiles {
294 files: vec![FileRequest {
295 path: "/test/file.txt".to_string(),
296 line: Some(10),
297 column: Some(5),
298 end_line: None,
299 end_column: None,
300 message: None,
301 }],
302 wait: false,
303 },
304 ];
305
306 for variant in variants {
307 let json = serde_json::to_string(&variant).unwrap();
308 let _: ClientControl = serde_json::from_str(&json).unwrap();
309 }
310 }
311
312 #[test]
313 fn test_all_server_control_variants_serialize() {
314 let variants: Vec<ServerControl> = vec![
315 ServerControl::Hello(ServerHello::new("test".to_string())),
316 ServerControl::Pong,
317 ServerControl::SetTitle {
318 title: "Test".to_string(),
319 },
320 ServerControl::Bell,
321 ServerControl::Quit {
322 reason: "test".to_string(),
323 },
324 ServerControl::Error {
325 message: "error".to_string(),
326 },
327 ServerControl::WaitComplete,
328 ServerControl::SetClipboard {
329 text: "hello".to_string(),
330 use_osc52: true,
331 use_system_clipboard: true,
332 },
333 ServerControl::SuspendClient,
334 ];
335
336 for variant in variants {
337 let json = serde_json::to_string(&variant).unwrap();
338 let _: ServerControl = serde_json::from_str(&json).unwrap();
339 }
340 }
341}