//! HyprKVM Daemon - Main entry point //! //! The daemon handles: //! - Hyprland IPC communication //! - Edge detection (mouse and keyboard) //! - Network connections to peer machines //! - Input capture and injection use clap::{Parser, Subcommand}; use tracing::{info, Level}; use tracing_subscriber::FmtSubscriber; mod config; mod hyprland; mod input; mod ipc; mod network; mod state; mod transfer; use config::Config; /// Convert keycode to human-readable name for logging fn keycode_to_name(keycode: u32) -> &'static str { match keycode { 1 => "ESC", 14 => "BACKSPACE", 15 => "TAB", 28 => "ENTER", 29 => "LEFTCTRL", 42 => "LEFTSHIFT", 54 => "RIGHTSHIFT", 56 => "LEFTALT", 57 => "SPACE", 58 => "CAPSLOCK", 97 => "RIGHTCTRL", 100 => "RIGHTALT", 103 => "UP", 105 => "LEFT", 106 => "RIGHT", 108 => "DOWN", 125 => "LEFTMETA", 126 => "RIGHTMETA", _ => "OTHER", } } #[derive(Parser)] #[command(name = "hyprkvm")] #[command(about = "Hyprland-native software KVM switch")] #[command(version)] struct Cli { /// Config file path #[arg(short, long)] config: Option, /// Increase log verbosity (-v, -vv, -vvv) #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8, #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Start the HyprKVM daemon Daemon, /// Show daemon status Status, /// Handle a move request (called by keybinding script) Move { /// Direction to move direction: String, }, /// Configuration management Config { #[command(subcommand)] action: ConfigAction, }, } #[derive(Subcommand)] enum ConfigAction { /// Show current configuration Show, /// Reload configuration Reload, } #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); // Set up logging let log_level = match cli.verbose { 0 => Level::INFO, 1 => Level::DEBUG, _ => Level::TRACE, }; FmtSubscriber::builder() .with_max_level(log_level) .with_target(false) .init(); // Load configuration let config_path = cli.config.unwrap_or_else(|| { dirs::config_dir() .unwrap_or_else(|| std::path::PathBuf::from(".")) .join("hyprkvm") .join("hyprkvm.toml") }); match cli.command { Commands::Daemon => { info!("Starting HyprKVM daemon..."); run_daemon(&config_path).await } Commands::Status => { show_status().await } Commands::Move { direction } => { handle_move(&direction).await } Commands::Config { action } => { match action { ConfigAction::Show => show_config(&config_path), ConfigAction::Reload => reload_config().await, } } } } async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use tokio::sync::RwLock; use hyprkvm_common::Direction; use hyprkvm_common::protocol::{Message, HelloPayload, PROTOCOL_VERSION}; // Load or create default config let config = match Config::load(config_path) { Ok(cfg) => cfg, Err(e) => { tracing::warn!("Failed to load config: {e}, using defaults"); Config::default() } }; info!("Machine name: {}", config.machines.self_name); info!("Listening on port: {}", config.network.listen_port); // Track daemon start time for uptime reporting let daemon_start_time = std::time::Instant::now(); // State flags for CLI control let barrier_enabled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let shutdown_requested = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); // Connect to Hyprland info!("Connecting to Hyprland..."); let hypr_client = hyprland::ipc::HyprlandClient::new().await?; // Query monitors to validate connection let monitors = hypr_client.monitors().await?; info!("Connected to Hyprland. Monitors: {}", monitors.len()); for mon in &monitors { info!(" {} at ({}, {}) {}x{}", mon.name, mon.x, mon.y, mon.width, mon.height); } // Calculate screen bounds let screen_width: u32 = monitors.iter().map(|m| m.x as u32 + m.width).max().unwrap_or(1920); let screen_height: u32 = monitors.iter().map(|m| m.y as u32 + m.height).max().unwrap_or(1080); // Determine which edges have network neighbors let mut enabled_edges = Vec::new(); let mut neighbor_map: HashMap = HashMap::new(); for neighbor in &config.machines.neighbors { enabled_edges.push(neighbor.direction); neighbor_map.insert(neighbor.direction, neighbor.address); info!(" Network neighbor: {} ({}) at {}", neighbor.name, neighbor.direction, neighbor.address); } // If no neighbors configured, just run in demo mode if enabled_edges.is_empty() { info!("No neighbors configured. Add neighbors in config to enable control transfer."); enabled_edges = vec![Direction::Left, Direction::Right]; } // Start edge capture info!("Starting edge capture for: {:?}", enabled_edges); let monitor_infos: Vec = monitors.iter().map(|m| input::MonitorInfo { name: m.name.clone(), x: m.x, y: m.y, width: m.width, height: m.height, }).collect(); let edge_capture = input::EdgeCapture::new(input::EdgeCaptureConfig { barrier_size: 1, enabled_edges: enabled_edges.clone(), monitors: monitor_infos, })?; // Create input grabber (for when we send control elsewhere) // Use evdev-based grabber for reliable input capture at kernel level let input_grabber = input::EvdevGrabber::new()?; // Create input emulator (for when we receive control from elsewhere) // This is created lazily when we first need to inject let mut input_emulator: Option = None; // Create transfer manager let (transfer_manager, mut transfer_events) = transfer::TransferManager::new( config.machines.self_name.clone(), ); let transfer_manager = Arc::new(transfer_manager); // Track which direction we're capturing for let mut capture_direction: Option = None; let mut input_sequence: u64 = 0; // Escape key detection // KEY_SCROLLLOCK = 70 in Linux evdev keycodes const KEY_SCROLLLOCK: u32 = 70; const KEY_LEFTSHIFT: u32 = 42; const KEY_RIGHTSHIFT: u32 = 54; let mut shift_tap_times: Vec = Vec::new(); let triple_tap_window = std::time::Duration::from_millis( config.input.escape_hotkey.triple_tap_window_ms ); // Cursor-based edge detection state let mut last_cursor_pos: Option<(i32, i32)> = None; let mut edge_dwell_start: Option<(Direction, std::time::Instant)> = None; const EDGE_THRESHOLD: i32 = 2; // Pixels from edge to count as "at edge" const EDGE_DWELL_MS: u64 = 50; // How long cursor must be at edge to trigger // Cooldown after control returns to prevent immediate bounce-back let mut last_control_return: Option = None; const CONTROL_RETURN_COOLDOWN_MS: u64 = 500; // 500ms cooldown after control returns // Connection storage: direction -> peer connection let peers: Arc>> = Arc::new(RwLock::new(HashMap::new())); // Start network server let listen_addr: SocketAddr = format!("0.0.0.0:{}", config.network.listen_port).parse()?; let server = network::Server::bind(listen_addr).await?; info!("Listening for connections on {}", server.local_addr()); // Spawn task to accept incoming connections let machine_name = config.machines.self_name.clone(); let neighbors_for_accept = config.machines.neighbors.clone(); let peers_for_accept = peers.clone(); let accept_handle = tokio::spawn(async move { loop { match server.accept().await { Ok(mut conn) => { let addr = conn.remote_addr(); info!("Incoming connection from {}", addr); // Receive Hello match conn.recv().await { Ok(Some(Message::Hello(hello))) => { info!("Peer {} connected (protocol v{})", hello.machine_name, hello.protocol_version); // Send HelloAck let ack = Message::HelloAck(hyprkvm_common::protocol::HelloAckPayload { accepted: true, protocol_version: PROTOCOL_VERSION, machine_name: machine_name.clone(), error: None, }); if let Err(e) = conn.send(&ack).await { tracing::error!("Failed to send HelloAck: {}", e); continue; } // Determine direction based on peer's machine name let direction = neighbors_for_accept .iter() .find(|n| n.name == hello.machine_name) .map(|n| n.direction); if let Some(dir) = direction { let mut peers = peers_for_accept.write().await; if peers.contains_key(&dir) { info!("Already have connection for {:?}, dropping incoming from {}", dir, hello.machine_name); // Drop the incoming connection, keep the existing one } else { info!("Storing incoming connection from {} as {:?}", hello.machine_name, dir); peers.insert(dir, conn); } } else { tracing::warn!( "Unknown peer '{}' connected - not in neighbors list", hello.machine_name ); // Connection will be dropped } } Ok(Some(other)) => { tracing::warn!("Expected Hello, got {:?}", other); } Ok(None) => { tracing::debug!("Connection closed during handshake"); } Err(e) => { tracing::error!("Handshake error: {}", e); } } } Err(e) => { tracing::error!("Accept error: {}", e); } } } }); // Connect to configured peers (with retry loop) for neighbor in &config.machines.neighbors { let addr = neighbor.address; let direction = neighbor.direction; let peers_clone = peers.clone(); let machine_name = config.machines.self_name.clone(); tokio::spawn(async move { loop { // Check if already connected { let peers = peers_clone.read().await; if peers.contains_key(&direction) { // Already connected, wait and check again drop(peers); tokio::time::sleep(std::time::Duration::from_secs(5)).await; continue; } } tracing::debug!("Connecting to {} at {}...", direction, addr); match network::connect(addr).await { Ok(mut conn) => { // Send Hello let hello = Message::Hello(HelloPayload { protocol_version: PROTOCOL_VERSION, machine_name: machine_name.clone(), capabilities: vec![], }); if let Err(e) = conn.send(&hello).await { tracing::error!("Failed to send Hello to {}: {}", direction, e); tokio::time::sleep(std::time::Duration::from_secs(3)).await; continue; } // Wait for HelloAck match conn.recv().await { Ok(Some(Message::HelloAck(ack))) => { if ack.accepted { let mut peers = peers_clone.write().await; if peers.contains_key(&direction) { info!("Already have connection for {:?}, dropping outbound to {}", direction, ack.machine_name); // Drop this connection, keep the existing one } else { info!("Connected to {} ({})", ack.machine_name, direction); peers.insert(direction, conn); } // Stay in loop to reconnect if connection drops } else { tracing::error!("Connection rejected: {:?}", ack.error); } } Ok(Some(other)) => { tracing::warn!("Expected HelloAck, got {:?}", other); } Ok(None) => { tracing::warn!("Connection closed during handshake"); } Err(e) => { tracing::error!("Handshake error: {}", e); } } } Err(e) => { tracing::debug!("Failed to connect to {} ({}): {}", direction, addr, e); } } // Retry after delay tokio::time::sleep(std::time::Duration::from_secs(3)).await; } }); } // Listen for Hyprland events let mut event_stream = hyprland::events::HyprlandEventStream::connect().await?; // Start IPC server for CLI commands let (ipc_tx, mut ipc_rx) = tokio::sync::mpsc::channel::<( hyprkvm_common::protocol::IpcRequest, tokio::sync::oneshot::Sender, )>(16); tokio::spawn(async move { let server = match ipc::IpcServer::bind().await { Ok(s) => s, Err(e) => { tracing::error!("Failed to start IPC server: {}", e); return; } }; loop { match server.accept().await { Ok(mut conn) => { tracing::debug!("IPC: connection accepted"); let ipc_tx = ipc_tx.clone(); tokio::spawn(async move { match conn.recv().await { Ok(Some(request)) => { tracing::debug!("IPC: received {:?}", request); let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); if ipc_tx.send((request, resp_tx)).await.is_ok() { tracing::debug!("IPC: sent to main loop, awaiting response"); match resp_rx.await { Ok(response) => { tracing::debug!("IPC: got response, sending to client"); if let Err(e) = conn.send(&response).await { tracing::error!("IPC: failed to send response: {}", e); } } Err(e) => { tracing::error!("IPC: response channel error: {}", e); } } } else { tracing::error!("IPC: failed to send request to main loop"); } } Ok(None) => { tracing::debug!("IPC: connection closed by client"); } Err(e) => { tracing::debug!("IPC recv error: {}", e); } } }); } Err(e) => { tracing::error!("IPC accept error: {}", e); } } } }); info!("Daemon running. Move mouse to screen edges to trigger transfer. Press Ctrl+C to stop."); loop { tokio::select! { // Check for edge events, grabber events, and poll peer messages _ = tokio::time::sleep(std::time::Duration::from_micros(100)) => { // Forward grabbed input to remote peer if let Some(cap_dir) = capture_direction { let mut should_escape = false; // Coalesce motion events - drain queue and accumulate let mut motion_dx: f64 = 0.0; let mut motion_dy: f64 = 0.0; let mut scroll_h: f64 = 0.0; let mut scroll_v: f64 = 0.0; let mut other_events: Vec = Vec::new(); while let Some(grab_event) = input_grabber.try_recv() { // Check for escape key before forwarding match &grab_event { input::GrabEvent::KeyDown { keycode } => { tracing::debug!("CAPTURE KeyDown: keycode={} ({})", keycode, keycode_to_name(*keycode)); // Check for scroll_lock if *keycode == KEY_SCROLLLOCK { info!("Scroll Lock pressed - returning control to local"); should_escape = true; continue; // Don't forward this key } // Check for triple-tap shift if config.input.escape_hotkey.triple_tap_enabled { if *keycode == KEY_LEFTSHIFT || *keycode == KEY_RIGHTSHIFT { let now = std::time::Instant::now(); // Remove old taps outside the window shift_tap_times.retain(|t| now.duration_since(*t) < triple_tap_window); shift_tap_times.push(now); if shift_tap_times.len() >= 3 { info!("Triple-tap Shift detected - returning control to local"); should_escape = true; shift_tap_times.clear(); continue; } } } other_events.push(grab_event); } input::GrabEvent::KeyUp { keycode } => { tracing::debug!("CAPTURE KeyUp: keycode={} ({})", keycode, keycode_to_name(*keycode)); other_events.push(grab_event); } input::GrabEvent::PointerMotion { dx, dy } => { motion_dx += dx; motion_dy += dy; } input::GrabEvent::PointerButton { .. } => { other_events.push(grab_event); } input::GrabEvent::Scroll { horizontal, vertical } => { scroll_h += horizontal; scroll_v += vertical; } input::GrabEvent::ModifiersChanged { .. } => { other_events.push(grab_event); } input::GrabEvent::RecoveryHotkey { .. } => { // Should not happen during capture, ignore tracing::warn!("RecoveryHotkey received during capture, ignoring"); } } } // Send non-motion events first (preserve order for key events) { let mut peers_guard = peers.write().await; if let Some(peer) = peers_guard.get_mut(&cap_dir) { for event in other_events { let payload = event.to_protocol(input_sequence); input_sequence += 1; if let Err(e) = peer.send(&Message::InputEvent(payload)).await { tracing::error!("Failed to send input event: {}", e); } } // Send coalesced motion as single event if motion_dx != 0.0 || motion_dy != 0.0 { let motion_event = input::GrabEvent::PointerMotion { dx: motion_dx, dy: motion_dy }; let payload = motion_event.to_protocol(input_sequence); input_sequence += 1; if let Err(e) = peer.send(&Message::InputEvent(payload)).await { tracing::error!("Failed to send motion event: {}", e); } } // Send coalesced scroll as single event if scroll_h != 0.0 || scroll_v != 0.0 { let scroll_event = input::GrabEvent::Scroll { horizontal: scroll_h, vertical: scroll_v }; let payload = scroll_event.to_protocol(input_sequence); input_sequence += 1; if let Err(e) = peer.send(&Message::InputEvent(payload)).await { tracing::error!("Failed to send scroll event: {}", e); } } } } // If escape was triggered, stop capture and send Leave if should_escape { info!("Escape triggered - stopping capture"); capture_direction = None; input_grabber.stop(None); // No recovery needed for escape // Send Leave message - we're leaving in the opposite direction (returning to us) let leave = Message::Leave(hyprkvm_common::protocol::LeavePayload { to_direction: cap_dir.opposite(), cursor_pos: hyprkvm_common::protocol::CursorEntryPos::EdgeRelative(0.5), modifiers: hyprkvm_common::ModifierState::default(), transfer_id: input_sequence, // Use as a simple unique ID }); let mut peers_guard = peers.write().await; if let Some(peer) = peers_guard.get_mut(&cap_dir) { if let Err(e) = peer.send(&leave).await { tracing::error!("Failed to send Leave: {}", e); } } } } else { // Not capturing - check for RecoveryHotkey events from recovery mode // These bypass libinput's stale state by detecting keypresses directly at evdev level while let Some(grab_event) = input_grabber.try_recv() { if let input::GrabEvent::RecoveryHotkey { direction } = grab_event { info!("RECOVERY HOTKEY: Super+{:?} detected via evdev", direction); // Same at_edge check as IPC Move - only transfer if at edge monitor+window let at_edge = 'edge_check: { // Get monitors and find focused one let monitors = match hypr_client.monitors().await { Ok(m) => m, Err(e) => { info!(" RECOVERY edge_check: monitors query failed: {}", e); break 'edge_check false; } }; let focused_monitor = match monitors.iter().find(|m| m.focused) { Some(m) => m, None => { info!(" RECOVERY edge_check: no focused monitor found"); break 'edge_check false; } }; // Check if there's another monitor in the requested direction let has_monitor_in_direction = monitors.iter().any(|m| { if m.id == focused_monitor.id { return false; } match direction { Direction::Left => m.x + m.width as i32 <= focused_monitor.x, Direction::Right => m.x >= focused_monitor.x + focused_monitor.width as i32, Direction::Up => m.y + m.height as i32 <= focused_monitor.y, Direction::Down => m.y >= focused_monitor.y + focused_monitor.height as i32, } }); if has_monitor_in_direction { info!(" RECOVERY edge_check: has monitor in direction {:?}", direction); break 'edge_check false; } // On edge monitor. Check if at edge window. let active_window: serde_json::Value = match hypr_client.query("activewindow").await { Ok(w) => w, Err(e) => { info!(" RECOVERY edge_check: activewindow query failed: {}", e); break 'edge_check false; } }; let win_x = active_window.get("at").and_then(|a| a.get(0)).and_then(|x| x.as_i64()).unwrap_or(0) as i32; let win_y = active_window.get("at").and_then(|a| a.get(1)).and_then(|y| y.as_i64()).unwrap_or(0) as i32; let win_w = active_window.get("size").and_then(|s| s.get(0)).and_then(|w| w.as_i64()).unwrap_or(100) as i32; let win_h = active_window.get("size").and_then(|s| s.get(1)).and_then(|h| h.as_i64()).unwrap_or(100) as i32; // Get all clients (windows) let clients: Vec = match hypr_client.query("clients").await { Ok(c) => c, Err(e) => { info!(" RECOVERY edge_check: clients query failed: {}", e); break 'edge_check false; } }; // Check if any window is further in the requested direction on same monitor let has_window_in_direction = clients.iter().any(|client| { let mon = client.get("monitor").and_then(|m| m.as_i64()).unwrap_or(-1) as i32; if mon != focused_monitor.id { return false; } let cx = client.get("at").and_then(|a| a.get(0)).and_then(|x| x.as_i64()).unwrap_or(0) as i32; let cy = client.get("at").and_then(|a| a.get(1)).and_then(|y| y.as_i64()).unwrap_or(0) as i32; let cw = client.get("size").and_then(|s| s.get(0)).and_then(|w| w.as_i64()).unwrap_or(0) as i32; let ch = client.get("size").and_then(|s| s.get(1)).and_then(|h| h.as_i64()).unwrap_or(0) as i32; match direction { Direction::Left => cx + cw < win_x + 10, Direction::Right => cx > win_x + win_w - 10, Direction::Up => cy + ch < win_y + 10, Direction::Down => cy > win_y + win_h - 10, } }); info!(" RECOVERY edge_check: has_window_in_direction={} -> at_edge={}", has_window_in_direction, !has_window_in_direction); !has_window_in_direction }; // Check if we have a peer in this direction let has_peer = { let peers = peers.read().await; peers.contains_key(&direction) }; if at_edge && has_peer { // Get cursor position for transfer let cursor_pos = hypr_client.cursor_pos().await .map(|c| (c.x, c.y)) .unwrap_or((0, 0)); info!("RECOVERY HOTKEY: At edge with peer, initiating transfer to {:?}", direction); if let Err(e) = transfer_manager.initiate_transfer( direction, cursor_pos, screen_height, screen_width, ).await { tracing::error!("Failed to initiate transfer from recovery hotkey: {}", e); } } else if !at_edge { // Not at edge - need to do movefocus ourselves because libinput // DROPPED the keypress due to stale state (it thinks the arrow key // is still pressed from before the grab). This is the whole reason // recovery mode exists. let hypr_dir = match direction { Direction::Left => "l", Direction::Right => "r", Direction::Up => "u", Direction::Down => "d", }; info!("RECOVERY HOTKEY: Not at edge, doing movefocus {} (libinput dropped the keypress)", hypr_dir); match hypr_client.dispatch("movefocus", hypr_dir).await { Ok(()) => info!(" RECOVERY movefocus succeeded"), Err(e) => tracing::error!(" RECOVERY movefocus failed: {}", e), } } else { info!("RECOVERY HOTKEY: No peer in direction {:?}", direction); } } } } // Handle edge events from layer-shell barriers (for inter-monitor edges) while let Some(edge_event) = edge_capture.try_recv() { let direction = edge_event.direction; // Check if we have a peer in this direction let has_peer = { let peers = peers.read().await; peers.contains_key(&direction) }; if has_peer { // Check if we're in ReceivedControl state from this direction // If so, return control instead of initiating a new transfer let current_state = transfer_manager.state().await; if let transfer::TransferState::ReceivedControl { from, .. } = current_state { if from == direction { info!( "EDGE: {:?} at ({}, {}) - returning control", direction, edge_event.position.0, edge_event.position.1 ); if let Err(e) = transfer_manager.return_control().await { tracing::warn!("Failed to return control: {}", e); } continue; } } // Check cooldown to prevent bounce-back loops if let Some(last_return) = last_control_return { if last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 { tracing::debug!("EDGE: {:?} - in cooldown, ignoring", direction); continue; } } info!( "EDGE: {:?} at ({}, {}) - initiating transfer", direction, edge_event.position.0, edge_event.position.1 ); if let Err(e) = transfer_manager.initiate_transfer( direction, edge_event.position, screen_height, screen_width, ).await { tracing::warn!("Failed to initiate transfer: {}", e); } } else { tracing::debug!( "EDGE: {:?} but no peer connected", direction ); } } // Cursor-based edge detection (for absolute screen edges) // This catches the case where cursor is at the edge and can't go further if capture_direction.is_none() { if let Ok(cursor) = hypr_client.cursor_pos().await { let (cx, cy) = (cursor.x, cursor.y); // Determine if cursor is at a screen edge let at_edge: Option = if cx <= EDGE_THRESHOLD { Some(Direction::Left) } else if cx >= screen_width as i32 - EDGE_THRESHOLD { Some(Direction::Right) } else if cy <= EDGE_THRESHOLD { Some(Direction::Up) } else if cy >= screen_height as i32 - EDGE_THRESHOLD { Some(Direction::Down) } else { None }; // Check if we should trigger based on dwell time and movement if let Some(edge_dir) = at_edge { // Only care about edges with neighbors if enabled_edges.contains(&edge_dir) { let now = std::time::Instant::now(); // Check if cursor is moving toward the edge (or staying at it) let moving_toward_edge = if let Some((last_x, last_y)) = last_cursor_pos { match edge_dir { Direction::Left => cx <= last_x, Direction::Right => cx >= last_x, Direction::Up => cy <= last_y, Direction::Down => cy >= last_y, } } else { true }; if moving_toward_edge { match &edge_dwell_start { Some((dir, start)) if *dir == edge_dir => { // Already tracking this edge, check if dwell time exceeded if now.duration_since(*start).as_millis() >= EDGE_DWELL_MS as u128 { // Trigger! let has_peer = { let peers = peers.read().await; peers.contains_key(&edge_dir) }; if has_peer { // Check if we're in ReceivedControl state from this direction let current_state = transfer_manager.state().await; if let transfer::TransferState::ReceivedControl { from, .. } = current_state { if from == edge_dir { info!( "CURSOR EDGE: {:?} at ({}, {}) - returning control", edge_dir, cx, cy ); if let Err(e) = transfer_manager.return_control().await { tracing::warn!("Failed to return control: {}", e); } edge_dwell_start = None; continue; } } // Check cooldown to prevent bounce-back if let Some(last_return) = last_control_return { if last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 { tracing::debug!("CURSOR EDGE: {:?} - in cooldown", edge_dir); edge_dwell_start = None; continue; } } info!( "CURSOR EDGE: {:?} at ({}, {}) - initiating transfer", edge_dir, cx, cy ); if let Err(e) = transfer_manager.initiate_transfer( edge_dir, (cx, cy), screen_height, screen_width, ).await { tracing::warn!("Failed to initiate transfer: {}", e); } } else { tracing::debug!( "CURSOR EDGE: {:?} at ({}, {}) but no peer connected", edge_dir, cx, cy ); } // Reset to avoid repeated triggers edge_dwell_start = None; } } _ => { // Start tracking this edge tracing::trace!("Started edge dwell tracking for {:?} at ({}, {})", edge_dir, cx, cy); edge_dwell_start = Some((edge_dir, now)); } } } else { // Moving away from edge, reset edge_dwell_start = None; } } } else { // Not at any edge, reset edge_dwell_start = None; } last_cursor_pos = Some((cx, cy)); } } // Check for transfer timeout (stuck in Initiating state) if let transfer::TransferState::Initiating { started_at, .. } = transfer_manager.state().await { const TRANSFER_TIMEOUT_MS: u128 = 3000; if started_at.elapsed().as_millis() > TRANSFER_TIMEOUT_MS { tracing::warn!("Transfer timed out after {}ms, aborting", TRANSFER_TIMEOUT_MS); transfer_manager.abort().await; } } // Poll for incoming messages from peers (non-blocking) let directions: Vec = { let peers = peers.read().await; peers.keys().cloned().collect() }; // Debug: log state and peers occasionally (every ~5 seconds at 100μs polling) static POLL_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); let count = POLL_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); if count % 50000 == 0 { let state = transfer_manager.state().await; tracing::info!("Poll #{}: state={:?}, peers={:?}", count, state, directions); } for direction in directions { let mut peers = peers.write().await; if let Some(peer) = peers.get_mut(&direction) { // Try non-blocking receive using tokio timeout // Use minimal timeout to avoid blocking the event loop match tokio::time::timeout( std::time::Duration::from_micros(50), peer.recv() ).await { Ok(Ok(Some(msg))) => { tracing::debug!("Received from {:?}: {:?}", direction, msg); // Handle incoming message match msg { Message::Enter(payload) => { info!("Received Enter from {:?}", direction); match transfer_manager.handle_enter( direction, payload, screen_width, screen_height, ).await { Ok(pos) => { info!("Positioned cursor at {:?}", pos); } Err(e) => { tracing::error!("Failed to handle Enter: {}", e); } } } Message::EnterAck(ack) => { info!("Received EnterAck: success={}", ack.success); if let Err(e) = transfer_manager.handle_enter_ack(ack).await { // Usually a benign race condition (collision resolved) tracing::debug!("Failed to handle EnterAck: {}", e); } } Message::Leave(payload) => { info!("Received Leave from {:?}", direction); if let Err(e) = transfer_manager.handle_leave(payload).await { // Usually a benign race condition tracing::debug!("Failed to handle Leave: {}", e); } // Set cooldown to prevent bounce-back loop // When we receive Leave, control is returning to us last_control_return = Some(std::time::Instant::now()); tracing::debug!("Set control return cooldown"); } Message::LeaveAck => { info!("Received LeaveAck"); // Transfer complete } Message::InputEvent(input_payload) => { // Inject input via emulation module if let Some(ref mut emu) = input_emulator { use hyprkvm_common::protocol::InputEventType; match input_payload.event { InputEventType::KeyDown { keycode } => { tracing::debug!("RECV KeyDown: keycode={} ({})", keycode, keycode_to_name(keycode)); emu.keyboard.key(keycode, hyprkvm_common::KeyState::Pressed); } InputEventType::KeyUp { keycode } => { tracing::debug!("RECV KeyUp: keycode={} ({})", keycode, keycode_to_name(keycode)); emu.keyboard.key(keycode, hyprkvm_common::KeyState::Released); } InputEventType::PointerMotion { dx, dy } => { emu.pointer.motion(dx, dy); } InputEventType::PointerButton { button, pressed } => { let state = if pressed { hyprkvm_common::ButtonState::Pressed } else { hyprkvm_common::ButtonState::Released }; emu.pointer.button(button, state); } InputEventType::Scroll { horizontal, vertical } => { emu.pointer.scroll(horizontal, vertical); } InputEventType::ModifierState { .. } => { // Modifier state is informational } } } } Message::Ping { timestamp } => { let _ = peer.send(&Message::Pong { timestamp }).await; } Message::Pong { timestamp } => { tracing::trace!("Pong received, rtt={}ms", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64 - timestamp ); } _ => { tracing::debug!("Unhandled message: {:?}", msg); } } } Ok(Ok(None)) => { // Connection closed info!("Peer {:?} disconnected", direction); peers.remove(&direction); } Ok(Err(e)) => { tracing::error!("Error receiving from {:?}: {}", direction, e); peers.remove(&direction); } Err(_) => { // Timeout - no message available, that's fine } } } } } // Handle transfer events Some(event) = transfer_events.recv() => { match event { transfer::TransferEvent::SendMessage { direction, message } => { let mut peers = peers.write().await; if let Some(peer) = peers.get_mut(&direction) { info!("Sending {:?} to {:?}", message, direction); if let Err(e) = peer.send(&message).await { tracing::error!("Failed to send message to {:?}: {}", direction, e); // If send fails, abort the transfer transfer_manager.abort().await; } } else { tracing::warn!("No peer for direction {:?}, aborting transfer", direction); transfer_manager.abort().await; } } transfer::TransferEvent::StartCapture { direction: cap_dir } => { info!("Starting input capture for {:?}", cap_dir); capture_direction = Some(cap_dir); // Send synthetic Super key-down as first event. // The transfer was likely initiated via Super+Arrow keybinding, // which means Super was already held when the grab started. // The evdev grabber won't see the initial Super key-down, // so we need to send it explicitly so the destination knows // Super is pressed for subsequent keybindings. { let mut peers_guard = peers.write().await; if let Some(peer) = peers_guard.get_mut(&cap_dir) { let super_down = input::GrabEvent::KeyDown { keycode: 125 }; // KEY_LEFTMETA let payload = super_down.to_protocol(input_sequence); input_sequence += 1; tracing::debug!("Sending synthetic Super key-down to destination"); if let Err(e) = peer.send(&Message::InputEvent(payload)).await { tracing::error!("Failed to send synthetic Super: {}", e); } } } input_grabber.start(); } transfer::TransferEvent::StopCapture => { info!("Stopping input capture"); let was_capturing_direction = capture_direction; capture_direction = None; // Release the evdev grab and enter recovery mode for the stale direction // The stale key is the arrow key used to initiate the original outgoing transfer input_grabber.stop(was_capturing_direction); // Drain any remaining events while input_grabber.try_recv().is_some() {} // CRITICAL FIX: After releasing the evdev grab, libinput has stale state. // The arrow key that initiated the original transfer (before we went remote) // is still seen as "pressed" by libinput because it never saw the release. // // We use uinput to create a virtual keyboard and send synthetic key-up // events for ALL arrow keys. This gives libinput fresh key-up events, // which should clear the stale state. if let Some(dir) = was_capturing_direction { // The stale key is the one used to initiate the OUTGOING transfer let stale_keycode: u16 = match dir { Direction::Left => 105, // KEY_LEFT Direction::Right => 106, // KEY_RIGHT Direction::Up => 103, // KEY_UP Direction::Down => 108, // KEY_DOWN }; tracing::info!("Sending synthetic key-ups via uinput to clear stale libinput state"); // Send key-ups for all arrow keys to be safe let all_arrows: [u16; 4] = [103, 105, 106, 108]; if let Err(e) = input::send_synthetic_key_ups(&all_arrows) { tracing::warn!("Failed to send synthetic key-ups: {}", e); } // Also inject via virtual keyboard for Wayland-level cleanup if input_emulator.is_none() { if let Ok(emu) = input::InputEmulator::new() { input_emulator = Some(emu); } } if let Some(ref mut emu) = input_emulator { emu.keyboard.key(stale_keycode as u32, hyprkvm_common::KeyState::Released); emu.keyboard.reset_all_keys(); } } } transfer::TransferEvent::StartInjection { from } => { info!("Starting input injection from {:?}", from); // Create input emulator if not exists if input_emulator.is_none() { match input::InputEmulator::new() { Ok(emu) => { info!("Input emulator created"); input_emulator = Some(emu); } Err(e) => { tracing::error!("Failed to create input emulator: {}", e); } } } } transfer::TransferEvent::StopInjection => { info!("Stopping input injection"); // Reset ALL pressed keys so next session starts clean // This prevents Hyprland from seeing stale key state // (e.g., arrow key that triggered return was never released) if let Some(ref mut emu) = input_emulator { emu.keyboard.reset_all_keys(); } } } } // Hyprland events event = event_stream.next_event() => { match event { Ok(evt) => { tracing::trace!("Hyprland event: {:?}", evt); } Err(e) => { tracing::error!("Event error: {e}"); break; } } } // Handle IPC requests from CLI Some((request, response_tx)) = ipc_rx.recv() => { use hyprkvm_common::protocol::{IpcRequest, IpcResponse}; let response = match request { IpcRequest::Move { direction } => { // Log current state for debugging let current_state = transfer_manager.state().await; info!("IPC Move {:?}: state={:?}", direction, current_state); // For keyboard navigation, check if we're at the absolute edge: // 1. On edge monitor (no monitor in that direction) // 2. On edge window of that monitor (no window further in that direction) let at_edge = 'edge_check: { // Get monitors and find focused one let monitors = match hypr_client.monitors().await { Ok(m) => m, Err(e) => { info!(" edge_check: monitors query failed: {}", e); break 'edge_check false; } }; let focused_monitor = match monitors.iter().find(|m| m.focused) { Some(m) => m, None => { info!(" edge_check: no focused monitor found"); break 'edge_check false; } }; // Check if there's another monitor in the requested direction let has_monitor_in_direction = monitors.iter().any(|m| { if m.id == focused_monitor.id { return false; } match direction { Direction::Left => m.x + m.width as i32 <= focused_monitor.x, Direction::Right => m.x >= focused_monitor.x + focused_monitor.width as i32, Direction::Up => m.y + m.height as i32 <= focused_monitor.y, Direction::Down => m.y >= focused_monitor.y + focused_monitor.height as i32, } }); if has_monitor_in_direction { // There's a monitor in that direction, not at edge info!(" edge_check: has monitor in direction {:?}", direction); break 'edge_check false; } // We're on the edge monitor. Now check if we're on the edge window. // Get active window position let active_window: serde_json::Value = match hypr_client.query("activewindow").await { Ok(w) => w, Err(e) => { info!(" edge_check: activewindow query failed: {}", e); break 'edge_check false; } }; let win_x = active_window.get("at").and_then(|a| a.get(0)).and_then(|x| x.as_i64()).unwrap_or(0) as i32; let win_y = active_window.get("at").and_then(|a| a.get(1)).and_then(|y| y.as_i64()).unwrap_or(0) as i32; let win_w = active_window.get("size").and_then(|s| s.get(0)).and_then(|w| w.as_i64()).unwrap_or(100) as i32; let win_h = active_window.get("size").and_then(|s| s.get(1)).and_then(|h| h.as_i64()).unwrap_or(100) as i32; // Get all clients (windows) let clients: Vec = match hypr_client.query("clients").await { Ok(c) => c, Err(e) => { info!(" edge_check: clients query failed: {}", e); break 'edge_check false; } }; info!(" edge_check: active window at ({},{}) size {}x{}, {} clients on monitor", win_x, win_y, win_w, win_h, clients.iter().filter(|c| c.get("monitor").and_then(|m| m.as_i64()).unwrap_or(-1) as i32 == focused_monitor.id).count()); // Check if any window is further in the requested direction on same monitor let has_window_in_direction = clients.iter().any(|client| { let mon = client.get("monitor").and_then(|m| m.as_i64()).unwrap_or(-1) as i32; if mon != focused_monitor.id { return false; } let cx = client.get("at").and_then(|a| a.get(0)).and_then(|x| x.as_i64()).unwrap_or(0) as i32; let cy = client.get("at").and_then(|a| a.get(1)).and_then(|y| y.as_i64()).unwrap_or(0) as i32; let cw = client.get("size").and_then(|s| s.get(0)).and_then(|w| w.as_i64()).unwrap_or(0) as i32; let ch = client.get("size").and_then(|s| s.get(1)).and_then(|h| h.as_i64()).unwrap_or(0) as i32; match direction { Direction::Left => cx + cw < win_x + 10, // Window is to the left Direction::Right => cx > win_x + win_w - 10, // Window is to the right Direction::Up => cy + ch < win_y + 10, Direction::Down => cy > win_y + win_h - 10, } }); info!(" edge_check: has_window_in_direction={} -> at_edge={}", has_window_in_direction, !has_window_in_direction); !has_window_in_direction }; // Check if we have a peer in this direction let has_peer = { let peers = peers.read().await; peers.contains_key(&direction) }; // Get neighbor name if configured let neighbor_name = config.machines.neighbors .iter() .find(|n| n.direction == direction) .map(|n| n.name.clone()); info!("IPC Move {:?}: at_edge={}, has_peer={}, neighbor={:?}", direction, at_edge, has_peer, neighbor_name); // At edge with peer: either return control or initiate transfer if at_edge && has_peer && neighbor_name.is_some() { // Check if we're in ReceivedControl state from this direction if let transfer::TransferState::ReceivedControl { from, .. } = current_state { if from == direction { // Return control to source machine tracing::info!("Keyboard return: at edge, returning control to {:?}", direction); if let Err(e) = transfer_manager.return_control().await { tracing::warn!("Failed to return control: {}", e); IpcResponse::Error { message: format!("Return failed: {}", e) } } else { IpcResponse::Transferred { to_machine: neighbor_name.unwrap() } } } else { // At edge with peer but received control from different direction // Initiate new transfer let cursor_pos = hypr_client.cursor_pos().await .map(|c| (c.x, c.y)) .unwrap_or((0, 0)); if let Err(e) = transfer_manager.initiate_transfer( direction, cursor_pos, screen_height, screen_width, ).await { IpcResponse::Error { message: format!("Transfer failed: {}", e) } } else { IpcResponse::Transferred { to_machine: neighbor_name.unwrap() } } } } else { // Not in ReceivedControl - initiate new transfer let cursor_pos = hypr_client.cursor_pos().await .map(|c| (c.x, c.y)) .unwrap_or((0, 0)); if let Err(e) = transfer_manager.initiate_transfer( direction, cursor_pos, screen_height, screen_width, ).await { IpcResponse::Error { message: format!("Transfer failed: {}", e) } } else { IpcResponse::Transferred { to_machine: neighbor_name.unwrap() } } } } else { // Either not at edge, or at edge but no peer - do local movefocus let hypr_dir = match direction { Direction::Left => "l", Direction::Right => "r", Direction::Up => "u", Direction::Down => "d", }; info!("IPC Move {:?}: doing local movefocus {}", direction, hypr_dir); match hypr_client.dispatch("movefocus", hypr_dir).await { Ok(()) => info!(" movefocus succeeded"), Err(e) => tracing::error!(" movefocus failed: {}", e), } IpcResponse::DoLocalMove } } IpcRequest::Status => { let state = format!("{:?}", transfer_manager.state().await); let connected_peers: Vec = { let peers = peers.read().await; config.machines.neighbors .iter() .filter(|n| peers.contains_key(&n.direction)) .map(|n| n.name.clone()) .collect() }; let uptime_secs = daemon_start_time.elapsed().as_secs(); IpcResponse::Status { state, connected_peers, uptime_secs, machine_name: config.machines.self_name.clone(), } } IpcRequest::ListPeers => { let peers_guard = peers.read().await; let peer_list: Vec = config.machines.neighbors .iter() .map(|n| { let connected = peers_guard.contains_key(&n.direction); let status = if connected { "connected".to_string() } else { "disconnected".to_string() }; hyprkvm_common::protocol::PeerInfo { name: n.name.clone(), direction: n.direction, connected, address: n.address.to_string(), status, } }) .collect(); IpcResponse::Peers { peers: peer_list } } IpcRequest::PingPeer { peer_name } => { // Find the peer by name let neighbor = config.machines.neighbors .iter() .find(|n| n.name == peer_name); match neighbor { Some(n) => { let direction = n.direction; let mut peers_guard = peers.write().await; if let Some(peer_conn) = peers_guard.get_mut(&direction) { // Send Ping with current timestamp let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64; if let Err(e) = peer_conn.send(&Message::Ping { timestamp }).await { IpcResponse::PingResult { peer_name, latency_ms: None, error: Some(format!("Send failed: {}", e)), } } else { // Wait for Pong with timeout match tokio::time::timeout( std::time::Duration::from_secs(5), peer_conn.recv() ).await { Ok(Ok(Some(Message::Pong { timestamp: pong_ts }))) => { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis() as u64; let latency = now.saturating_sub(pong_ts); IpcResponse::PingResult { peer_name, latency_ms: Some(latency), error: None, } } Ok(Ok(Some(_))) => { IpcResponse::PingResult { peer_name, latency_ms: None, error: Some("Unexpected response".to_string()), } } Ok(Ok(None)) => { // Connection closed peers_guard.remove(&direction); IpcResponse::PingResult { peer_name, latency_ms: None, error: Some("Connection closed".to_string()), } } Ok(Err(e)) => { IpcResponse::PingResult { peer_name, latency_ms: None, error: Some(format!("Receive error: {}", e)), } } Err(_) => { IpcResponse::PingResult { peer_name, latency_ms: None, error: Some("Timeout".to_string()), } } } } } else { IpcResponse::PingResult { peer_name, latency_ms: None, error: Some("Peer not connected".to_string()), } } } None => { IpcResponse::Error { message: format!("Unknown peer: {}", peer_name), } } } } // ================================================================ // CLI Expansion: Control Transfer // ================================================================ IpcRequest::Switch { target } => { use hyprkvm_common::protocol::SwitchTarget; // Resolve target to a direction let direction = match &target { SwitchTarget::Direction(dir) => Some(*dir), SwitchTarget::MachineName(name) => { config.machines.neighbors .iter() .find(|n| &n.name == name) .map(|n| n.direction) } }; match direction { Some(dir) => { let peers_guard = peers.read().await; if peers_guard.get(&dir).is_some() { drop(peers_guard); // Get cursor position and screen size from Hyprland let (cursor_pos, screen_width, screen_height) = match hypr_client.monitors().await { Ok(monitors) => { if let Some(focused) = monitors.iter().find(|m| m.focused) { // Use center of screen as cursor position for switch let cx = focused.x + focused.width as i32 / 2; let cy = focused.y + focused.height as i32 / 2; ((cx, cy), focused.width, focused.height) } else { ((0, 0), 1920, 1080) // Fallback } } Err(_) => ((0, 0), 1920, 1080), // Fallback }; // Initiate transfer match transfer_manager.initiate_transfer(dir, cursor_pos, screen_height, screen_width).await { Ok(()) => { let machine_name = config.machines.neighbors .iter() .find(|n| n.direction == dir) .map(|n| n.name.clone()) .unwrap_or_else(|| format!("{:?}", dir)); IpcResponse::Transferred { to_machine: machine_name } } Err(e) => IpcResponse::Error { message: format!("Transfer failed: {}", e), } } } else { IpcResponse::Error { message: format!("Peer not connected in direction {:?}", dir), } } } None => { let name = match target { SwitchTarget::MachineName(n) => n, _ => "unknown".to_string(), }; IpcResponse::Error { message: format!("Unknown machine: {}", name), } } } } IpcRequest::Return => { match transfer_manager.return_control().await { Ok(()) => IpcResponse::Ok { message: "Control returned".to_string(), }, Err(e) => IpcResponse::Error { message: format!("Return failed: {}", e), } } } // ================================================================ // CLI Expansion: Input Management // ================================================================ IpcRequest::Release => { // Stop input grabbing input_grabber.stop(None); // Abort any pending transfer transfer_manager.abort().await; IpcResponse::Ok { message: "Input released".to_string(), } } IpcRequest::SetBarrier { enabled } => { barrier_enabled.store(enabled, std::sync::atomic::Ordering::SeqCst); let status = if enabled { "enabled" } else { "disabled" }; IpcResponse::Ok { message: format!("Barrier {}", status), } } // ================================================================ // CLI Expansion: Connection Management // ================================================================ IpcRequest::Disconnect { peer_name } => { let neighbor = config.machines.neighbors .iter() .find(|n| n.name == peer_name); match neighbor { Some(n) => { let direction = n.direction; let mut peers_guard = peers.write().await; if let Some(mut peer_conn) = peers_guard.remove(&direction) { // Send goodbye before disconnecting let _ = peer_conn.send(&Message::Goodbye).await; IpcResponse::Ok { message: format!("Disconnected from {}", peer_name), } } else { IpcResponse::Error { message: format!("Peer {} not connected", peer_name), } } } None => IpcResponse::Error { message: format!("Unknown peer: {}", peer_name), } } } IpcRequest::Reconnect { peer_name } => { let neighbor = config.machines.neighbors .iter() .find(|n| n.name == peer_name) .cloned(); match neighbor { Some(n) => { let direction = n.direction; let addr = n.address; // Remove existing connection if any { let mut peers_guard = peers.write().await; if let Some(mut peer_conn) = peers_guard.remove(&direction) { let _ = peer_conn.send(&Message::Goodbye).await; } } // Spawn reconnection task (same logic as initial connection) let peers_clone = peers.clone(); let machine_name = config.machines.self_name.clone(); let neighbor_name = n.name.clone(); tokio::spawn(async move { match network::connect(addr).await { Ok(mut conn) => { // Send Hello let hello = Message::Hello(HelloPayload { protocol_version: PROTOCOL_VERSION, machine_name, capabilities: vec![], }); if let Err(e) = conn.send(&hello).await { tracing::error!("Reconnect: failed to send Hello: {}", e); return; } // Wait for HelloAck match conn.recv().await { Ok(Some(Message::HelloAck(ack))) if ack.accepted => { let mut peers_guard = peers_clone.write().await; peers_guard.insert(direction, conn); info!("Reconnected to {}", neighbor_name); } Ok(Some(Message::HelloAck(ack))) => { tracing::error!("Reconnect rejected: {:?}", ack.error); } _ => { tracing::error!("Reconnect: handshake failed"); } } } Err(e) => { tracing::error!("Reconnect: connection failed: {}", e); } } }); IpcResponse::Ok { message: format!("Reconnecting to {}", peer_name), } } None => IpcResponse::Error { message: format!("Unknown peer: {}", peer_name), } } } // ================================================================ // CLI Expansion: Configuration // ================================================================ IpcRequest::GetConfig => { match toml::to_string_pretty(&config) { Ok(toml_str) => IpcResponse::Config { toml: toml_str }, Err(e) => IpcResponse::Error { message: format!("Failed to serialize config: {}", e), } } } IpcRequest::Reload => { // TODO: Implement config hot-reload IpcResponse::Error { message: "Config reload not yet implemented".to_string(), } } // ================================================================ // CLI Expansion: Daemon Control // ================================================================ IpcRequest::Shutdown => { info!("Shutdown requested via IPC"); shutdown_requested.store(true, std::sync::atomic::Ordering::SeqCst); IpcResponse::Ok { message: "Shutting down...".to_string(), } } IpcRequest::GetLogs { lines, follow: _ } => { // Read from log file let log_path = dirs::data_local_dir() .unwrap_or_else(|| std::path::PathBuf::from("/tmp")) .join("hyprkvm") .join("daemon.log"); if log_path.exists() { match std::fs::read_to_string(&log_path) { Ok(content) => { let n = lines.unwrap_or(50) as usize; let log_lines: Vec = content .lines() .rev() .take(n) .map(|s| s.to_string()) .collect::>() .into_iter() .rev() .collect(); IpcResponse::Logs { lines: log_lines } } Err(e) => IpcResponse::Error { message: format!("Failed to read log file: {}", e), } } } else { IpcResponse::Logs { lines: vec!["Log file not found. File logging may not be configured.".to_string()], } } } }; let _ = response_tx.send(response); } // Shutdown (Ctrl+C or IPC request) _ = tokio::signal::ctrl_c() => { info!("Shutting down (Ctrl+C)..."); accept_handle.abort(); break; } // Check for IPC shutdown request _ = async { while !shutdown_requested.load(std::sync::atomic::Ordering::SeqCst) { tokio::time::sleep(std::time::Duration::from_millis(100)).await; } } => { info!("Shutting down (IPC request)..."); accept_handle.abort(); break; } } } Ok(()) } async fn show_status() -> anyhow::Result<()> { // TODO: Connect to running daemon via IPC and get status println!("HyprKVM Status"); println!("=============="); println!("Daemon: not implemented yet"); Ok(()) } async fn handle_move(direction: &str) -> anyhow::Result<()> { use hyprkvm_common::Direction; use hyprkvm_common::protocol::{IpcRequest, IpcResponse}; let dir: Direction = direction.parse()?; // Try to connect to daemon match ipc::IpcClient::connect().await { Ok(mut client) => { // Ask daemon to handle the move (it does movefocus internally) let request = IpcRequest::Move { direction: dir }; match client.request(&request).await { Ok(IpcResponse::Transferred { to_machine }) => { tracing::info!("Transferred control to {}", to_machine); } Ok(IpcResponse::DoLocalMove) => { // Daemon handled it } Ok(IpcResponse::Error { message }) => { tracing::warn!("Daemon error: {}", message); } Ok(_) => { tracing::warn!("Unexpected response from daemon"); } Err(e) => { tracing::debug!("IPC request failed: {}, falling back to local", e); do_local_move(dir).await?; } } } Err(e) => { tracing::debug!("Daemon not running ({}), doing local move", e); do_local_move(dir).await?; } } Ok(()) } async fn do_local_move(dir: hyprkvm_common::Direction) -> anyhow::Result<()> { use hyprkvm_common::Direction; let hypr_dir = match dir { Direction::Left => "l", Direction::Right => "r", Direction::Up => "u", Direction::Down => "d", }; let output = tokio::process::Command::new("hyprctl") .args(["dispatch", "movefocus", hypr_dir]) .output() .await?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); tracing::error!("hyprctl failed: {}", stderr); } Ok(()) } fn show_config(config_path: &std::path::Path) -> anyhow::Result<()> { if config_path.exists() { let content = std::fs::read_to_string(config_path)?; println!("{}", content); } else { println!("No config file at {:?}", config_path); println!("\nDefault configuration:"); let default = Config::default(); println!("{}", toml::to_string_pretty(&default)?); } Ok(()) } async fn reload_config() -> anyhow::Result<()> { // TODO: Send reload signal to daemon println!("Config reload not implemented yet"); Ok(()) }