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}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(untagged)]
174pub enum ControlMessage {
175 Client(ClientControl),
176 Server(ServerControl),
177}
178
179pub fn read_control_message<R: std::io::BufRead>(reader: &mut R) -> std::io::Result<String> {
181 let mut line = String::new();
182 reader.read_line(&mut line)?;
183 Ok(line)
184}
185
186pub fn write_control_message<W: std::io::Write>(
188 writer: &mut W,
189 msg: &impl Serialize,
190) -> std::io::Result<()> {
191 let json = serde_json::to_string(msg).map_err(|e| std::io::Error::other(e.to_string()))?;
192 writeln!(writer, "{}", json)?;
193 writer.flush()
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_client_hello_captures_protocol_version() {
202 let hello = ClientHello::new(TermSize::new(80, 24));
203 assert_eq!(hello.protocol_version, PROTOCOL_VERSION);
204 }
205
206 #[test]
207 fn test_client_hello_roundtrip() {
208 let hello = ClientHello::new(TermSize::new(120, 40));
209 let json = serde_json::to_string(&hello).unwrap();
210 let parsed: ClientHello = serde_json::from_str(&json).unwrap();
211 assert_eq!(parsed.term_size.cols, 120);
212 assert_eq!(parsed.term_size.rows, 40);
213 }
214
215 #[test]
216 fn test_control_messages_use_snake_case_tags() {
217 let resize = ClientControl::Resize {
218 cols: 100,
219 rows: 50,
220 };
221 let json = serde_json::to_string(&resize).unwrap();
222 assert!(json.contains("\"type\":\"resize\""));
224 }
225
226 #[test]
227 fn test_server_hello_includes_session_id() {
228 let hello = ServerHello::new("my-session".to_string());
229 assert_eq!(hello.session_id, "my-session");
230 assert_eq!(hello.protocol_version, PROTOCOL_VERSION);
231 }
232
233 #[test]
234 fn test_version_mismatch_roundtrip() {
235 let mismatch = VersionMismatch {
236 server_version: "1.0.0".to_string(),
237 client_version: "2.0.0".to_string(),
238 action: "upgrade_server".to_string(),
239 message: "Version mismatch".to_string(),
240 };
241 let msg = ServerControl::VersionMismatch(mismatch);
242 let json = serde_json::to_string(&msg).unwrap();
243 let parsed: ServerControl = serde_json::from_str(&json).unwrap();
244
245 match parsed {
246 ServerControl::VersionMismatch(m) => {
247 assert_eq!(m.server_version, "1.0.0");
248 assert_eq!(m.client_version, "2.0.0");
249 }
250 _ => panic!("Expected VersionMismatch"),
251 }
252 }
253
254 #[test]
255 fn test_truecolor_detection() {
256 let mut hello = ClientHello::new(TermSize::new(80, 24));
257
258 hello.env.remove("COLORTERM");
260 assert!(!hello.supports_truecolor());
261
262 hello
264 .env
265 .insert("COLORTERM".to_string(), Some("truecolor".to_string()));
266 assert!(hello.supports_truecolor());
267
268 hello
270 .env
271 .insert("COLORTERM".to_string(), Some("24bit".to_string()));
272 assert!(hello.supports_truecolor());
273 }
274
275 #[test]
276 fn test_all_client_control_variants_serialize() {
277 let variants: Vec<ClientControl> = vec![
278 ClientControl::Hello(ClientHello::new(TermSize::new(80, 24))),
279 ClientControl::Resize {
280 cols: 100,
281 rows: 50,
282 },
283 ClientControl::Ping,
284 ClientControl::Detach,
285 ClientControl::Quit,
286 ClientControl::OpenFiles {
287 files: vec![FileRequest {
288 path: "/test/file.txt".to_string(),
289 line: Some(10),
290 column: Some(5),
291 end_line: None,
292 end_column: None,
293 message: None,
294 }],
295 wait: false,
296 },
297 ];
298
299 for variant in variants {
300 let json = serde_json::to_string(&variant).unwrap();
301 let _: ClientControl = serde_json::from_str(&json).unwrap();
302 }
303 }
304
305 #[test]
306 fn test_all_server_control_variants_serialize() {
307 let variants: Vec<ServerControl> = vec![
308 ServerControl::Hello(ServerHello::new("test".to_string())),
309 ServerControl::Pong,
310 ServerControl::SetTitle {
311 title: "Test".to_string(),
312 },
313 ServerControl::Bell,
314 ServerControl::Quit {
315 reason: "test".to_string(),
316 },
317 ServerControl::Error {
318 message: "error".to_string(),
319 },
320 ServerControl::WaitComplete,
321 ServerControl::SetClipboard {
322 text: "hello".to_string(),
323 use_osc52: true,
324 use_system_clipboard: true,
325 },
326 ];
327
328 for variant in variants {
329 let json = serde_json::to_string(&variant).unwrap();
330 let _: ServerControl = serde_json::from_str(&json).unwrap();
331 }
332 }
333}