1use serde::{Deserialize, Serialize};
11use strop_core::frontend_input::{Input, Key};
12use strop_core::id::{BufferRevision, DocumentId};
13
14pub const PROTOCOL_VERSION: u32 = 1;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub struct BaseStamp {
21 pub incarnation: u64,
22 pub generation: u64,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct ClientInfo {
28 pub name: String,
29 pub version: String,
30}
31
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub struct ClientCapabilities {
36 pub clipboard_write: bool,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct BackendInfo {
44 pub name: String,
45 pub version: String,
46 pub build: Option<String>,
47 pub incarnation: u64,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53pub struct ActionLimits {
54 pub max_frame_bytes: usize,
55 pub max_pending_requests: usize,
56 pub max_viewport_cells: u32,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62pub struct ServerCapabilities {
63 pub terminals: bool,
64 pub workspace_search: bool,
65 pub filesystem: bool,
66 pub clipboard_write: bool,
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73#[serde(tag = "action", content = "data", rename_all = "snake_case")]
74pub enum AdmittedAction {
75 Input(Input),
77 EditorKey(Key),
79 Paste(String),
81 Resize { columns: u16, rows: u16 },
83 QuitIntent,
85 Focus(bool),
87}
88
89#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92pub struct Geometry {
93 pub columns: u16,
94 pub rows: u16,
95}
96
97#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99pub struct Rect {
100 pub x: u16,
101 pub y: u16,
102 pub width: u16,
103 pub height: u16,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum ViewBounds {
111 Complete,
112 Partial,
113 Loading,
114 Stale,
115 Error,
116}
117
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct PaneSnapshot {
122 pub document: DocumentId,
123 pub revision: BufferRevision,
124 pub bounds: ViewBounds,
125 pub cursor: usize,
126 pub view_top: usize,
127 pub hscroll: usize,
128 pub terminal_input: bool,
129 pub overlays: bool,
130 pub rect: Rect,
131 pub budget: Rect,
132 pub window_top: usize,
135 pub lines: Vec<String>,
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct ViewSnapshot {
142 pub generation: u64,
143 pub geometry: Geometry,
144 pub active_pane: usize,
145 pub panes: Vec<PaneSnapshot>,
146 pub state: serde_json::Value,
147}
148
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153pub struct ViewDelta {
154 pub base: u64,
155 pub generation: u64,
156 pub geometry: Option<Geometry>,
157 pub active_pane: Option<usize>,
158 pub panes: Vec<PaneDelta>,
159 pub state: Option<serde_json::Value>,
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165pub enum PaneDelta {
166 Unchanged,
167 Changed(PaneSnapshot),
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172#[serde(tag = "outcome", rename_all = "snake_case")]
173pub enum AckOutcome {
174 Applied { applied: u64, generation: u64 },
177 Refused { refusal: Refusal },
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, thiserror::Error)]
183#[serde(tag = "reason", rename_all = "snake_case")]
184pub enum Refusal {
185 #[error("stale base generation; the current generation is {current}")]
188 StaleGeneration { current: u64 },
189 #[error("future base generation; the current generation is {current}")]
191 FutureGeneration { current: u64 },
192 #[error("wrong backend incarnation; the current incarnation is {current}")]
194 WrongIncarnation { current: u64 },
195 #[error("bound exceeded: {message}")]
197 Limit { message: String },
198 #[error("engine: {message}")]
200 Engine { message: String },
201 #[error("backend is closing")]
203 Closed,
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
210#[serde(tag = "kind", rename_all = "snake_case")]
211pub enum EffectRequest {
212 ClipboardWrite { text: String },
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(tag = "outcome", rename_all = "snake_case")]
218pub enum EffectOutcome {
219 Applied,
220 Refused { reason: String },
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub enum ShutdownReason {
227 Requested,
229 Quit,
231 Disconnect,
233 ProtocolViolation,
235}
236
237#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, thiserror::Error)]
239#[serde(tag = "kind", rename_all = "snake_case")]
240pub enum ProtocolError {
241 #[error("protocol version {offered} is not supported (supported: {supported})")]
243 Version { supported: u32, offered: u32 },
244 #[error("undecodable message: {message}")]
246 Decode { message: String },
247 #[error("frame violation: {message}")]
249 Frame { message: String },
250 #[error("unexpected message: {message}")]
253 Unexpected { message: String },
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258#[serde(tag = "type", rename_all = "snake_case")]
259pub enum ClientMessage {
260 Hello {
262 protocol: u32,
263 client: ClientInfo,
264 capabilities: ClientCapabilities,
265 },
266 Act {
270 seq: u64,
271 base: BaseStamp,
272 actions: Vec<AdmittedAction>,
273 },
274 Viewport { seq: u64, columns: u16, rows: u16 },
277 Resync { seq: u64 },
280 EffectResult { id: u64, outcome: EffectOutcome },
282 Shutdown { seq: u64 },
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
289#[serde(tag = "type", rename_all = "snake_case")]
290pub enum ServerMessage {
291 Welcome {
294 protocol: u32,
295 backend: BackendInfo,
296 limits: ActionLimits,
297 capabilities: ServerCapabilities,
298 },
299 Ack { seq: u64, outcome: AckOutcome },
301 Snapshot {
303 incarnation: u64,
304 view: ViewSnapshot,
305 },
306 Delta { incarnation: u64, delta: ViewDelta },
308 Effect { id: u64, effect: EffectRequest },
310 Error {
313 seq: Option<u64>,
314 error: ProtocolError,
315 },
316 Bye { reason: ShutdownReason },
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
327 fn envelopes_round_trip() {
328 let messages = [
329 ClientMessage::Hello {
330 protocol: PROTOCOL_VERSION,
331 client: ClientInfo {
332 name: "driver".into(),
333 version: "0.1".into(),
334 },
335 capabilities: ClientCapabilities {
336 clipboard_write: true,
337 },
338 },
339 ClientMessage::Act {
340 seq: 7,
341 base: BaseStamp {
342 incarnation: 42,
343 generation: 3,
344 },
345 actions: vec![
346 AdmittedAction::Input(Input::Text("héllo".into())),
347 AdmittedAction::EditorKey(Key::Enter),
348 AdmittedAction::Resize {
349 columns: 120,
350 rows: 40,
351 },
352 AdmittedAction::QuitIntent,
353 ],
354 },
355 ClientMessage::Resync { seq: 8 },
356 ClientMessage::EffectResult {
357 id: 1,
358 outcome: EffectOutcome::Refused {
359 reason: "no clipboard".into(),
360 },
361 },
362 ClientMessage::Shutdown { seq: 9 },
363 ];
364 for message in messages {
365 let bytes = serde_json::to_vec(&message).unwrap();
366 assert_eq!(
367 serde_json::from_slice::<ClientMessage>(&bytes).unwrap(),
368 message
369 );
370 }
371 }
372
373 #[test]
374 fn server_envelopes_round_trip() {
375 let messages = [
376 ServerMessage::Ack {
377 seq: 1,
378 outcome: AckOutcome::Applied {
379 applied: 5,
380 generation: 9,
381 },
382 },
383 ServerMessage::Ack {
384 seq: 2,
385 outcome: AckOutcome::Refused {
386 refusal: Refusal::StaleGeneration { current: 11 },
387 },
388 },
389 ServerMessage::Effect {
390 id: 0,
391 effect: EffectRequest::ClipboardWrite {
392 text: "payload".into(),
393 },
394 },
395 ServerMessage::Error {
396 seq: None,
397 error: ProtocolError::Version {
398 supported: 1,
399 offered: 99,
400 },
401 },
402 ServerMessage::Bye {
403 reason: ShutdownReason::ProtocolViolation,
404 },
405 ];
406 for message in messages {
407 let bytes = serde_json::to_vec(&message).unwrap();
408 assert_eq!(
409 serde_json::from_slice::<ServerMessage>(&bytes).unwrap(),
410 message
411 );
412 }
413 }
414
415 #[test]
417 fn unknown_envelopes_fail_decoding() {
418 assert!(serde_json::from_slice::<ClientMessage>(br#"{"type":"teleport"}"#).is_err());
419 assert!(serde_json::from_slice::<ServerMessage>(br#"{"type":"teleport"}"#).is_err());
420 }
421}