tenseleyflow/hyprkvm / 325e06e

Browse files

Add input capture and injection for control transfer

- Create InputGrabber using layer-shell with exclusive keyboard grab
- Forward captured input events to remote peer
- Inject received input events via virtual pointer/keyboard
- Wire up StartCapture/StopCapture/StartInjection events
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
325e06ea4ebf2bad95216c820b37c22148375a2b
Parents
9bf0cfd
Tree
cd38ea9

3 changed files

StatusFile+-
A hyprkvm-daemon/src/input/grabber.rs 525 0
M hyprkvm-daemon/src/input/mod.rs 2 3
M hyprkvm-daemon/src/main.rs 78 9
hyprkvm-daemon/src/input/grabber.rsadded
@@ -0,0 +1,525 @@
1
+//! Input grabber - captures all keyboard/mouse input when active
2
+//!
3
+//! Uses layer-shell with exclusive keyboard grab to intercept all input.
4
+
5
+use std::sync::atomic::{AtomicBool, Ordering};
6
+use std::sync::Arc;
7
+use std::thread;
8
+
9
+use smithay_client_toolkit::{
10
+    compositor::{CompositorHandler, CompositorState},
11
+    delegate_compositor, delegate_keyboard, delegate_layer, delegate_output, delegate_pointer,
12
+    delegate_registry, delegate_seat,
13
+    output::{OutputHandler, OutputState},
14
+    registry::{ProvidesRegistryState, RegistryState},
15
+    registry_handlers,
16
+    seat::{
17
+        keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers},
18
+        pointer::{PointerEvent, PointerEventKind, PointerHandler},
19
+        Capability, SeatHandler, SeatState,
20
+    },
21
+    shell::{
22
+        wlr_layer::{
23
+            Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
24
+            LayerSurfaceConfigure,
25
+        },
26
+        WaylandSurface,
27
+    },
28
+};
29
+use wayland_client::{
30
+    globals::registry_queue_init,
31
+    protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_surface},
32
+    Connection, QueueHandle,
33
+};
34
+
35
+use hyprkvm_common::protocol::{InputEventPayload, InputEventType};
36
+
37
+/// Input event from the grabber
38
+#[derive(Debug, Clone)]
39
+pub enum GrabEvent {
40
+    KeyDown { keycode: u32 },
41
+    KeyUp { keycode: u32 },
42
+    PointerMotion { dx: f64, dy: f64 },
43
+    PointerButton { button: u32, pressed: bool },
44
+    Scroll { horizontal: f64, vertical: f64 },
45
+    ModifiersChanged { mods: Modifiers },
46
+}
47
+
48
+impl GrabEvent {
49
+    pub fn to_protocol(&self, seq: u64) -> InputEventPayload {
50
+        let timestamp_us = std::time::SystemTime::now()
51
+            .duration_since(std::time::UNIX_EPOCH)
52
+            .unwrap()
53
+            .as_micros() as u64;
54
+
55
+        let event = match self {
56
+            GrabEvent::KeyDown { keycode } => InputEventType::KeyDown { keycode: *keycode },
57
+            GrabEvent::KeyUp { keycode } => InputEventType::KeyUp { keycode: *keycode },
58
+            GrabEvent::PointerMotion { dx, dy } => InputEventType::PointerMotion { dx: *dx, dy: *dy },
59
+            GrabEvent::PointerButton { button, pressed } => {
60
+                InputEventType::PointerButton {
61
+                    button: *button,
62
+                    pressed: *pressed,
63
+                }
64
+            }
65
+            GrabEvent::Scroll { horizontal, vertical } => {
66
+                InputEventType::Scroll {
67
+                    horizontal: *horizontal,
68
+                    vertical: *vertical,
69
+                }
70
+            }
71
+            GrabEvent::ModifiersChanged { mods } => {
72
+                InputEventType::ModifierState {
73
+                    shift: mods.shift,
74
+                    ctrl: mods.ctrl,
75
+                    alt: mods.alt,
76
+                    super_key: mods.logo,
77
+                }
78
+            }
79
+        };
80
+
81
+        InputEventPayload {
82
+            sequence: seq,
83
+            timestamp_us,
84
+            event,
85
+        }
86
+    }
87
+}
88
+
89
+/// Input grabber configuration
90
+pub struct InputGrabberConfig {
91
+    /// Hide the cursor while grabbing
92
+    pub hide_cursor: bool,
93
+}
94
+
95
+impl Default for InputGrabberConfig {
96
+    fn default() -> Self {
97
+        Self { hide_cursor: true }
98
+    }
99
+}
100
+
101
+/// Input grabber - intercepts all keyboard and mouse input
102
+pub struct InputGrabber {
103
+    active: Arc<AtomicBool>,
104
+    event_rx: std::sync::mpsc::Receiver<GrabEvent>,
105
+    _thread: thread::JoinHandle<()>,
106
+}
107
+
108
+impl InputGrabber {
109
+    /// Create a new input grabber
110
+    pub fn new(config: InputGrabberConfig) -> Result<Self, GrabberError> {
111
+        let active = Arc::new(AtomicBool::new(false));
112
+        let active_clone = active.clone();
113
+
114
+        let (event_tx, event_rx) = std::sync::mpsc::channel();
115
+
116
+        let thread = thread::Builder::new()
117
+            .name("input-grabber".to_string())
118
+            .spawn(move || {
119
+                if let Err(e) = run_grabber(active_clone, event_tx, config) {
120
+                    tracing::error!("Grabber thread error: {}", e);
121
+                }
122
+            })
123
+            .map_err(|e| GrabberError::Thread(e.to_string()))?;
124
+
125
+        Ok(Self {
126
+            active,
127
+            event_rx,
128
+            _thread: thread,
129
+        })
130
+    }
131
+
132
+    /// Start grabbing input
133
+    pub fn start(&self) {
134
+        tracing::info!("Starting input grab");
135
+        self.active.store(true, Ordering::SeqCst);
136
+    }
137
+
138
+    /// Stop grabbing input
139
+    pub fn stop(&self) {
140
+        tracing::info!("Stopping input grab");
141
+        self.active.store(false, Ordering::SeqCst);
142
+    }
143
+
144
+    /// Check if currently grabbing
145
+    pub fn is_active(&self) -> bool {
146
+        self.active.load(Ordering::SeqCst)
147
+    }
148
+
149
+    /// Try to receive a grab event (non-blocking)
150
+    pub fn try_recv(&self) -> Option<GrabEvent> {
151
+        self.event_rx.try_recv().ok()
152
+    }
153
+}
154
+
155
+fn run_grabber(
156
+    active: Arc<AtomicBool>,
157
+    event_tx: std::sync::mpsc::Sender<GrabEvent>,
158
+    _config: InputGrabberConfig,
159
+) -> Result<(), GrabberError> {
160
+    let conn = Connection::connect_to_env()
161
+        .map_err(|e| GrabberError::Connection(e.to_string()))?;
162
+
163
+    let (globals, mut event_queue) = registry_queue_init(&conn)
164
+        .map_err(|e| GrabberError::Registry(e.to_string()))?;
165
+
166
+    let qh = event_queue.handle();
167
+
168
+    let mut state = GrabberState {
169
+        active,
170
+        event_tx,
171
+        registry_state: RegistryState::new(&globals),
172
+        seat_state: SeatState::new(&globals, &qh),
173
+        output_state: OutputState::new(&globals, &qh),
174
+        compositor_state: CompositorState::bind(&globals, &qh)
175
+            .map_err(|e| GrabberError::Protocol(e.to_string()))?,
176
+        layer_shell: LayerShell::bind(&globals, &qh)
177
+            .map_err(|e| GrabberError::Protocol(e.to_string()))?,
178
+
179
+        layer_surface: None,
180
+        keyboard: None,
181
+        pointer: None,
182
+        last_pointer_pos: (0.0, 0.0),
183
+        configured: false,
184
+        running: true,
185
+    };
186
+
187
+    // Wait for first output
188
+    event_queue
189
+        .roundtrip(&mut state)
190
+        .map_err(|e| GrabberError::Dispatch(e.to_string()))?;
191
+
192
+    // Create grabber surface (invisible fullscreen layer)
193
+    if let Some(output) = state.output_state.outputs().next() {
194
+        let surface = state.compositor_state.create_surface(&qh);
195
+
196
+        let layer = state.layer_shell.create_layer_surface(
197
+            &qh,
198
+            surface,
199
+            Layer::Overlay,
200
+            Some("hyprkvm-grabber"),
201
+            Some(&output),
202
+        );
203
+
204
+        // Configure for input grab
205
+        layer.set_anchor(Anchor::all());
206
+        layer.set_exclusive_zone(-1); // Don't push other windows
207
+        layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
208
+        layer.set_size(1, 1); // Minimal size, invisible
209
+
210
+        layer.commit();
211
+        state.layer_surface = Some(layer);
212
+    }
213
+
214
+    // Main loop
215
+    loop {
216
+        if !state.running {
217
+            break;
218
+        }
219
+
220
+        event_queue
221
+            .blocking_dispatch(&mut state)
222
+            .map_err(|e| GrabberError::Dispatch(e.to_string()))?;
223
+    }
224
+
225
+    Ok(())
226
+}
227
+
228
+struct GrabberState {
229
+    active: Arc<AtomicBool>,
230
+    event_tx: std::sync::mpsc::Sender<GrabEvent>,
231
+    registry_state: RegistryState,
232
+    seat_state: SeatState,
233
+    output_state: OutputState,
234
+    compositor_state: CompositorState,
235
+    layer_shell: LayerShell,
236
+
237
+    layer_surface: Option<LayerSurface>,
238
+    keyboard: Option<wl_keyboard::WlKeyboard>,
239
+    pointer: Option<wl_pointer::WlPointer>,
240
+    last_pointer_pos: (f64, f64),
241
+    configured: bool,
242
+    running: bool,
243
+}
244
+
245
+impl CompositorHandler for GrabberState {
246
+    fn scale_factor_changed(
247
+        &mut self,
248
+        _: &Connection,
249
+        _: &QueueHandle<Self>,
250
+        _: &wl_surface::WlSurface,
251
+        _: i32,
252
+    ) {
253
+    }
254
+
255
+    fn frame(
256
+        &mut self,
257
+        _: &Connection,
258
+        _: &QueueHandle<Self>,
259
+        _: &wl_surface::WlSurface,
260
+        _: u32,
261
+    ) {
262
+    }
263
+
264
+    fn transform_changed(
265
+        &mut self,
266
+        _: &Connection,
267
+        _: &QueueHandle<Self>,
268
+        _: &wl_surface::WlSurface,
269
+        _: wl_output::Transform,
270
+    ) {
271
+    }
272
+
273
+    fn surface_enter(
274
+        &mut self,
275
+        _: &Connection,
276
+        _: &QueueHandle<Self>,
277
+        _: &wl_surface::WlSurface,
278
+        _: &wl_output::WlOutput,
279
+    ) {
280
+    }
281
+
282
+    fn surface_leave(
283
+        &mut self,
284
+        _: &Connection,
285
+        _: &QueueHandle<Self>,
286
+        _: &wl_surface::WlSurface,
287
+        _: &wl_output::WlOutput,
288
+    ) {
289
+    }
290
+}
291
+
292
+impl OutputHandler for GrabberState {
293
+    fn output_state(&mut self) -> &mut OutputState {
294
+        &mut self.output_state
295
+    }
296
+
297
+    fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
298
+    fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
299
+    fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
300
+}
301
+
302
+impl LayerShellHandler for GrabberState {
303
+    fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
304
+        self.running = false;
305
+    }
306
+
307
+    fn configure(
308
+        &mut self,
309
+        _: &Connection,
310
+        _qh: &QueueHandle<Self>,
311
+        layer: &LayerSurface,
312
+        _configure: LayerSurfaceConfigure,
313
+        _serial: u32,
314
+    ) {
315
+        if !self.configured {
316
+            self.configured = true;
317
+            // Commit to acknowledge the configure
318
+            layer.commit();
319
+        }
320
+    }
321
+}
322
+
323
+impl SeatHandler for GrabberState {
324
+    fn seat_state(&mut self) -> &mut SeatState {
325
+        &mut self.seat_state
326
+    }
327
+
328
+    fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
329
+
330
+    fn new_capability(
331
+        &mut self,
332
+        _: &Connection,
333
+        qh: &QueueHandle<Self>,
334
+        seat: wl_seat::WlSeat,
335
+        capability: Capability,
336
+    ) {
337
+        if capability == Capability::Keyboard && self.keyboard.is_none() {
338
+            let keyboard = self.seat_state.get_keyboard(qh, &seat, None)
339
+                .expect("Failed to get keyboard");
340
+            self.keyboard = Some(keyboard);
341
+        }
342
+
343
+        if capability == Capability::Pointer && self.pointer.is_none() {
344
+            let pointer = self.seat_state.get_pointer(qh, &seat)
345
+                .expect("Failed to get pointer");
346
+            self.pointer = Some(pointer);
347
+        }
348
+    }
349
+
350
+    fn remove_capability(
351
+        &mut self,
352
+        _: &Connection,
353
+        _: &QueueHandle<Self>,
354
+        _: wl_seat::WlSeat,
355
+        capability: Capability,
356
+    ) {
357
+        if capability == Capability::Keyboard {
358
+            if let Some(keyboard) = self.keyboard.take() {
359
+                keyboard.release();
360
+            }
361
+        }
362
+        if capability == Capability::Pointer {
363
+            if let Some(pointer) = self.pointer.take() {
364
+                pointer.release();
365
+            }
366
+        }
367
+    }
368
+
369
+    fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
370
+}
371
+
372
+impl KeyboardHandler for GrabberState {
373
+    fn enter(
374
+        &mut self,
375
+        _: &Connection,
376
+        _: &QueueHandle<Self>,
377
+        _: &wl_keyboard::WlKeyboard,
378
+        _: &wl_surface::WlSurface,
379
+        _: u32,
380
+        _: &[u32],
381
+        _: &[Keysym],
382
+    ) {
383
+    }
384
+
385
+    fn leave(
386
+        &mut self,
387
+        _: &Connection,
388
+        _: &QueueHandle<Self>,
389
+        _: &wl_keyboard::WlKeyboard,
390
+        _: &wl_surface::WlSurface,
391
+        _: u32,
392
+    ) {
393
+    }
394
+
395
+    fn press_key(
396
+        &mut self,
397
+        _: &Connection,
398
+        _: &QueueHandle<Self>,
399
+        _: &wl_keyboard::WlKeyboard,
400
+        _: u32,
401
+        event: KeyEvent,
402
+    ) {
403
+        if self.active.load(Ordering::SeqCst) {
404
+            let _ = self.event_tx.send(GrabEvent::KeyDown {
405
+                keycode: event.raw_code,
406
+            });
407
+        }
408
+    }
409
+
410
+    fn release_key(
411
+        &mut self,
412
+        _: &Connection,
413
+        _: &QueueHandle<Self>,
414
+        _: &wl_keyboard::WlKeyboard,
415
+        _: u32,
416
+        event: KeyEvent,
417
+    ) {
418
+        if self.active.load(Ordering::SeqCst) {
419
+            let _ = self.event_tx.send(GrabEvent::KeyUp {
420
+                keycode: event.raw_code,
421
+            });
422
+        }
423
+    }
424
+
425
+    fn update_modifiers(
426
+        &mut self,
427
+        _: &Connection,
428
+        _: &QueueHandle<Self>,
429
+        _: &wl_keyboard::WlKeyboard,
430
+        _: u32,
431
+        modifiers: Modifiers,
432
+        _: u32,
433
+    ) {
434
+        if self.active.load(Ordering::SeqCst) {
435
+            let _ = self.event_tx.send(GrabEvent::ModifiersChanged { mods: modifiers });
436
+        }
437
+    }
438
+}
439
+
440
+impl PointerHandler for GrabberState {
441
+    fn pointer_frame(
442
+        &mut self,
443
+        _: &Connection,
444
+        _: &QueueHandle<Self>,
445
+        _: &wl_pointer::WlPointer,
446
+        events: &[PointerEvent],
447
+    ) {
448
+        if !self.active.load(Ordering::SeqCst) {
449
+            return;
450
+        }
451
+
452
+        for event in events {
453
+            match event.kind {
454
+                PointerEventKind::Motion { .. } => {
455
+                    let (x, y) = event.position;
456
+                    let dx = x - self.last_pointer_pos.0;
457
+                    let dy = y - self.last_pointer_pos.1;
458
+                    self.last_pointer_pos = (x, y);
459
+
460
+                    // Only send if there's actual motion
461
+                    if dx.abs() > 0.001 || dy.abs() > 0.001 {
462
+                        let _ = self.event_tx.send(GrabEvent::PointerMotion { dx, dy });
463
+                    }
464
+                }
465
+                PointerEventKind::Press { button, .. } => {
466
+                    let _ = self.event_tx.send(GrabEvent::PointerButton {
467
+                        button,
468
+                        pressed: true,
469
+                    });
470
+                }
471
+                PointerEventKind::Release { button, .. } => {
472
+                    let _ = self.event_tx.send(GrabEvent::PointerButton {
473
+                        button,
474
+                        pressed: false,
475
+                    });
476
+                }
477
+                PointerEventKind::Axis {
478
+                    horizontal,
479
+                    vertical,
480
+                    ..
481
+                } => {
482
+                    let _ = self.event_tx.send(GrabEvent::Scroll {
483
+                        horizontal: horizontal.absolute,
484
+                        vertical: vertical.absolute,
485
+                    });
486
+                }
487
+                _ => {}
488
+            }
489
+        }
490
+    }
491
+}
492
+
493
+impl ProvidesRegistryState for GrabberState {
494
+    fn registry(&mut self) -> &mut RegistryState {
495
+        &mut self.registry_state
496
+    }
497
+
498
+    registry_handlers![OutputState, SeatState];
499
+}
500
+
501
+delegate_compositor!(GrabberState);
502
+delegate_output!(GrabberState);
503
+delegate_seat!(GrabberState);
504
+delegate_keyboard!(GrabberState);
505
+delegate_pointer!(GrabberState);
506
+delegate_layer!(GrabberState);
507
+delegate_registry!(GrabberState);
508
+
509
+#[derive(Debug, thiserror::Error)]
510
+pub enum GrabberError {
511
+    #[error("Failed to connect to Wayland: {0}")]
512
+    Connection(String),
513
+
514
+    #[error("Failed to initialize registry: {0}")]
515
+    Registry(String),
516
+
517
+    #[error("Protocol not available: {0}")]
518
+    Protocol(String),
519
+
520
+    #[error("Dispatch error: {0}")]
521
+    Dispatch(String),
522
+
523
+    #[error("Thread error: {0}")]
524
+    Thread(String),
525
+}
hyprkvm-daemon/src/input/mod.rsmodified
@@ -4,9 +4,8 @@
4
 
4
 
5
 pub mod capture;
5
 pub mod capture;
6
 pub mod emulation;
6
 pub mod emulation;
7
-
7
+pub mod grabber;
8
-// TODO: Sprint 3 - Implement input handling
9
-// pub mod router;
10
 
8
 
11
 pub use capture::{EdgeCapture, EdgeCaptureConfig, EdgeCaptureError, EdgeEvent};
9
 pub use capture::{EdgeCapture, EdgeCaptureConfig, EdgeCaptureError, EdgeEvent};
12
 pub use emulation::{InputEmulator, VirtualKeyboard, VirtualPointer, EmulationError, button_codes};
10
 pub use emulation::{InputEmulator, VirtualKeyboard, VirtualPointer, EmulationError, button_codes};
11
+pub use grabber::{InputGrabber, InputGrabberConfig, GrabEvent, GrabberError};
hyprkvm-daemon/src/main.rsmodified
@@ -166,12 +166,23 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
166
         enabled_edges: enabled_edges.clone(),
166
         enabled_edges: enabled_edges.clone(),
167
     })?;
167
     })?;
168
 
168
 
169
+    // Create input grabber (for when we send control elsewhere)
170
+    let input_grabber = input::InputGrabber::new(input::InputGrabberConfig::default())?;
171
+
172
+    // Create input emulator (for when we receive control from elsewhere)
173
+    // This is created lazily when we first need to inject
174
+    let mut input_emulator: Option<input::InputEmulator> = None;
175
+
169
     // Create transfer manager
176
     // Create transfer manager
170
     let (transfer_manager, mut transfer_events) = transfer::TransferManager::new(
177
     let (transfer_manager, mut transfer_events) = transfer::TransferManager::new(
171
         config.machines.self_name.clone(),
178
         config.machines.self_name.clone(),
172
     );
179
     );
173
     let transfer_manager = Arc::new(transfer_manager);
180
     let transfer_manager = Arc::new(transfer_manager);
174
 
181
 
182
+    // Track which direction we're capturing for
183
+    let mut capture_direction: Option<Direction> = None;
184
+    let mut input_sequence: u64 = 0;
185
+
175
     // Connection storage: direction -> peer connection
186
     // Connection storage: direction -> peer connection
176
     let peers: Arc<RwLock<HashMap<Direction, network::FramedConnection>>> =
187
     let peers: Arc<RwLock<HashMap<Direction, network::FramedConnection>>> =
177
         Arc::new(RwLock::new(HashMap::new()));
188
         Arc::new(RwLock::new(HashMap::new()));
@@ -296,8 +307,24 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
296
 
307
 
297
     loop {
308
     loop {
298
         tokio::select! {
309
         tokio::select! {
299
-            // Check for edge events and poll peer messages
310
+            // Check for edge events, grabber events, and poll peer messages
300
             _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
311
             _ = tokio::time::sleep(std::time::Duration::from_millis(10)) => {
312
+                // Forward grabbed input to remote peer
313
+                if let Some(cap_dir) = capture_direction {
314
+                    while let Some(grab_event) = input_grabber.try_recv() {
315
+                        let payload = grab_event.to_protocol(input_sequence);
316
+                        input_sequence += 1;
317
+
318
+                        let msg = Message::InputEvent(payload);
319
+                        let mut peers = peers.write().await;
320
+                        if let Some(peer) = peers.get_mut(&cap_dir) {
321
+                            if let Err(e) = peer.send(&msg).await {
322
+                                tracing::error!("Failed to send input event: {}", e);
323
+                            }
324
+                        }
325
+                    }
326
+                }
327
+
301
                 // Handle edge events
328
                 // Handle edge events
302
                 while let Some(edge_event) = edge_capture.try_recv() {
329
                 while let Some(edge_event) = edge_capture.try_recv() {
303
                     let direction = edge_event.direction;
330
                     let direction = edge_event.direction;
@@ -382,9 +409,37 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
382
                                         info!("Received LeaveAck");
409
                                         info!("Received LeaveAck");
383
                                         // Transfer complete
410
                                         // Transfer complete
384
                                     }
411
                                     }
385
-                                    Message::InputEvent(input) => {
412
+                                    Message::InputEvent(input_payload) => {
386
-                                        tracing::trace!("Received input event: {:?}", input);
413
+                                        tracing::trace!("Received input event: {:?}", input_payload);
387
-                                        // TODO: Inject input via emulation module
414
+                                        // Inject input via emulation module
415
+                                        if let Some(ref emu) = input_emulator {
416
+                                            use hyprkvm_common::protocol::InputEventType;
417
+                                            match input_payload.event {
418
+                                                InputEventType::KeyDown { keycode } => {
419
+                                                    emu.keyboard.key(keycode, hyprkvm_common::KeyState::Pressed);
420
+                                                }
421
+                                                InputEventType::KeyUp { keycode } => {
422
+                                                    emu.keyboard.key(keycode, hyprkvm_common::KeyState::Released);
423
+                                                }
424
+                                                InputEventType::PointerMotion { dx, dy } => {
425
+                                                    emu.pointer.motion(dx, dy);
426
+                                                }
427
+                                                InputEventType::PointerButton { button, pressed } => {
428
+                                                    let state = if pressed {
429
+                                                        hyprkvm_common::ButtonState::Pressed
430
+                                                    } else {
431
+                                                        hyprkvm_common::ButtonState::Released
432
+                                                    };
433
+                                                    emu.pointer.button(button, state);
434
+                                                }
435
+                                                InputEventType::Scroll { horizontal, vertical } => {
436
+                                                    emu.pointer.scroll(horizontal, vertical);
437
+                                                }
438
+                                                InputEventType::ModifierState { .. } => {
439
+                                                    // Modifier state is informational
440
+                                                }
441
+                                            }
442
+                                        }
388
                                     }
443
                                     }
389
                                     Message::Ping { timestamp } => {
444
                                     Message::Ping { timestamp } => {
390
                                         let _ = peer.send(&Message::Pong { timestamp }).await;
445
                                         let _ = peer.send(&Message::Pong { timestamp }).await;
@@ -433,20 +488,34 @@ async fn run_daemon(config_path: &std::path::Path) -> anyhow::Result<()> {
433
                             tracing::warn!("No peer for direction {:?}", direction);
488
                             tracing::warn!("No peer for direction {:?}", direction);
434
                         }
489
                         }
435
                     }
490
                     }
436
-                    transfer::TransferEvent::StartCapture { direction } => {
491
+                    transfer::TransferEvent::StartCapture { direction: cap_dir } => {
437
-                        info!("Starting input capture for {:?}", direction);
492
+                        info!("Starting input capture for {:?}", cap_dir);
438
-                        // TODO: Implement actual input capture
493
+                        capture_direction = Some(cap_dir);
439
-                        // For now, just log
494
+                        input_grabber.start();
440
                     }
495
                     }
441
                     transfer::TransferEvent::StopCapture => {
496
                     transfer::TransferEvent::StopCapture => {
442
                         info!("Stopping input capture");
497
                         info!("Stopping input capture");
498
+                        capture_direction = None;
499
+                        input_grabber.stop();
443
                     }
500
                     }
444
                     transfer::TransferEvent::StartInjection { from } => {
501
                     transfer::TransferEvent::StartInjection { from } => {
445
                         info!("Starting input injection from {:?}", from);
502
                         info!("Starting input injection from {:?}", from);
446
-                        // TODO: Implement actual input injection
503
+                        // Create input emulator if not exists
504
+                        if input_emulator.is_none() {
505
+                            match input::InputEmulator::new() {
506
+                                Ok(emu) => {
507
+                                    info!("Input emulator created");
508
+                                    input_emulator = Some(emu);
509
+                                }
510
+                                Err(e) => {
511
+                                    tracing::error!("Failed to create input emulator: {}", e);
512
+                                }
513
+                            }
514
+                        }
447
                     }
515
                     }
448
                     transfer::TransferEvent::StopInjection => {
516
                     transfer::TransferEvent::StopInjection => {
449
                         info!("Stopping input injection");
517
                         info!("Stopping input injection");
518
+                        // Keep emulator around for next time
450
                     }
519
                     }
451
                 }
520
                 }
452
             }
521
             }