@@ -8,7 +8,8 @@ use clap::{Parser, Subcommand}; |
| 8 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; | 8 | use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; |
| 9 | use tokio::net::UnixStream; | 9 | use tokio::net::UnixStream; |
| 10 | | 10 | |
| 11 | -use hyprkvm_common::protocol::{IpcRequest, IpcResponse}; | 11 | +use hyprkvm_common::protocol::{IpcRequest, IpcResponse, SwitchTarget}; |
| | 12 | +use hyprkvm_common::Direction; |
| 12 | | 13 | |
| 13 | #[derive(Parser)] | 14 | #[derive(Parser)] |
| 14 | #[command(name = "hyprkvm-ctl")] | 15 | #[command(name = "hyprkvm-ctl")] |
@@ -21,6 +22,9 @@ struct Cli { |
| 21 | | 22 | |
| 22 | #[derive(Subcommand)] | 23 | #[derive(Subcommand)] |
| 23 | enum Commands { | 24 | enum Commands { |
| | 25 | + // ======================================================================== |
| | 26 | + // Status & Diagnostics |
| | 27 | + // ======================================================================== |
| 24 | /// Show daemon status | 28 | /// Show daemon status |
| 25 | Status { | 29 | Status { |
| 26 | /// Output as JSON | 30 | /// Output as JSON |
@@ -40,6 +44,81 @@ enum Commands { |
| 40 | /// Peer name to ping | 44 | /// Peer name to ping |
| 41 | peer: String, | 45 | peer: String, |
| 42 | }, | 46 | }, |
| | 47 | + |
| | 48 | + // ======================================================================== |
| | 49 | + // Control Transfer |
| | 50 | + // ======================================================================== |
| | 51 | + /// Transfer control to another machine |
| | 52 | + Switch { |
| | 53 | + /// Direction (left/right/up/down) or machine name |
| | 54 | + target: String, |
| | 55 | + }, |
| | 56 | + |
| | 57 | + /// Return control to this machine |
| | 58 | + Return, |
| | 59 | + |
| | 60 | + // ======================================================================== |
| | 61 | + // Input Management |
| | 62 | + // ======================================================================== |
| | 63 | + /// Force release input capture (recovery) |
| | 64 | + Release, |
| | 65 | + |
| | 66 | + /// Enable/disable edge barrier (lock cursor to this machine) |
| | 67 | + Barrier { |
| | 68 | + #[command(subcommand)] |
| | 69 | + action: BarrierAction, |
| | 70 | + }, |
| | 71 | + |
| | 72 | + // ======================================================================== |
| | 73 | + // Connection Management |
| | 74 | + // ======================================================================== |
| | 75 | + /// Disconnect from a peer |
| | 76 | + Disconnect { |
| | 77 | + /// Peer name to disconnect |
| | 78 | + peer: String, |
| | 79 | + }, |
| | 80 | + |
| | 81 | + /// Reconnect to a peer |
| | 82 | + Reconnect { |
| | 83 | + /// Peer name to reconnect |
| | 84 | + peer: String, |
| | 85 | + }, |
| | 86 | + |
| | 87 | + // ======================================================================== |
| | 88 | + // Configuration & Daemon |
| | 89 | + // ======================================================================== |
| | 90 | + /// Show current configuration |
| | 91 | + Config { |
| | 92 | + #[command(subcommand)] |
| | 93 | + action: ConfigAction, |
| | 94 | + }, |
| | 95 | + |
| | 96 | + /// Reload configuration from file |
| | 97 | + Reload, |
| | 98 | + |
| | 99 | + /// Shutdown the daemon |
| | 100 | + Shutdown, |
| | 101 | + |
| | 102 | + /// Show daemon logs |
| | 103 | + Logs { |
| | 104 | + /// Number of lines to show |
| | 105 | + #[arg(short = 'n', default_value = "50")] |
| | 106 | + lines: u32, |
| | 107 | + }, |
| | 108 | +} |
| | 109 | + |
| | 110 | +#[derive(Subcommand)] |
| | 111 | +enum BarrierAction { |
| | 112 | + /// Enable barrier (prevent cursor from leaving) |
| | 113 | + On, |
| | 114 | + /// Disable barrier (allow cursor to leave) |
| | 115 | + Off, |
| | 116 | +} |
| | 117 | + |
| | 118 | +#[derive(Subcommand)] |
| | 119 | +enum ConfigAction { |
| | 120 | + /// Show current configuration |
| | 121 | + Show, |
| 43 | } | 122 | } |
| 44 | | 123 | |
| 45 | // ============================================================================ | 124 | // ============================================================================ |
@@ -84,6 +163,17 @@ impl IpcClient { |
| 84 | } | 163 | } |
| 85 | } | 164 | } |
| 86 | | 165 | |
| | 166 | +/// Connect to daemon or exit with error |
| | 167 | +async fn connect_or_exit() -> IpcClient { |
| | 168 | + match IpcClient::connect().await { |
| | 169 | + Ok(c) => c, |
| | 170 | + Err(e) => { |
| | 171 | + eprintln!("Error: daemon not running ({})", e); |
| | 172 | + std::process::exit(1); |
| | 173 | + } |
| | 174 | + } |
| | 175 | +} |
| | 176 | + |
| 87 | // ============================================================================ | 177 | // ============================================================================ |
| 88 | // Helpers | 178 | // Helpers |
| 89 | // ============================================================================ | 179 | // ============================================================================ |
@@ -116,6 +206,39 @@ fn status_indicator(status: &str) -> &'static str { |
| 116 | } | 206 | } |
| 117 | } | 207 | } |
| 118 | | 208 | |
| | 209 | +/// Parse target as direction or machine name |
| | 210 | +fn parse_switch_target(target: &str) -> SwitchTarget { |
| | 211 | + match target.to_lowercase().as_str() { |
| | 212 | + "left" | "l" => SwitchTarget::Direction(Direction::Left), |
| | 213 | + "right" | "r" => SwitchTarget::Direction(Direction::Right), |
| | 214 | + "up" | "u" => SwitchTarget::Direction(Direction::Up), |
| | 215 | + "down" | "d" => SwitchTarget::Direction(Direction::Down), |
| | 216 | + _ => SwitchTarget::MachineName(target.to_string()), |
| | 217 | + } |
| | 218 | +} |
| | 219 | + |
| | 220 | +/// Handle common response types |
| | 221 | +fn handle_ok_or_error(response: IpcResponse) -> anyhow::Result<()> { |
| | 222 | + match response { |
| | 223 | + IpcResponse::Ok { message } => { |
| | 224 | + println!("{}", message); |
| | 225 | + Ok(()) |
| | 226 | + } |
| | 227 | + IpcResponse::Transferred { to_machine } => { |
| | 228 | + println!("Control transferred to {}", to_machine); |
| | 229 | + Ok(()) |
| | 230 | + } |
| | 231 | + IpcResponse::Error { message } => { |
| | 232 | + eprintln!("Error: {}", message); |
| | 233 | + std::process::exit(1); |
| | 234 | + } |
| | 235 | + _ => { |
| | 236 | + eprintln!("Unexpected response from daemon"); |
| | 237 | + std::process::exit(1); |
| | 238 | + } |
| | 239 | + } |
| | 240 | +} |
| | 241 | + |
| 119 | // ============================================================================ | 242 | // ============================================================================ |
| 120 | // Command Handlers | 243 | // Command Handlers |
| 121 | // ============================================================================ | 244 | // ============================================================================ |
@@ -249,13 +372,7 @@ async fn handle_peers(json_output: bool) -> anyhow::Result<()> { |
| 249 | } | 372 | } |
| 250 | | 373 | |
| 251 | async fn handle_ping(peer_name: String) -> anyhow::Result<()> { | 374 | async fn handle_ping(peer_name: String) -> anyhow::Result<()> { |
| 252 | - let mut client = match IpcClient::connect().await { | 375 | + let mut client = connect_or_exit().await; |
| 253 | - Ok(c) => c, | | |
| 254 | - Err(e) => { | | |
| 255 | - eprintln!("Error: daemon not running ({})", e); | | |
| 256 | - std::process::exit(1); | | |
| 257 | - } | | |
| 258 | - }; | | |
| 259 | | 376 | |
| 260 | println!("Pinging {}...", peer_name); | 377 | println!("Pinging {}...", peer_name); |
| 261 | | 378 | |
@@ -294,6 +411,102 @@ async fn handle_ping(peer_name: String) -> anyhow::Result<()> { |
| 294 | Ok(()) | 411 | Ok(()) |
| 295 | } | 412 | } |
| 296 | | 413 | |
| | 414 | +async fn handle_switch(target: String) -> anyhow::Result<()> { |
| | 415 | + let mut client = connect_or_exit().await; |
| | 416 | + let switch_target = parse_switch_target(&target); |
| | 417 | + let response = client.request(&IpcRequest::Switch { target: switch_target }).await?; |
| | 418 | + handle_ok_or_error(response) |
| | 419 | +} |
| | 420 | + |
| | 421 | +async fn handle_return() -> anyhow::Result<()> { |
| | 422 | + let mut client = connect_or_exit().await; |
| | 423 | + let response = client.request(&IpcRequest::Return).await?; |
| | 424 | + handle_ok_or_error(response) |
| | 425 | +} |
| | 426 | + |
| | 427 | +async fn handle_release() -> anyhow::Result<()> { |
| | 428 | + let mut client = connect_or_exit().await; |
| | 429 | + let response = client.request(&IpcRequest::Release).await?; |
| | 430 | + handle_ok_or_error(response) |
| | 431 | +} |
| | 432 | + |
| | 433 | +async fn handle_barrier(enabled: bool) -> anyhow::Result<()> { |
| | 434 | + let mut client = connect_or_exit().await; |
| | 435 | + let response = client.request(&IpcRequest::SetBarrier { enabled }).await?; |
| | 436 | + handle_ok_or_error(response) |
| | 437 | +} |
| | 438 | + |
| | 439 | +async fn handle_disconnect(peer: String) -> anyhow::Result<()> { |
| | 440 | + let mut client = connect_or_exit().await; |
| | 441 | + let response = client.request(&IpcRequest::Disconnect { peer_name: peer }).await?; |
| | 442 | + handle_ok_or_error(response) |
| | 443 | +} |
| | 444 | + |
| | 445 | +async fn handle_reconnect(peer: String) -> anyhow::Result<()> { |
| | 446 | + let mut client = connect_or_exit().await; |
| | 447 | + let response = client.request(&IpcRequest::Reconnect { peer_name: peer }).await?; |
| | 448 | + handle_ok_or_error(response) |
| | 449 | +} |
| | 450 | + |
| | 451 | +async fn handle_config_show() -> anyhow::Result<()> { |
| | 452 | + let mut client = connect_or_exit().await; |
| | 453 | + let response = client.request(&IpcRequest::GetConfig).await?; |
| | 454 | + |
| | 455 | + match response { |
| | 456 | + IpcResponse::Config { toml } => { |
| | 457 | + println!("{}", toml); |
| | 458 | + Ok(()) |
| | 459 | + } |
| | 460 | + IpcResponse::Error { message } => { |
| | 461 | + eprintln!("Error: {}", message); |
| | 462 | + std::process::exit(1); |
| | 463 | + } |
| | 464 | + _ => { |
| | 465 | + eprintln!("Unexpected response from daemon"); |
| | 466 | + std::process::exit(1); |
| | 467 | + } |
| | 468 | + } |
| | 469 | +} |
| | 470 | + |
| | 471 | +async fn handle_reload() -> anyhow::Result<()> { |
| | 472 | + let mut client = connect_or_exit().await; |
| | 473 | + let response = client.request(&IpcRequest::Reload).await?; |
| | 474 | + handle_ok_or_error(response) |
| | 475 | +} |
| | 476 | + |
| | 477 | +async fn handle_shutdown() -> anyhow::Result<()> { |
| | 478 | + let mut client = connect_or_exit().await; |
| | 479 | + let response = client.request(&IpcRequest::Shutdown).await?; |
| | 480 | + handle_ok_or_error(response) |
| | 481 | +} |
| | 482 | + |
| | 483 | +async fn handle_logs(lines: u32) -> anyhow::Result<()> { |
| | 484 | + let mut client = connect_or_exit().await; |
| | 485 | + let response = client |
| | 486 | + .request(&IpcRequest::GetLogs { |
| | 487 | + lines: Some(lines), |
| | 488 | + follow: false, |
| | 489 | + }) |
| | 490 | + .await?; |
| | 491 | + |
| | 492 | + match response { |
| | 493 | + IpcResponse::Logs { lines } => { |
| | 494 | + for line in lines { |
| | 495 | + println!("{}", line); |
| | 496 | + } |
| | 497 | + Ok(()) |
| | 498 | + } |
| | 499 | + IpcResponse::Error { message } => { |
| | 500 | + eprintln!("Error: {}", message); |
| | 501 | + std::process::exit(1); |
| | 502 | + } |
| | 503 | + _ => { |
| | 504 | + eprintln!("Unexpected response from daemon"); |
| | 505 | + std::process::exit(1); |
| | 506 | + } |
| | 507 | + } |
| | 508 | +} |
| | 509 | + |
| 297 | // ============================================================================ | 510 | // ============================================================================ |
| 298 | // Main | 511 | // Main |
| 299 | // ============================================================================ | 512 | // ============================================================================ |
@@ -303,9 +516,33 @@ async fn main() -> anyhow::Result<()> { |
| 303 | let cli = Cli::parse(); | 516 | let cli = Cli::parse(); |
| 304 | | 517 | |
| 305 | match cli.command { | 518 | match cli.command { |
| | 519 | + // Status & Diagnostics |
| 306 | Commands::Status { json } => handle_status(json).await?, | 520 | Commands::Status { json } => handle_status(json).await?, |
| 307 | Commands::Peers { json } => handle_peers(json).await?, | 521 | Commands::Peers { json } => handle_peers(json).await?, |
| 308 | Commands::Ping { peer } => handle_ping(peer).await?, | 522 | Commands::Ping { peer } => handle_ping(peer).await?, |
| | 523 | + |
| | 524 | + // Control Transfer |
| | 525 | + Commands::Switch { target } => handle_switch(target).await?, |
| | 526 | + Commands::Return => handle_return().await?, |
| | 527 | + |
| | 528 | + // Input Management |
| | 529 | + Commands::Release => handle_release().await?, |
| | 530 | + Commands::Barrier { action } => { |
| | 531 | + let enabled = matches!(action, BarrierAction::On); |
| | 532 | + handle_barrier(enabled).await? |
| | 533 | + } |
| | 534 | + |
| | 535 | + // Connection Management |
| | 536 | + Commands::Disconnect { peer } => handle_disconnect(peer).await?, |
| | 537 | + Commands::Reconnect { peer } => handle_reconnect(peer).await?, |
| | 538 | + |
| | 539 | + // Configuration & Daemon |
| | 540 | + Commands::Config { action } => match action { |
| | 541 | + ConfigAction::Show => handle_config_show().await?, |
| | 542 | + }, |
| | 543 | + Commands::Reload => handle_reload().await?, |
| | 544 | + Commands::Shutdown => handle_shutdown().await?, |
| | 545 | + Commands::Logs { lines } => handle_logs(lines).await?, |
| 309 | } | 546 | } |
| 310 | | 547 | |
| 311 | Ok(()) | 548 | Ok(()) |