@desklink/host 0.5.9 → 0.5.11
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 +19 -44
- 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/native/agent_overlay_mac.m +19 -12
- package/engine/native/agent_overlay_wayland.c +15 -12
- package/engine/native/agent_overlay_x11.c +66 -17
- 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
|
@@ -163,6 +163,50 @@ int desklink_agent_overlay_main(const char *display_name) {
|
|
|
163
163
|
return 0;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// The ring is 72 logical px across, a 6px band with a pulse wave that grows
|
|
167
|
+
// out from it every PULSE_SECONDS, so the cue reads at a glance. The label is
|
|
168
|
+
// the core "fixed" font drawn at 2x, because a core X server offers nothing larger.
|
|
169
|
+
#define POINT_RING 36
|
|
170
|
+
#define POINT_PULSE_SECONDS 1.2
|
|
171
|
+
#define POINT_BADGE_H 40
|
|
172
|
+
#define POINT_FRAME_SECONDS 0.033
|
|
173
|
+
|
|
174
|
+
static void point_shape(Display *d, Window window, Pixmap mask, GC shape, int x, int y,
|
|
175
|
+
int badge_x, int badge_y, int badge_w, double elapsed) {
|
|
176
|
+
double phase = fmod(elapsed, POINT_PULSE_SECONDS) / POINT_PULSE_SECONDS;
|
|
177
|
+
int wave = POINT_RING + (int)lround(18*phase), wave_width = 6 - (int)lround(4*phase);
|
|
178
|
+
XSetForeground(d, shape, 0); XFillRectangle(d, mask, shape, 0, 0, 0xffff, 0xffff);
|
|
179
|
+
XSetForeground(d, shape, 1);
|
|
180
|
+
XSetLineAttributes(d, shape, 6, LineSolid, CapRound, JoinRound);
|
|
181
|
+
XDrawArc(d, mask, shape, x-POINT_RING, y-POINT_RING, 2*POINT_RING, 2*POINT_RING, 0, 360*64);
|
|
182
|
+
XFillArc(d, mask, shape, x-6, y-6, 12, 12, 0, 360*64);
|
|
183
|
+
XSetLineAttributes(d, shape, wave_width, LineSolid, CapRound, JoinRound);
|
|
184
|
+
XDrawArc(d, mask, shape, x-wave, y-wave, 2*wave, 2*wave, 0, 360*64);
|
|
185
|
+
if (badge_w) XFillRectangle(d, mask, shape, badge_x, badge_y, badge_w, POINT_BADGE_H);
|
|
186
|
+
XShapeCombineMask(d, window, ShapeBounding, 0, 0, mask, ShapeSet);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Text is rasterised once at 1x and drawn as 2x2 blocks.
|
|
190
|
+
static void point_text(Display *d, Window window, GC ink, XFontStruct *font, const char *label,
|
|
191
|
+
int len, int x, int y) {
|
|
192
|
+
int w = XTextWidth(font, label, len), h = font->ascent + font->descent;
|
|
193
|
+
Pixmap bits = XCreatePixmap(d, window, w, h, 1);
|
|
194
|
+
GC gc = XCreateGC(d, bits, 0, NULL);
|
|
195
|
+
XSetFont(d, gc, font->fid);
|
|
196
|
+
XSetForeground(d, gc, 0); XFillRectangle(d, bits, gc, 0, 0, w, h);
|
|
197
|
+
XSetForeground(d, gc, 1); XDrawString(d, bits, gc, 0, font->ascent, label, len);
|
|
198
|
+
XImage *image = XGetImage(d, bits, 0, 0, w, h, 1, ZPixmap);
|
|
199
|
+
XRectangle *blocks = malloc(sizeof(XRectangle) * (size_t)w * h);
|
|
200
|
+
int count = 0;
|
|
201
|
+
if (image && blocks)
|
|
202
|
+
for (int row = 0; row < h; row++) for (int col = 0; col < w; col++)
|
|
203
|
+
if (XGetPixel(image, col, row))
|
|
204
|
+
blocks[count++] = (XRectangle){(short)(x+2*col), (short)(y+2*row), 2, 2};
|
|
205
|
+
if (count) XFillRectangles(d, window, ink, blocks, count);
|
|
206
|
+
free(blocks); if (image) XDestroyImage(image);
|
|
207
|
+
XFreeGC(d, gc); XFreePixmap(d, bits);
|
|
208
|
+
}
|
|
209
|
+
|
|
166
210
|
// Point cues are independent of the real cursor. A shaped override-redirect
|
|
167
211
|
// window works without a compositor; an empty input shape passes every event
|
|
168
212
|
// through and mapping it never asks for keyboard focus. It intentionally appears
|
|
@@ -185,12 +229,11 @@ int desklink_point_overlay_main(const char *display_name) {
|
|
|
185
229
|
Pixmap mask = XCreatePixmap(d, root, width, height, 1);
|
|
186
230
|
GC shape = XCreateGC(d, mask, 0, NULL), ink = XCreateGC(d, window, 0, NULL);
|
|
187
231
|
XFontStruct *font = XLoadQueryFont(d, "fixed");
|
|
188
|
-
if (font) XSetFont(d, ink, font->fid);
|
|
189
|
-
XSetLineAttributes(d, shape, 4, LineSolid, CapRound, JoinRound);
|
|
190
232
|
fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);
|
|
191
233
|
puts("READY"); fflush(stdout);
|
|
192
234
|
char pending[512]; size_t used = 0;
|
|
193
|
-
double deadline = 0;
|
|
235
|
+
double deadline = 0, started = 0, drawn = 0;
|
|
236
|
+
int x = 0, y = 0, badge_x = 0, badge_y = 0, badge_w = 0;
|
|
194
237
|
for (;;) {
|
|
195
238
|
char bytes[512]; ssize_t n = read(STDIN_FILENO, bytes, sizeof(bytes));
|
|
196
239
|
if (n == 0) break;
|
|
@@ -200,35 +243,41 @@ int desklink_point_overlay_main(const char *display_name) {
|
|
|
200
243
|
double px, py; unsigned timeout; int offset = 0;
|
|
201
244
|
if (sscanf(pending, "P %lf %lf %u %n", &px, &py, &timeout, &offset) != 3 || !offset) continue;
|
|
202
245
|
const char *label = pending + offset;
|
|
203
|
-
int
|
|
204
|
-
|
|
246
|
+
int len = (int)strlen(label);
|
|
247
|
+
x = (int)px; y = (int)py;
|
|
248
|
+
int text_w = font ? 2*XTextWidth(font, label, len) : 12*len;
|
|
249
|
+
badge_w = len ? text_w + 24 : 0;
|
|
205
250
|
if (badge_w > width) badge_w = width;
|
|
206
|
-
|
|
251
|
+
badge_x = x + POINT_RING + 30; badge_y = y - POINT_BADGE_H/2;
|
|
207
252
|
if (badge_x + badge_w > width) badge_x = width - badge_w;
|
|
208
253
|
if (badge_y < 0) badge_y = 0;
|
|
209
|
-
if (badge_y +
|
|
254
|
+
if (badge_y + POINT_BADGE_H > height) badge_y = height - POINT_BADGE_H;
|
|
210
255
|
XUnmapWindow(d, window);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
XDrawArc(d, mask, shape, x-18, y-18, 36, 36, 0, 360*64);
|
|
214
|
-
XFillArc(d, mask, shape, x-3, y-3, 6, 6, 0, 360*64);
|
|
215
|
-
if (len) XFillRectangle(d, mask, shape, badge_x, badge_y, badge_w, 28);
|
|
216
|
-
XShapeCombineMask(d, window, ShapeBounding, 0, 0, mask, ShapeSet);
|
|
256
|
+
started = now();
|
|
257
|
+
point_shape(d, window, mask, shape, x, y, badge_x, badge_y, badge_w, 0);
|
|
217
258
|
XMapRaised(d, window);
|
|
218
259
|
XClearWindow(d, window);
|
|
219
260
|
if (len) {
|
|
220
261
|
XSetForeground(d, ink, 0x123047);
|
|
221
|
-
XFillRectangle(d, window, ink, badge_x, badge_y, badge_w,
|
|
262
|
+
XFillRectangle(d, window, ink, badge_x, badge_y, badge_w, POINT_BADGE_H);
|
|
222
263
|
XSetForeground(d, ink, 0xffffff);
|
|
223
|
-
|
|
264
|
+
if (font) point_text(d, window, ink, font, label, len, badge_x+12,
|
|
265
|
+
badge_y + (POINT_BADGE_H - 2*(font->ascent+font->descent))/2);
|
|
224
266
|
}
|
|
225
267
|
XSync(d, False);
|
|
226
|
-
|
|
268
|
+
drawn = now();
|
|
269
|
+
deadline = drawn + timeout/1000.0;
|
|
270
|
+
}
|
|
271
|
+
double t = now();
|
|
272
|
+
if (deadline && t >= deadline) { XUnmapWindow(d, window); XFlush(d); deadline = 0; }
|
|
273
|
+
else if (deadline && t - drawn >= POINT_FRAME_SECONDS) {
|
|
274
|
+
point_shape(d, window, mask, shape, x, y, badge_x, badge_y, badge_w, t - started);
|
|
275
|
+
XFlush(d); drawn = t;
|
|
227
276
|
}
|
|
228
|
-
if (deadline && now() >= deadline) { XUnmapWindow(d, window); XFlush(d); deadline = 0; }
|
|
229
277
|
usleep(5000);
|
|
230
278
|
}
|
|
231
279
|
XDestroyWindow(d, window); XFreeGC(d, shape); XFreeGC(d, ink);
|
|
232
280
|
XFreePixmap(d, mask); if (font) XFreeFont(d, font); XCloseDisplay(d);
|
|
233
281
|
return 0;
|
|
234
282
|
}
|
|
283
|
+
|
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
|
+
}
|