@desklink/host 0.5.9 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +2 -1
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +1 -0
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +58 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/docs/PROTOCOL.md +56 -0
- package/engine/Cargo.lock +2 -1
- package/engine/Cargo.toml +3 -1
- package/engine/src/input_mac.rs +433 -0
- package/engine/src/keymap_mac.rs +257 -0
- package/engine/src/mac.rs +8 -0
- package/engine/src/main.rs +12 -1
- package/engine/src/observe.rs +94 -0
- package/engine/src/portal.rs +83 -1
- package/engine/src/protocol.rs +30 -1
- package/engine/src/session.rs +59 -0
- package/package.json +3 -3
package/engine/src/input_mac.rs
CHANGED
|
@@ -299,3 +299,436 @@ impl InputDevices {
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
|
+
|
|
303
|
+
/// Whether the engine may listen to the desktop's input. Listening needs Input
|
|
304
|
+
/// Monitoring, a separate grant from the Accessibility one that posting needs.
|
|
305
|
+
pub fn observe_probe() -> Result<(), InputUnavailable> {
|
|
306
|
+
if unsafe { crate::mac::CGPreflightListenEventAccess() } {
|
|
307
|
+
Ok(())
|
|
308
|
+
} else {
|
|
309
|
+
let app = crate::mac::tcc_responsible_app_name();
|
|
310
|
+
Err(InputUnavailable {
|
|
311
|
+
reason: format!("Input Monitoring permission has not been granted to {app}."),
|
|
312
|
+
remedy: format!("Allow {app} in System Settings › Privacy & Security › Input Monitoring, then reconnect."),
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Shows the system's Input Monitoring prompt, once, and lists the app there.
|
|
318
|
+
pub fn request_observe_access() -> bool {
|
|
319
|
+
unsafe { crate::mac::CGRequestListenEventAccess() }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#[cfg(desklink_vpx)]
|
|
323
|
+
pub use observer::observe;
|
|
324
|
+
|
|
325
|
+
#[cfg(desklink_vpx)]
|
|
326
|
+
mod observer {
|
|
327
|
+
use super::*;
|
|
328
|
+
use crate::observe::{Observation, Sink, Tap, Unavailable};
|
|
329
|
+
use crate::protocol::{timestamp_us, ObservedInput, PointerPhase};
|
|
330
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
331
|
+
use std::sync::Arc;
|
|
332
|
+
use std::time::Instant;
|
|
333
|
+
|
|
334
|
+
type TapCallback = extern "C" fn(EventRef, u32, EventRef, *mut c_void) -> EventRef;
|
|
335
|
+
|
|
336
|
+
#[link(name = "ApplicationServices", kind = "framework")]
|
|
337
|
+
unsafe extern "C" {
|
|
338
|
+
fn CGEventTapCreate(tap: u32, place: u32, options: u32, events_of_interest: u64, callback: TapCallback, user_info: *mut c_void) -> CfTypeRef;
|
|
339
|
+
fn CGEventTapEnable(tap: CfTypeRef, enable: bool);
|
|
340
|
+
fn CGEventCreate(source: SourceRef) -> EventRef;
|
|
341
|
+
fn CGEventGetLocation(event: EventRef) -> Point;
|
|
342
|
+
fn CGEventGetFlags(event: EventRef) -> u64;
|
|
343
|
+
fn CGEventGetIntegerValueField(event: EventRef, field: u32) -> i64;
|
|
344
|
+
fn CGEventKeyboardGetUnicodeString(event: EventRef, max_length: usize, actual_length: *mut usize, buffer: *mut u16);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#[link(name = "CoreFoundation", kind = "framework")]
|
|
348
|
+
unsafe extern "C" {
|
|
349
|
+
fn CFMachPortCreateRunLoopSource(allocator: CfTypeRef, port: CfTypeRef, order: isize) -> CfTypeRef;
|
|
350
|
+
fn CFMachPortInvalidate(port: CfTypeRef);
|
|
351
|
+
fn CFRunLoopGetCurrent() -> CfTypeRef;
|
|
352
|
+
fn CFRunLoopAddSource(run_loop: CfTypeRef, source: CfTypeRef, mode: CfTypeRef);
|
|
353
|
+
fn CFRunLoopRunInMode(mode: CfTypeRef, seconds: f64, return_after_source: u8) -> i32;
|
|
354
|
+
static kCFRunLoopCommonModes: CfTypeRef;
|
|
355
|
+
static kCFRunLoopDefaultMode: CfTypeRef;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// CGEventType values this tap listens for.
|
|
359
|
+
const LEFT_DOWN: u32 = 1; const LEFT_UP: u32 = 2; const RIGHT_DOWN: u32 = 3; const RIGHT_UP: u32 = 4;
|
|
360
|
+
const MOVED: u32 = 5; const LEFT_DRAGGED: u32 = 6; const RIGHT_DRAGGED: u32 = 7; const KEY_DOWN: u32 = 10;
|
|
361
|
+
const KEY_UP: u32 = 11; const FLAGS_CHANGED: u32 = 12; const OTHER_DOWN: u32 = 25; const OTHER_UP: u32 = 26;
|
|
362
|
+
const OTHER_DRAGGED: u32 = 27;
|
|
363
|
+
// The system switches a tap off when its callback is slow or on its own
|
|
364
|
+
// say-so; these arrive in its place.
|
|
365
|
+
const DISABLED_BY_TIMEOUT: u32 = 0xFFFF_FFFE;
|
|
366
|
+
const DISABLED_BY_USER_INPUT: u32 = 0xFFFF_FFFF;
|
|
367
|
+
|
|
368
|
+
const BUTTON_NUMBER: u32 = 3;
|
|
369
|
+
const KEYCODE: u32 = 9;
|
|
370
|
+
const AUTOREPEAT: u32 = 8;
|
|
371
|
+
|
|
372
|
+
/// Where the selected display is on the desktop, so a point the system
|
|
373
|
+
/// reports (global, in points) becomes the source's own pixel.
|
|
374
|
+
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
375
|
+
pub struct Geometry {
|
|
376
|
+
origin: (f64, f64),
|
|
377
|
+
points_per_pixel: (f64, f64),
|
|
378
|
+
pixels: (i64, i64),
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
impl Geometry {
|
|
382
|
+
pub fn new(origin: (f64, f64), points: (f64, f64), pixels: (i64, i64)) -> Self {
|
|
383
|
+
Self {
|
|
384
|
+
origin,
|
|
385
|
+
points_per_pixel: (points.0 / pixels.0 as f64, points.1 / pixels.1 as f64),
|
|
386
|
+
pixels,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// `None` when the point is on another display.
|
|
391
|
+
pub fn to_source(&self, x: f64, y: f64) -> Option<(i64, i64)> {
|
|
392
|
+
let px = ((x - self.origin.0) / self.points_per_pixel.0).floor() as i64;
|
|
393
|
+
let py = ((y - self.origin.1) / self.points_per_pixel.1).floor() as i64;
|
|
394
|
+
((0..self.pixels.0).contains(&px) && (0..self.pixels.1).contains(&py))
|
|
395
|
+
.then_some((px, py))
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/// The control channel's pointer button numbers: 1 left, 2 middle, 3 right,
|
|
400
|
+
/// and 4 and up for the extra buttons.
|
|
401
|
+
pub fn observed_button(event_type: u32, button_number: i64) -> Option<(PointerPhase, i64)> {
|
|
402
|
+
let extra = |down| match button_number {
|
|
403
|
+
2 => Some((down, 2)),
|
|
404
|
+
n if n > 2 => Some((down, n + 1)),
|
|
405
|
+
_ => None,
|
|
406
|
+
};
|
|
407
|
+
match event_type {
|
|
408
|
+
LEFT_DOWN => Some((PointerPhase::Down, 1)),
|
|
409
|
+
LEFT_UP => Some((PointerPhase::Up, 1)),
|
|
410
|
+
RIGHT_DOWN => Some((PointerPhase::Down, 3)),
|
|
411
|
+
RIGHT_UP => Some((PointerPhase::Up, 3)),
|
|
412
|
+
OTHER_DOWN => extra(PointerPhase::Down),
|
|
413
|
+
OTHER_UP => extra(PointerPhase::Up),
|
|
414
|
+
_ => None,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
struct Observer {
|
|
419
|
+
geometry: Geometry,
|
|
420
|
+
sink: Sink,
|
|
421
|
+
cursor: crate::cursor::Reporter,
|
|
422
|
+
last_inside: (i64, i64),
|
|
423
|
+
port: CfTypeRef,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
impl Observer {
|
|
427
|
+
fn handle(&mut self, event_type: u32, event: EventRef) {
|
|
428
|
+
match event_type {
|
|
429
|
+
DISABLED_BY_TIMEOUT | DISABLED_BY_USER_INPUT => {
|
|
430
|
+
log::warn!("the input event tap was switched off by the system; re-enabling it");
|
|
431
|
+
unsafe { CGEventTapEnable(self.port, true) };
|
|
432
|
+
}
|
|
433
|
+
MOVED | LEFT_DRAGGED | RIGHT_DRAGGED | OTHER_DRAGGED => {
|
|
434
|
+
let at = unsafe { CGEventGetLocation(event) };
|
|
435
|
+
self.report_pointer(at);
|
|
436
|
+
}
|
|
437
|
+
LEFT_DOWN | LEFT_UP | RIGHT_DOWN | RIGHT_UP | OTHER_DOWN | OTHER_UP => {
|
|
438
|
+
let number = unsafe { CGEventGetIntegerValueField(event, BUTTON_NUMBER) };
|
|
439
|
+
let Some((phase, button)) = observed_button(event_type, number) else {
|
|
440
|
+
return;
|
|
441
|
+
};
|
|
442
|
+
let at = unsafe { CGEventGetLocation(event) };
|
|
443
|
+
// A click on another display is not on this source.
|
|
444
|
+
let Some((x, y)) = self.geometry.to_source(at.x, at.y) else {
|
|
445
|
+
return;
|
|
446
|
+
};
|
|
447
|
+
self.last_inside = (x, y);
|
|
448
|
+
self.send(ObservedInput::Pointer { phase, x, y, button });
|
|
449
|
+
}
|
|
450
|
+
KEY_DOWN | KEY_UP | FLAGS_CHANGED => {
|
|
451
|
+
let code = unsafe { CGEventGetIntegerValueField(event, KEYCODE) } as i16;
|
|
452
|
+
let flags = unsafe { CGEventGetFlags(event) };
|
|
453
|
+
let repeat = unsafe { CGEventGetIntegerValueField(event, AUTOREPEAT) } != 0;
|
|
454
|
+
let text = if event_type == FLAGS_CHANGED {
|
|
455
|
+
String::new()
|
|
456
|
+
} else {
|
|
457
|
+
let mut buffer = [0u16; 8];
|
|
458
|
+
let mut length = 0usize;
|
|
459
|
+
unsafe {
|
|
460
|
+
CGEventKeyboardGetUnicodeString(
|
|
461
|
+
event,
|
|
462
|
+
buffer.len(),
|
|
463
|
+
&mut length,
|
|
464
|
+
buffer.as_mut_ptr(),
|
|
465
|
+
)
|
|
466
|
+
};
|
|
467
|
+
String::from_utf16_lossy(&buffer[..length.min(buffer.len())])
|
|
468
|
+
};
|
|
469
|
+
if let Some(input) =
|
|
470
|
+
crate::keymap::observed_key(event_type, code, flags, repeat, &text)
|
|
471
|
+
{
|
|
472
|
+
self.send(input);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
_ => {}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/// Reports where the pointer is now, whether or not it moves.
|
|
480
|
+
fn report_pointer(&mut self, at: Point) {
|
|
481
|
+
match self.geometry.to_source(at.x, at.y) {
|
|
482
|
+
Some(inside) => {
|
|
483
|
+
self.last_inside = inside;
|
|
484
|
+
self.cursor.update(inside.0 as i32, inside.1 as i32, true);
|
|
485
|
+
}
|
|
486
|
+
None => self.cursor.update(
|
|
487
|
+
self.last_inside.0 as i32,
|
|
488
|
+
self.last_inside.1 as i32,
|
|
489
|
+
false,
|
|
490
|
+
),
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
fn send(&self, input: ObservedInput) {
|
|
495
|
+
(self.sink)(Observation::Input {
|
|
496
|
+
timestamp_us: timestamp_us(Instant::now()),
|
|
497
|
+
input,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
extern "C" fn tap_callback(
|
|
503
|
+
_proxy: EventRef,
|
|
504
|
+
event_type: u32,
|
|
505
|
+
event: EventRef,
|
|
506
|
+
user_info: *mut c_void,
|
|
507
|
+
) -> EventRef {
|
|
508
|
+
// The run loop of the tap's own thread is the only caller.
|
|
509
|
+
let observer = unsafe { &mut *(user_info as *mut Observer) };
|
|
510
|
+
observer.handle(event_type, event);
|
|
511
|
+
event
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/// The observer thread and the flag that ends it.
|
|
515
|
+
struct Running {
|
|
516
|
+
stop: Arc<AtomicBool>,
|
|
517
|
+
thread: Option<std::thread::JoinHandle<()>>,
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
impl Drop for Running {
|
|
521
|
+
fn drop(&mut self) {
|
|
522
|
+
self.stop.store(true, Ordering::Release);
|
|
523
|
+
if let Some(thread) = self.thread.take() {
|
|
524
|
+
let _ = thread.join();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
fn unavailable(code: &'static str, reason: String, remedy: String) -> Unavailable {
|
|
530
|
+
Unavailable {
|
|
531
|
+
code,
|
|
532
|
+
reason,
|
|
533
|
+
remedy,
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/// Listens to the pointer and keyboard of the display `display_id` with a
|
|
538
|
+
/// listen-only event tap: it sees events, it cannot change or block them.
|
|
539
|
+
pub fn observe(
|
|
540
|
+
display_id: u32,
|
|
541
|
+
pixels: (usize, usize),
|
|
542
|
+
sink: Sink,
|
|
543
|
+
) -> Result<Tap, Unavailable> {
|
|
544
|
+
let app = crate::mac::tcc_responsible_app_name();
|
|
545
|
+
// Preflight never prompts; the owner grants with `setup-input` or by
|
|
546
|
+
// adding the app in System Settings.
|
|
547
|
+
if let Err(missing) = observe_probe() {
|
|
548
|
+
return Err(unavailable(
|
|
549
|
+
"missing-input-monitoring",
|
|
550
|
+
missing.reason,
|
|
551
|
+
missing.remedy,
|
|
552
|
+
));
|
|
553
|
+
}
|
|
554
|
+
let (origin_x, origin_y, width, height) = crate::mac::display_geometry(display_id)
|
|
555
|
+
.ok_or_else(|| {
|
|
556
|
+
unavailable(
|
|
557
|
+
"display-unavailable",
|
|
558
|
+
String::from("the selected display is no longer active"),
|
|
559
|
+
String::from("select an active display and reconnect"),
|
|
560
|
+
)
|
|
561
|
+
})?;
|
|
562
|
+
let geometry = Geometry::new(
|
|
563
|
+
(origin_x, origin_y),
|
|
564
|
+
(width, height),
|
|
565
|
+
(pixels.0 as i64, pixels.1 as i64),
|
|
566
|
+
);
|
|
567
|
+
let (started_tx, started_rx) = std::sync::mpsc::channel();
|
|
568
|
+
let stop = Arc::new(AtomicBool::new(false));
|
|
569
|
+
let stopped = stop.clone();
|
|
570
|
+
let thread = std::thread::Builder::new()
|
|
571
|
+
.name(String::from("input-observer"))
|
|
572
|
+
.spawn(move || {
|
|
573
|
+
let cursor_sink = sink.clone();
|
|
574
|
+
let observer = Box::into_raw(Box::new(Observer {
|
|
575
|
+
geometry,
|
|
576
|
+
sink,
|
|
577
|
+
cursor: crate::cursor::Reporter::new(Box::new(move |position| {
|
|
578
|
+
cursor_sink(Observation::Cursor(position))
|
|
579
|
+
})),
|
|
580
|
+
last_inside: (0, 0),
|
|
581
|
+
port: std::ptr::null(),
|
|
582
|
+
}));
|
|
583
|
+
let mask: u64 = [
|
|
584
|
+
LEFT_DOWN,
|
|
585
|
+
LEFT_UP,
|
|
586
|
+
RIGHT_DOWN,
|
|
587
|
+
RIGHT_UP,
|
|
588
|
+
MOVED,
|
|
589
|
+
LEFT_DRAGGED,
|
|
590
|
+
RIGHT_DRAGGED,
|
|
591
|
+
KEY_DOWN,
|
|
592
|
+
KEY_UP,
|
|
593
|
+
FLAGS_CHANGED,
|
|
594
|
+
OTHER_DOWN,
|
|
595
|
+
OTHER_UP,
|
|
596
|
+
OTHER_DRAGGED,
|
|
597
|
+
]
|
|
598
|
+
.iter()
|
|
599
|
+
.fold(0u64, |mask, event| mask | (1u64 << *event));
|
|
600
|
+
// kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly.
|
|
601
|
+
let port =
|
|
602
|
+
unsafe { CGEventTapCreate(1, 0, 1, mask, tap_callback, observer.cast()) };
|
|
603
|
+
if port.is_null() {
|
|
604
|
+
drop(unsafe { Box::from_raw(observer) });
|
|
605
|
+
let _ = started_tx.send(Err(()));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
unsafe { (*observer).port = port };
|
|
609
|
+
let source = unsafe { CFMachPortCreateRunLoopSource(std::ptr::null(), port, 0) };
|
|
610
|
+
let run_loop = unsafe { CFRunLoopGetCurrent() };
|
|
611
|
+
unsafe {
|
|
612
|
+
CFRunLoopAddSource(run_loop, source, kCFRunLoopCommonModes);
|
|
613
|
+
CGEventTapEnable(port, true);
|
|
614
|
+
// Where the pointer already is, so a consumer does not wait for it to move.
|
|
615
|
+
let here = CGEventCreate(std::ptr::null());
|
|
616
|
+
if !here.is_null() {
|
|
617
|
+
(*observer).report_pointer(CGEventGetLocation(here));
|
|
618
|
+
CFRelease(here);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
let _ = started_tx.send(Ok(()));
|
|
622
|
+
// Polled rather than stopped from outside: stopping a run loop
|
|
623
|
+
// that has not started yet is lost, and Drop would wait forever.
|
|
624
|
+
while !stopped.load(Ordering::Acquire) {
|
|
625
|
+
unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, 0) };
|
|
626
|
+
}
|
|
627
|
+
unsafe {
|
|
628
|
+
CGEventTapEnable(port, false);
|
|
629
|
+
CFMachPortInvalidate(port);
|
|
630
|
+
CFRelease(source);
|
|
631
|
+
CFRelease(port);
|
|
632
|
+
drop(Box::from_raw(observer));
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
.map_err(|error| {
|
|
636
|
+
unavailable(
|
|
637
|
+
"event-tap-failed",
|
|
638
|
+
format!("could not start the input observer thread: {error}"),
|
|
639
|
+
String::from("retry; if it persists, restart the engine"),
|
|
640
|
+
)
|
|
641
|
+
})?;
|
|
642
|
+
match started_rx.recv() {
|
|
643
|
+
Ok(Ok(())) => Ok(Tap {
|
|
644
|
+
mechanism: "cgeventtap",
|
|
645
|
+
guard: Box::new(Running {
|
|
646
|
+
stop,
|
|
647
|
+
thread: Some(thread),
|
|
648
|
+
}),
|
|
649
|
+
}),
|
|
650
|
+
_ => {
|
|
651
|
+
let _ = thread.join();
|
|
652
|
+
Err(unavailable(
|
|
653
|
+
"event-tap-failed",
|
|
654
|
+
format!("macOS refused to create the input event tap for {app}."),
|
|
655
|
+
format!("Remove {app} from System Settings › Privacy & Security › Input Monitoring, add it again, then reconnect."),
|
|
656
|
+
))
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
#[cfg(test)]
|
|
662
|
+
mod tests {
|
|
663
|
+
use super::*;
|
|
664
|
+
|
|
665
|
+
// A 3420x2224 pixel panel shown as 1710x1112 points, with a second
|
|
666
|
+
// display to its right.
|
|
667
|
+
fn geometry() -> Geometry {
|
|
668
|
+
Geometry::new((0.0, 0.0), (1710.0, 1112.0), (3420, 2224))
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
#[test]
|
|
672
|
+
fn a_point_becomes_the_source_pixel_under_it() {
|
|
673
|
+
assert_eq!(geometry().to_source(100.5, 50.0), Some((201, 100)));
|
|
674
|
+
assert_eq!(geometry().to_source(0.0, 0.0), Some((0, 0)));
|
|
675
|
+
assert_eq!(geometry().to_source(1709.9, 1111.9), Some((3419, 2223)));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
#[test]
|
|
679
|
+
fn a_point_on_another_display_is_not_on_this_source() {
|
|
680
|
+
assert_eq!(geometry().to_source(1710.0, 10.0), None);
|
|
681
|
+
assert_eq!(geometry().to_source(-1.0, 10.0), None);
|
|
682
|
+
assert_eq!(geometry().to_source(10.0, 1112.0), None);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
#[test]
|
|
686
|
+
fn a_display_left_of_the_main_one_maps_from_its_own_origin() {
|
|
687
|
+
let left = Geometry::new((-1920.0, 0.0), (1920.0, 1080.0), (1920, 1080));
|
|
688
|
+
assert_eq!(left.to_source(-1920.0, 0.0), Some((0, 0)));
|
|
689
|
+
assert_eq!(left.to_source(-1.0, 1.0), Some((1919, 1)));
|
|
690
|
+
assert_eq!(left.to_source(0.0, 1.0), None);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
#[test]
|
|
694
|
+
fn buttons_use_the_control_channels_numbers() {
|
|
695
|
+
assert_eq!(observed_button(LEFT_DOWN, 0), Some((PointerPhase::Down, 1)));
|
|
696
|
+
assert_eq!(observed_button(LEFT_UP, 0), Some((PointerPhase::Up, 1)));
|
|
697
|
+
assert_eq!(observed_button(RIGHT_DOWN, 1), Some((PointerPhase::Down, 3)));
|
|
698
|
+
assert_eq!(observed_button(RIGHT_UP, 1), Some((PointerPhase::Up, 3)));
|
|
699
|
+
assert_eq!(observed_button(OTHER_DOWN, 2), Some((PointerPhase::Down, 2)));
|
|
700
|
+
assert_eq!(observed_button(OTHER_UP, 3), Some((PointerPhase::Up, 4)));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#[test]
|
|
704
|
+
fn a_motion_or_a_key_is_not_a_button() {
|
|
705
|
+
assert_eq!(observed_button(MOVED, 0), None);
|
|
706
|
+
assert_eq!(observed_button(KEY_DOWN, 0), None);
|
|
707
|
+
assert_eq!(observed_button(OTHER_DOWN, 1), None);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
#[test]
|
|
711
|
+
fn without_input_monitoring_the_tap_is_typed_unavailable_and_silent() {
|
|
712
|
+
// The CI and lab Macs this runs on have not granted Input Monitoring
|
|
713
|
+
// to the test binary; where one has, there is nothing to refuse.
|
|
714
|
+
if observe_probe().is_ok() {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
let delivered = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
718
|
+
let counted = delivered.clone();
|
|
719
|
+
let refused = observe(
|
|
720
|
+
1,
|
|
721
|
+
(1920, 1080),
|
|
722
|
+
std::sync::Arc::new(move |_| {
|
|
723
|
+
counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
724
|
+
}),
|
|
725
|
+
);
|
|
726
|
+
let Err(refused) = refused else {
|
|
727
|
+
panic!("a tap was created without the grant");
|
|
728
|
+
};
|
|
729
|
+
assert_eq!(refused.code, "missing-input-monitoring");
|
|
730
|
+
assert!(refused.remedy.contains("Input Monitoring"));
|
|
731
|
+
assert_eq!(delivered.load(std::sync::atomic::Ordering::SeqCst), 0);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
package/engine/src/keymap_mac.rs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
|
+
#[cfg(desklink_vpx)]
|
|
3
|
+
use crate::protocol::ObservedInput;
|
|
2
4
|
|
|
3
5
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
4
6
|
pub struct Keystroke {
|
|
@@ -182,3 +184,258 @@ fn us_ansi_key(character: char) -> Option<(i16, bool)> {
|
|
|
182
184
|
_ => return None,
|
|
183
185
|
})
|
|
184
186
|
}
|
|
187
|
+
|
|
188
|
+
/// Event types of a CGEventTap that carry a key.
|
|
189
|
+
#[cfg(desklink_vpx)]
|
|
190
|
+
pub mod key_event {
|
|
191
|
+
pub const DOWN: u32 = 10;
|
|
192
|
+
pub const UP: u32 = 11;
|
|
193
|
+
pub const FLAGS_CHANGED: u32 = 12;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Generic modifier bits of a CGEvent's flags.
|
|
197
|
+
#[cfg(desklink_vpx)]
|
|
198
|
+
const SHIFT: u64 = 0x2_0000;
|
|
199
|
+
#[cfg(desklink_vpx)]
|
|
200
|
+
const CONTROL: u64 = 0x4_0000;
|
|
201
|
+
#[cfg(desklink_vpx)]
|
|
202
|
+
const ALT: u64 = 0x8_0000;
|
|
203
|
+
#[cfg(desklink_vpx)]
|
|
204
|
+
const META: u64 = 0x10_0000;
|
|
205
|
+
|
|
206
|
+
/// A modifier key: its name, and the bit of the event flags that is set while
|
|
207
|
+
/// this key (not just its twin on the other side) is held. Caps Lock and Fn have
|
|
208
|
+
/// no per-side bit, so theirs is the generic one.
|
|
209
|
+
#[cfg(desklink_vpx)]
|
|
210
|
+
const MODIFIER_KEYS: [(i16, &str, u64); 10] = [
|
|
211
|
+
(59, "Control", 0x1), (62, "ControlRight", 0x2000), (56, "Shift", 0x2),
|
|
212
|
+
(60, "ShiftRight", 0x4), (58, "Alt", 0x20), (61, "AltRight", 0x40),
|
|
213
|
+
(55, "Meta", 0x8), (54, "MetaRight", 0x10), (57, "CapsLock", 0x1_0000),
|
|
214
|
+
(63, "Fn", 0x80_0000),
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
/// Keys that have a name rather than a character, following `KeyboardEvent.key`.
|
|
218
|
+
/// Every name here that the control channel accepts maps to the same virtual key
|
|
219
|
+
/// code through `named_key`.
|
|
220
|
+
#[cfg(desklink_vpx)]
|
|
221
|
+
const NAMED_KEYS: [(i16, &str); 36] = [
|
|
222
|
+
(36, "Enter"), (76, "Enter"), (48, "Tab"), (53, "Escape"),
|
|
223
|
+
(51, "Backspace"), (117, "Delete"), (126, "ArrowUp"), (125, "ArrowDown"),
|
|
224
|
+
(123, "ArrowLeft"), (124, "ArrowRight"), (115, "Home"), (119, "End"),
|
|
225
|
+
(116, "PageUp"), (121, "PageDown"), (122, "F1"), (120, "F2"),
|
|
226
|
+
(99, "F3"), (118, "F4"), (96, "F5"), (97, "F6"),
|
|
227
|
+
(98, "F7"), (100, "F8"), (101, "F9"), (109, "F10"),
|
|
228
|
+
(103, "F11"), (111, "F12"), (105, "F13"), (107, "F14"),
|
|
229
|
+
(113, "F15"), (106, "F16"), (64, "F17"), (79, "F18"),
|
|
230
|
+
(80, "F19"), (90, "F20"), (114, "Help"), (110, "ContextMenu"),
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
/// Names of the modifiers held in `flags`, in the order the control channel's
|
|
234
|
+
/// own examples use.
|
|
235
|
+
#[cfg(desklink_vpx)]
|
|
236
|
+
pub fn modifier_names(flags: u64) -> Vec<String> {
|
|
237
|
+
[
|
|
238
|
+
(CONTROL, "Control"),
|
|
239
|
+
(ALT, "Alt"),
|
|
240
|
+
(SHIFT, "Shift"),
|
|
241
|
+
(META, "Meta"),
|
|
242
|
+
]
|
|
243
|
+
.into_iter()
|
|
244
|
+
.filter(|(bit, _)| flags & bit != 0)
|
|
245
|
+
.map(|(_, name)| name.to_owned())
|
|
246
|
+
.collect()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// The character a key makes on the US ANSI map this engine injects with. Only
|
|
250
|
+
/// a fallback: a control chord, or an event the system gave no text, has none.
|
|
251
|
+
#[cfg(desklink_vpx)]
|
|
252
|
+
fn base_character(code: i16, shift: bool) -> Option<char> {
|
|
253
|
+
(' '..='~').find(|character| us_ansi_key(*character) == Some((code, shift)))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// The single printable character a key produced on the layout in use.
|
|
257
|
+
#[cfg(desklink_vpx)]
|
|
258
|
+
fn printable(text: &str) -> Option<char> {
|
|
259
|
+
let mut characters = text.chars();
|
|
260
|
+
let character = characters.next()?;
|
|
261
|
+
// U+F700..U+F8FF is where AppKit puts the function and arrow keys.
|
|
262
|
+
(characters.next().is_none() && !character.is_control() && !('\u{f700}'..='\u{f8ff}').contains(&character))
|
|
263
|
+
.then_some(character)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// What a CGEventTap key event means, in the control channel's `key` vocabulary.
|
|
267
|
+
///
|
|
268
|
+
/// `text` is what the event types on the active layout. Auto-repeat is not an
|
|
269
|
+
/// event: a held key is one down and one up. A modifier arrives as a flags
|
|
270
|
+
/// change, whose direction is its own bit being set.
|
|
271
|
+
#[cfg(desklink_vpx)]
|
|
272
|
+
pub fn observed_key(
|
|
273
|
+
event_type: u32,
|
|
274
|
+
code: i16,
|
|
275
|
+
flags: u64,
|
|
276
|
+
autorepeat: bool,
|
|
277
|
+
text: &str,
|
|
278
|
+
) -> Option<ObservedInput> {
|
|
279
|
+
let modifiers = modifier_names(flags);
|
|
280
|
+
if event_type == key_event::FLAGS_CHANGED {
|
|
281
|
+
let (_, name, bit) = MODIFIER_KEYS.iter().find(|(key, _, _)| *key == code)?;
|
|
282
|
+
return Some(ObservedInput::Key {
|
|
283
|
+
name: Some((*name).to_owned()),
|
|
284
|
+
character: None,
|
|
285
|
+
down: flags & bit != 0,
|
|
286
|
+
modifiers,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
let down = match event_type {
|
|
290
|
+
key_event::DOWN if autorepeat => return None,
|
|
291
|
+
key_event::DOWN => true,
|
|
292
|
+
key_event::UP => false,
|
|
293
|
+
_ => return None,
|
|
294
|
+
};
|
|
295
|
+
let named = NAMED_KEYS.iter().find(|(key, _)| *key == code);
|
|
296
|
+
let character = if named.is_some() {
|
|
297
|
+
None
|
|
298
|
+
} else {
|
|
299
|
+
printable(text).or_else(|| base_character(code, flags & SHIFT != 0))
|
|
300
|
+
};
|
|
301
|
+
let name = match (named, character) {
|
|
302
|
+
(Some((_, name)), _) => Some((*name).to_owned()),
|
|
303
|
+
(None, Some(_)) => None,
|
|
304
|
+
(None, None) => Some(String::from("Unidentified")),
|
|
305
|
+
};
|
|
306
|
+
Some(ObservedInput::Key {
|
|
307
|
+
name,
|
|
308
|
+
character: character.map(String::from),
|
|
309
|
+
down,
|
|
310
|
+
modifiers,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[cfg(all(test, desklink_vpx))]
|
|
315
|
+
mod tests {
|
|
316
|
+
use super::*;
|
|
317
|
+
|
|
318
|
+
fn key(
|
|
319
|
+
event_type: u32,
|
|
320
|
+
code: i16,
|
|
321
|
+
flags: u64,
|
|
322
|
+
autorepeat: bool,
|
|
323
|
+
text: &str,
|
|
324
|
+
) -> Option<ObservedInput> {
|
|
325
|
+
observed_key(event_type, code, flags, autorepeat, text)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn a_letter_is_a_character_with_the_modifier_state_after_the_event() {
|
|
330
|
+
assert_eq!(
|
|
331
|
+
key(key_event::DOWN, 0, SHIFT, false, "A"),
|
|
332
|
+
Some(ObservedInput::Key {
|
|
333
|
+
name: None,
|
|
334
|
+
character: Some(String::from("A")),
|
|
335
|
+
down: true,
|
|
336
|
+
modifiers: vec![String::from("Shift")],
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#[test]
|
|
342
|
+
fn a_named_key_carries_no_character() {
|
|
343
|
+
assert_eq!(
|
|
344
|
+
key(key_event::UP, 36, 0, false, "\r"),
|
|
345
|
+
Some(ObservedInput::Key {
|
|
346
|
+
name: Some(String::from("Enter")),
|
|
347
|
+
character: None,
|
|
348
|
+
down: false,
|
|
349
|
+
modifiers: Vec::new(),
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[test]
|
|
355
|
+
fn a_chord_with_no_text_falls_back_to_the_key_it_is() {
|
|
356
|
+
// Control-C types U+0003, which is not a character a keycap can show.
|
|
357
|
+
let Some(ObservedInput::Key {
|
|
358
|
+
character,
|
|
359
|
+
modifiers,
|
|
360
|
+
..
|
|
361
|
+
}) = key(key_event::DOWN, 8, CONTROL, false, "\u{3}")
|
|
362
|
+
else {
|
|
363
|
+
panic!("a chord is a key");
|
|
364
|
+
};
|
|
365
|
+
assert_eq!(character.as_deref(), Some("c"));
|
|
366
|
+
assert_eq!(modifiers, vec![String::from("Control")]);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn an_event_with_no_text_is_still_the_shifted_character() {
|
|
371
|
+
let Some(ObservedInput::Key { character, .. }) =
|
|
372
|
+
key(key_event::DOWN, 4, SHIFT, false, "")
|
|
373
|
+
else {
|
|
374
|
+
panic!("a letter is a key");
|
|
375
|
+
};
|
|
376
|
+
assert_eq!(character.as_deref(), Some("H"));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[test]
|
|
380
|
+
fn a_held_key_is_one_down_and_one_up() {
|
|
381
|
+
assert_eq!(key(key_event::DOWN, 0, 0, true, "a"), None);
|
|
382
|
+
assert!(key(key_event::UP, 0, 0, false, "a").is_some());
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[test]
|
|
386
|
+
fn releasing_one_shift_while_the_other_is_held_is_an_up() {
|
|
387
|
+
// Left shift went up; right shift (bit 0x4) and the generic bit remain.
|
|
388
|
+
let Some(ObservedInput::Key {
|
|
389
|
+
name,
|
|
390
|
+
down,
|
|
391
|
+
modifiers,
|
|
392
|
+
..
|
|
393
|
+
}) = key(key_event::FLAGS_CHANGED, 56, SHIFT | 0x4, false, "")
|
|
394
|
+
else {
|
|
395
|
+
panic!("a modifier change is a key");
|
|
396
|
+
};
|
|
397
|
+
assert_eq!(name.as_deref(), Some("Shift"));
|
|
398
|
+
assert!(!down);
|
|
399
|
+
assert_eq!(modifiers, vec![String::from("Shift")]);
|
|
400
|
+
let Some(ObservedInput::Key { down, .. }) =
|
|
401
|
+
key(key_event::FLAGS_CHANGED, 56, SHIFT | 0x2 | 0x4, false, "")
|
|
402
|
+
else {
|
|
403
|
+
panic!("a modifier change is a key");
|
|
404
|
+
};
|
|
405
|
+
assert!(down);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#[test]
|
|
409
|
+
fn a_key_with_no_name_and_no_text_is_unidentified_not_empty() {
|
|
410
|
+
let Some(ObservedInput::Key {
|
|
411
|
+
name, character, ..
|
|
412
|
+
}) = key(key_event::DOWN, 127, 0, false, "")
|
|
413
|
+
else {
|
|
414
|
+
panic!("an unknown key is still a key");
|
|
415
|
+
};
|
|
416
|
+
assert_eq!(name.as_deref(), Some("Unidentified"));
|
|
417
|
+
assert_eq!(character, None);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[test]
|
|
421
|
+
fn the_private_use_characters_appkit_gives_arrows_are_not_text() {
|
|
422
|
+
assert_eq!(printable("\u{f702}"), None);
|
|
423
|
+
assert_eq!(printable("é"), Some('é'));
|
|
424
|
+
assert_eq!(printable("ab"), None);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#[test]
|
|
428
|
+
fn what_is_observed_is_what_the_control_channel_would_inject() {
|
|
429
|
+
for (code, name) in NAMED_KEYS {
|
|
430
|
+
if let Some(stroke) = named_key(name) {
|
|
431
|
+
// Keypad Enter (76) shares its name with Return (36).
|
|
432
|
+
assert!(stroke.code == code || name == "Enter", "{name}");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (code, name, _) in MODIFIER_KEYS {
|
|
436
|
+
if let Some(injected) = modifier_key(name) {
|
|
437
|
+
assert_eq!(injected, code, "{name}");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|