merge: PERF-1/2/3 scrollback, RDP binary IPC, settings dedup

This commit is contained in:
Vantz Stockwell 2026-03-29 16:41:14 -04:00
commit 15c95841be
5 changed files with 73 additions and 18 deletions

View File

@ -4,6 +4,7 @@
//! delegate to the `RdpService` via `State<AppState>`. //! delegate to the `RdpService` via `State<AppState>`.
use tauri::{AppHandle, State}; use tauri::{AppHandle, State};
use tauri::ipc::Response;
use crate::rdp::{RdpConfig, RdpSessionInfo}; use crate::rdp::{RdpConfig, RdpSessionInfo};
use crate::AppState; use crate::AppState;
@ -18,16 +19,18 @@ pub fn connect_rdp(
state.rdp.connect(config, app_handle) 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. /// 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] #[tauri::command]
pub async fn rdp_get_frame( pub async fn rdp_get_frame(
session_id: String, session_id: String,
state: State<'_, AppState>, state: State<'_, AppState>,
) -> Result<Vec<u8>, String> { ) -> Result<Response, String> {
state.rdp.get_frame(&session_id).await let frame = state.rdp.get_frame(&session_id).await?;
Ok(Response::new(frame))
} }
/// Send a mouse event to an RDP session. /// Send a mouse event to an RDP session.

View File

@ -60,17 +60,18 @@ impl AppState {
std::fs::create_dir_all(&data_dir)?; std::fs::create_dir_all(&data_dir)?;
let database = Database::open(&data_dir.join("wraith.db"))?; let database = Database::open(&data_dir.join("wraith.db"))?;
database.migrate()?; database.migrate()?;
let settings = SettingsService::new(database.clone());
Ok(Self { Ok(Self {
db: database.clone(), db: database.clone(),
vault: Mutex::new(None), vault: Mutex::new(None),
settings: SettingsService::new(database.clone()),
connections: ConnectionService::new(database.clone()), connections: ConnectionService::new(database.clone()),
credentials: Mutex::new(None), credentials: Mutex::new(None),
ssh: SshService::new(database.clone()), ssh: SshService::new(database.clone()),
sftp: SftpService::new(), sftp: SftpService::new(),
rdp: RdpService::new(), rdp: RdpService::new(),
theme: ThemeService::new(database.clone()), theme: ThemeService::new(database),
workspace: WorkspaceService::new(SettingsService::new(database.clone())), workspace: WorkspaceService::new(settings.clone()),
settings,
pty: PtyService::new(), pty: PtyService::new(),
scrollback: ScrollbackRegistry::new(), scrollback: ScrollbackRegistry::new(),
error_watcher: std::sync::Arc::new(ErrorWatcher::new()), error_watcher: std::sync::Arc::new(ErrorWatcher::new()),

View File

@ -40,13 +40,25 @@ impl ScrollbackBuffer {
/// Append bytes to the buffer. Old data is overwritten when full. /// Append bytes to the buffer. Old data is overwritten when full.
pub fn push(&self, bytes: &[u8]) { pub fn push(&self, bytes: &[u8]) {
let mut buf = self.inner.lock().unwrap(); if bytes.is_empty() {
for &b in bytes { return;
let pos = buf.write_pos;
buf.data[pos] = b;
buf.write_pos = (pos + 1) % buf.capacity;
buf.total_written += 1;
} }
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. /// 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 buf.push(b"ABCD"); // 4 more, wraps
assert_eq!(buf.total_written(), 12); 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");
}
} }

View File

@ -8,6 +8,7 @@ use crate::db::Database;
/// ///
/// All operations acquire the shared DB mutex for their duration and /// All operations acquire the shared DB mutex for their duration and
/// return immediately — no async needed for a local SQLite store. /// return immediately — no async needed for a local SQLite store.
#[derive(Clone)]
pub struct SettingsService { pub struct SettingsService {
db: Database, db: Database,
} }

View File

@ -184,7 +184,7 @@ export interface UseRdpReturn {
* Composable that manages an RDP session's rendering and input. * Composable that manages an RDP session's rendering and input.
* *
* Uses Tauri's invoke() to call Rust commands: * 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_mouse fire-and-forget
* rdp_send_key fire-and-forget * rdp_send_key fire-and-forget
* rdp_send_clipboard fire-and-forget * rdp_send_clipboard fire-and-forget
@ -209,16 +209,16 @@ export function useRdp(): UseRdpReturn {
width: number, width: number,
height: number, height: number,
): Promise<ImageData | null> { ): Promise<ImageData | null> {
let raw: number[]; let raw: ArrayBuffer;
try { try {
raw = await invoke<number[]>("rdp_get_frame", { sessionId }); raw = await invoke<ArrayBuffer>("rdp_get_frame", { sessionId });
} catch { } catch {
return null; return null;
} }
if (!raw || raw.length === 0) return null; if (!raw || raw.byteLength === 0) return null;
// Binary IPC — Tauri returns Vec<u8> as number array // Binary IPC — tauri::ipc::Response delivers raw bytes as ArrayBuffer
const bytes = new Uint8ClampedArray(raw); const bytes = new Uint8ClampedArray(raw);
const expected = width * height * 4; const expected = width * height * 4;