@@ -13,6 +13,7 @@ use tracing_subscriber::FmtSubscriber; |
| 13 | 13 | mod config; |
| 14 | 14 | mod hyprland; |
| 15 | 15 | mod input; |
| 16 | +mod ipc; |
| 16 | 17 | mod network; |
| 17 | 18 | mod state; |
| 18 | 19 | mod transfer; |
@@ -356,6 +357,49 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> { |
| 356 | 357 | // Listen for Hyprland events |
| 357 | 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 | 403 | info!("Daemon running. Move mouse to screen edges to trigger transfer. Press Ctrl+C to stop."); |
| 360 | 404 | |
| 361 | 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 | 985 | // Shutdown |
| 840 | 986 | _ = tokio::signal::ctrl_c() => { |
| 841 | 987 | info!("Shutting down..."); |
@@ -858,12 +1004,45 @@ async fn show_status() -> anyhow::Result<()> { |
| 858 | 1004 | |
| 859 | 1005 | async fn handle_move(direction: &str) -> anyhow::Result<()> { |
| 860 | 1006 | use hyprkvm_common::Direction; |
| 1007 | + use hyprkvm_common::protocol::{IpcRequest, IpcResponse}; |
| 861 | 1008 | |
| 862 | 1009 | let dir: Direction = direction.parse()?; |
| 863 | | - tracing::debug!("Move request: {}", dir); |
| 864 | 1010 | |
| 865 | | - // TODO: Connect to daemon, check if network switch needed |
| 866 | | - // For now, just execute local hyprctl move |
| 1011 | + // Try to connect to daemon |
| 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 | 1046 | let output = tokio::process::Command::new("hyprctl") |
| 868 | 1047 | .args(["dispatch", "movefocus", &dir.to_string()]) |
| 869 | 1048 | .output() |