@@ -13,6 +13,7 @@ use tracing_subscriber::FmtSubscriber; |
| 13 | mod config; | 13 | mod config; |
| 14 | mod hyprland; | 14 | mod hyprland; |
| 15 | mod input; | 15 | mod input; |
| | 16 | +mod ipc; |
| 16 | mod network; | 17 | mod network; |
| 17 | mod state; | 18 | mod state; |
| 18 | mod transfer; | 19 | mod transfer; |
@@ -356,6 +357,49 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 356 | // Listen for Hyprland events | 357 | // Listen for Hyprland events |
| 357 | let mut event_stream = hyprland::events::HyprlandEventStream::connect().await?; | 358 | let mut event_stream = hyprland::events::HyprlandEventStream::connect().await?; |
| 358 | | 359 | |
| | 360 | + // Start IPC server for CLI commands |
| | 361 | + let (ipc_tx, mut ipc_rx) = tokio::sync::mpsc::channel::<( |
| | 362 | + hyprkvm_common::protocol::IpcRequest, |
| | 363 | + tokio::sync::oneshot::Sender<hyprkvm_common::protocol::IpcResponse>, |
| | 364 | + )>(16); |
| | 365 | + |
| | 366 | + tokio::spawn(async move { |
| | 367 | + let server = match ipc::IpcServer::bind().await { |
| | 368 | + Ok(s) => s, |
| | 369 | + Err(e) => { |
| | 370 | + tracing::error!("Failed to start IPC server: {}", e); |
| | 371 | + return; |
| | 372 | + } |
| | 373 | + }; |
| | 374 | + |
| | 375 | + loop { |
| | 376 | + match server.accept().await { |
| | 377 | + Ok(mut conn) => { |
| | 378 | + let ipc_tx = ipc_tx.clone(); |
| | 379 | + tokio::spawn(async move { |
| | 380 | + match conn.recv().await { |
| | 381 | + Ok(Some(request)) => { |
| | 382 | + let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); |
| | 383 | + if ipc_tx.send((request, resp_tx)).await.is_ok() { |
| | 384 | + if let Ok(response) = resp_rx.await { |
| | 385 | + let _ = conn.send(&response).await; |
| | 386 | + } |
| | 387 | + } |
| | 388 | + } |
| | 389 | + Ok(None) => {} |
| | 390 | + Err(e) => { |
| | 391 | + tracing::debug!("IPC recv error: {}", e); |
| | 392 | + } |
| | 393 | + } |
| | 394 | + }); |
| | 395 | + } |
| | 396 | + Err(e) => { |
| | 397 | + tracing::error!("IPC accept error: {}", e); |
| | 398 | + } |
| | 399 | + } |
| | 400 | + } |
| | 401 | + }); |
| | 402 | + |
| 359 | info!("Daemon running. Move mouse to screen edges to trigger transfer. Press Ctrl+C to stop."); | 403 | info!("Daemon running. Move mouse to screen edges to trigger transfer. Press Ctrl+C to stop."); |
| 360 | | 404 | |
| 361 | loop { | 405 | loop { |
@@ -836,6 +880,108 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 836 | } | 880 | } |
| 837 | } | 881 | } |
| 838 | | 882 | |
| | 883 | + // Handle IPC requests from CLI |
| | 884 | + Some((request, response_tx)) = ipc_rx.recv() => { |
| | 885 | + use hyprkvm_common::protocol::{IpcRequest, IpcResponse}; |
| | 886 | + |
| | 887 | + let response = match request { |
| | 888 | + IpcRequest::Move { direction } => { |
| | 889 | + // Check if we're at the edge of our screen in this direction |
| | 890 | + // and have a peer connected in that direction |
| | 891 | + let at_edge = match direction { |
| | 892 | + Direction::Left => { |
| | 893 | + // Check if cursor is at left edge |
| | 894 | + if let Ok(cursor) = hypr_client.cursor_pos().await { |
| | 895 | + cursor.x <= EDGE_THRESHOLD |
| | 896 | + } else { |
| | 897 | + false |
| | 898 | + } |
| | 899 | + } |
| | 900 | + Direction::Right => { |
| | 901 | + if let Ok(cursor) = hypr_client.cursor_pos().await { |
| | 902 | + cursor.x >= screen_width as i32 - EDGE_THRESHOLD |
| | 903 | + } else { |
| | 904 | + false |
| | 905 | + } |
| | 906 | + } |
| | 907 | + Direction::Up => { |
| | 908 | + if let Ok(cursor) = hypr_client.cursor_pos().await { |
| | 909 | + cursor.y <= EDGE_THRESHOLD |
| | 910 | + } else { |
| | 911 | + false |
| | 912 | + } |
| | 913 | + } |
| | 914 | + Direction::Down => { |
| | 915 | + if let Ok(cursor) = hypr_client.cursor_pos().await { |
| | 916 | + cursor.y >= screen_height as i32 - EDGE_THRESHOLD |
| | 917 | + } else { |
| | 918 | + false |
| | 919 | + } |
| | 920 | + } |
| | 921 | + }; |
| | 922 | + |
| | 923 | + // Check if we have a peer in this direction |
| | 924 | + let has_peer = { |
| | 925 | + let peers = peers.read().await; |
| | 926 | + peers.contains_key(&direction) |
| | 927 | + }; |
| | 928 | + |
| | 929 | + // Get neighbor name if configured |
| | 930 | + let neighbor_name = config.machines.neighbors |
| | 931 | + .iter() |
| | 932 | + .find(|n| n.direction == direction) |
| | 933 | + .map(|n| n.name.clone()); |
| | 934 | + |
| | 935 | + if at_edge && has_peer && neighbor_name.is_some() { |
| | 936 | + // Initiate transfer |
| | 937 | + let cursor_pos = hypr_client.cursor_pos().await |
| | 938 | + .map(|c| (c.x, c.y)) |
| | 939 | + .unwrap_or((0, 0)); |
| | 940 | + |
| | 941 | + if let Err(e) = transfer_manager.initiate_transfer( |
| | 942 | + direction, |
| | 943 | + cursor_pos, |
| | 944 | + screen_height, |
| | 945 | + screen_width, |
| | 946 | + ).await { |
| | 947 | + IpcResponse::Error { message: format!("Transfer failed: {}", e) } |
| | 948 | + } else { |
| | 949 | + IpcResponse::Transferred { to_machine: neighbor_name.unwrap() } |
| | 950 | + } |
| | 951 | + } else { |
| | 952 | + // Let the CLI handle it locally |
| | 953 | + IpcResponse::DoLocalMove |
| | 954 | + } |
| | 955 | + } |
| | 956 | + IpcRequest::Status => { |
| | 957 | + let state = format!("{:?}", transfer_manager.state().await); |
| | 958 | + let connected_peers: Vec<String> = { |
| | 959 | + let peers = peers.read().await; |
| | 960 | + config.machines.neighbors |
| | 961 | + .iter() |
| | 962 | + .filter(|n| peers.contains_key(&n.direction)) |
| | 963 | + .map(|n| n.name.clone()) |
| | 964 | + .collect() |
| | 965 | + }; |
| | 966 | + IpcResponse::Status { state, connected_peers } |
| | 967 | + } |
| | 968 | + IpcRequest::ListPeers => { |
| | 969 | + let peers_guard = peers.read().await; |
| | 970 | + let peer_list: Vec<hyprkvm_common::protocol::PeerInfo> = config.machines.neighbors |
| | 971 | + .iter() |
| | 972 | + .map(|n| hyprkvm_common::protocol::PeerInfo { |
| | 973 | + name: n.name.clone(), |
| | 974 | + direction: n.direction, |
| | 975 | + connected: peers_guard.contains_key(&n.direction), |
| | 976 | + }) |
| | 977 | + .collect(); |
| | 978 | + IpcResponse::Peers { peers: peer_list } |
| | 979 | + } |
| | 980 | + }; |
| | 981 | + |
| | 982 | + let _ = response_tx.send(response); |
| | 983 | + } |
| | 984 | + |
| 839 | // Shutdown | 985 | // Shutdown |
| 840 | _ = tokio::signal::ctrl_c() => { | 986 | _ = tokio::signal::ctrl_c() => { |
| 841 | info!("Shutting down..."); | 987 | info!("Shutting down..."); |
@@ -858,12 +1004,45 @@ async fn show_status() -> anyhow::Result<()> { |
| 858 | | 1004 | |
| 859 | async fn handle_move(direction: &str) -> anyhow::Result<()> { | 1005 | async fn handle_move(direction: &str) -> anyhow::Result<()> { |
| 860 | use hyprkvm_common::Direction; | 1006 | use hyprkvm_common::Direction; |
| | 1007 | + use hyprkvm_common::protocol::{IpcRequest, IpcResponse}; |
| 861 | | 1008 | |
| 862 | let dir: Direction = direction.parse()?; | 1009 | let dir: Direction = direction.parse()?; |
| 863 | - tracing::debug!("Move request: {}", dir); | | |
| 864 | | 1010 | |
| 865 | - // TODO: Connect to daemon, check if network switch needed | 1011 | + // Try to connect to daemon |
| 866 | - // For now, just execute local hyprctl move | 1012 | + match ipc::IpcClient::connect().await { |
| | 1013 | + Ok(mut client) => { |
| | 1014 | + // Ask daemon if we should transfer or move locally |
| | 1015 | + let request = IpcRequest::Move { direction: dir }; |
| | 1016 | + match client.request(&request).await { |
| | 1017 | + Ok(IpcResponse::Transferred { to_machine }) => { |
| | 1018 | + // Transfer was initiated by daemon |
| | 1019 | + tracing::info!("Transferred control to {}", to_machine); |
| | 1020 | + return Ok(()); |
| | 1021 | + } |
| | 1022 | + Ok(IpcResponse::DoLocalMove) => { |
| | 1023 | + // Fall through to local move |
| | 1024 | + } |
| | 1025 | + Ok(IpcResponse::Error { message }) => { |
| | 1026 | + tracing::warn!("Daemon error: {}", message); |
| | 1027 | + // Fall through to local move |
| | 1028 | + } |
| | 1029 | + Ok(_) => { |
| | 1030 | + tracing::warn!("Unexpected response from daemon"); |
| | 1031 | + // Fall through to local move |
| | 1032 | + } |
| | 1033 | + Err(e) => { |
| | 1034 | + tracing::debug!("IPC request failed: {}, doing local move", e); |
| | 1035 | + // Fall through to local move |
| | 1036 | + } |
| | 1037 | + } |
| | 1038 | + } |
| | 1039 | + Err(e) => { |
| | 1040 | + tracing::debug!("Daemon not running ({}), doing local move", e); |
| | 1041 | + // Fall through to local move |
| | 1042 | + } |
| | 1043 | + } |
| | 1044 | + |
| | 1045 | + // Execute local hyprctl move |
| 867 | let output = tokio::process::Command::new("hyprctl") | 1046 | let output = tokio::process::Command::new("hyprctl") |
| 868 | .args(["dispatch", "movefocus", &dir.to_string()]) | 1047 | .args(["dispatch", "movefocus", &dir.to_string()]) |
| 869 | .output() | 1048 | .output() |