use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", content = "data")]
pub enum UxEvent {
#[serde(rename = "ux.terminal.write")]
TerminalWrite(TerminalWrite),
#[serde(rename = "ux.terminal.resize")]
TerminalResize(TerminalResize),
#[serde(rename = "ux.terminal.color_mode")]
TerminalColorMode(TerminalColorMode),
#[serde(rename = "ux.tui.frame")]
TuiFrame(TuiFrame),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminalWrite {
pub bytes: String,
pub stdout: bool,
pub offset_ms: u64,
}
impl TerminalWrite {
pub fn new(raw_bytes: &[u8], stdout: bool, offset_ms: u64) -> Self {
use base64::Engine;
Self {
bytes: base64::engine::general_purpose::STANDARD.encode(raw_bytes),
stdout,
offset_ms,
}
}
pub fn decode_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
use base64::Engine;
base64::engine::general_purpose::STANDARD.decode(&self.bytes)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminalResize {
pub width: u16,
pub height: u16,
pub offset_ms: u64,
}
impl TerminalResize {
pub fn new(width: u16, height: u16, offset_ms: u64) -> Self {
Self {
width,
height,
offset_ms,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerminalColorMode {
pub mode: String,
pub detected: String,
pub offset_ms: u64,
}
impl TerminalColorMode {
pub fn new(mode: impl Into<String>, detected: impl Into<String>, offset_ms: u64) -> Self {
Self {
mode: mode.into(),
detected: detected.into(),
offset_ms,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TuiFrame {
pub frame_id: u64,
pub width: u16,
pub height: u16,
pub cells: String,
pub offset_ms: u64,
}
impl TuiFrame {
pub fn new(frame_id: u64, width: u16, height: u16, cells: String, offset_ms: u64) -> Self {
Self {
frame_id,
width,
height,
cells,
offset_ms,
}
}
}
pub trait FrameCapture: Send + Sync {
fn take_captures(&mut self) -> Vec<UxEvent>;
fn has_captures(&self) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_terminal_write_roundtrip() {
let original = b"Hello, \x1b[32mWorld\x1b[0m!";
let write = TerminalWrite::new(original, true, 100);
assert!(write.stdout);
assert_eq!(write.offset_ms, 100);
let decoded = write.decode_bytes().unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_ux_event_serialization() {
let event = UxEvent::TerminalWrite(TerminalWrite::new(b"test", true, 0));
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("ux.terminal.write"));
let parsed: UxEvent = serde_json::from_str(&json).unwrap();
if let UxEvent::TerminalWrite(write) = parsed {
assert!(write.stdout);
} else {
panic!("Expected TerminalWrite variant");
}
}
#[test]
fn test_terminal_resize_serialization() {
let event = UxEvent::TerminalResize(TerminalResize::new(120, 30, 500));
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("ux.terminal.resize"));
assert!(json.contains("120"));
assert!(json.contains("30"));
}
}