1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "event", content = "data")]
12pub enum UxEvent {
13 #[serde(rename = "ux.terminal.write")]
15 TerminalWrite(TerminalWrite),
16
17 #[serde(rename = "ux.terminal.resize")]
19 TerminalResize(TerminalResize),
20
21 #[serde(rename = "ux.terminal.color_mode")]
23 TerminalColorMode(TerminalColorMode),
24
25 #[serde(rename = "ux.tui.frame")]
27 TuiFrame(TuiFrame),
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TerminalWrite {
36 pub bytes: String,
38
39 pub stdout: bool,
41
42 pub offset_ms: u64,
44}
45
46impl TerminalWrite {
47 pub fn new(raw_bytes: &[u8], stdout: bool, offset_ms: u64) -> Self {
49 use base64::Engine;
50 Self {
51 bytes: base64::engine::general_purpose::STANDARD.encode(raw_bytes),
52 stdout,
53 offset_ms,
54 }
55 }
56
57 pub fn decode_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
63 use base64::Engine;
64 base64::engine::general_purpose::STANDARD.decode(&self.bytes)
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TerminalResize {
71 pub width: u16,
73
74 pub height: u16,
76
77 pub offset_ms: u64,
79}
80
81impl TerminalResize {
82 pub fn new(width: u16, height: u16, offset_ms: u64) -> Self {
84 Self {
85 width,
86 height,
87 offset_ms,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct TerminalColorMode {
95 pub mode: String,
97
98 pub detected: String,
100
101 pub offset_ms: u64,
103}
104
105impl TerminalColorMode {
106 pub fn new(mode: impl Into<String>, detected: impl Into<String>, offset_ms: u64) -> Self {
108 Self {
109 mode: mode.into(),
110 detected: detected.into(),
111 offset_ms,
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct TuiFrame {
122 pub frame_id: u64,
124
125 pub width: u16,
127
128 pub height: u16,
130
131 pub cells: String,
133
134 pub offset_ms: u64,
136}
137
138impl TuiFrame {
139 pub fn new(frame_id: u64, width: u16, height: u16, cells: String, offset_ms: u64) -> Self {
141 Self {
142 frame_id,
143 width,
144 height,
145 cells,
146 offset_ms,
147 }
148 }
149}
150
151pub trait FrameCapture: Send + Sync {
157 fn take_captures(&mut self) -> Vec<UxEvent>;
159
160 fn has_captures(&self) -> bool;
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_terminal_write_roundtrip() {
170 let original = b"Hello, \x1b[32mWorld\x1b[0m!";
171 let write = TerminalWrite::new(original, true, 100);
172
173 assert!(write.stdout);
174 assert_eq!(write.offset_ms, 100);
175
176 let decoded = write.decode_bytes().unwrap();
177 assert_eq!(decoded, original);
178 }
179
180 #[test]
181 fn test_ux_event_serialization() {
182 let event = UxEvent::TerminalWrite(TerminalWrite::new(b"test", true, 0));
183 let json = serde_json::to_string(&event).unwrap();
184
185 assert!(json.contains("ux.terminal.write"));
186
187 let parsed: UxEvent = serde_json::from_str(&json).unwrap();
188 if let UxEvent::TerminalWrite(write) = parsed {
189 assert!(write.stdout);
190 } else {
191 panic!("Expected TerminalWrite variant");
192 }
193 }
194
195 #[test]
196 fn test_terminal_resize_serialization() {
197 let event = UxEvent::TerminalResize(TerminalResize::new(120, 30, 500));
198 let json = serde_json::to_string(&event).unwrap();
199
200 assert!(json.contains("ux.terminal.resize"));
201 assert!(json.contains("120"));
202 assert!(json.contains("30"));
203 }
204}