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