Unify agent and viewer into single guruconnect binary
- Renamed package from guruconnect-agent to guruconnect - Added CLI subcommands: agent, view, install, uninstall, launch - Moved viewer code into agent/src/viewer module - Added install module with: - UAC elevation attempt with user-install fallback - Protocol handler registration (guruconnect://) - System-wide install to Program Files or user install to LocalAppData - Single binary now handles both receiving and initiating connections - Protocol URL format: guruconnect://view/SESSION_ID?token=API_KEY Usage: guruconnect agent - Run as background agent guruconnect view <session_id> - View a remote session guruconnect install - Install and register protocol guruconnect launch <url> - Handle guruconnect:// URL 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
121
agent/src/viewer/mod.rs
Normal file
121
agent/src/viewer/mod.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
//! 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::<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(())
|
||||
}
|
||||
Reference in New Issue
Block a user