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:
AZ Computer Guru
2025-12-29 18:56:18 -07:00
parent a8ffa4bd83
commit 05ab8a8bf4
12 changed files with 1463 additions and 396 deletions

173
agent/src/viewer/input.rs Normal file
View File

@@ -0,0 +1,173 @@
//! Low-level keyboard hook for capturing all keys including Win key
use super::InputEvent;
#[cfg(windows)]
use crate::proto;
use anyhow::Result;
use tokio::sync::mpsc;
#[cfg(windows)]
use tracing::trace;
#[cfg(windows)]
use windows::{
Win32::Foundation::{LPARAM, LRESULT, WPARAM},
Win32::UI::WindowsAndMessaging::{
CallNextHookEx, DispatchMessageW, GetMessageW, PeekMessageW, SetWindowsHookExW,
TranslateMessage, UnhookWindowsHookEx, HHOOK, KBDLLHOOKSTRUCT, MSG, PM_REMOVE,
WH_KEYBOARD_LL, WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP,
},
};
#[cfg(windows)]
use std::sync::OnceLock;
#[cfg(windows)]
static INPUT_TX: OnceLock<mpsc::Sender<InputEvent>> = OnceLock::new();
#[cfg(windows)]
static mut HOOK_HANDLE: HHOOK = HHOOK(std::ptr::null_mut());
/// Virtual key codes for special keys
#[cfg(windows)]
mod vk {
pub const VK_LWIN: u32 = 0x5B;
pub const VK_RWIN: u32 = 0x5C;
pub const VK_APPS: u32 = 0x5D;
pub const VK_LSHIFT: u32 = 0xA0;
pub const VK_RSHIFT: u32 = 0xA1;
pub const VK_LCONTROL: u32 = 0xA2;
pub const VK_RCONTROL: u32 = 0xA3;
pub const VK_LMENU: u32 = 0xA4; // Left Alt
pub const VK_RMENU: u32 = 0xA5; // Right Alt
pub const VK_TAB: u32 = 0x09;
pub const VK_ESCAPE: u32 = 0x1B;
pub const VK_SNAPSHOT: u32 = 0x2C; // Print Screen
}
#[cfg(windows)]
pub struct KeyboardHook {
_hook: HHOOK,
}
#[cfg(windows)]
impl KeyboardHook {
pub fn new(input_tx: mpsc::Sender<InputEvent>) -> Result<Self> {
// Store the sender globally for the hook callback
INPUT_TX.set(input_tx).map_err(|_| anyhow::anyhow!("Input TX already set"))?;
unsafe {
let hook = SetWindowsHookExW(
WH_KEYBOARD_LL,
Some(keyboard_hook_proc),
None,
0,
)?;
HOOK_HANDLE = hook;
Ok(Self { _hook: hook })
}
}
}
#[cfg(windows)]
impl Drop for KeyboardHook {
fn drop(&mut self) {
unsafe {
if !HOOK_HANDLE.0.is_null() {
let _ = UnhookWindowsHookEx(HOOK_HANDLE);
HOOK_HANDLE = HHOOK(std::ptr::null_mut());
}
}
}
}
#[cfg(windows)]
unsafe extern "system" fn keyboard_hook_proc(
code: i32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if code >= 0 {
let kb_struct = &*(lparam.0 as *const KBDLLHOOKSTRUCT);
let vk_code = kb_struct.vkCode;
let scan_code = kb_struct.scanCode;
let is_down = wparam.0 as u32 == WM_KEYDOWN || wparam.0 as u32 == WM_SYSKEYDOWN;
let is_up = wparam.0 as u32 == WM_KEYUP || wparam.0 as u32 == WM_SYSKEYUP;
if is_down || is_up {
// Check if this is a key we want to intercept (Win key, Alt+Tab, etc.)
let should_intercept = matches!(
vk_code,
vk::VK_LWIN | vk::VK_RWIN | vk::VK_APPS
);
// Send the key event to the remote
if let Some(tx) = INPUT_TX.get() {
let event = proto::KeyEvent {
down: is_down,
key_type: proto::KeyEventType::KeyVk as i32,
vk_code,
scan_code,
unicode: String::new(),
modifiers: Some(get_current_modifiers()),
};
let _ = tx.try_send(InputEvent::Key(event));
trace!("Key hook: vk={:#x} scan={} down={}", vk_code, scan_code, is_down);
}
// For Win key, consume the event so it doesn't open Start menu locally
if should_intercept {
return LRESULT(1);
}
}
}
CallNextHookEx(HOOK_HANDLE, code, wparam, lparam)
}
#[cfg(windows)]
fn get_current_modifiers() -> proto::Modifiers {
use windows::Win32::UI::Input::KeyboardAndMouse::GetAsyncKeyState;
unsafe {
proto::Modifiers {
ctrl: GetAsyncKeyState(0x11) < 0, // VK_CONTROL
alt: GetAsyncKeyState(0x12) < 0, // VK_MENU
shift: GetAsyncKeyState(0x10) < 0, // VK_SHIFT
meta: GetAsyncKeyState(0x5B) < 0 || GetAsyncKeyState(0x5C) < 0, // VK_LWIN/RWIN
caps_lock: GetAsyncKeyState(0x14) & 1 != 0, // VK_CAPITAL
num_lock: GetAsyncKeyState(0x90) & 1 != 0, // VK_NUMLOCK
}
}
}
/// Pump Windows message queue (required for hooks to work)
#[cfg(windows)]
pub fn pump_messages() {
unsafe {
let mut msg = MSG::default();
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
// Non-Windows stubs
#[cfg(not(windows))]
#[allow(dead_code)]
pub struct KeyboardHook;
#[cfg(not(windows))]
#[allow(dead_code)]
impl KeyboardHook {
pub fn new(_input_tx: mpsc::Sender<InputEvent>) -> Result<Self> {
Ok(Self)
}
}
#[cfg(not(windows))]
#[allow(dead_code)]
pub fn pump_messages() {}