Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 2m59s
Build and Test / Build Agent (Windows) (push) Has started running
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled
First run of the build-and-test CI gate (cargo fmt --all -- --check) surfaced pre-existing formatting drift across the agent and server crates. Apply rustfmt across the workspace so the codebase meets its own CI gate. Pure formatting; no logic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
4.3 KiB
Rust
124 lines
4.3 KiB
Rust
//! Viewer module - Native remote desktop viewer with full keyboard capture
|
|
//!
|
|
//! This module provides the viewer functionality for connecting to remote
|
|
//! GuruConnect sessions with low-level keyboard hooks for Win key capture.
|
|
|
|
mod input;
|
|
mod render;
|
|
mod transport;
|
|
|
|
use crate::proto;
|
|
use anyhow::Result;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{mpsc, Mutex};
|
|
use tracing::{error, info, warn};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ViewerEvent {
|
|
Connected,
|
|
Disconnected(String),
|
|
Frame(render::FrameData),
|
|
CursorPosition(i32, i32, bool),
|
|
CursorShape(proto::CursorShape),
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum InputEvent {
|
|
Mouse(proto::MouseEvent),
|
|
Key(proto::KeyEvent),
|
|
SpecialKey(proto::SpecialKeyEvent),
|
|
}
|
|
|
|
/// Run the viewer to connect to a remote session
|
|
pub async fn run(server_url: &str, session_id: &str, api_key: &str) -> Result<()> {
|
|
info!("GuruConnect Viewer starting");
|
|
info!("Server: {}", server_url);
|
|
info!("Session: {}", session_id);
|
|
|
|
// Create channels for communication between components
|
|
let (viewer_tx, viewer_rx) = mpsc::channel::<ViewerEvent>(100);
|
|
let (input_tx, input_rx) = mpsc::channel::<InputEvent>(100);
|
|
|
|
// Connect to server
|
|
let ws_url = format!("{}?session_id={}", server_url, session_id);
|
|
info!("Connecting to {}", ws_url);
|
|
|
|
let (ws_sender, mut ws_receiver) = transport::connect(&ws_url, api_key).await?;
|
|
let ws_sender = Arc::new(Mutex::new(ws_sender));
|
|
|
|
info!("Connected to server");
|
|
let _ = viewer_tx.send(ViewerEvent::Connected).await;
|
|
|
|
// Clone sender for input forwarding
|
|
let ws_sender_input = ws_sender.clone();
|
|
|
|
// Spawn task to forward input events to server
|
|
let mut input_rx = input_rx;
|
|
let input_task = tokio::spawn(async move {
|
|
while let Some(event) = input_rx.recv().await {
|
|
let msg = match event {
|
|
InputEvent::Mouse(m) => proto::Message {
|
|
payload: Some(proto::message::Payload::MouseEvent(m)),
|
|
},
|
|
InputEvent::Key(k) => proto::Message {
|
|
payload: Some(proto::message::Payload::KeyEvent(k)),
|
|
},
|
|
InputEvent::SpecialKey(s) => proto::Message {
|
|
payload: Some(proto::message::Payload::SpecialKey(s)),
|
|
},
|
|
};
|
|
|
|
if let Err(e) = transport::send_message(&ws_sender_input, &msg).await {
|
|
error!("Failed to send input: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Spawn task to receive messages from server
|
|
let viewer_tx_recv = viewer_tx.clone();
|
|
let receive_task = tokio::spawn(async move {
|
|
while let Some(msg) = ws_receiver.recv().await {
|
|
match msg.payload {
|
|
Some(proto::message::Payload::VideoFrame(frame)) => {
|
|
if let Some(proto::video_frame::Encoding::Raw(raw)) = frame.encoding {
|
|
let frame_data = render::FrameData {
|
|
width: raw.width as u32,
|
|
height: raw.height as u32,
|
|
data: raw.data,
|
|
compressed: raw.compressed,
|
|
is_keyframe: raw.is_keyframe,
|
|
};
|
|
let _ = viewer_tx_recv.send(ViewerEvent::Frame(frame_data)).await;
|
|
}
|
|
}
|
|
Some(proto::message::Payload::CursorPosition(pos)) => {
|
|
let _ = viewer_tx_recv
|
|
.send(ViewerEvent::CursorPosition(pos.x, pos.y, pos.visible))
|
|
.await;
|
|
}
|
|
Some(proto::message::Payload::CursorShape(shape)) => {
|
|
let _ = viewer_tx_recv.send(ViewerEvent::CursorShape(shape)).await;
|
|
}
|
|
Some(proto::message::Payload::Disconnect(d)) => {
|
|
warn!("Server disconnected: {}", d.reason);
|
|
let _ = viewer_tx_recv
|
|
.send(ViewerEvent::Disconnected(d.reason))
|
|
.await;
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Run the window (this blocks until window closes)
|
|
render::run_window(viewer_rx, input_tx).await?;
|
|
|
|
// Cleanup
|
|
input_task.abort();
|
|
receive_task.abort();
|
|
|
|
Ok(())
|
|
}
|