diff --git a/src-tauri/src/commands/rdp_commands.rs b/src-tauri/src/commands/rdp_commands.rs index 79ec24a..67ebffb 100644 --- a/src-tauri/src/commands/rdp_commands.rs +++ b/src-tauri/src/commands/rdp_commands.rs @@ -4,6 +4,7 @@ //! delegate to the `RdpService` via `State`. use tauri::{AppHandle, State}; +use tauri::ipc::Response; use crate::rdp::{RdpConfig, RdpSessionInfo}; use crate::AppState; @@ -18,16 +19,18 @@ pub fn connect_rdp( state.rdp.connect(config, app_handle) } -/// Get the current frame buffer as raw RGBA bytes (binary IPC — no base64). +/// Get the current frame buffer as raw RGBA bytes via binary IPC. /// +/// Uses `tauri::ipc::Response` to return raw bytes without JSON serialization. /// Pixel format: RGBA, 4 bytes per pixel, row-major, top-left origin. -/// Returns empty Vec if frame hasn't changed since last call. +/// Returns empty payload if frame hasn't changed since last call. #[tauri::command] pub async fn rdp_get_frame( session_id: String, state: State<'_, AppState>, -) -> Result, String> { - state.rdp.get_frame(&session_id).await +) -> Result { + let frame = state.rdp.get_frame(&session_id).await?; + Ok(Response::new(frame)) } /// Send a mouse event to an RDP session. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 21a9a59..d53485e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -60,17 +60,18 @@ impl AppState { std::fs::create_dir_all(&data_dir)?; let database = Database::open(&data_dir.join("wraith.db"))?; database.migrate()?; + let settings = SettingsService::new(database.clone()); Ok(Self { db: database.clone(), vault: Mutex::new(None), - settings: SettingsService::new(database.clone()), connections: ConnectionService::new(database.clone()), credentials: Mutex::new(None), ssh: SshService::new(database.clone()), sftp: SftpService::new(), rdp: RdpService::new(), - theme: ThemeService::new(database.clone()), - workspace: WorkspaceService::new(SettingsService::new(database.clone())), + theme: ThemeService::new(database), + workspace: WorkspaceService::new(settings.clone()), + settings, pty: PtyService::new(), scrollback: ScrollbackRegistry::new(), error_watcher: std::sync::Arc::new(ErrorWatcher::new()), diff --git a/src-tauri/src/mcp/scrollback.rs b/src-tauri/src/mcp/scrollback.rs index cd1e201..c8cc0d9 100644 --- a/src-tauri/src/mcp/scrollback.rs +++ b/src-tauri/src/mcp/scrollback.rs @@ -40,13 +40,25 @@ impl ScrollbackBuffer { /// Append bytes to the buffer. Old data is overwritten when full. pub fn push(&self, bytes: &[u8]) { - let mut buf = self.inner.lock().unwrap(); - for &b in bytes { - let pos = buf.write_pos; - buf.data[pos] = b; - buf.write_pos = (pos + 1) % buf.capacity; - buf.total_written += 1; + if bytes.is_empty() { + return; } + let mut buf = self.inner.lock().unwrap(); + let cap = buf.capacity; + // If input exceeds capacity, only keep the last `cap` bytes + let data = if bytes.len() > cap { + &bytes[bytes.len() - cap..] + } else { + bytes + }; + let write_pos = buf.write_pos; + let first_len = (cap - write_pos).min(data.len()); + buf.data[write_pos..write_pos + first_len].copy_from_slice(&data[..first_len]); + if first_len < data.len() { + buf.data[..data.len() - first_len].copy_from_slice(&data[first_len..]); + } + buf.write_pos = (write_pos + data.len()) % cap; + buf.total_written += bytes.len(); } /// Read the last `n` lines from the buffer, with ANSI escape codes stripped. @@ -192,4 +204,42 @@ mod tests { buf.push(b"ABCD"); // 4 more, wraps assert_eq!(buf.total_written(), 12); } + + #[test] + fn push_empty_is_noop() { + let buf = ScrollbackBuffer::with_capacity(8); + buf.push(b"hello"); + buf.push(b""); + assert_eq!(buf.total_written(), 5); + assert!(buf.read_raw().contains("hello")); + } + + #[test] + fn push_larger_than_capacity() { + let buf = ScrollbackBuffer::with_capacity(4); + buf.push(b"ABCDEFGH"); // 8 bytes into 4-byte buffer + let raw = buf.read_raw(); + assert_eq!(raw, "EFGH"); // only last 4 bytes kept + assert_eq!(buf.total_written(), 8); + } + + #[test] + fn push_exact_capacity() { + let buf = ScrollbackBuffer::with_capacity(8); + buf.push(b"12345678"); + let raw = buf.read_raw(); + assert_eq!(raw, "12345678"); + assert_eq!(buf.total_written(), 8); + } + + #[test] + fn push_wrap_around_boundary() { + let buf = ScrollbackBuffer::with_capacity(8); + buf.push(b"123456"); // write_pos = 6 + buf.push(b"ABCD"); // wraps: 2 at end, 2 at start + let raw = buf.read_raw(); + // Buffer: [C, D, 3, 4, 5, 6, A, B], write_pos=2 + // Read from pos 2: "3456AB" + wrap: no, read from write_pos to end then start + assert_eq!(raw, "3456ABCD"); + } } diff --git a/src-tauri/src/settings/mod.rs b/src-tauri/src/settings/mod.rs index b82a1a1..9e1f48a 100644 --- a/src-tauri/src/settings/mod.rs +++ b/src-tauri/src/settings/mod.rs @@ -8,6 +8,7 @@ use crate::db::Database; /// /// All operations acquire the shared DB mutex for their duration and /// return immediately — no async needed for a local SQLite store. +#[derive(Clone)] pub struct SettingsService { db: Database, } diff --git a/src/composables/useRdp.ts b/src/composables/useRdp.ts index dcdc285..e82f1cf 100644 --- a/src/composables/useRdp.ts +++ b/src/composables/useRdp.ts @@ -184,7 +184,7 @@ export interface UseRdpReturn { * Composable that manages an RDP session's rendering and input. * * Uses Tauri's invoke() to call Rust commands: - * rdp_get_frame → base64 RGBA string + * rdp_get_frame → raw RGBA ArrayBuffer (binary IPC) * rdp_send_mouse → fire-and-forget * rdp_send_key → fire-and-forget * rdp_send_clipboard → fire-and-forget @@ -208,16 +208,16 @@ export function useRdp(): UseRdpReturn { width: number, height: number, ): Promise { - let raw: number[]; + let raw: ArrayBuffer; try { - raw = await invoke("rdp_get_frame", { sessionId }); + raw = await invoke("rdp_get_frame", { sessionId }); } catch { return null; } - if (!raw || raw.length === 0) return null; + if (!raw || raw.byteLength === 0) return null; - // Binary IPC — Tauri returns Vec as number array + // Binary IPC — tauri::ipc::Response delivers raw bytes as ArrayBuffer const bytes = new Uint8ClampedArray(raw); const expected = width * height * 4;