use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Information about a single window on the client machine. /// `label` is a human-readable, lowercase identifier (e.g. "google_chrome", "discord"). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WindowInfo { pub id: u64, pub title: String, pub label: String, pub visible: bool, } /// Validate a device/window label: lowercase, no whitespace, only a-z 0-9 - _ pub fn is_valid_label(s: &str) -> bool { !s.is_empty() && s.chars() .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_') } /// Convert an arbitrary string into a valid label. /// Lowercase, replace whitespace and invalid chars with '_', collapse runs. pub fn sanitize_label(s: &str) -> String { let mut result = String::with_capacity(s.len()); let mut prev_underscore = false; for c in s.chars() { if c.is_ascii_alphanumeric() { result.push(c.to_ascii_lowercase()); prev_underscore = false; } else if c == '-' { result.push('-'); prev_underscore = false; } else { // Replace whitespace and other chars with _ if !prev_underscore && !result.is_empty() { result.push('_'); prev_underscore = true; } } } // Trim trailing _ result.trim_end_matches('_').to_string() } /// Messages sent from the relay server to a connected client #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ServerMessage { /// Request a full-screen screenshot ScreenshotRequest { request_id: Uuid }, /// Capture a specific window by its HWND WindowScreenshotRequest { request_id: Uuid, window_id: u64 }, /// Fetch the last N lines of the client log file LogsRequest { request_id: Uuid, lines: u32 }, /// Show a MessageBox on the client asking the user to do something PromptRequest { request_id: Uuid, message: String, title: Option, }, /// Show a non-blocking notification to the user (fire-and-forget) InformRequest { request_id: Uuid, message: String, title: Option, }, /// Execute a shell command on the client ExecRequest { request_id: Uuid, command: String, timeout_ms: Option, }, /// Acknowledge a client message Ack { request_id: Uuid }, /// Server-side error response Error { request_id: Option, message: String, }, /// List all visible windows on the client ListWindowsRequest { request_id: Uuid }, /// Minimize all windows (like Win+D) MinimizeAllRequest { request_id: Uuid }, /// Bring a window to the foreground FocusWindowRequest { request_id: Uuid, window_id: u64 }, /// Maximize a window and bring it to the foreground MaximizeAndFocusRequest { request_id: Uuid, window_id: u64 }, /// Request client version info VersionRequest { request_id: Uuid }, /// Upload a file to the client UploadRequest { request_id: Uuid, path: String, content_base64: String, }, /// Download a file from the client DownloadRequest { request_id: Uuid, path: String, }, /// Launch a program on the client (fire-and-forget) RunRequest { request_id: Uuid, program: String, args: Vec, }, /// Get the contents of the client's clipboard ClipboardGetRequest { request_id: Uuid }, /// Set the contents of the client's clipboard ClipboardSetRequest { request_id: Uuid, text: String }, /// Request client to self-update and restart UpdateRequest { request_id: Uuid }, } /// Messages sent from the client to the relay server #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ClientMessage { /// Client registers itself with its device label Hello { label: String }, /// Response to a screenshot request — base64-encoded PNG ScreenshotResponse { request_id: Uuid, image_base64: String, width: u32, height: u32, }, /// Response to an exec request ExecResponse { request_id: Uuid, stdout: String, stderr: String, exit_code: i32, }, /// Generic acknowledgement Ack { request_id: Uuid }, /// Client error response Error { request_id: Uuid, message: String, }, /// Response to a list-windows request ListWindowsResponse { request_id: Uuid, windows: Vec, }, /// Response to a version request VersionResponse { request_id: Uuid, version: String, commit: String, }, LogsResponse { request_id: Uuid, content: String, log_path: String, }, /// Response to a download request DownloadResponse { request_id: Uuid, content_base64: String, size: u64, }, /// Response to a clipboard-get request ClipboardGetResponse { request_id: Uuid, text: String }, /// Response to a prompt request PromptResponse { request_id: Uuid, answer: String }, /// Response to an update request UpdateResponse { request_id: Uuid, success: bool, message: String, }, } #[cfg(test)] mod tests { use super::*; #[test] fn test_valid_labels() { assert!(is_valid_label("moritz_pc")); assert!(is_valid_label("my-desktop")); assert!(is_valid_label("pc01")); assert!(!is_valid_label("Moritz PC")); assert!(!is_valid_label("")); assert!(!is_valid_label("has spaces")); assert!(!is_valid_label("UPPER")); } #[test] fn test_sanitize_label() { assert_eq!(sanitize_label("Moritz PC"), "moritz_pc"); assert_eq!(sanitize_label("My Desktop!!"), "my_desktop"); assert_eq!(sanitize_label("hello-world"), "hello-world"); assert_eq!(sanitize_label("DESKTOP-ABC123"), "desktop-abc123"); } #[test] fn test_server_message_serialization() { let msg = ServerMessage::ExecRequest { request_id: Uuid::nil(), command: "echo hello".into(), timeout_ms: None, }; let json = serde_json::to_string(&msg).unwrap(); assert!(json.contains("exec_request")); assert!(json.contains("echo hello")); } #[test] fn test_client_message_serialization() { let msg = ClientMessage::Hello { label: "test-pc".into() }; let json = serde_json::to_string(&msg).unwrap(); assert!(json.contains("hello")); assert!(json.contains("test-pc")); } }