Rust · 147124 bytes Raw Blame History
1 //! HyprKVM Daemon - Main entry point
2 //!
3 //! The daemon handles:
4 //! - Hyprland IPC communication
5 //! - Edge detection (mouse and keyboard)
6 //! - Network connections to peer machines
7 //! - Input capture and injection
8
9 use clap::{Parser, Subcommand};
10 use tracing::{info, Level};
11 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
12
13 mod clipboard;
14 mod config;
15 #[cfg(feature = "gui")]
16 mod gui;
17 mod hyprland;
18 mod input;
19 mod ipc;
20 mod network;
21 mod state;
22 mod transfer;
23
24 use config::Config;
25
26 /// Convert keycode to human-readable name for logging
27 fn keycode_to_name(keycode: u32) -> &'static str {
28 match keycode {
29 1 => "ESC",
30 14 => "BACKSPACE",
31 15 => "TAB",
32 28 => "ENTER",
33 29 => "LEFTCTRL",
34 42 => "LEFTSHIFT",
35 54 => "RIGHTSHIFT",
36 56 => "LEFTALT",
37 57 => "SPACE",
38 58 => "CAPSLOCK",
39 97 => "RIGHTCTRL",
40 100 => "RIGHTALT",
41 103 => "UP",
42 105 => "LEFT",
43 106 => "RIGHT",
44 108 => "DOWN",
45 125 => "LEFTMETA",
46 126 => "RIGHTMETA",
47 _ => "OTHER",
48 }
49 }
50
51 #[derive(Parser)]
52 #[command(name = "hyprkvm")]
53 #[command(about = "Hyprland-native software KVM switch")]
54 #[command(version)]
55 struct Cli {
56 /// Config file path
57 #[arg(short, long)]
58 config: Option<std::path::PathBuf>,
59
60 /// Increase log verbosity (-v, -vv, -vvv)
61 #[arg(short, long, action = clap::ArgAction::Count)]
62 verbose: u8,
63
64 #[command(subcommand)]
65 command: Option<Commands>,
66 }
67
68 #[derive(Subcommand)]
69 enum Commands {
70 /// Start the HyprKVM daemon
71 Daemon,
72
73 /// Show daemon status
74 Status,
75
76 /// Handle a move request (called by keybinding script)
77 Move {
78 /// Direction to move
79 direction: String,
80 },
81
82 /// Configuration management
83 Config {
84 #[command(subcommand)]
85 action: ConfigAction,
86 },
87
88 /// Launch the graphical configuration interface
89 #[cfg(feature = "gui")]
90 Gui,
91 }
92
93 #[derive(Subcommand)]
94 enum ConfigAction {
95 /// Show current configuration
96 Show,
97 /// Reload configuration
98 Reload,
99 }
100
101 fn main() -> anyhow::Result<()> {
102 let cli = Cli::parse();
103
104 // Set up logging with dual output (stderr + file)
105 let log_level = match cli.verbose {
106 0 => Level::INFO,
107 1 => Level::DEBUG,
108 _ => Level::TRACE,
109 };
110
111 // Create log directory
112 let log_dir = dirs::data_local_dir()
113 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
114 .join("hyprkvm");
115 std::fs::create_dir_all(&log_dir).ok();
116
117 // File appender with daily rotation
118 let file_appender = tracing_appender::rolling::daily(&log_dir, "daemon.log");
119 let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
120
121 // Build subscriber with both stderr and file layers
122 let subscriber = tracing_subscriber::registry()
123 .with(
124 tracing_subscriber::fmt::layer()
125 .with_target(false)
126 .with_writer(std::io::stderr)
127 )
128 .with(
129 tracing_subscriber::fmt::layer()
130 .with_target(true)
131 .with_ansi(false)
132 .with_writer(non_blocking)
133 )
134 .with(
135 tracing_subscriber::filter::LevelFilter::from_level(log_level)
136 );
137 subscriber.init();
138
139 // Load configuration path
140 let config_path = cli.config.unwrap_or_else(|| {
141 dirs::config_dir()
142 .unwrap_or_else(|| std::path::PathBuf::from("."))
143 .join("hyprkvm")
144 .join("hyprkvm.toml")
145 });
146
147 // Handle GUI: either explicit `gui` command OR no command (default)
148 #[cfg(feature = "gui")]
149 if cli.command.is_none() || matches!(cli.command, Some(Commands::Gui)) {
150 info!("Starting HyprKVM GUI...");
151 return gui::run_gui(&config_path);
152 }
153
154 // If GUI feature not enabled and no command given, show helpful message
155 #[cfg(not(feature = "gui"))]
156 if cli.command.is_none() {
157 eprintln!("No command specified. Available commands:");
158 eprintln!(" hyprkvm daemon - Start the KVM daemon");
159 eprintln!(" hyprkvm status - Show daemon status");
160 eprintln!(" hyprkvm config - Configuration management");
161 eprintln!();
162 eprintln!("To enable the GUI, rebuild with: cargo build --features gui");
163 std::process::exit(1);
164 }
165
166 // Run async commands in tokio runtime
167 // At this point we know command is Some(...) because None cases are handled above
168 let command = cli.command.expect("command should be Some at this point");
169
170 tokio::runtime::Builder::new_multi_thread()
171 .enable_all()
172 .build()?
173 .block_on(async {
174 match command {
175 Commands::Daemon => {
176 info!("Starting HyprKVM daemon...");
177 run_daemon(&config_path).await
178 }
179 Commands::Status => {
180 show_status().await
181 }
182 Commands::Move { direction } => {
183 handle_move(&direction).await
184 }
185 Commands::Config { action } => {
186 match action {
187 ConfigAction::Show => show_config(&config_path),
188 ConfigAction::Reload => reload_config().await,
189 }
190 }
191 #[cfg(feature = "gui")]
192 Commands::Gui => unreachable!("GUI handled above"),
193 }
194 })
195 }
196
197 async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
198 use std::collections::HashMap;
199 use std::net::SocketAddr;
200 use std::sync::Arc;
201 use tokio::sync::RwLock;
202 use hyprkvm_common::Direction;
203 use hyprkvm_common::protocol::{Message, HelloPayload, PROTOCOL_VERSION};
204
205 // Load or create default config
206 let mut config = match Config::load(config_path) {
207 Ok(cfg) => cfg,
208 Err(e) => {
209 tracing::warn!("Failed to load config: {e}, using defaults");
210 Config::default()
211 }
212 };
213
214 info!("Machine name: {}", config.machines.self_name);
215 info!("Listening on port: {}", config.network.listen_port);
216
217 // Track daemon start time for uptime reporting
218 let daemon_start_time = std::time::Instant::now();
219
220 // State flags for CLI control
221 let barrier_enabled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
222 let shutdown_requested = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
223
224 // Connect to Hyprland
225 info!("Connecting to Hyprland...");
226 let hypr_client = hyprland::ipc::HyprlandClient::new().await?;
227
228 // Query monitors to validate connection
229 let monitors = hypr_client.monitors().await?;
230 info!("Connected to Hyprland. Monitors: {}", monitors.len());
231 for mon in &monitors {
232 info!(" {} at ({}, {}) {}x{}", mon.name, mon.x, mon.y, mon.width, mon.height);
233 }
234
235 // Calculate screen bounds in LOGICAL coordinates (cursor position uses logical coords)
236 // Hyprland reports physical dimensions, but cursor_pos() returns logical coordinates
237 // Logical size = physical size / scale
238 let screen_min_x: i32 = monitors.iter().map(|m| m.x).min().unwrap_or(0);
239 let screen_min_y: i32 = monitors.iter().map(|m| m.y).min().unwrap_or(0);
240 let screen_max_x: i32 = monitors.iter().map(|m| {
241 let logical_width = (m.width as f32 / m.scale).round() as i32;
242 m.x + logical_width
243 }).max().unwrap_or(1920);
244 let screen_max_y: i32 = monitors.iter().map(|m| {
245 let logical_height = (m.height as f32 / m.scale).round() as i32;
246 m.y + logical_height
247 }).max().unwrap_or(1080);
248 let screen_width: u32 = (screen_max_x - screen_min_x) as u32;
249 let screen_height: u32 = (screen_max_y - screen_min_y) as u32;
250 info!("Screen bounds (logical): ({}, {}) to ({}, {}), dimensions: {}x{}",
251 screen_min_x, screen_min_y, screen_max_x, screen_max_y, screen_width, screen_height);
252
253 // Determine which edges have network neighbors
254 let mut enabled_edges = Vec::new();
255 let mut neighbor_map: HashMap<Direction, SocketAddr> = HashMap::new();
256 for neighbor in &config.machines.neighbors {
257 enabled_edges.push(neighbor.direction);
258 neighbor_map.insert(neighbor.direction, neighbor.address);
259 info!(" Network neighbor: {} ({}) at {}", neighbor.name, neighbor.direction, neighbor.address);
260 }
261
262 // If no neighbors configured, just run in demo mode
263 if enabled_edges.is_empty() {
264 info!("No neighbors configured. Add neighbors in config to enable control transfer.");
265 enabled_edges = vec![Direction::Left, Direction::Right];
266 }
267
268 // Start edge capture
269 info!("Starting edge capture for: {:?}", enabled_edges);
270 let monitor_infos: Vec<input::MonitorInfo> = monitors.iter().map(|m| input::MonitorInfo {
271 name: m.name.clone(),
272 x: m.x,
273 y: m.y,
274 width: m.width,
275 height: m.height,
276 scale: m.scale,
277 }).collect();
278
279 // Store per-monitor logical bounds for cursor edge detection
280 // Each tuple: (x, y, logical_width, logical_height)
281 let monitor_logical_bounds: Vec<(i32, i32, i32, i32)> = monitors.iter().map(|m| {
282 let logical_width = (m.width as f32 / m.scale).round() as i32;
283 let logical_height = (m.height as f32 / m.scale).round() as i32;
284 (m.x, m.y, logical_width, logical_height)
285 }).collect();
286
287 let edge_capture = input::EdgeCapture::new(input::EdgeCaptureConfig {
288 barrier_size: 1,
289 enabled_edges: enabled_edges.clone(),
290 monitors: monitor_infos,
291 })?;
292
293 // Create input grabber (for when we send control elsewhere)
294 // Use evdev-based grabber for reliable input capture at kernel level
295 let input_grabber = input::EvdevGrabber::new()?;
296
297 // Create input emulator (for when we receive control from elsewhere)
298 // This is created lazily when we first need to inject
299 let mut input_emulator: Option<input::InputEmulator> = None;
300
301 // Create transfer manager
302 let (transfer_manager, mut transfer_events) = transfer::TransferManager::new(
303 config.machines.self_name.clone(),
304 );
305 let transfer_manager = Arc::new(transfer_manager);
306
307 // Create clipboard manager
308 let clipboard_manager = std::sync::Arc::new(clipboard::ClipboardManager::new(
309 config.clipboard.clone(),
310 ));
311
312 // Track which direction we're capturing for
313 let mut capture_direction: Option<Direction> = None;
314 // Track relay mode: (from, to) when forwarding input from one peer to another
315 let mut relay_mode: Option<(Direction, Direction)> = None;
316 let mut input_sequence: u64 = 0;
317
318 // Escape key detection
319 // KEY_SCROLLLOCK = 70 in Linux evdev keycodes
320 const KEY_SCROLLLOCK: u32 = 70;
321 const KEY_LEFTSHIFT: u32 = 42;
322 const KEY_RIGHTSHIFT: u32 = 54;
323 let mut shift_tap_times: Vec<std::time::Instant> = Vec::new();
324 let triple_tap_window = std::time::Duration::from_millis(
325 config.input.escape_hotkey.triple_tap_window_ms
326 );
327
328 // Cursor-based edge detection state
329 let mut last_cursor_pos: Option<(i32, i32)> = None;
330 let mut edge_dwell_start: Option<(Direction, std::time::Instant)> = None;
331 const EDGE_THRESHOLD: i32 = 2; // Pixels from edge to count as "at edge"
332 const EDGE_DWELL_MS: u64 = 50; // How long cursor must be at edge to trigger
333
334 // Cooldown after control returns to prevent immediate bounce-back
335 let mut last_control_return: Option<std::time::Instant> = None;
336 const CONTROL_RETURN_COOLDOWN_MS: u64 = 1000; // 1000ms cooldown after control returns (prevents bounce-back)
337
338 // Connection storage: direction -> peer connection
339 let peers: Arc<RwLock<HashMap<Direction, network::FramedConnection>>> =
340 Arc::new(RwLock::new(HashMap::new()));
341
342 // TLS setup (if enabled)
343 let tls_enabled = config.network.tls.enabled;
344 if tls_enabled {
345 info!("TLS is enabled, ensuring certificates exist...");
346 let cert_path = std::path::Path::new(&config.network.tls.cert_path);
347 let key_path = std::path::Path::new(&config.network.tls.key_path);
348
349 if let Err(e) = network::ensure_certificate(cert_path, key_path, &config.machines.self_name) {
350 anyhow::bail!("Failed to setup TLS certificates: {}", e);
351 }
352
353 // Print certificate fingerprint for users to share
354 match network::get_cert_fingerprint(cert_path) {
355 Ok(fp) => {
356 info!("Certificate fingerprint: {}", fp);
357 info!("Share this fingerprint with peers for secure verification");
358 }
359 Err(e) => {
360 tracing::warn!("Could not read certificate fingerprint: {}", e);
361 }
362 }
363 } else {
364 info!("TLS is disabled (plain TCP mode for backwards compatibility)");
365 }
366
367 // Load known hosts for TOFU
368 let known_hosts_path = network::KnownHosts::default_path();
369 let known_hosts = match network::KnownHosts::load(&known_hosts_path) {
370 Ok(kh) => {
371 if !kh.hosts.is_empty() {
372 info!("Loaded {} known host(s) from {:?}", kh.hosts.len(), known_hosts_path);
373 }
374 Arc::new(RwLock::new(kh))
375 }
376 Err(e) => {
377 tracing::warn!("Failed to load known hosts: {}, starting fresh", e);
378 Arc::new(RwLock::new(network::KnownHosts::default()))
379 }
380 };
381 let tofu_enabled = config.network.tls.tofu;
382
383 // Start network server
384 let listen_addr: SocketAddr = format!("0.0.0.0:{}", config.network.listen_port).parse()?;
385 let server = if tls_enabled {
386 let cert_path = std::path::Path::new(&config.network.tls.cert_path);
387 let key_path = std::path::Path::new(&config.network.tls.key_path);
388 network::Server::bind_tls(listen_addr, cert_path, key_path).await?
389 } else {
390 network::Server::bind(listen_addr).await?
391 };
392 info!("Listening for connections on {} (TLS: {})", server.local_addr(), tls_enabled);
393
394 // Channel for signaling config changes that require restart
395 let (restart_tx, mut restart_rx) = tokio::sync::mpsc::channel::<String>(1);
396
397 // Spawn task to accept incoming connections
398 let machine_name = config.machines.self_name.clone();
399 let neighbors_for_accept = config.machines.neighbors.clone();
400 let peers_for_accept = peers.clone();
401 let known_hosts_for_accept = known_hosts.clone();
402 let known_hosts_path_for_accept = known_hosts_path.clone();
403 let config_path_for_accept = config_path.to_path_buf();
404 let restart_tx_for_accept = restart_tx.clone();
405 let accept_handle = tokio::spawn(async move {
406 loop {
407 match server.accept().await {
408 Ok(mut conn) => {
409 let addr = conn.remote_addr();
410 info!("Incoming connection from {}", addr);
411
412 // Receive Hello
413 match conn.recv().await {
414 Ok(Some(Message::Hello(hello))) => {
415 info!("Peer {} connected (protocol v{})", hello.machine_name, hello.protocol_version);
416
417 // Verify TLS fingerprint if this is a TLS connection
418 if let Some(peer_fp) = conn.peer_fingerprint() {
419 let mut kh = known_hosts_for_accept.write().await;
420 match kh.is_trusted(&hello.machine_name, peer_fp) {
421 network::TrustStatus::Trusted => {
422 info!("Peer {} fingerprint verified", hello.machine_name);
423 kh.touch(&hello.machine_name);
424 }
425 network::TrustStatus::Unknown => {
426 if tofu_enabled {
427 info!("TOFU: Trusting new peer {} with fingerprint {}", hello.machine_name, peer_fp);
428 kh.trust_host(&hello.machine_name, peer_fp);
429 if let Err(e) = kh.save(&known_hosts_path_for_accept) {
430 tracing::error!("Failed to save known hosts: {}", e);
431 }
432 } else {
433 tracing::warn!("Unknown peer {} fingerprint and TOFU disabled, rejecting", hello.machine_name);
434 continue;
435 }
436 }
437 network::TrustStatus::Changed { old_fingerprint, new_fingerprint } => {
438 tracing::error!(
439 "SECURITY WARNING: Peer {} fingerprint CHANGED!\n Old: {}\n New: {}\n This could indicate a man-in-the-middle attack!",
440 hello.machine_name, old_fingerprint, new_fingerprint
441 );
442 continue; // Reject the connection
443 }
444 }
445 }
446
447 // Send HelloAck
448 let ack = Message::HelloAck(hyprkvm_common::protocol::HelloAckPayload {
449 accepted: true,
450 protocol_version: PROTOCOL_VERSION,
451 machine_name: machine_name.clone(),
452 error: None,
453 });
454 if let Err(e) = conn.send(&ack).await {
455 tracing::error!("Failed to send HelloAck: {}", e);
456 continue;
457 }
458
459 // Determine direction: use opposite of what peer told us, or fall back to config
460 let direction = if let Some(peer_dir) = hello.my_direction_for_you {
461 // Peer says "I have you as X", so we store them as opposite(X)
462 Some(peer_dir.opposite())
463 } else {
464 // Legacy: look up in our config
465 neighbors_for_accept
466 .iter()
467 .find(|n| n.name == hello.machine_name)
468 .map(|n| n.direction)
469 };
470
471 // NOTE: We no longer auto-correct direction based on peer claims.
472 // Direction changes are now handled explicitly via DirectionChange messages
473 // sent when the user changes direction in the GUI.
474 // This prevents the config from being overwritten on reconnect.
475 if let Some(peer_dir) = hello.my_direction_for_you {
476 let new_dir = peer_dir.opposite();
477 let existing = neighbors_for_accept
478 .iter()
479 .find(|n| n.name == hello.machine_name);
480
481 if let Some(existing_neighbor) = existing {
482 if existing_neighbor.direction != new_dir {
483 // Just log the mismatch, don't auto-correct
484 // The user should update both configs via the GUI
485 tracing::warn!(
486 "Direction mismatch for {}: our config says {:?}, peer claims {:?}. \
487 Use the GUI to update directions on both machines.",
488 hello.machine_name, existing_neighbor.direction, new_dir
489 );
490 }
491 }
492 }
493
494 if let Some(dir) = direction {
495 let mut peers = peers_for_accept.write().await;
496 if peers.contains_key(&dir) {
497 info!("Already have connection for {:?}, dropping incoming from {}", dir, hello.machine_name);
498 // Drop the incoming connection, keep the existing one
499 } else {
500 info!("Storing incoming connection from {} as {:?} (peer claimed {:?})",
501 hello.machine_name, dir, hello.my_direction_for_you);
502 peers.insert(dir, conn);
503 }
504 } else {
505 tracing::warn!(
506 "Unknown peer '{}' connected - not in neighbors list and no direction provided",
507 hello.machine_name
508 );
509 // Connection will be dropped
510 }
511 }
512 Ok(Some(other)) => {
513 tracing::warn!("Expected Hello, got {:?}", other);
514 }
515 Ok(None) => {
516 tracing::debug!("Connection closed during handshake");
517 }
518 Err(e) => {
519 tracing::error!("Handshake error: {}", e);
520 }
521 }
522 }
523 Err(e) => {
524 tracing::error!("Accept error: {}", e);
525 }
526 }
527 }
528 });
529
530 // Connect to configured peers (with retry loop)
531 for neighbor in &config.machines.neighbors {
532 let addr = neighbor.address;
533 let direction = neighbor.direction;
534 let peers_clone = peers.clone();
535 let machine_name = config.machines.self_name.clone();
536 let neighbor_name = neighbor.name.clone();
537
538 // Determine if TLS should be used for this neighbor
539 // Per-neighbor override takes precedence over global setting
540 let use_tls = neighbor.tls.unwrap_or(tls_enabled);
541 let fingerprint = neighbor.fingerprint.clone();
542 let tofu_enabled = config.network.tls.tofu;
543 let known_hosts_clone = known_hosts.clone();
544 let known_hosts_path_clone = known_hosts_path.clone();
545
546 tokio::spawn(async move {
547 loop {
548 // Check if already connected
549 {
550 let peers = peers_clone.read().await;
551 if peers.contains_key(&direction) {
552 // Already connected, wait and check again
553 drop(peers);
554 tokio::time::sleep(std::time::Duration::from_secs(5)).await;
555 continue;
556 }
557 }
558
559 tracing::debug!("Connecting to {} at {} (TLS: {})...", direction, addr, use_tls);
560
561 // Connect with or without TLS
562 let conn_result = if use_tls {
563 network::connect_tls(
564 addr,
565 &neighbor_name,
566 fingerprint.as_deref(),
567 tofu_enabled,
568 ).await
569 } else {
570 network::connect(addr).await
571 };
572
573 match conn_result {
574 Ok(mut conn) => {
575 // Send Hello with our direction for this peer
576 // Peer will use the opposite direction to store us
577 let hello = Message::Hello(HelloPayload {
578 protocol_version: PROTOCOL_VERSION,
579 machine_name: machine_name.clone(),
580 capabilities: vec![],
581 my_direction_for_you: Some(direction),
582 });
583
584 if let Err(e) = conn.send(&hello).await {
585 tracing::error!("Failed to send Hello to {}: {}", direction, e);
586 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
587 continue;
588 }
589
590 // Wait for HelloAck
591 match conn.recv().await {
592 Ok(Some(Message::HelloAck(ack))) => {
593 if ack.accepted {
594 // Verify TLS fingerprint if this is a TLS connection
595 if let Some(peer_fp) = conn.peer_fingerprint() {
596 let mut kh = known_hosts_clone.write().await;
597 match kh.is_trusted(&neighbor_name, peer_fp) {
598 network::TrustStatus::Trusted => {
599 info!("Peer {} fingerprint verified", neighbor_name);
600 kh.touch(&neighbor_name);
601 }
602 network::TrustStatus::Unknown => {
603 if tofu_enabled {
604 info!("TOFU: Trusting new peer {} with fingerprint {}", neighbor_name, peer_fp);
605 kh.trust_host(&neighbor_name, peer_fp);
606 if let Err(e) = kh.save(&known_hosts_path_clone) {
607 tracing::error!("Failed to save known hosts: {}", e);
608 }
609 } else {
610 tracing::warn!("Unknown peer {} fingerprint and TOFU disabled, rejecting", neighbor_name);
611 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
612 continue;
613 }
614 }
615 network::TrustStatus::Changed { old_fingerprint, new_fingerprint } => {
616 tracing::error!(
617 "SECURITY WARNING: Peer {} fingerprint CHANGED!\n Old: {}\n New: {}\n This could indicate a man-in-the-middle attack!",
618 neighbor_name, old_fingerprint, new_fingerprint
619 );
620 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
621 continue; // Reject and retry
622 }
623 }
624 }
625
626 let mut peers = peers_clone.write().await;
627 if peers.contains_key(&direction) {
628 info!("Already have connection for {:?}, dropping outbound to {}", direction, ack.machine_name);
629 // Drop this connection, keep the existing one
630 } else {
631 info!("Connected to {} ({})", ack.machine_name, direction);
632 peers.insert(direction, conn);
633 }
634 // Stay in loop to reconnect if connection drops
635 } else {
636 tracing::error!("Connection rejected: {:?}", ack.error);
637 }
638 }
639 Ok(Some(other)) => {
640 tracing::warn!("Expected HelloAck, got {:?}", other);
641 }
642 Ok(None) => {
643 tracing::warn!("Connection closed during handshake");
644 }
645 Err(e) => {
646 tracing::error!("Handshake error: {}", e);
647 }
648 }
649 }
650 Err(e) => {
651 tracing::debug!("Failed to connect to {} ({}): {}", direction, addr, e);
652 }
653 }
654
655 // Retry after delay
656 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
657 }
658 });
659 }
660
661 // Listen for Hyprland events
662 let mut event_stream = hyprland::events::HyprlandEventStream::connect().await?;
663
664 // Start IPC server for CLI commands
665 let (ipc_tx, mut ipc_rx) = tokio::sync::mpsc::channel::<(
666 hyprkvm_common::protocol::IpcRequest,
667 tokio::sync::oneshot::Sender<hyprkvm_common::protocol::IpcResponse>,
668 )>(16);
669
670 tokio::spawn(async move {
671 let server = match ipc::IpcServer::bind().await {
672 Ok(s) => s,
673 Err(e) => {
674 tracing::error!("Failed to start IPC server: {}", e);
675 return;
676 }
677 };
678
679 loop {
680 match server.accept().await {
681 Ok(mut conn) => {
682 tracing::debug!("IPC: connection accepted");
683 let ipc_tx = ipc_tx.clone();
684 tokio::spawn(async move {
685 match conn.recv().await {
686 Ok(Some(request)) => {
687 tracing::debug!("IPC: received {:?}", request);
688 let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
689 if ipc_tx.send((request, resp_tx)).await.is_ok() {
690 tracing::debug!("IPC: sent to main loop, awaiting response");
691 match resp_rx.await {
692 Ok(response) => {
693 tracing::debug!("IPC: got response, sending to client");
694 if let Err(e) = conn.send(&response).await {
695 tracing::error!("IPC: failed to send response: {}", e);
696 }
697 }
698 Err(e) => {
699 tracing::error!("IPC: response channel error: {}", e);
700 }
701 }
702 } else {
703 tracing::error!("IPC: failed to send request to main loop");
704 }
705 }
706 Ok(None) => {
707 tracing::debug!("IPC: connection closed by client");
708 }
709 Err(e) => {
710 tracing::debug!("IPC recv error: {}", e);
711 }
712 }
713 });
714 }
715 Err(e) => {
716 tracing::error!("IPC accept error: {}", e);
717 }
718 }
719 }
720 });
721
722 info!("Daemon running. Move mouse to screen edges to trigger transfer. Press Ctrl+C to stop.");
723
724 loop {
725 tokio::select! {
726 // Check for edge events, grabber events, and poll peer messages
727 _ = tokio::time::sleep(std::time::Duration::from_micros(100)) => {
728 // Forward grabbed input to remote peer
729 if let Some(cap_dir) = capture_direction {
730 let mut should_escape = false;
731
732 // Coalesce motion events - drain queue and accumulate
733 let mut motion_dx: f64 = 0.0;
734 let mut motion_dy: f64 = 0.0;
735 let mut scroll_h: f64 = 0.0;
736 let mut scroll_v: f64 = 0.0;
737 let mut other_events: Vec<input::GrabEvent> = Vec::new();
738
739 while let Some(grab_event) = input_grabber.try_recv() {
740 // Check for escape key before forwarding
741 match &grab_event {
742 input::GrabEvent::KeyDown { keycode } => {
743 tracing::debug!("CAPTURE KeyDown: keycode={} ({})",
744 keycode, keycode_to_name(*keycode));
745
746 // Check for scroll_lock
747 if *keycode == KEY_SCROLLLOCK {
748 info!("Scroll Lock pressed - returning control to local");
749 should_escape = true;
750 continue; // Don't forward this key
751 }
752
753 // Check for triple-tap shift
754 if config.input.escape_hotkey.triple_tap_enabled {
755 if *keycode == KEY_LEFTSHIFT || *keycode == KEY_RIGHTSHIFT {
756 let now = std::time::Instant::now();
757 // Remove old taps outside the window
758 shift_tap_times.retain(|t| now.duration_since(*t) < triple_tap_window);
759 shift_tap_times.push(now);
760
761 if shift_tap_times.len() >= 3 {
762 info!("Triple-tap Shift detected - returning control to local");
763 should_escape = true;
764 shift_tap_times.clear();
765 continue;
766 }
767 }
768 }
769 other_events.push(grab_event);
770 }
771 input::GrabEvent::KeyUp { keycode } => {
772 tracing::debug!("CAPTURE KeyUp: keycode={} ({})",
773 keycode, keycode_to_name(*keycode));
774 other_events.push(grab_event);
775 }
776 input::GrabEvent::PointerMotion { dx, dy } => {
777 motion_dx += dx;
778 motion_dy += dy;
779 }
780 input::GrabEvent::PointerButton { .. } => {
781 other_events.push(grab_event);
782 }
783 input::GrabEvent::Scroll { horizontal, vertical } => {
784 scroll_h += horizontal;
785 scroll_v += vertical;
786 }
787 input::GrabEvent::ModifiersChanged { .. } => {
788 other_events.push(grab_event);
789 }
790 input::GrabEvent::RecoveryHotkey { .. } => {
791 // Should not happen during capture, ignore
792 tracing::warn!("RecoveryHotkey received during capture, ignoring");
793 }
794 }
795 }
796
797 // Send non-motion events first (preserve order for key events)
798 {
799 let mut peers_guard = peers.write().await;
800 if let Some(peer) = peers_guard.get_mut(&cap_dir) {
801 for event in other_events {
802 let payload = event.to_protocol(input_sequence);
803 input_sequence += 1;
804 if let Err(e) = peer.send(&Message::InputEvent(payload)).await {
805 tracing::error!("Failed to send input event: {}", e);
806 }
807 }
808
809 // Send coalesced motion as single event
810 if motion_dx != 0.0 || motion_dy != 0.0 {
811 let motion_event = input::GrabEvent::PointerMotion { dx: motion_dx, dy: motion_dy };
812 let payload = motion_event.to_protocol(input_sequence);
813 input_sequence += 1;
814 if let Err(e) = peer.send(&Message::InputEvent(payload)).await {
815 tracing::error!("Failed to send motion event: {}", e);
816 }
817 }
818
819 // Send coalesced scroll as single event
820 if scroll_h != 0.0 || scroll_v != 0.0 {
821 let scroll_event = input::GrabEvent::Scroll { horizontal: scroll_h, vertical: scroll_v };
822 let payload = scroll_event.to_protocol(input_sequence);
823 input_sequence += 1;
824 if let Err(e) = peer.send(&Message::InputEvent(payload)).await {
825 tracing::error!("Failed to send scroll event: {}", e);
826 }
827 }
828 }
829 }
830
831 // If escape was triggered, stop capture and send Leave
832 if should_escape {
833 info!("Escape triggered - stopping capture");
834 capture_direction = None;
835 input_grabber.stop(None); // No recovery needed for escape
836
837 // Send Leave message - we're leaving in the opposite direction (returning to us)
838 let leave = Message::Leave(hyprkvm_common::protocol::LeavePayload {
839 to_direction: cap_dir.opposite(),
840 cursor_pos: hyprkvm_common::protocol::CursorEntryPos::EdgeRelative(0.5),
841 modifiers: hyprkvm_common::ModifierState::default(),
842 transfer_id: input_sequence, // Use as a simple unique ID
843 });
844 let mut peers_guard = peers.write().await;
845 if let Some(peer) = peers_guard.get_mut(&cap_dir) {
846 if let Err(e) = peer.send(&leave).await {
847 tracing::error!("Failed to send Leave: {}", e);
848 }
849 }
850 }
851 } else {
852 // Not capturing - check for RecoveryHotkey events from recovery mode
853 // These bypass libinput's stale state by detecting keypresses directly at evdev level
854 while let Some(grab_event) = input_grabber.try_recv() {
855 if let input::GrabEvent::RecoveryHotkey { direction } = grab_event {
856 info!("RECOVERY HOTKEY: Super+{:?} detected via evdev", direction);
857
858 // Same at_edge check as IPC Move - only transfer if at edge monitor+window
859 let at_edge = 'edge_check: {
860 // Get monitors and find focused one
861 let monitors = match hypr_client.monitors().await {
862 Ok(m) => m,
863 Err(e) => {
864 info!(" RECOVERY edge_check: monitors query failed: {}", e);
865 break 'edge_check false;
866 }
867 };
868 let focused_monitor = match monitors.iter().find(|m| m.focused) {
869 Some(m) => m,
870 None => {
871 info!(" RECOVERY edge_check: no focused monitor found");
872 break 'edge_check false;
873 }
874 };
875
876 // Check if there's another monitor in the requested direction
877 // Use logical dimensions (physical / scale) since positions are logical
878 let has_monitor_in_direction = monitors.iter().any(|m| {
879 if m.id == focused_monitor.id { return false; }
880 let m_logical_w = (m.width as f32 / m.scale).round() as i32;
881 let m_logical_h = (m.height as f32 / m.scale).round() as i32;
882 let focused_logical_w = (focused_monitor.width as f32 / focused_monitor.scale).round() as i32;
883 let focused_logical_h = (focused_monitor.height as f32 / focused_monitor.scale).round() as i32;
884 match direction {
885 Direction::Left => m.x + m_logical_w <= focused_monitor.x,
886 Direction::Right => m.x >= focused_monitor.x + focused_logical_w,
887 Direction::Up => m.y + m_logical_h <= focused_monitor.y,
888 Direction::Down => m.y >= focused_monitor.y + focused_logical_h,
889 }
890 });
891
892 if has_monitor_in_direction {
893 info!(" RECOVERY edge_check: has monitor in direction {:?}", direction);
894 break 'edge_check false;
895 }
896
897 // On edge monitor. Check if at edge window.
898 let active_window: serde_json::Value = match hypr_client.query("activewindow").await {
899 Ok(w) => w,
900 Err(e) => {
901 info!(" RECOVERY edge_check: activewindow query failed: {}", e);
902 break 'edge_check false;
903 }
904 };
905
906 let win_x = active_window.get("at").and_then(|a| a.get(0)).and_then(|x| x.as_i64()).unwrap_or(0) as i32;
907 let win_y = active_window.get("at").and_then(|a| a.get(1)).and_then(|y| y.as_i64()).unwrap_or(0) as i32;
908 let win_w = active_window.get("size").and_then(|s| s.get(0)).and_then(|w| w.as_i64()).unwrap_or(100) as i32;
909 let win_h = active_window.get("size").and_then(|s| s.get(1)).and_then(|h| h.as_i64()).unwrap_or(100) as i32;
910
911 // Get all clients (windows)
912 let clients: Vec<serde_json::Value> = match hypr_client.query("clients").await {
913 Ok(c) => c,
914 Err(e) => {
915 info!(" RECOVERY edge_check: clients query failed: {}", e);
916 break 'edge_check false;
917 }
918 };
919
920 // Check if any OTHER window on this monitor is further in that direction
921 // Use window-based detection for all directions (consistent with IPC Move)
922 let has_window_in_direction = clients.iter().any(|client| {
923 let mon = client.get("monitor").and_then(|m| m.as_i64()).unwrap_or(-1) as i32;
924 if mon != focused_monitor.id { return false; }
925
926 let cx = client.get("at").and_then(|a| a.get(0)).and_then(|x| x.as_i64()).unwrap_or(0) as i32;
927 let cy = client.get("at").and_then(|a| a.get(1)).and_then(|y| y.as_i64()).unwrap_or(0) as i32;
928 let cw = client.get("size").and_then(|s| s.get(0)).and_then(|w| w.as_i64()).unwrap_or(0) as i32;
929 let ch = client.get("size").and_then(|s| s.get(1)).and_then(|h| h.as_i64()).unwrap_or(0) as i32;
930
931 // Skip the active window itself
932 if cx == win_x && cy == win_y && cw == win_w && ch == win_h {
933 return false;
934 }
935
936 match direction {
937 Direction::Left => cx + cw <= win_x + 10,
938 Direction::Right => cx >= win_x + win_w - 10,
939 Direction::Up => cy + ch <= win_y + 10,
940 Direction::Down => cy >= win_y + win_h - 10,
941 }
942 });
943
944 info!(" RECOVERY edge_check: has_window_in_direction={} -> at_edge={}", has_window_in_direction, !has_window_in_direction);
945 !has_window_in_direction
946 };
947
948 // Check if we have a peer in this direction
949 let has_peer = {
950 let peers = peers.read().await;
951 peers.contains_key(&direction)
952 };
953
954 if at_edge && has_peer {
955 // Get cursor position for transfer
956 let cursor_pos = hypr_client.cursor_pos().await
957 .map(|c| (c.x, c.y))
958 .unwrap_or((0, 0));
959
960 if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
961 info!("RECOVERY HOTKEY: Barrier enabled, blocking transfer");
962 } else {
963 info!("RECOVERY HOTKEY: At edge with peer, initiating transfer to {:?}", direction);
964 if let Err(e) = transfer_manager.initiate_transfer(
965 direction,
966 cursor_pos,
967 screen_min_x,
968 screen_min_y,
969 screen_max_x,
970 screen_max_y,
971 true, // keyboard-initiated (recovery hotkey)
972 ).await {
973 tracing::error!("Failed to initiate transfer from recovery hotkey: {}", e);
974 }
975 }
976 } else if !at_edge {
977 // Not at edge - need to do movefocus ourselves because libinput
978 // DROPPED the keypress due to stale state (it thinks the arrow key
979 // is still pressed from before the grab). This is the whole reason
980 // recovery mode exists.
981 let hypr_dir = match direction {
982 Direction::Left => "l",
983 Direction::Right => "r",
984 Direction::Up => "u",
985 Direction::Down => "d",
986 };
987 info!("RECOVERY HOTKEY: Not at edge, doing movefocus {} (libinput dropped the keypress)", hypr_dir);
988 match hypr_client.dispatch("movefocus", hypr_dir).await {
989 Ok(()) => info!(" RECOVERY movefocus succeeded"),
990 Err(e) => tracing::error!(" RECOVERY movefocus failed: {}", e),
991 }
992 } else {
993 info!("RECOVERY HOTKEY: No peer in direction {:?}", direction);
994 }
995 }
996 }
997 }
998
999 // Handle edge events from layer-shell barriers (for inter-monitor edges)
1000 while let Some(edge_event) = edge_capture.try_recv() {
1001 let direction = edge_event.direction;
1002
1003 // Verify cursor is actually at screen boundary using Hyprland
1004 // (barrier placement can be wrong on multi-monitor setups)
1005 let cursor_pos = match hypr_client.cursor_pos().await {
1006 Ok(pos) => (pos.x, pos.y),
1007 Err(_) => continue, // Can't verify, skip this event
1008 };
1009 let is_at_screen_edge = match direction {
1010 Direction::Left => cursor_pos.0 <= screen_min_x + 5,
1011 Direction::Right => cursor_pos.0 >= screen_max_x - 5,
1012 Direction::Up => cursor_pos.1 <= screen_min_y + 5,
1013 Direction::Down => cursor_pos.1 >= screen_max_y - 5,
1014 };
1015
1016 if !is_at_screen_edge {
1017 tracing::debug!(
1018 "EDGE: {:?} barrier triggered but cursor at ({}, {}) not at screen edge (bounds: {} to {}), ignoring",
1019 direction,
1020 cursor_pos.0,
1021 cursor_pos.1,
1022 screen_min_x,
1023 screen_max_x
1024 );
1025 continue;
1026 }
1027
1028 // Check if we have a peer in this direction
1029 let has_peer = {
1030 let peers = peers.read().await;
1031 peers.contains_key(&direction)
1032 };
1033
1034 if has_peer {
1035 // Check if we're in ReceivedControl state from this direction
1036 // If so, return control instead of initiating a new transfer
1037 let current_state = transfer_manager.state().await;
1038 if let transfer::TransferState::ReceivedControl { from, .. } = current_state {
1039 if from == direction {
1040 info!(
1041 "EDGE: {:?} at ({}, {}) - returning control",
1042 direction,
1043 cursor_pos.0,
1044 cursor_pos.1
1045 );
1046 if let Err(e) = transfer_manager.return_control().await {
1047 tracing::warn!("Failed to return control: {}", e);
1048 } else {
1049 // Set cooldown to prevent immediate re-transfer
1050 last_control_return = Some(std::time::Instant::now());
1051 }
1052 continue;
1053 }
1054 }
1055
1056 // Check cooldown to prevent bounce-back loops
1057 if let Some(last_return) = last_control_return {
1058 if last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
1059 tracing::debug!("EDGE: {:?} - in cooldown, ignoring", direction);
1060 continue;
1061 }
1062 }
1063
1064 // Re-check state for the has_devices check
1065 let current_state = transfer_manager.state().await;
1066
1067 if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1068 info!(
1069 "EDGE: {:?} at ({}, {}) - barrier enabled, blocking",
1070 direction,
1071 cursor_pos.0,
1072 cursor_pos.1
1073 );
1074 } else if !input_grabber.has_devices() && current_state.is_local() {
1075 // No devices and in Local state - can't initiate
1076 tracing::debug!(
1077 "EDGE: {:?} at ({}, {}) - no devices, can't initiate from Local",
1078 direction,
1079 cursor_pos.0,
1080 cursor_pos.1
1081 );
1082 } else {
1083 info!(
1084 "EDGE: {:?} at ({}, {}) - initiating transfer",
1085 direction,
1086 cursor_pos.0,
1087 cursor_pos.1
1088 );
1089
1090 if let Err(e) = transfer_manager.initiate_transfer(
1091 direction,
1092 cursor_pos,
1093 screen_min_x,
1094 screen_min_y,
1095 screen_max_x,
1096 screen_max_y,
1097 false, // not keyboard-initiated (mouse edge)
1098 ).await {
1099 tracing::warn!("Failed to initiate transfer: {}", e);
1100 }
1101 }
1102 } else {
1103 tracing::debug!(
1104 "EDGE: {:?} but no peer connected",
1105 direction
1106 );
1107 }
1108 }
1109
1110 // Cursor-based edge detection (for absolute screen edges)
1111 // This catches the case where cursor is at the edge and can't go further
1112 if capture_direction.is_none() {
1113 if let Ok(cursor) = hypr_client.cursor_pos().await {
1114 let (cx, cy) = (cursor.x, cursor.y);
1115
1116 // Determine if cursor is at a screen edge
1117 // For Left/Right: use global screen bounds
1118 // For Up/Down: check per-monitor bounds (different monitors have different heights)
1119 let at_edge: Option<Direction> = if cx <= EDGE_THRESHOLD {
1120 Some(Direction::Left)
1121 } else if cx >= screen_width as i32 - EDGE_THRESHOLD {
1122 Some(Direction::Right)
1123 } else {
1124 // Check per-monitor Up/Down edges
1125 let mut edge_found = None;
1126 for &(mon_x, mon_y, mon_w, mon_h) in &monitor_logical_bounds {
1127 // Check if cursor is within this monitor's x range
1128 if cx >= mon_x && cx < mon_x + mon_w {
1129 // Check Up edge (top of this monitor)
1130 if cy <= mon_y + EDGE_THRESHOLD && cy >= mon_y {
1131 edge_found = Some(Direction::Up);
1132 break;
1133 }
1134 // Check Down edge (bottom of this monitor)
1135 if cy >= mon_y + mon_h - EDGE_THRESHOLD && cy <= mon_y + mon_h {
1136 edge_found = Some(Direction::Down);
1137 break;
1138 }
1139 }
1140 }
1141 edge_found
1142 };
1143
1144 // Debug: Log when cursor is at Up/Down edge
1145 if matches!(at_edge, Some(Direction::Up) | Some(Direction::Down)) {
1146 let current_state = transfer_manager.state().await;
1147 tracing::debug!(
1148 "CURSOR at {:?} edge: pos=({}, {}), bounds=(0,0)-({}x{}), state={:?}, enabled_edges={:?}",
1149 at_edge, cx, cy, screen_width, screen_height, current_state, enabled_edges
1150 );
1151 }
1152
1153 // Check if we should trigger based on dwell time and movement
1154 if let Some(edge_dir) = at_edge {
1155 // Only care about edges with neighbors
1156 if enabled_edges.contains(&edge_dir) {
1157 let now = std::time::Instant::now();
1158
1159 // Check if cursor is moving toward the edge (or staying at it)
1160 let moving_toward_edge = if let Some((last_x, last_y)) = last_cursor_pos {
1161 match edge_dir {
1162 Direction::Left => cx <= last_x,
1163 Direction::Right => cx >= last_x,
1164 Direction::Up => cy <= last_y,
1165 Direction::Down => cy >= last_y,
1166 }
1167 } else {
1168 true
1169 };
1170
1171 if moving_toward_edge {
1172 match &edge_dwell_start {
1173 Some((dir, start)) if *dir == edge_dir => {
1174 // Already tracking this edge, check if dwell time exceeded
1175 if now.duration_since(*start).as_millis() >= EDGE_DWELL_MS as u128 {
1176 // Trigger!
1177 let has_peer = {
1178 let peers = peers.read().await;
1179 peers.contains_key(&edge_dir)
1180 };
1181
1182 if has_peer {
1183 // Check if we're in ReceivedControl state from this direction
1184 let current_state = transfer_manager.state().await;
1185 if let transfer::TransferState::ReceivedControl { from, entered_at, .. } = &current_state {
1186 // Add cooldown after entering ReceivedControl to prevent immediate return
1187 // (cursor warp may not have taken effect yet, or stale position from polling)
1188 const RECEIVED_CONTROL_COOLDOWN_MS: u128 = 500;
1189 let time_in_state = entered_at.elapsed().as_millis();
1190
1191 if time_in_state < RECEIVED_CONTROL_COOLDOWN_MS {
1192 tracing::debug!(
1193 "CURSOR EDGE: {:?} - just entered ReceivedControl {}ms ago, waiting",
1194 edge_dir, time_in_state
1195 );
1196 edge_dwell_start = None;
1197 continue;
1198 }
1199
1200 if *from == edge_dir {
1201 info!(
1202 "CURSOR EDGE: {:?} at ({}, {}) - returning control",
1203 edge_dir, cx, cy
1204 );
1205 if let Err(e) = transfer_manager.return_control().await {
1206 tracing::warn!("Failed to return control: {}", e);
1207 } else {
1208 // Set cooldown to prevent immediate re-transfer
1209 last_control_return = Some(std::time::Instant::now());
1210 }
1211 edge_dwell_start = None;
1212 continue;
1213 } else {
1214 tracing::debug!(
1215 "CURSOR EDGE: {:?} - ReceivedControl from {:?}, not matching",
1216 edge_dir, from
1217 );
1218 }
1219 } else {
1220 tracing::debug!(
1221 "CURSOR EDGE: {:?} - state is {:?}, not ReceivedControl",
1222 edge_dir, current_state
1223 );
1224 }
1225
1226 // Check cooldown to prevent bounce-back
1227 if let Some(last_return) = last_control_return {
1228 if last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128 {
1229 tracing::debug!("CURSOR EDGE: {:?} - in cooldown", edge_dir);
1230 edge_dwell_start = None;
1231 continue;
1232 }
1233 }
1234
1235 if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1236 info!(
1237 "CURSOR EDGE: {:?} at ({}, {}) - barrier enabled, blocking",
1238 edge_dir, cx, cy
1239 );
1240 } else if !input_grabber.has_devices() && current_state.is_local() {
1241 // No devices and in Local state - can't initiate (no input to grab)
1242 // Note: if in ReceivedControl, we can still relay
1243 tracing::debug!(
1244 "CURSOR EDGE: {:?} at ({}, {}) - no devices, can't initiate from Local",
1245 edge_dir, cx, cy
1246 );
1247 } else {
1248 info!(
1249 "CURSOR EDGE: {:?} at ({}, {}) - initiating transfer",
1250 edge_dir, cx, cy
1251 );
1252
1253 if let Err(e) = transfer_manager.initiate_transfer(
1254 edge_dir,
1255 (cx, cy),
1256 screen_min_x,
1257 screen_min_y,
1258 screen_max_x,
1259 screen_max_y,
1260 false, // not keyboard-initiated (cursor edge)
1261 ).await {
1262 tracing::warn!("Failed to initiate transfer: {}", e);
1263 }
1264 }
1265 } else {
1266 tracing::debug!(
1267 "CURSOR EDGE: {:?} at ({}, {}) but no peer connected",
1268 edge_dir, cx, cy
1269 );
1270 }
1271
1272 // Reset to avoid repeated triggers
1273 edge_dwell_start = None;
1274 }
1275 }
1276 _ => {
1277 // Start tracking this edge
1278 tracing::trace!("Started edge dwell tracking for {:?} at ({}, {})", edge_dir, cx, cy);
1279 edge_dwell_start = Some((edge_dir, now));
1280 }
1281 }
1282 } else {
1283 // Moving away from edge, reset
1284 edge_dwell_start = None;
1285 }
1286 }
1287 } else {
1288 // Not at any edge, reset
1289 edge_dwell_start = None;
1290 }
1291
1292 last_cursor_pos = Some((cx, cy));
1293 }
1294 }
1295
1296 // Check for transfer timeout (stuck in Initiating state)
1297 if let transfer::TransferState::Initiating { started_at, .. } = transfer_manager.state().await {
1298 const TRANSFER_TIMEOUT_MS: u128 = 3000;
1299 if started_at.elapsed().as_millis() > TRANSFER_TIMEOUT_MS {
1300 tracing::warn!("Transfer timed out after {}ms, aborting", TRANSFER_TIMEOUT_MS);
1301 transfer_manager.abort().await;
1302 }
1303 }
1304
1305 // Poll for incoming messages from peers (non-blocking)
1306 let directions: Vec<Direction> = {
1307 let peers = peers.read().await;
1308 peers.keys().cloned().collect()
1309 };
1310
1311 // Debug: log state and peers occasionally (every ~5 seconds at 100μs polling)
1312 static POLL_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1313 let count = POLL_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1314 if count % 50000 == 0 {
1315 let state = transfer_manager.state().await;
1316 tracing::info!("Poll #{}: state={:?}, peers={:?}", count, state, directions);
1317 }
1318
1319 for direction in directions {
1320 // Clone Arc before shadowing for use in spawned tasks
1321 let peers_arc = peers.clone();
1322 let mut peers = peers.write().await;
1323 if let Some(peer) = peers.get_mut(&direction) {
1324 // Try non-blocking receive using tokio timeout
1325 // Use minimal timeout to avoid blocking the event loop
1326 match tokio::time::timeout(
1327 std::time::Duration::from_micros(50),
1328 peer.recv()
1329 ).await {
1330 Ok(Ok(Some(msg))) => {
1331 tracing::debug!("Received from {:?}: {:?}", direction, msg);
1332 // Handle incoming message
1333 match msg {
1334 Message::Enter(payload) => {
1335 info!("Received Enter from {:?}", direction);
1336 match transfer_manager.handle_enter(
1337 direction,
1338 payload,
1339 screen_min_x,
1340 screen_min_y,
1341 screen_max_x,
1342 screen_max_y,
1343 ).await {
1344 Ok(pos) => {
1345 info!("Positioned cursor at {:?}", pos);
1346 }
1347 Err(e) => {
1348 tracing::error!("Failed to handle Enter: {}", e);
1349 }
1350 }
1351 }
1352 Message::EnterAck(ack) => {
1353 info!("Received EnterAck: success={}", ack.success);
1354 if let Err(e) = transfer_manager.handle_enter_ack(ack).await {
1355 // Usually a benign race condition (collision resolved)
1356 tracing::debug!("Failed to handle EnterAck: {}", e);
1357 }
1358 }
1359 Message::Leave(payload) => {
1360 info!("Received Leave from {:?}", direction);
1361 if let Err(e) = transfer_manager.handle_leave(payload).await {
1362 // Usually a benign race condition
1363 tracing::debug!("Failed to handle Leave: {}", e);
1364 }
1365 // Set cooldown to prevent bounce-back loop
1366 // When we receive Leave, control is returning to us
1367 last_control_return = Some(std::time::Instant::now());
1368 tracing::debug!("Set control return cooldown");
1369 }
1370 Message::LeaveAck => {
1371 info!("Received LeaveAck");
1372 // Transfer complete
1373 }
1374 Message::InputEvent(input_payload) => {
1375 // Check if we're in relay mode and this is from the relay source
1376 if let Some((relay_from, relay_to)) = relay_mode {
1377 if direction == relay_from {
1378 // Forward to relay target instead of injecting locally
1379 tracing::trace!("Relaying input from {:?} to {:?}", relay_from, relay_to);
1380 // Need to drop current borrow and get relay target
1381 drop(peers);
1382 let mut peers_guard = peers_arc.write().await;
1383 if let Some(target_peer) = peers_guard.get_mut(&relay_to) {
1384 if let Err(e) = target_peer.send(&Message::InputEvent(input_payload)).await {
1385 tracing::error!("Failed to relay input to {:?}: {}", relay_to, e);
1386 }
1387 }
1388 continue; // Skip local injection
1389 }
1390 }
1391
1392 // Normal case: inject input via emulation module
1393 if let Some(ref mut emu) = input_emulator {
1394 use hyprkvm_common::protocol::InputEventType;
1395 match input_payload.event {
1396 InputEventType::KeyDown { keycode } => {
1397 tracing::debug!("RECV KeyDown: keycode={} ({})",
1398 keycode, keycode_to_name(keycode));
1399 emu.keyboard.key(keycode, hyprkvm_common::KeyState::Pressed);
1400 }
1401 InputEventType::KeyUp { keycode } => {
1402 tracing::debug!("RECV KeyUp: keycode={} ({})",
1403 keycode, keycode_to_name(keycode));
1404 emu.keyboard.key(keycode, hyprkvm_common::KeyState::Released);
1405 }
1406 InputEventType::PointerMotion { dx, dy } => {
1407 emu.pointer.motion(dx, dy);
1408 }
1409 InputEventType::PointerButton { button, pressed } => {
1410 let state = if pressed {
1411 hyprkvm_common::ButtonState::Pressed
1412 } else {
1413 hyprkvm_common::ButtonState::Released
1414 };
1415 emu.pointer.button(button, state);
1416 }
1417 InputEventType::Scroll { horizontal, vertical } => {
1418 emu.pointer.scroll(horizontal, vertical);
1419 }
1420 InputEventType::ModifierState { .. } => {
1421 // Modifier state is informational
1422 }
1423 }
1424 }
1425 }
1426 Message::Ping { timestamp } => {
1427 let _ = peer.send(&Message::Pong { timestamp }).await;
1428 }
1429 Message::Pong { timestamp } => {
1430 tracing::trace!("Pong received, rtt={}ms",
1431 std::time::SystemTime::now()
1432 .duration_since(std::time::UNIX_EPOCH)
1433 .unwrap()
1434 .as_millis() as u64 - timestamp
1435 );
1436 }
1437 Message::ClipboardOffer(offer) => {
1438 // Handle clipboard offer from peer
1439 let cm = clipboard_manager.clone();
1440 let peers_clone = peers_arc.clone();
1441 let dir = direction;
1442 tokio::spawn(async move {
1443 if let Some(request) = cm.handle_offer(offer).await {
1444 let mut peers_guard = peers_clone.write().await;
1445 if let Some(peer) = peers_guard.get_mut(&dir) {
1446 if let Err(e) = peer.send(&Message::ClipboardRequest(request)).await {
1447 tracing::warn!("Failed to send clipboard request: {}", e);
1448 }
1449 }
1450 }
1451 });
1452 }
1453 Message::ClipboardRequest(request) => {
1454 // Handle clipboard request from peer
1455 let cm = clipboard_manager.clone();
1456 let peers_clone = peers_arc.clone();
1457 let dir = direction;
1458 tokio::spawn(async move {
1459 match cm.handle_request(request).await {
1460 Ok(data_chunks) => {
1461 let mut peers_guard = peers_clone.write().await;
1462 if let Some(peer) = peers_guard.get_mut(&dir) {
1463 for chunk in data_chunks {
1464 if let Err(e) = peer.send(&Message::ClipboardData(chunk)).await {
1465 tracing::warn!("Failed to send clipboard data: {}", e);
1466 break;
1467 }
1468 }
1469 }
1470 }
1471 Err(e) => {
1472 tracing::warn!("Clipboard request failed: {}", e);
1473 }
1474 }
1475 });
1476 }
1477 Message::ClipboardData(data) => {
1478 // Handle clipboard data from peer
1479 let cm = clipboard_manager.clone();
1480 tokio::spawn(async move {
1481 if let Err(e) = cm.handle_data(data).await {
1482 tracing::warn!("Clipboard data handling failed: {}", e);
1483 }
1484 });
1485 }
1486 Message::DirectionChange(payload) => {
1487 // Peer is notifying us that they changed our relative direction
1488 // We need to update our config to store them in the opposite direction
1489 let new_dir_for_peer = payload.your_direction_from_me.opposite();
1490 info!("Received DirectionChange: peer says we are {:?} from them, so we store them as {:?}",
1491 payload.your_direction_from_me, new_dir_for_peer);
1492
1493 // Find the peer's name from config based on direction
1494 let peer_name = config.machines.neighbors
1495 .iter()
1496 .find(|n| n.direction == direction)
1497 .map(|n| n.name.clone())
1498 .unwrap_or_else(|| "unknown".to_string());
1499
1500 // Load, update, and save config
1501 let config_path_clone = config_path.clone();
1502 match Config::load(&config_path_clone) {
1503 Ok(mut cfg) => {
1504 let mut found = false;
1505 for neighbor in &mut cfg.machines.neighbors {
1506 if neighbor.name == peer_name {
1507 info!("DirectionChange: updating {} direction {:?} -> {:?}",
1508 neighbor.name, neighbor.direction, new_dir_for_peer);
1509 neighbor.direction = new_dir_for_peer;
1510 found = true;
1511 break;
1512 }
1513 }
1514
1515 if found {
1516 if let Err(e) = cfg.save(&config_path_clone) {
1517 tracing::error!("DirectionChange: failed to save config: {}", e);
1518 let _ = peer.send(&Message::DirectionChangeAck { success: false }).await;
1519 } else {
1520 info!("DirectionChange: config updated, signaling restart");
1521 let _ = peer.send(&Message::DirectionChangeAck { success: true }).await;
1522 // Signal restart to apply new edge barriers
1523 let _ = restart_tx.try_send(format!("Direction sync from {}", peer_name));
1524 }
1525 } else {
1526 tracing::warn!("DirectionChange: peer {} not found in config", peer_name);
1527 let _ = peer.send(&Message::DirectionChangeAck { success: false }).await;
1528 }
1529 }
1530 Err(e) => {
1531 tracing::error!("DirectionChange: failed to load config: {}", e);
1532 let _ = peer.send(&Message::DirectionChangeAck { success: false }).await;
1533 }
1534 }
1535 }
1536 Message::DirectionChangeAck { success } => {
1537 if success {
1538 info!("DirectionChangeAck: peer acknowledged direction update");
1539 } else {
1540 tracing::warn!("DirectionChangeAck: peer failed to update direction");
1541 }
1542 }
1543 _ => {
1544 tracing::debug!("Unhandled message: {:?}", msg);
1545 }
1546 }
1547 }
1548 Ok(Ok(None)) => {
1549 // Connection closed
1550 info!("Peer {:?} disconnected", direction);
1551 peers.remove(&direction);
1552 }
1553 Ok(Err(e)) => {
1554 tracing::error!("Error receiving from {:?}: {}", direction, e);
1555 peers.remove(&direction);
1556 }
1557 Err(_) => {
1558 // Timeout - no message available, that's fine
1559 }
1560 }
1561 }
1562 }
1563 }
1564
1565 // Handle transfer events
1566 Some(event) = transfer_events.recv() => {
1567 match event {
1568 transfer::TransferEvent::SendMessage { direction, message } => {
1569 let mut peers = peers.write().await;
1570 if let Some(peer) = peers.get_mut(&direction) {
1571 info!("Sending {:?} to {:?}", message, direction);
1572 if let Err(e) = peer.send(&message).await {
1573 tracing::error!("Failed to send message to {:?}: {}", direction, e);
1574 // If send fails, abort the transfer
1575 transfer_manager.abort().await;
1576 }
1577 } else {
1578 tracing::warn!("No peer for direction {:?}, aborting transfer", direction);
1579 transfer_manager.abort().await;
1580 }
1581 }
1582 transfer::TransferEvent::StartCapture { direction: cap_dir, keyboard_initiated } => {
1583 info!("StartCapture event received for {:?}, keyboard_initiated={}", cap_dir, keyboard_initiated);
1584 capture_direction = Some(cap_dir);
1585
1586 // Only send synthetic Super key-down if the transfer was keyboard-initiated.
1587 // When triggered via Super+Arrow keybinding, Super was already held when
1588 // the grab started. The evdev grabber won't see the initial Super key-down,
1589 // so we send it explicitly so the destination knows Super is pressed.
1590 // For CLI-initiated switches, the user isn't holding Super, so don't send it.
1591 if keyboard_initiated {
1592 let mut peers_guard = peers.write().await;
1593 if let Some(peer) = peers_guard.get_mut(&cap_dir) {
1594 let super_down = input::GrabEvent::KeyDown { keycode: 125 }; // KEY_LEFTMETA
1595 let payload = super_down.to_protocol(input_sequence);
1596 input_sequence += 1;
1597 tracing::debug!("Sending synthetic Super key-down to destination (keyboard-initiated)");
1598 if let Err(e) = peer.send(&Message::InputEvent(payload)).await {
1599 tracing::error!("Failed to send synthetic Super: {}", e);
1600 }
1601 }
1602 } else {
1603 tracing::debug!("Skipping synthetic Super key-down (CLI-initiated switch)");
1604 // Add delay for CLI-initiated switches to let the terminal settle
1605 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1606 }
1607
1608 input_grabber.start();
1609 }
1610 transfer::TransferEvent::StopCapture => {
1611 info!("Stopping input capture");
1612 let was_capturing_direction = capture_direction;
1613 capture_direction = None;
1614
1615 // Release the evdev grab and enter recovery mode for the stale direction
1616 // The stale key is the arrow key used to initiate the original outgoing transfer
1617 input_grabber.stop(was_capturing_direction);
1618
1619 // Drain any remaining events
1620 while input_grabber.try_recv().is_some() {}
1621
1622 // CRITICAL FIX: After releasing the evdev grab, libinput has stale state.
1623 // The arrow key that initiated the original transfer (before we went remote)
1624 // is still seen as "pressed" by libinput because it never saw the release.
1625 //
1626 // We use uinput to create a virtual keyboard and send synthetic key-up
1627 // events for ALL arrow keys. This gives libinput fresh key-up events,
1628 // which should clear the stale state.
1629 if let Some(dir) = was_capturing_direction {
1630 // The stale key is the one used to initiate the OUTGOING transfer
1631 let stale_keycode: u16 = match dir {
1632 Direction::Left => 105, // KEY_LEFT
1633 Direction::Right => 106, // KEY_RIGHT
1634 Direction::Up => 103, // KEY_UP
1635 Direction::Down => 108, // KEY_DOWN
1636 };
1637
1638 tracing::info!("Sending synthetic key-ups via uinput to clear stale libinput state");
1639
1640 // Send key-ups for all arrow keys to be safe
1641 let all_arrows: [u16; 4] = [103, 105, 106, 108];
1642 if let Err(e) = input::send_synthetic_key_ups(&all_arrows) {
1643 tracing::warn!("Failed to send synthetic key-ups: {}", e);
1644 }
1645
1646 // Also inject via virtual keyboard for Wayland-level cleanup
1647 if input_emulator.is_none() {
1648 if let Ok(emu) = input::InputEmulator::new() {
1649 input_emulator = Some(emu);
1650 }
1651 }
1652 if let Some(ref mut emu) = input_emulator {
1653 emu.keyboard.key(stale_keycode as u32, hyprkvm_common::KeyState::Released);
1654 emu.keyboard.reset_all_keys();
1655 }
1656 }
1657 }
1658 transfer::TransferEvent::StartInjection { from } => {
1659 info!("Starting input injection from {:?}", from);
1660 // Create input emulator if not exists
1661 if input_emulator.is_none() {
1662 match input::InputEmulator::new() {
1663 Ok(emu) => {
1664 info!("Input emulator created");
1665 input_emulator = Some(emu);
1666 }
1667 Err(e) => {
1668 tracing::error!("Failed to create input emulator: {}", e);
1669 }
1670 }
1671 }
1672 }
1673 transfer::TransferEvent::StopInjection => {
1674 info!("Stopping input injection");
1675 // Reset ALL pressed keys so next session starts clean
1676 // This prevents Hyprland from seeing stale key state
1677 // (e.g., arrow key that triggered return was never released)
1678 if let Some(ref mut emu) = input_emulator {
1679 emu.keyboard.reset_all_keys();
1680 }
1681 }
1682 transfer::TransferEvent::StartRelay { from, to } => {
1683 info!("Starting input relay: {:?} -> {:?}", from, to);
1684 relay_mode = Some((from, to));
1685 // No local device grabbing needed - we're forwarding received input
1686 }
1687 transfer::TransferEvent::StopRelay => {
1688 info!("Stopping input relay");
1689 relay_mode = None;
1690 }
1691 transfer::TransferEvent::SyncClipboardOutgoing { direction } => {
1692 // Sync clipboard to the peer in the given direction
1693 // Check if clipboard sync is enabled and appropriate for this event
1694 if config.clipboard.enabled {
1695 let cm = clipboard_manager.clone();
1696 let peers_clone = peers.clone();
1697 tokio::spawn(async move {
1698 match cm.create_offer().await {
1699 Ok(Some(offer)) => {
1700 let mut peers_guard = peers_clone.write().await;
1701 if let Some(peer) = peers_guard.get_mut(&direction) {
1702 info!("Syncing clipboard to {:?}", direction);
1703 if let Err(e) = peer.send(&Message::ClipboardOffer(offer)).await {
1704 tracing::warn!("Failed to send clipboard offer: {}", e);
1705 }
1706 }
1707 }
1708 Ok(None) => {
1709 tracing::debug!("No clipboard content to sync");
1710 }
1711 Err(e) => {
1712 tracing::warn!("Failed to read clipboard for sync: {}", e);
1713 }
1714 }
1715 });
1716 }
1717 }
1718 }
1719 }
1720
1721 // Hyprland events
1722 event = event_stream.next_event() => {
1723 match event {
1724 Ok(evt) => {
1725 tracing::trace!("Hyprland event: {:?}", evt);
1726 }
1727 Err(e) => {
1728 tracing::error!("Event error: {e}");
1729 break;
1730 }
1731 }
1732 }
1733
1734 // Handle IPC requests from CLI
1735 Some((request, response_tx)) = ipc_rx.recv() => {
1736 use hyprkvm_common::protocol::{IpcRequest, IpcResponse};
1737
1738 let response = match request {
1739 IpcRequest::Move { direction } => {
1740 // Log current state for debugging
1741 let current_state = transfer_manager.state().await;
1742 info!("IPC Move {:?}: state={:?}", direction, current_state);
1743
1744 // Early exit: if in ReceivedControl and within cooldown, ignore
1745 // (prevents the Super+Arrow keypress that triggered the transfer from
1746 // causing a double navigation on the receiving machine)
1747 if let transfer::TransferState::ReceivedControl { entered_at, .. } = &current_state {
1748 const RECEIVED_CONTROL_IPC_COOLDOWN_MS: u128 = 1000;
1749 let time_in_state = entered_at.elapsed().as_millis();
1750 if time_in_state < RECEIVED_CONTROL_IPC_COOLDOWN_MS {
1751 tracing::info!("IPC Move {:?}: in ReceivedControl cooldown ({}ms), ignoring", direction, time_in_state);
1752 response_tx.send(IpcResponse::Ok { message: "in cooldown".to_string() }).ok();
1753 continue;
1754 }
1755 }
1756
1757 // If we're already initiating a transfer in this direction, skip entirely
1758 // (prevents double-action when both RECOVERY hotkey and IPC Move fire)
1759 if let transfer::TransferState::Initiating { target, .. } = &current_state {
1760 if *target == direction {
1761 tracing::info!("IPC Move {:?}: already initiating transfer, skipping", direction);
1762 IpcResponse::Ok { message: "transfer already initiating".to_string() }
1763 } else {
1764 // Different direction - this shouldn't happen normally, but do movefocus
1765 let hypr_dir = match direction {
1766 Direction::Left => "l",
1767 Direction::Right => "r",
1768 Direction::Up => "u",
1769 Direction::Down => "d",
1770 };
1771 match hypr_client.dispatch("movefocus", hypr_dir).await {
1772 Ok(_) => IpcResponse::Ok { message: "movefocus".to_string() },
1773 Err(e) => IpcResponse::Error { message: format!("movefocus failed: {}", e) },
1774 }
1775 }
1776 } else {
1777
1778 // For keyboard navigation, check if we're at the absolute edge:
1779 // 1. On edge monitor (no monitor in that direction)
1780 // 2. On edge window of that monitor (no window further in that direction)
1781
1782 let at_edge = 'edge_check: {
1783 // Get monitors and find focused one
1784 let monitors = match hypr_client.monitors().await {
1785 Ok(m) => m,
1786 Err(e) => {
1787 info!(" edge_check: monitors query failed: {}", e);
1788 break 'edge_check false;
1789 }
1790 };
1791 let focused_monitor = match monitors.iter().find(|m| m.focused) {
1792 Some(m) => m,
1793 None => {
1794 info!(" edge_check: no focused monitor found");
1795 break 'edge_check false;
1796 }
1797 };
1798
1799 // Check if there's another monitor in the requested direction
1800 // Use logical dimensions (physical / scale) since positions are logical
1801 let has_monitor_in_direction = monitors.iter().any(|m| {
1802 if m.id == focused_monitor.id { return false; }
1803 let m_logical_w = (m.width as f32 / m.scale).round() as i32;
1804 let m_logical_h = (m.height as f32 / m.scale).round() as i32;
1805 let focused_logical_w = (focused_monitor.width as f32 / focused_monitor.scale).round() as i32;
1806 let focused_logical_h = (focused_monitor.height as f32 / focused_monitor.scale).round() as i32;
1807 match direction {
1808 Direction::Left => m.x + m_logical_w <= focused_monitor.x,
1809 Direction::Right => m.x >= focused_monitor.x + focused_logical_w,
1810 Direction::Up => m.y + m_logical_h <= focused_monitor.y,
1811 Direction::Down => m.y >= focused_monitor.y + focused_logical_h,
1812 }
1813 });
1814
1815 if has_monitor_in_direction {
1816 // There's a monitor in that direction, not at edge
1817 info!(" edge_check: has monitor in direction {:?}", direction);
1818 break 'edge_check false;
1819 }
1820
1821 // We're on the edge monitor. Now check if we're on the edge window.
1822 // Get active window position
1823 let active_window: serde_json::Value = match hypr_client.query("activewindow").await {
1824 Ok(w) => w,
1825 Err(e) => {
1826 info!(" edge_check: activewindow query failed: {}", e);
1827 break 'edge_check false;
1828 }
1829 };
1830
1831 let win_x = active_window.get("at").and_then(|a| a.get(0)).and_then(|x| x.as_i64()).unwrap_or(0) as i32;
1832 let win_y = active_window.get("at").and_then(|a| a.get(1)).and_then(|y| y.as_i64()).unwrap_or(0) as i32;
1833 let win_w = active_window.get("size").and_then(|s| s.get(0)).and_then(|w| w.as_i64()).unwrap_or(100) as i32;
1834 let win_h = active_window.get("size").and_then(|s| s.get(1)).and_then(|h| h.as_i64()).unwrap_or(100) as i32;
1835
1836 // Get all clients (windows)
1837 let clients: Vec<serde_json::Value> = match hypr_client.query("clients").await {
1838 Ok(c) => c,
1839 Err(e) => {
1840 info!(" edge_check: clients query failed: {}", e);
1841 break 'edge_check false;
1842 }
1843 };
1844
1845 // Calculate monitor bounds in logical coordinates
1846 let mon_logical_h = (focused_monitor.height as f32 / focused_monitor.scale).round() as i32;
1847
1848 info!(" edge_check: active window at ({},{}) size {}x{}, {} clients on monitor, mon_y={}, mon_h={}",
1849 win_x, win_y, win_w, win_h,
1850 clients.iter().filter(|c| c.get("monitor").and_then(|m| m.as_i64()).unwrap_or(-1) as i32 == focused_monitor.id).count(),
1851 focused_monitor.y, mon_logical_h);
1852
1853 // Check if any OTHER window on this monitor is further in that direction
1854 let has_window_in_direction = clients.iter().any(|client| {
1855 let mon = client.get("monitor").and_then(|m| m.as_i64()).unwrap_or(-1) as i32;
1856 if mon != focused_monitor.id { return false; }
1857
1858 let cx = client.get("at").and_then(|a| a.get(0)).and_then(|x| x.as_i64()).unwrap_or(0) as i32;
1859 let cy = client.get("at").and_then(|a| a.get(1)).and_then(|y| y.as_i64()).unwrap_or(0) as i32;
1860 let cw = client.get("size").and_then(|s| s.get(0)).and_then(|w| w.as_i64()).unwrap_or(0) as i32;
1861 let ch = client.get("size").and_then(|s| s.get(1)).and_then(|h| h.as_i64()).unwrap_or(0) as i32;
1862
1863 // Skip the active window itself
1864 if cx == win_x && cy == win_y && cw == win_w && ch == win_h {
1865 return false;
1866 }
1867
1868 match direction {
1869 Direction::Left => cx + cw <= win_x + 10,
1870 Direction::Right => cx >= win_x + win_w - 10,
1871 Direction::Up => cy + ch <= win_y + 10,
1872 Direction::Down => cy >= win_y + win_h - 10,
1873 }
1874 });
1875
1876 info!(" edge_check: has_window_in_direction={} -> at_edge={}", has_window_in_direction, !has_window_in_direction);
1877 !has_window_in_direction
1878 };
1879
1880 // Check if we have a peer in this direction
1881 let has_peer = {
1882 let peers = peers.read().await;
1883 peers.contains_key(&direction)
1884 };
1885
1886 // Get neighbor name if configured
1887 let neighbor_name = config.machines.neighbors
1888 .iter()
1889 .find(|n| n.direction == direction)
1890 .map(|n| n.name.clone());
1891
1892 info!("IPC Move {:?}: at_edge={}, has_peer={}, neighbor={:?}", direction, at_edge, has_peer, neighbor_name);
1893
1894 // At edge with peer: either return control or initiate transfer
1895 if at_edge && has_peer && neighbor_name.is_some() {
1896 // Check if we're in ReceivedControl state from this direction
1897 if let transfer::TransferState::ReceivedControl { from, .. } = current_state {
1898 if from == direction {
1899 // Return control to source machine
1900 tracing::info!("Keyboard return: at edge, returning control to {:?}", direction);
1901 if let Err(e) = transfer_manager.return_control().await {
1902 tracing::warn!("Failed to return control: {}", e);
1903 IpcResponse::Error { message: format!("Return failed: {}", e) }
1904 } else {
1905 // Set cooldown to prevent immediate re-transfer (bounce-back)
1906 last_control_return = Some(std::time::Instant::now());
1907 IpcResponse::Transferred { to_machine: neighbor_name.unwrap() }
1908 }
1909 } else {
1910 // At edge with peer but received control from different direction
1911 if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1912 IpcResponse::Error { message: "Barrier enabled".to_string() }
1913 } else {
1914 // Initiate new transfer
1915 let cursor_pos = hypr_client.cursor_pos().await
1916 .map(|c| (c.x, c.y))
1917 .unwrap_or((0, 0));
1918
1919 if let Err(e) = transfer_manager.initiate_transfer(
1920 direction,
1921 cursor_pos,
1922 screen_min_x,
1923 screen_min_y,
1924 screen_max_x,
1925 screen_max_y,
1926 true, // keyboard-initiated (IPC Move from keybind)
1927 ).await {
1928 IpcResponse::Error { message: format!("Transfer failed: {}", e) }
1929 } else {
1930 IpcResponse::Transferred { to_machine: neighbor_name.unwrap() }
1931 }
1932 }
1933 }
1934 } else {
1935 // Not in ReceivedControl - check cooldown first
1936 let in_cooldown = if let Some(last_return) = last_control_return {
1937 last_return.elapsed().as_millis() < CONTROL_RETURN_COOLDOWN_MS as u128
1938 } else {
1939 false
1940 };
1941
1942 if in_cooldown {
1943 tracing::info!("IPC Move {:?}: in cooldown, doing local movefocus", direction);
1944 let hypr_dir = match direction {
1945 Direction::Left => "l",
1946 Direction::Right => "r",
1947 Direction::Up => "u",
1948 Direction::Down => "d",
1949 };
1950 match hypr_client.dispatch("movefocus", hypr_dir).await {
1951 Ok(_) => IpcResponse::Ok { message: "movefocus (cooldown)".to_string() },
1952 Err(e) => IpcResponse::Error { message: format!("movefocus failed: {}", e) },
1953 }
1954 } else if barrier_enabled.load(std::sync::atomic::Ordering::SeqCst) {
1955 IpcResponse::Error { message: "Barrier enabled".to_string() }
1956 } else {
1957 // Initiate new transfer
1958 let cursor_pos = hypr_client.cursor_pos().await
1959 .map(|c| (c.x, c.y))
1960 .unwrap_or((0, 0));
1961
1962 if let Err(e) = transfer_manager.initiate_transfer(
1963 direction,
1964 cursor_pos,
1965 screen_min_x,
1966 screen_min_y,
1967 screen_max_x,
1968 screen_max_y,
1969 true, // keyboard-initiated (IPC Move from keybind)
1970 ).await {
1971 IpcResponse::Error { message: format!("Transfer failed: {}", e) }
1972 } else {
1973 IpcResponse::Transferred { to_machine: neighbor_name.unwrap() }
1974 }
1975 }
1976 }
1977 } else {
1978 // Either not at edge, or at edge but no peer - do local movefocus
1979 let hypr_dir = match direction {
1980 Direction::Left => "l",
1981 Direction::Right => "r",
1982 Direction::Up => "u",
1983 Direction::Down => "d",
1984 };
1985 info!("IPC Move {:?}: doing local movefocus {}", direction, hypr_dir);
1986 match hypr_client.dispatch("movefocus", hypr_dir).await {
1987 Ok(()) => info!(" movefocus succeeded"),
1988 Err(e) => tracing::error!(" movefocus failed: {}", e),
1989 }
1990 IpcResponse::DoLocalMove
1991 }
1992 } // end of else block for Initiating check
1993 }
1994 IpcRequest::Status => {
1995 let state = format!("{:?}", transfer_manager.state().await);
1996 let connected_peers: Vec<String> = {
1997 let peers = peers.read().await;
1998 config.machines.neighbors
1999 .iter()
2000 .filter(|n| peers.contains_key(&n.direction))
2001 .map(|n| n.name.clone())
2002 .collect()
2003 };
2004 let uptime_secs = daemon_start_time.elapsed().as_secs();
2005 IpcResponse::Status {
2006 state,
2007 connected_peers,
2008 uptime_secs,
2009 machine_name: config.machines.self_name.clone(),
2010 }
2011 }
2012 IpcRequest::ListPeers => {
2013 let peers_guard = peers.read().await;
2014 let peer_list: Vec<hyprkvm_common::protocol::PeerInfo> = config.machines.neighbors
2015 .iter()
2016 .map(|n| {
2017 let connected = peers_guard.contains_key(&n.direction);
2018 let status = if connected {
2019 "connected".to_string()
2020 } else {
2021 "disconnected".to_string()
2022 };
2023 hyprkvm_common::protocol::PeerInfo {
2024 name: n.name.clone(),
2025 direction: n.direction,
2026 connected,
2027 address: n.address.to_string(),
2028 status,
2029 }
2030 })
2031 .collect();
2032 IpcResponse::Peers { peers: peer_list }
2033 }
2034 IpcRequest::PingPeer { peer_name } => {
2035 // Find the peer by name
2036 let neighbor = config.machines.neighbors
2037 .iter()
2038 .find(|n| n.name == peer_name);
2039
2040 match neighbor {
2041 Some(n) => {
2042 let direction = n.direction;
2043 let mut peers_guard = peers.write().await;
2044
2045 if let Some(peer_conn) = peers_guard.get_mut(&direction) {
2046 // Send Ping with current timestamp
2047 let timestamp = std::time::SystemTime::now()
2048 .duration_since(std::time::UNIX_EPOCH)
2049 .unwrap()
2050 .as_millis() as u64;
2051
2052 if let Err(e) = peer_conn.send(&Message::Ping { timestamp }).await {
2053 IpcResponse::PingResult {
2054 peer_name,
2055 latency_ms: None,
2056 error: Some(format!("Send failed: {}", e)),
2057 }
2058 } else {
2059 // Wait for Pong with timeout
2060 match tokio::time::timeout(
2061 std::time::Duration::from_secs(5),
2062 peer_conn.recv()
2063 ).await {
2064 Ok(Ok(Some(Message::Pong { timestamp: pong_ts }))) => {
2065 let now = std::time::SystemTime::now()
2066 .duration_since(std::time::UNIX_EPOCH)
2067 .unwrap()
2068 .as_millis() as u64;
2069 let latency = now.saturating_sub(pong_ts);
2070 IpcResponse::PingResult {
2071 peer_name,
2072 latency_ms: Some(latency),
2073 error: None,
2074 }
2075 }
2076 Ok(Ok(Some(_))) => {
2077 IpcResponse::PingResult {
2078 peer_name,
2079 latency_ms: None,
2080 error: Some("Unexpected response".to_string()),
2081 }
2082 }
2083 Ok(Ok(None)) => {
2084 // Connection closed
2085 peers_guard.remove(&direction);
2086 IpcResponse::PingResult {
2087 peer_name,
2088 latency_ms: None,
2089 error: Some("Connection closed".to_string()),
2090 }
2091 }
2092 Ok(Err(e)) => {
2093 IpcResponse::PingResult {
2094 peer_name,
2095 latency_ms: None,
2096 error: Some(format!("Receive error: {}", e)),
2097 }
2098 }
2099 Err(_) => {
2100 IpcResponse::PingResult {
2101 peer_name,
2102 latency_ms: None,
2103 error: Some("Timeout".to_string()),
2104 }
2105 }
2106 }
2107 }
2108 } else {
2109 IpcResponse::PingResult {
2110 peer_name,
2111 latency_ms: None,
2112 error: Some("Peer not connected".to_string()),
2113 }
2114 }
2115 }
2116 None => {
2117 IpcResponse::Error {
2118 message: format!("Unknown peer: {}", peer_name),
2119 }
2120 }
2121 }
2122 }
2123
2124 // ================================================================
2125 // CLI Expansion: Control Transfer
2126 // ================================================================
2127
2128 IpcRequest::Switch { target } => {
2129 use hyprkvm_common::protocol::SwitchTarget;
2130
2131 // Resolve target to a direction
2132 let direction = match &target {
2133 SwitchTarget::Direction(dir) => Some(*dir),
2134 SwitchTarget::MachineName(name) => {
2135 config.machines.neighbors
2136 .iter()
2137 .find(|n| &n.name == name)
2138 .map(|n| n.direction)
2139 }
2140 };
2141
2142 match direction {
2143 Some(dir) => {
2144 let peers_guard = peers.read().await;
2145 if peers_guard.get(&dir).is_some() {
2146 drop(peers_guard);
2147
2148 // Get cursor position (use center of total screen)
2149 let cursor_pos = hypr_client.cursor_pos().await
2150 .map(|c| (c.x, c.y))
2151 .unwrap_or(((screen_min_x + screen_max_x) / 2, (screen_min_y + screen_max_y) / 2));
2152
2153 // Initiate transfer (CLI-initiated, not keyboard)
2154 info!("IPC Switch: calling initiate_transfer");
2155 match transfer_manager.initiate_transfer(dir, cursor_pos, screen_min_x, screen_min_y, screen_max_x, screen_max_y, false).await {
2156 Ok(()) => {
2157 let machine_name = config.machines.neighbors
2158 .iter()
2159 .find(|n| n.direction == dir)
2160 .map(|n| n.name.clone())
2161 .unwrap_or_else(|| format!("{:?}", dir));
2162 info!("IPC Switch: initiate_transfer succeeded, returning response to CLI");
2163 IpcResponse::Transferred { to_machine: machine_name }
2164 }
2165 Err(e) => IpcResponse::Error {
2166 message: format!("Transfer failed: {}", e),
2167 }
2168 }
2169 } else {
2170 IpcResponse::Error {
2171 message: format!("Peer not connected in direction {:?}", dir),
2172 }
2173 }
2174 }
2175 None => {
2176 let name = match target {
2177 SwitchTarget::MachineName(n) => n,
2178 _ => "unknown".to_string(),
2179 };
2180 IpcResponse::Error {
2181 message: format!("Unknown machine: {}", name),
2182 }
2183 }
2184 }
2185 }
2186
2187 IpcRequest::Return => {
2188 match transfer_manager.return_control().await {
2189 Ok(()) => IpcResponse::Ok {
2190 message: "Control returned".to_string(),
2191 },
2192 Err(e) => IpcResponse::Error {
2193 message: format!("Return failed: {}", e),
2194 }
2195 }
2196 }
2197
2198 // ================================================================
2199 // CLI Expansion: Input Management
2200 // ================================================================
2201
2202 IpcRequest::Release => {
2203 // Stop input grabbing
2204 input_grabber.stop(None);
2205 // Abort any pending transfer
2206 transfer_manager.abort().await;
2207 IpcResponse::Ok {
2208 message: "Input released".to_string(),
2209 }
2210 }
2211
2212 IpcRequest::SetBarrier { enabled } => {
2213 barrier_enabled.store(enabled, std::sync::atomic::Ordering::SeqCst);
2214 let status = if enabled { "enabled" } else { "disabled" };
2215 IpcResponse::Ok {
2216 message: format!("Barrier {}", status),
2217 }
2218 }
2219
2220 // ================================================================
2221 // CLI Expansion: Connection Management
2222 // ================================================================
2223
2224 IpcRequest::Disconnect { peer_name } => {
2225 let neighbor = config.machines.neighbors
2226 .iter()
2227 .find(|n| n.name == peer_name);
2228
2229 match neighbor {
2230 Some(n) => {
2231 let direction = n.direction;
2232 let mut peers_guard = peers.write().await;
2233 if let Some(mut peer_conn) = peers_guard.remove(&direction) {
2234 // Send goodbye before disconnecting
2235 let _ = peer_conn.send(&Message::Goodbye).await;
2236 IpcResponse::Ok {
2237 message: format!("Disconnected from {}", peer_name),
2238 }
2239 } else {
2240 IpcResponse::Error {
2241 message: format!("Peer {} not connected", peer_name),
2242 }
2243 }
2244 }
2245 None => IpcResponse::Error {
2246 message: format!("Unknown peer: {}", peer_name),
2247 }
2248 }
2249 }
2250
2251 IpcRequest::Reconnect { peer_name } => {
2252 let neighbor = config.machines.neighbors
2253 .iter()
2254 .find(|n| n.name == peer_name)
2255 .cloned();
2256
2257 match neighbor {
2258 Some(n) => {
2259 let direction = n.direction;
2260 let addr = n.address;
2261 // Remove existing connection if any
2262 {
2263 let mut peers_guard = peers.write().await;
2264 if let Some(mut peer_conn) = peers_guard.remove(&direction) {
2265 let _ = peer_conn.send(&Message::Goodbye).await;
2266 }
2267 }
2268 // Spawn reconnection task (same logic as initial connection)
2269 let peers_clone = peers.clone();
2270 let machine_name = config.machines.self_name.clone();
2271 let neighbor_name = n.name.clone();
2272
2273 // Determine TLS settings for this neighbor
2274 let use_tls = n.tls.unwrap_or(tls_enabled);
2275 let fingerprint = n.fingerprint.clone();
2276 let tofu = config.network.tls.tofu;
2277
2278 tokio::spawn(async move {
2279 // Connect with or without TLS
2280 let conn_result = if use_tls {
2281 network::connect_tls(addr, &neighbor_name, fingerprint.as_deref(), tofu).await
2282 } else {
2283 network::connect(addr).await
2284 };
2285
2286 match conn_result {
2287 Ok(mut conn) => {
2288 // Send Hello with direction for peer sync
2289 let hello = Message::Hello(HelloPayload {
2290 protocol_version: PROTOCOL_VERSION,
2291 machine_name,
2292 capabilities: vec![],
2293 my_direction_for_you: Some(direction),
2294 });
2295 if let Err(e) = conn.send(&hello).await {
2296 tracing::error!("Reconnect: failed to send Hello: {}", e);
2297 return;
2298 }
2299 // Wait for HelloAck
2300 match conn.recv().await {
2301 Ok(Some(Message::HelloAck(ack))) if ack.accepted => {
2302 let mut peers_guard = peers_clone.write().await;
2303 peers_guard.insert(direction, conn);
2304 info!("Reconnected to {}", neighbor_name);
2305 }
2306 Ok(Some(Message::HelloAck(ack))) => {
2307 tracing::error!("Reconnect rejected: {:?}", ack.error);
2308 }
2309 _ => {
2310 tracing::error!("Reconnect: handshake failed");
2311 }
2312 }
2313 }
2314 Err(e) => {
2315 tracing::error!("Reconnect: connection failed: {}", e);
2316 }
2317 }
2318 });
2319 IpcResponse::Ok {
2320 message: format!("Reconnecting to {}", peer_name),
2321 }
2322 }
2323 None => IpcResponse::Error {
2324 message: format!("Unknown peer: {}", peer_name),
2325 }
2326 }
2327 }
2328
2329 // ================================================================
2330 // CLI Expansion: Configuration
2331 // ================================================================
2332
2333 IpcRequest::GetConfig => {
2334 match toml::to_string_pretty(&config) {
2335 Ok(toml_str) => IpcResponse::Config { toml: toml_str },
2336 Err(e) => IpcResponse::Error {
2337 message: format!("Failed to serialize config: {}", e),
2338 }
2339 }
2340 }
2341
2342 IpcRequest::Reload => {
2343 // Re-read and validate config file
2344 match config::Config::load(&config_path) {
2345 Ok(new_config) => {
2346 // Check what changed
2347 let mut changes = Vec::new();
2348 let mut needs_restart = false;
2349
2350 if new_config.machines.self_name != config.machines.self_name {
2351 changes.push(format!(
2352 "machine name: {} -> {} (requires restart)",
2353 config.machines.self_name, new_config.machines.self_name
2354 ));
2355 needs_restart = true;
2356 }
2357
2358 if new_config.network.listen_port != config.network.listen_port {
2359 changes.push(format!(
2360 "listen port: {} -> {} (requires restart)",
2361 config.network.listen_port, new_config.network.listen_port
2362 ));
2363 needs_restart = true;
2364 }
2365
2366 if new_config.machines.neighbors.len() != config.machines.neighbors.len() {
2367 changes.push(format!(
2368 "neighbors: {} -> {} (requires restart)",
2369 config.machines.neighbors.len(), new_config.machines.neighbors.len()
2370 ));
2371 needs_restart = true;
2372 }
2373
2374 // Check for direction changes (requires restart for edge barriers)
2375 // Also send DirectionChange messages to notify peers
2376 let mut direction_changes: Vec<(String, Direction)> = Vec::new();
2377 for new_neighbor in &new_config.machines.neighbors {
2378 if let Some(old_neighbor) = config.machines.neighbors
2379 .iter()
2380 .find(|n| n.name == new_neighbor.name)
2381 {
2382 if old_neighbor.direction != new_neighbor.direction {
2383 changes.push(format!(
2384 "neighbor '{}' direction: {:?} -> {:?} (requires restart)",
2385 new_neighbor.name, old_neighbor.direction, new_neighbor.direction
2386 ));
2387 needs_restart = true;
2388 // Track this change to notify the peer
2389 direction_changes.push((new_neighbor.name.clone(), new_neighbor.direction));
2390 }
2391 }
2392 }
2393
2394 // Send DirectionChange messages to affected peers
2395 // We need to send on the OLD direction since that's where the peer is connected
2396 for (peer_name, new_direction) in &direction_changes {
2397 // Find the OLD direction for this peer from current config
2398 let old_direction = config.machines.neighbors
2399 .iter()
2400 .find(|n| &n.name == peer_name)
2401 .map(|n| n.direction);
2402
2403 if let Some(old_dir) = old_direction {
2404 let mut peers_guard = peers.write().await;
2405 if let Some(peer) = peers_guard.get_mut(&old_dir) {
2406 info!("Sending DirectionChange to {}: you are now {:?} from me",
2407 peer_name, new_direction);
2408 let msg = Message::DirectionChange(
2409 hyprkvm_common::protocol::DirectionChangePayload {
2410 your_direction_from_me: *new_direction,
2411 }
2412 );
2413 if let Err(e) = peer.send(&msg).await {
2414 tracing::warn!("Failed to send DirectionChange to {}: {}", peer_name, e);
2415 }
2416 } else {
2417 tracing::warn!("Peer {} not connected on {:?}", peer_name, old_dir);
2418 }
2419 }
2420 }
2421
2422 // Apply the new config (only if no restart needed)
2423 if !needs_restart {
2424 config = new_config;
2425 }
2426
2427 if changes.is_empty() {
2428 IpcResponse::Ok {
2429 message: "Config unchanged".to_string(),
2430 }
2431 } else if needs_restart {
2432 IpcResponse::Ok {
2433 message: format!(
2434 "Config saved (restart required to apply):\n - {}",
2435 changes.join("\n - ")
2436 ),
2437 }
2438 } else {
2439 IpcResponse::Ok {
2440 message: format!(
2441 "Config reloaded:\n - {}",
2442 changes.join("\n - ")
2443 ),
2444 }
2445 }
2446 }
2447 Err(e) => IpcResponse::Error {
2448 message: format!("Failed to load config: {}", e),
2449 }
2450 }
2451 }
2452
2453 // ================================================================
2454 // CLI Expansion: Daemon Control
2455 // ================================================================
2456
2457 IpcRequest::Shutdown => {
2458 info!("Shutdown requested via IPC");
2459 shutdown_requested.store(true, std::sync::atomic::Ordering::SeqCst);
2460 IpcResponse::Ok {
2461 message: "Shutting down...".to_string(),
2462 }
2463 }
2464
2465 IpcRequest::GetLogs { lines, follow: _ } => {
2466 // Find log files (rolling appender creates daemon.log.YYYY-MM-DD)
2467 let log_dir = dirs::data_local_dir()
2468 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
2469 .join("hyprkvm");
2470
2471 // Find the most recent log file
2472 let log_file = std::fs::read_dir(&log_dir)
2473 .ok()
2474 .and_then(|entries| {
2475 entries
2476 .filter_map(|e| e.ok())
2477 .filter(|e| {
2478 e.file_name()
2479 .to_string_lossy()
2480 .starts_with("daemon.log")
2481 })
2482 .max_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()))
2483 .map(|e| e.path())
2484 });
2485
2486 match log_file {
2487 Some(path) => {
2488 match std::fs::read_to_string(&path) {
2489 Ok(content) => {
2490 let n = lines.unwrap_or(50) as usize;
2491 let log_lines: Vec<String> = content
2492 .lines()
2493 .rev()
2494 .take(n)
2495 .map(|s| s.to_string())
2496 .collect::<Vec<_>>()
2497 .into_iter()
2498 .rev()
2499 .collect();
2500 IpcResponse::Logs { lines: log_lines }
2501 }
2502 Err(e) => IpcResponse::Error {
2503 message: format!("Failed to read log file: {}", e),
2504 }
2505 }
2506 }
2507 None => {
2508 IpcResponse::Logs {
2509 lines: vec!["No log files found.".to_string()],
2510 }
2511 }
2512 }
2513 }
2514 };
2515
2516 let _ = response_tx.send(response);
2517 }
2518
2519 // Handle restart signal from direction change
2520 Some(reason) = restart_rx.recv() => {
2521 info!("Restart required: {}", reason);
2522 info!("Exiting to allow restart with updated config...");
2523 accept_handle.abort();
2524 // Exit with code 75 (EX_TEMPFAIL) to signal that we need to restart
2525 // This allows systemd or the GUI to restart us
2526 std::process::exit(75);
2527 }
2528
2529 // Shutdown (Ctrl+C or IPC request)
2530 _ = tokio::signal::ctrl_c() => {
2531 info!("Shutting down (Ctrl+C)...");
2532 accept_handle.abort();
2533 break;
2534 }
2535
2536 // Check for IPC shutdown request
2537 _ = async {
2538 while !shutdown_requested.load(std::sync::atomic::Ordering::SeqCst) {
2539 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2540 }
2541 } => {
2542 info!("Shutting down (IPC request)...");
2543 accept_handle.abort();
2544 break;
2545 }
2546 }
2547 }
2548
2549 Ok(())
2550 }
2551
2552 async fn show_status() -> anyhow::Result<()> {
2553 // TODO: Connect to running daemon via IPC and get status
2554 println!("HyprKVM Status");
2555 println!("==============");
2556 println!("Daemon: not implemented yet");
2557 Ok(())
2558 }
2559
2560 async fn handle_move(direction: &str) -> anyhow::Result<()> {
2561 use hyprkvm_common::Direction;
2562 use hyprkvm_common::protocol::{IpcRequest, IpcResponse};
2563
2564 let dir: Direction = direction.parse()?;
2565
2566 // Try to connect to daemon
2567 match ipc::IpcClient::connect().await {
2568 Ok(mut client) => {
2569 // Ask daemon to handle the move (it does movefocus internally)
2570 let request = IpcRequest::Move { direction: dir };
2571 match client.request(&request).await {
2572 Ok(IpcResponse::Transferred { to_machine }) => {
2573 tracing::info!("Transferred control to {}", to_machine);
2574 }
2575 Ok(IpcResponse::DoLocalMove) => {
2576 // Daemon handled it
2577 }
2578 Ok(IpcResponse::Error { message }) => {
2579 tracing::warn!("Daemon error: {}", message);
2580 }
2581 Ok(_) => {
2582 tracing::warn!("Unexpected response from daemon");
2583 }
2584 Err(e) => {
2585 tracing::debug!("IPC request failed: {}, falling back to local", e);
2586 do_local_move(dir).await?;
2587 }
2588 }
2589 }
2590 Err(e) => {
2591 tracing::debug!("Daemon not running ({}), doing local move", e);
2592 do_local_move(dir).await?;
2593 }
2594 }
2595
2596 Ok(())
2597 }
2598
2599 async fn do_local_move(dir: hyprkvm_common::Direction) -> anyhow::Result<()> {
2600 use hyprkvm_common::Direction;
2601
2602 let hypr_dir = match dir {
2603 Direction::Left => "l",
2604 Direction::Right => "r",
2605 Direction::Up => "u",
2606 Direction::Down => "d",
2607 };
2608
2609 let output = tokio::process::Command::new("hyprctl")
2610 .args(["dispatch", "movefocus", hypr_dir])
2611 .output()
2612 .await?;
2613
2614 if !output.status.success() {
2615 let stderr = String::from_utf8_lossy(&output.stderr);
2616 tracing::error!("hyprctl failed: {}", stderr);
2617 }
2618
2619 Ok(())
2620 }
2621
2622 fn show_config(config_path: &std::path::Path) -> anyhow::Result<()> {
2623 if config_path.exists() {
2624 let content = std::fs::read_to_string(config_path)?;
2625 println!("{}", content);
2626 } else {
2627 println!("No config file at {:?}", config_path);
2628 println!("\nDefault configuration:");
2629 let default = Config::default();
2630 println!("{}", toml::to_string_pretty(&default)?);
2631 }
2632 Ok(())
2633 }
2634
2635 async fn reload_config() -> anyhow::Result<()> {
2636 // TODO: Send reload signal to daemon
2637 println!("Config reload not implemented yet");
2638 Ok(())
2639 }
2640