//! 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::{info, error, 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::(100); let (input_tx, input_rx) = mpsc::channel::(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(()) }