Skip to main content

fresh/server/
protocol.rs

1//! Protocol definitions for client-server communication
2//!
3//! The protocol uses two channels:
4//! - **Data channel**: Raw bytes, no framing (stdin→server, server→stdout)
5//! - **Control channel**: JSON messages for out-of-band communication
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Protocol version - must match between client and server
11pub const PROTOCOL_VERSION: u32 = 1;
12
13/// Terminal size in columns and rows
14#[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/// Client hello message sent during handshake
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ClientHello {
29    /// Protocol version
30    pub protocol_version: u32,
31    /// Client binary version (e.g., "0.15.0")
32    pub client_version: String,
33    /// Initial terminal size
34    pub term_size: TermSize,
35    /// Environment variables relevant for rendering
36    /// Keys: TERM, COLORTERM, LANG, LC_ALL
37    pub env: HashMap<String, Option<String>>,
38}
39
40impl ClientHello {
41    /// Create a new ClientHello with current environment
42    pub fn new(term_size: TermSize) -> Self {
43        let mut env = HashMap::new();
44
45        // Collect terminal-relevant environment variables
46        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    /// Get the TERM value
59    pub fn term(&self) -> Option<&str> {
60        self.env.get("TERM").and_then(|v| v.as_deref())
61    }
62
63    /// Check if truecolor is supported
64    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/// Server hello message sent in response to ClientHello
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ServerHello {
76    /// Protocol version
77    pub protocol_version: u32,
78    /// Server binary version
79    pub server_version: String,
80    /// Session identifier (encoded working directory)
81    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/// Version mismatch error response
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct VersionMismatch {
97    pub server_version: String,
98    pub client_version: String,
99    /// Suggested action: "restart_server", "upgrade_client"
100    pub action: String,
101    pub message: String,
102}
103
104/// Control messages from client to server
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(tag = "type", rename_all = "snake_case")]
107pub enum ClientControl {
108    /// Initial handshake
109    Hello(ClientHello),
110    /// Terminal was resized
111    Resize { cols: u16, rows: u16 },
112    /// Keepalive ping
113    Ping,
114    /// Request to detach (keep server running)
115    Detach,
116    /// Request to quit (shutdown server if last client)
117    Quit,
118    /// Request to open files in the editor
119    OpenFiles {
120        files: Vec<FileRequest>,
121        #[serde(default)]
122        wait: bool,
123    },
124}
125
126/// A file to open with optional line/column position, range, and hover message
127#[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/// Control messages from server to client
141#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(tag = "type", rename_all = "snake_case")]
143pub enum ServerControl {
144    /// Handshake response
145    Hello(ServerHello),
146    /// Version mismatch error
147    VersionMismatch(VersionMismatch),
148    /// Keepalive pong
149    Pong,
150    /// Set terminal title
151    SetTitle { title: String },
152    /// Ring the bell
153    Bell,
154    /// Server is shutting down
155    Quit { reason: String },
156    /// Error message
157    Error { message: String },
158    /// Signal that a --wait operation has completed
159    WaitComplete,
160}
161
162/// Wrapper for control channel messages (used for JSON serialization)
163#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(untagged)]
165pub enum ControlMessage {
166    Client(ClientControl),
167    Server(ServerControl),
168}
169
170/// Read a JSON control message from a reader
171pub 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
177/// Write a JSON control message to a writer
178pub 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        // serde(rename_all = "snake_case") should produce "resize"
214        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        // No COLORTERM
250        hello.env.remove("COLORTERM");
251        assert!(!hello.supports_truecolor());
252
253        // truecolor
254        hello
255            .env
256            .insert("COLORTERM".to_string(), Some("truecolor".to_string()));
257        assert!(hello.supports_truecolor());
258
259        // 24bit
260        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}