tenseleyflow/hyprkvm / 226565d

Browse files

feat(cli): add control, connection, and daemon management commands

CLI Expansion (Sprint 6.5):
- switch: transfer control by direction or machine name
- return: return control to this machine
- release: force release input capture (recovery)
- barrier on/off: prevent cursor from leaving screen
- disconnect/reconnect: manage peer connections
- config show: display current configuration
- reload: hot-reload config (stub, not yet implemented)
- shutdown: graceful daemon shutdown
- logs: show daemon logs (needs file logging setup)

Protocol changes:
- Add SwitchTarget enum (Direction | MachineName)
- Add IpcRequest variants for all new commands
- Add IpcResponse::Ok, Config, Logs variants

Daemon handlers:
- Implement all new IPC request handlers
- Add barrier_enabled and shutdown_requested state flags
- Support IPC-triggered graceful shutdown
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
226565d8d51955ce866d68e34388b90c6f29a0df
Parents
98dc04d
Tree
5b4762b

3 changed files

StatusFile+-
M hyprkvm-cli/src/main.rs 245 8
M hyprkvm-common/src/protocol/mod.rs 51 0
M hyprkvm-daemon/src/main.rs 281 2
hyprkvm-cli/src/main.rsmodified
@@ -8,7 +8,8 @@ use clap::{Parser, Subcommand};
88
 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
99
 use tokio::net::UnixStream;
1010
 
11
-use hyprkvm_common::protocol::{IpcRequest, IpcResponse};
11
+use hyprkvm_common::protocol::{IpcRequest, IpcResponse, SwitchTarget};
12
+use hyprkvm_common::Direction;
1213
 
1314
 #[derive(Parser)]
1415
 #[command(name = "hyprkvm-ctl")]
@@ -21,6 +22,9 @@ struct Cli {
2122
 
2223
 #[derive(Subcommand)]
2324
 enum Commands {
25
+    // ========================================================================
26
+    // Status & Diagnostics
27
+    // ========================================================================
2428
     /// Show daemon status
2529
     Status {
2630
         /// Output as JSON
@@ -40,6 +44,81 @@ enum Commands {
4044
         /// Peer name to ping
4145
         peer: String,
4246
     },
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,
43122
 }
44123
 
45124
 // ============================================================================
@@ -84,6 +163,17 @@ impl IpcClient {
84163
     }
85164
 }
86165
 
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
+
87177
 // ============================================================================
88178
 // Helpers
89179
 // ============================================================================
@@ -116,6 +206,39 @@ fn status_indicator(status: &str) -> &'static str {
116206
     }
117207
 }
118208
 
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
+
119242
 // ============================================================================
120243
 // Command Handlers
121244
 // ============================================================================
@@ -249,13 +372,7 @@ async fn handle_peers(json_output: bool) -> anyhow::Result<()> {
249372
 }
250373
 
251374
 async fn handle_ping(peer_name: String) -> anyhow::Result<()> {
252
-    let mut client = match IpcClient::connect().await {
253
-        Ok(c) => c,
254
-        Err(e) => {
255
-            eprintln!("Error: daemon not running ({})", e);
256
-            std::process::exit(1);
257
-        }
258
-    };
375
+    let mut client = connect_or_exit().await;
259376
 
260377
     println!("Pinging {}...", peer_name);
261378
 
@@ -294,6 +411,102 @@ async fn handle_ping(peer_name: String) -> anyhow::Result<()> {
294411
     Ok(())
295412
 }
296413
 
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
+
297510
 // ============================================================================
298511
 // Main
299512
 // ============================================================================
@@ -303,9 +516,33 @@ async fn main() -> anyhow::Result<()> {
303516
     let cli = Cli::parse();
304517
 
305518
     match cli.command {
519
+        // Status & Diagnostics
306520
         Commands::Status { json } => handle_status(json).await?,
307521
         Commands::Peers { json } => handle_peers(json).await?,
308522
         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?,
309546
     }
310547
 
311548
     Ok(())
hyprkvm-common/src/protocol/mod.rsmodified
@@ -207,6 +207,14 @@ pub struct ClipboardDataPayload {
207207
 // IPC Messages (CLI <-> Daemon)
208208
 // ============================================================================
209209
 
210
+/// Target for switch command - either direction or machine name
211
+#[derive(Debug, Clone, Serialize, Deserialize)]
212
+#[serde(untagged)]
213
+pub enum SwitchTarget {
214
+    Direction(Direction),
215
+    MachineName(String),
216
+}
217
+
210218
 /// IPC request from CLI to daemon
211219
 #[derive(Debug, Clone, Serialize, Deserialize)]
212220
 #[serde(tag = "type", rename_all = "snake_case")]
@@ -219,6 +227,41 @@ pub enum IpcRequest {
219227
     ListPeers,
220228
     /// Ping a specific peer by name
221229
     PingPeer { peer_name: String },
230
+
231
+    // Control transfer
232
+    /// Transfer control to another machine (by direction or name)
233
+    Switch { target: SwitchTarget },
234
+    /// Return control to this machine
235
+    Return,
236
+
237
+    // Input management
238
+    /// Force release input capture
239
+    Release,
240
+    /// Enable/disable edge barrier (prevent cursor from leaving)
241
+    SetBarrier { enabled: bool },
242
+
243
+    // Connection management
244
+    /// Disconnect from a peer
245
+    Disconnect { peer_name: String },
246
+    /// Force reconnection to a peer
247
+    Reconnect { peer_name: String },
248
+
249
+    // Configuration
250
+    /// Get current configuration
251
+    GetConfig,
252
+    /// Reload configuration from file
253
+    Reload,
254
+
255
+    // Daemon control
256
+    /// Graceful shutdown
257
+    Shutdown,
258
+    /// Get daemon logs
259
+    GetLogs {
260
+        /// Number of lines to retrieve (default: 50)
261
+        lines: Option<u32>,
262
+        /// Stream new lines (not yet implemented)
263
+        follow: bool,
264
+    },
222265
 }
223266
 
224267
 /// IPC response from daemon to CLI
@@ -250,6 +293,14 @@ pub enum IpcResponse {
250293
     },
251294
     /// Error occurred
252295
     Error { message: String },
296
+
297
+    // New responses for CLI expansion
298
+    /// Generic success response
299
+    Ok { message: String },
300
+    /// Configuration dump
301
+    Config { toml: String },
302
+    /// Log lines
303
+    Logs { lines: Vec<String> },
253304
 }
254305
 
255306
 /// Info about a connected peer
hyprkvm-daemon/src/main.rsmodified
@@ -158,6 +158,10 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
158158
     // Track daemon start time for uptime reporting
159159
     let daemon_start_time = std::time::Instant::now();
160160
 
161
+    // State flags for CLI control
162
+    let barrier_enabled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
163
+    let shutdown_requested = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
164
+
161165
     // Connect to Hyprland
162166
     info!("Connecting to Hyprland...");
163167
     let hypr_client = hyprland::ipc::HyprlandClient::new().await?;
@@ -1469,14 +1473,289 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
14691473
                             }
14701474
                         }
14711475
                     }
1476
+
1477
+                    // ================================================================
1478
+                    // CLI Expansion: Control Transfer
1479
+                    // ================================================================
1480
+
1481
+                    IpcRequest::Switch { target } => {
1482
+                        use hyprkvm_common::protocol::SwitchTarget;
1483
+
1484
+                        // Resolve target to a direction
1485
+                        let direction = match &target {
1486
+                            SwitchTarget::Direction(dir) => Some(*dir),
1487
+                            SwitchTarget::MachineName(name) => {
1488
+                                config.machines.neighbors
1489
+                                    .iter()
1490
+                                    .find(|n| &n.name == name)
1491
+                                    .map(|n| n.direction)
1492
+                            }
1493
+                        };
1494
+
1495
+                        match direction {
1496
+                            Some(dir) => {
1497
+                                let peers_guard = peers.read().await;
1498
+                                if peers_guard.get(&dir).is_some() {
1499
+                                    drop(peers_guard);
1500
+
1501
+                                    // Get cursor position and screen size from Hyprland
1502
+                                    let (cursor_pos, screen_width, screen_height) = match hypr_client.monitors().await {
1503
+                                        Ok(monitors) => {
1504
+                                            if let Some(focused) = monitors.iter().find(|m| m.focused) {
1505
+                                                // Use center of screen as cursor position for switch
1506
+                                                let cx = focused.x + focused.width as i32 / 2;
1507
+                                                let cy = focused.y + focused.height as i32 / 2;
1508
+                                                ((cx, cy), focused.width, focused.height)
1509
+                                            } else {
1510
+                                                ((0, 0), 1920, 1080) // Fallback
1511
+                                            }
1512
+                                        }
1513
+                                        Err(_) => ((0, 0), 1920, 1080), // Fallback
1514
+                                    };
1515
+
1516
+                                    // Initiate transfer
1517
+                                    match transfer_manager.initiate_transfer(dir, cursor_pos, screen_height, screen_width).await {
1518
+                                        Ok(()) => {
1519
+                                            let machine_name = config.machines.neighbors
1520
+                                                .iter()
1521
+                                                .find(|n| n.direction == dir)
1522
+                                                .map(|n| n.name.clone())
1523
+                                                .unwrap_or_else(|| format!("{:?}", dir));
1524
+                                            IpcResponse::Transferred { to_machine: machine_name }
1525
+                                        }
1526
+                                        Err(e) => IpcResponse::Error {
1527
+                                            message: format!("Transfer failed: {}", e),
1528
+                                        }
1529
+                                    }
1530
+                                } else {
1531
+                                    IpcResponse::Error {
1532
+                                        message: format!("Peer not connected in direction {:?}", dir),
1533
+                                    }
1534
+                                }
1535
+                            }
1536
+                            None => {
1537
+                                let name = match target {
1538
+                                    SwitchTarget::MachineName(n) => n,
1539
+                                    _ => "unknown".to_string(),
1540
+                                };
1541
+                                IpcResponse::Error {
1542
+                                    message: format!("Unknown machine: {}", name),
1543
+                                }
1544
+                            }
1545
+                        }
1546
+                    }
1547
+
1548
+                    IpcRequest::Return => {
1549
+                        match transfer_manager.return_control().await {
1550
+                            Ok(()) => IpcResponse::Ok {
1551
+                                message: "Control returned".to_string(),
1552
+                            },
1553
+                            Err(e) => IpcResponse::Error {
1554
+                                message: format!("Return failed: {}", e),
1555
+                            }
1556
+                        }
1557
+                    }
1558
+
1559
+                    // ================================================================
1560
+                    // CLI Expansion: Input Management
1561
+                    // ================================================================
1562
+
1563
+                    IpcRequest::Release => {
1564
+                        // Stop input grabbing
1565
+                        input_grabber.stop(None);
1566
+                        // Abort any pending transfer
1567
+                        transfer_manager.abort().await;
1568
+                        IpcResponse::Ok {
1569
+                            message: "Input released".to_string(),
1570
+                        }
1571
+                    }
1572
+
1573
+                    IpcRequest::SetBarrier { enabled } => {
1574
+                        barrier_enabled.store(enabled, std::sync::atomic::Ordering::SeqCst);
1575
+                        let status = if enabled { "enabled" } else { "disabled" };
1576
+                        IpcResponse::Ok {
1577
+                            message: format!("Barrier {}", status),
1578
+                        }
1579
+                    }
1580
+
1581
+                    // ================================================================
1582
+                    // CLI Expansion: Connection Management
1583
+                    // ================================================================
1584
+
1585
+                    IpcRequest::Disconnect { peer_name } => {
1586
+                        let neighbor = config.machines.neighbors
1587
+                            .iter()
1588
+                            .find(|n| n.name == peer_name);
1589
+
1590
+                        match neighbor {
1591
+                            Some(n) => {
1592
+                                let direction = n.direction;
1593
+                                let mut peers_guard = peers.write().await;
1594
+                                if let Some(mut peer_conn) = peers_guard.remove(&direction) {
1595
+                                    // Send goodbye before disconnecting
1596
+                                    let _ = peer_conn.send(&Message::Goodbye).await;
1597
+                                    IpcResponse::Ok {
1598
+                                        message: format!("Disconnected from {}", peer_name),
1599
+                                    }
1600
+                                } else {
1601
+                                    IpcResponse::Error {
1602
+                                        message: format!("Peer {} not connected", peer_name),
1603
+                                    }
1604
+                                }
1605
+                            }
1606
+                            None => IpcResponse::Error {
1607
+                                message: format!("Unknown peer: {}", peer_name),
1608
+                            }
1609
+                        }
1610
+                    }
1611
+
1612
+                    IpcRequest::Reconnect { peer_name } => {
1613
+                        let neighbor = config.machines.neighbors
1614
+                            .iter()
1615
+                            .find(|n| n.name == peer_name)
1616
+                            .cloned();
1617
+
1618
+                        match neighbor {
1619
+                            Some(n) => {
1620
+                                let direction = n.direction;
1621
+                                let addr = n.address;
1622
+                                // Remove existing connection if any
1623
+                                {
1624
+                                    let mut peers_guard = peers.write().await;
1625
+                                    if let Some(mut peer_conn) = peers_guard.remove(&direction) {
1626
+                                        let _ = peer_conn.send(&Message::Goodbye).await;
1627
+                                    }
1628
+                                }
1629
+                                // Spawn reconnection task (same logic as initial connection)
1630
+                                let peers_clone = peers.clone();
1631
+                                let machine_name = config.machines.self_name.clone();
1632
+                                let neighbor_name = n.name.clone();
1633
+                                tokio::spawn(async move {
1634
+                                    match network::connect(addr).await {
1635
+                                        Ok(mut conn) => {
1636
+                                            // Send Hello
1637
+                                            let hello = Message::Hello(HelloPayload {
1638
+                                                protocol_version: PROTOCOL_VERSION,
1639
+                                                machine_name,
1640
+                                                capabilities: vec![],
1641
+                                            });
1642
+                                            if let Err(e) = conn.send(&hello).await {
1643
+                                                tracing::error!("Reconnect: failed to send Hello: {}", e);
1644
+                                                return;
1645
+                                            }
1646
+                                            // Wait for HelloAck
1647
+                                            match conn.recv().await {
1648
+                                                Ok(Some(Message::HelloAck(ack))) if ack.accepted => {
1649
+                                                    let mut peers_guard = peers_clone.write().await;
1650
+                                                    peers_guard.insert(direction, conn);
1651
+                                                    info!("Reconnected to {}", neighbor_name);
1652
+                                                }
1653
+                                                Ok(Some(Message::HelloAck(ack))) => {
1654
+                                                    tracing::error!("Reconnect rejected: {:?}", ack.error);
1655
+                                                }
1656
+                                                _ => {
1657
+                                                    tracing::error!("Reconnect: handshake failed");
1658
+                                                }
1659
+                                            }
1660
+                                        }
1661
+                                        Err(e) => {
1662
+                                            tracing::error!("Reconnect: connection failed: {}", e);
1663
+                                        }
1664
+                                    }
1665
+                                });
1666
+                                IpcResponse::Ok {
1667
+                                    message: format!("Reconnecting to {}", peer_name),
1668
+                                }
1669
+                            }
1670
+                            None => IpcResponse::Error {
1671
+                                message: format!("Unknown peer: {}", peer_name),
1672
+                            }
1673
+                        }
1674
+                    }
1675
+
1676
+                    // ================================================================
1677
+                    // CLI Expansion: Configuration
1678
+                    // ================================================================
1679
+
1680
+                    IpcRequest::GetConfig => {
1681
+                        match toml::to_string_pretty(&config) {
1682
+                            Ok(toml_str) => IpcResponse::Config { toml: toml_str },
1683
+                            Err(e) => IpcResponse::Error {
1684
+                                message: format!("Failed to serialize config: {}", e),
1685
+                            }
1686
+                        }
1687
+                    }
1688
+
1689
+                    IpcRequest::Reload => {
1690
+                        // TODO: Implement config hot-reload
1691
+                        IpcResponse::Error {
1692
+                            message: "Config reload not yet implemented".to_string(),
1693
+                        }
1694
+                    }
1695
+
1696
+                    // ================================================================
1697
+                    // CLI Expansion: Daemon Control
1698
+                    // ================================================================
1699
+
1700
+                    IpcRequest::Shutdown => {
1701
+                        info!("Shutdown requested via IPC");
1702
+                        shutdown_requested.store(true, std::sync::atomic::Ordering::SeqCst);
1703
+                        IpcResponse::Ok {
1704
+                            message: "Shutting down...".to_string(),
1705
+                        }
1706
+                    }
1707
+
1708
+                    IpcRequest::GetLogs { lines, follow: _ } => {
1709
+                        // Read from log file
1710
+                        let log_path = dirs::data_local_dir()
1711
+                            .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1712
+                            .join("hyprkvm")
1713
+                            .join("daemon.log");
1714
+
1715
+                        if log_path.exists() {
1716
+                            match std::fs::read_to_string(&log_path) {
1717
+                                Ok(content) => {
1718
+                                    let n = lines.unwrap_or(50) as usize;
1719
+                                    let log_lines: Vec<String> = content
1720
+                                        .lines()
1721
+                                        .rev()
1722
+                                        .take(n)
1723
+                                        .map(|s| s.to_string())
1724
+                                        .collect::<Vec<_>>()
1725
+                                        .into_iter()
1726
+                                        .rev()
1727
+                                        .collect();
1728
+                                    IpcResponse::Logs { lines: log_lines }
1729
+                                }
1730
+                                Err(e) => IpcResponse::Error {
1731
+                                    message: format!("Failed to read log file: {}", e),
1732
+                                }
1733
+                            }
1734
+                        } else {
1735
+                            IpcResponse::Logs {
1736
+                                lines: vec!["Log file not found. File logging may not be configured.".to_string()],
1737
+                            }
1738
+                        }
1739
+                    }
14721740
                 };
14731741
 
14741742
                 let _ = response_tx.send(response);
14751743
             }
14761744
 
1477
-            // Shutdown
1745
+            // Shutdown (Ctrl+C or IPC request)
14781746
             _ = tokio::signal::ctrl_c() => {
1479
-                info!("Shutting down...");
1747
+                info!("Shutting down (Ctrl+C)...");
1748
+                accept_handle.abort();
1749
+                break;
1750
+            }
1751
+
1752
+            // Check for IPC shutdown request
1753
+            _ = async {
1754
+                while !shutdown_requested.load(std::sync::atomic::Ordering::SeqCst) {
1755
+                    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1756
+                }
1757
+            } => {
1758
+                info!("Shutting down (IPC request)...");
14801759
                 accept_handle.abort();
14811760
                 break;
14821761
             }