@desklink/host 0.1.0 → 0.2.0

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.
Files changed (55) hide show
  1. package/NOTICE +3 -0
  2. package/README.md +72 -16
  3. package/bin/desklink-host.mjs +14 -3
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js +5 -1
  6. package/dist/bridge.js.map +1 -1
  7. package/dist/engineProcess.d.ts.map +1 -1
  8. package/dist/engineProcess.js +17 -5
  9. package/dist/engineProcess.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/keeper.d.ts +52 -0
  15. package/dist/keeper.d.ts.map +1 -0
  16. package/dist/keeper.js +101 -0
  17. package/dist/keeper.js.map +1 -0
  18. package/dist/protocol.d.ts +42 -5
  19. package/dist/protocol.d.ts.map +1 -1
  20. package/dist/protocol.js +1 -1
  21. package/dist/protocol.js.map +1 -1
  22. package/dist/resolveEngine.d.ts +3 -2
  23. package/dist/resolveEngine.d.ts.map +1 -1
  24. package/dist/resolveEngine.js +20 -5
  25. package/dist/resolveEngine.js.map +1 -1
  26. package/docs/BOUNDARY.md +11 -6
  27. package/docs/PROTOCOL.md +297 -33
  28. package/engine/Cargo.lock +196 -1
  29. package/engine/Cargo.toml +26 -5
  30. package/engine/build.rs +98 -23
  31. package/engine/native/agent_overlay_mac.m +133 -0
  32. package/engine/native/agent_overlay_wayland.c +192 -0
  33. package/engine/native/agent_overlay_x11.c +103 -0
  34. package/engine/native/mac_stream.mm +188 -0
  35. package/engine/src/capture.rs +169 -19
  36. package/engine/src/capture_mac.rs +145 -0
  37. package/engine/src/clipboard_mac.rs +182 -0
  38. package/engine/src/convert.rs +57 -1
  39. package/engine/src/indicator.rs +162 -0
  40. package/engine/src/input.rs +12 -0
  41. package/engine/src/input_mac.rs +312 -0
  42. package/engine/src/keeper.rs +622 -0
  43. package/engine/src/keymap.rs +25 -0
  44. package/engine/src/keymap_mac.rs +209 -0
  45. package/engine/src/mac.rs +722 -0
  46. package/engine/src/main.rs +158 -2
  47. package/engine/src/peer.rs +429 -22
  48. package/engine/src/portal.rs +4 -0
  49. package/engine/src/portal_mac.rs +68 -0
  50. package/engine/src/protocol.rs +186 -3
  51. package/engine/src/session.rs +1307 -100
  52. package/engine/src/x11.rs +23 -3
  53. package/engine/src/x11_mac.rs +40 -0
  54. package/examples/reference-client.html +33 -4
  55. package/package.json +2 -2
package/engine/Cargo.lock CHANGED
@@ -257,6 +257,15 @@ dependencies = [
257
257
  "generic-array",
258
258
  ]
259
259
 
260
+ [[package]]
261
+ name = "block2"
262
+ version = "0.6.2"
263
+ source = "registry+https://github.com/rust-lang/crates.io-index"
264
+ checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
265
+ dependencies = [
266
+ "objc2",
267
+ ]
268
+
260
269
  [[package]]
261
270
  name = "bumpalo"
262
271
  version = "3.20.3"
@@ -508,16 +517,23 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
508
517
 
509
518
  [[package]]
510
519
  name = "desklink-host"
511
- version = "0.1.0"
520
+ version = "0.2.0"
512
521
  dependencies = [
513
522
  "anyhow",
514
523
  "ashpd",
515
524
  "async-trait",
525
+ "base64 0.22.1",
526
+ "block2",
516
527
  "bytes",
517
528
  "cc",
518
529
  "futures-util",
519
530
  "libc",
520
531
  "log",
532
+ "objc2-core-foundation",
533
+ "objc2-core-graphics",
534
+ "objc2-core-media",
535
+ "objc2-foundation",
536
+ "objc2-screen-capture-kit",
521
537
  "pipewire",
522
538
  "rand 0.8.8",
523
539
  "rtc",
@@ -541,6 +557,16 @@ dependencies = [
541
557
  "subtle",
542
558
  ]
543
559
 
560
+ [[package]]
561
+ name = "dispatch2"
562
+ version = "0.3.1"
563
+ source = "registry+https://github.com/rust-lang/crates.io-index"
564
+ checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
565
+ dependencies = [
566
+ "bitflags 2.13.2",
567
+ "objc2",
568
+ ]
569
+
544
570
  [[package]]
545
571
  name = "displaydoc"
546
572
  version = "0.2.7"
@@ -1211,6 +1237,175 @@ dependencies = [
1211
1237
  "autocfg",
1212
1238
  ]
1213
1239
 
1240
+ [[package]]
1241
+ name = "objc2"
1242
+ version = "0.6.4"
1243
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1244
+ checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
1245
+ dependencies = [
1246
+ "objc2-encode",
1247
+ ]
1248
+
1249
+ [[package]]
1250
+ name = "objc2-av-foundation"
1251
+ version = "0.3.2"
1252
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1253
+ checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0"
1254
+ dependencies = [
1255
+ "objc2",
1256
+ "objc2-foundation",
1257
+ ]
1258
+
1259
+ [[package]]
1260
+ name = "objc2-core-audio"
1261
+ version = "0.3.2"
1262
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1263
+ checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
1264
+ dependencies = [
1265
+ "dispatch2",
1266
+ "objc2",
1267
+ "objc2-core-audio-types",
1268
+ "objc2-core-foundation",
1269
+ ]
1270
+
1271
+ [[package]]
1272
+ name = "objc2-core-audio-types"
1273
+ version = "0.3.2"
1274
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1275
+ checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
1276
+ dependencies = [
1277
+ "bitflags 2.13.2",
1278
+ "objc2",
1279
+ ]
1280
+
1281
+ [[package]]
1282
+ name = "objc2-core-foundation"
1283
+ version = "0.3.2"
1284
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1285
+ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
1286
+ dependencies = [
1287
+ "bitflags 2.13.2",
1288
+ "block2",
1289
+ "dispatch2",
1290
+ "libc",
1291
+ "objc2",
1292
+ ]
1293
+
1294
+ [[package]]
1295
+ name = "objc2-core-graphics"
1296
+ version = "0.3.2"
1297
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1298
+ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
1299
+ dependencies = [
1300
+ "bitflags 2.13.2",
1301
+ "block2",
1302
+ "dispatch2",
1303
+ "libc",
1304
+ "objc2",
1305
+ "objc2-core-foundation",
1306
+ "objc2-io-surface",
1307
+ "objc2-metal",
1308
+ ]
1309
+
1310
+ [[package]]
1311
+ name = "objc2-core-media"
1312
+ version = "0.3.2"
1313
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1314
+ checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d"
1315
+ dependencies = [
1316
+ "bitflags 2.13.2",
1317
+ "block2",
1318
+ "dispatch2",
1319
+ "objc2",
1320
+ "objc2-core-audio",
1321
+ "objc2-core-audio-types",
1322
+ "objc2-core-foundation",
1323
+ "objc2-core-video",
1324
+ ]
1325
+
1326
+ [[package]]
1327
+ name = "objc2-core-video"
1328
+ version = "0.3.2"
1329
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1330
+ checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
1331
+ dependencies = [
1332
+ "bitflags 2.13.2",
1333
+ "objc2",
1334
+ "objc2-core-foundation",
1335
+ "objc2-core-graphics",
1336
+ "objc2-io-surface",
1337
+ ]
1338
+
1339
+ [[package]]
1340
+ name = "objc2-encode"
1341
+ version = "4.1.0"
1342
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1343
+ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
1344
+
1345
+ [[package]]
1346
+ name = "objc2-foundation"
1347
+ version = "0.3.2"
1348
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1349
+ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
1350
+ dependencies = [
1351
+ "bitflags 2.13.2",
1352
+ "block2",
1353
+ "libc",
1354
+ "objc2",
1355
+ "objc2-core-foundation",
1356
+ ]
1357
+
1358
+ [[package]]
1359
+ name = "objc2-io-surface"
1360
+ version = "0.3.2"
1361
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1362
+ checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
1363
+ dependencies = [
1364
+ "bitflags 2.13.2",
1365
+ "objc2",
1366
+ "objc2-core-foundation",
1367
+ ]
1368
+
1369
+ [[package]]
1370
+ name = "objc2-metal"
1371
+ version = "0.3.2"
1372
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1373
+ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794"
1374
+ dependencies = [
1375
+ "bitflags 2.13.2",
1376
+ "objc2",
1377
+ "objc2-foundation",
1378
+ ]
1379
+
1380
+ [[package]]
1381
+ name = "objc2-screen-capture-kit"
1382
+ version = "0.3.2"
1383
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1384
+ checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394"
1385
+ dependencies = [
1386
+ "bitflags 2.13.2",
1387
+ "block2",
1388
+ "dispatch2",
1389
+ "libc",
1390
+ "objc2",
1391
+ "objc2-av-foundation",
1392
+ "objc2-core-foundation",
1393
+ "objc2-core-graphics",
1394
+ "objc2-core-media",
1395
+ "objc2-foundation",
1396
+ "objc2-uniform-type-identifiers",
1397
+ ]
1398
+
1399
+ [[package]]
1400
+ name = "objc2-uniform-type-identifiers"
1401
+ version = "0.3.2"
1402
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1403
+ checksum = "7902ac02859fc1f7045f8b598c63f1ae0cc7efeaa06a9bc9f3d9a3c955974fa4"
1404
+ dependencies = [
1405
+ "objc2",
1406
+ "objc2-foundation",
1407
+ ]
1408
+
1214
1409
  [[package]]
1215
1410
  name = "oid-registry"
1216
1411
  version = "0.7.1"
package/engine/Cargo.toml CHANGED
@@ -1,7 +1,8 @@
1
1
  [package]
2
2
  name = "desklink-host"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
+ rust-version = "1.97.1"
5
6
  license = "Apache-2.0"
6
7
  description = "Per-user on-demand desktop capture, encode and input engine speaking a versioned local control protocol"
7
8
  publish = false
@@ -11,23 +12,43 @@ name = "desklink-host"
11
12
  path = "src/main.rs"
12
13
 
13
14
  [dependencies]
14
- anyhow = "1"
15
15
  serde = { version = "1", features = ["derive"] }
16
16
  serde_json = "1"
17
+ base64 = "0.22"
18
+
19
+ [target.'cfg(target_os = "linux")'.dependencies]
20
+ anyhow = "1"
17
21
  tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
18
22
  futures-util = "0.3"
19
- ashpd = { version = "0.11", default-features = false, features = ["tokio"] }
20
- pipewire = { version = "0.10", features = ["v0_3_44"] }
21
23
  libc = "0.2"
22
24
  bytes = "1"
23
25
  webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
24
26
  rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
27
+ log = "0.4"
28
+ async-trait = "0.1"
29
+ rand = "0.8"
30
+ ashpd = { version = "0.11", default-features = false, features = ["tokio"] }
31
+ pipewire = { version = "0.10", features = ["v0_3_44"] }
25
32
  xkbcommon = "0.8"
26
33
  x11rb = { version = "0.14", default-features = false, features = ["allow-unsafe-code", "xtest"] }
27
34
  wl-clipboard-rs = "0.9"
28
- log = "0.4"
35
+
36
+ [target.'cfg(target_os = "macos")'.dependencies]
37
+ anyhow = "1"
29
38
  async-trait = "0.1"
39
+ bytes = "1"
40
+ libc = "0.2"
41
+ log = "0.4"
30
42
  rand = "0.8"
43
+ rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
44
+ tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
45
+ webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
46
+ objc2-screen-capture-kit = { version = "0.3.2", features = ["SCStream", "SCShareableContent", "SCError", "SCScreenshotManager", "block2", "dispatch2", "objc2-core-media", "objc2-core-graphics"] }
47
+ objc2-foundation = { version = "0.3.2", features = ["NSArray", "NSError", "NSString"] }
48
+ objc2-core-media = "0.3.2"
49
+ objc2-core-foundation = "0.3.2"
50
+ objc2-core-graphics = { version = "0.3.2", features = ["CGImage", "CGDataProvider"] }
51
+ block2 = "0.6"
31
52
 
32
53
  [build-dependencies]
33
54
  cc = "1"
package/engine/build.rs CHANGED
@@ -1,7 +1,8 @@
1
1
  //! Native dependencies of the engine.
2
2
  //!
3
- //! Two permissive C libraries, both built or linked from this machine's own
4
- //! sources — no prebuilt binary is fetched, and nothing is installed:
3
+ //! The Linux build uses two permissive C libraries, both built or linked from
4
+ //! this machine's own sources — no prebuilt binary is fetched, and nothing is
5
+ //! installed:
5
6
  //!
6
7
  //! * libvpx (BSD-3-Clause) — VP9 encode, through `native/vpx_shim.c` so the
7
8
  //! versioned encoder config struct is laid out by a C compiler.
@@ -9,9 +10,9 @@
9
10
  //! over `uinput`/`libevdev`, built by its own CMake project into a static
10
11
  //! library and called through its documented C API.
11
12
  //!
12
- //! Both are required to build the engine; the README lists the system packages
13
- //! they need. Whether input can actually be injected is a separate, runtime
14
- //! question the engine answers through its own `/dev/uinput` probe.
13
+ //! macOS builds only the libvpx shim; ScreenCaptureKit, Quartz input and
14
+ //! AppKit clipboard use native frameworks. Linux runtime input access is still
15
+ //! checked through `/dev/uinput`.
15
16
  //!
16
17
  //! `DESKLINK_VPX_STATIC_DIR` names a libvpx install prefix (`include/`, `lib/`)
17
18
  //! to link statically instead. The prebuilt engine uses it, because libvpx's
@@ -21,40 +22,114 @@
21
22
  use std::path::{Path, PathBuf};
22
23
 
23
24
  fn main() -> Result<(), Box<dyn std::error::Error>> {
25
+ println!("cargo:rustc-check-cfg=cfg(desklink_vpx)");
26
+ println!("cargo:rustc-check-cfg=cfg(desklink_macos_cli)");
24
27
  println!("cargo:rerun-if-changed=native/vpx_shim.c");
25
28
  println!("cargo:rerun-if-changed=native/inputtino_shim.cpp");
29
+ println!("cargo:rerun-if-changed=native/mac_stream.mm");
30
+ println!("cargo:rerun-if-changed=native/agent_overlay_mac.m");
31
+ println!("cargo:rerun-if-changed=native/agent_overlay_x11.c");
32
+ println!("cargo:rerun-if-changed=native/agent_overlay_wayland.c");
33
+ println!("cargo:rerun-if-changed=Info.plist");
26
34
  println!("cargo:rerun-if-changed=vendor/inputtino/src/uinput/include/inputtino/keyboard.hpp");
27
35
  println!("cargo:rerun-if-changed=vendor/inputtino/include/inputtino/input.h");
28
36
  println!("cargo:rerun-if-changed=vendor/inputtino/CMakeLists.txt");
29
37
 
30
38
  println!("cargo:rerun-if-env-changed=DESKLINK_VPX_STATIC_DIR");
39
+ println!("cargo:rerun-if-env-changed=DESKLINK_MACOS_CLI");
31
40
 
32
- let static_vpx = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from);
33
- let mut shim = cc::Build::new();
34
- shim.file("native/vpx_shim.c")
35
- .flag_if_supported("-Wno-unused-parameter");
36
- if let Some(prefix) = &static_vpx {
37
- shim.include(prefix.join("include"));
41
+ let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
42
+ if let Some(prefix) = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from) {
43
+ if !prefix.join("include/vpx/vp8cx.h").exists() || !prefix.join("lib/libvpx.a").exists() {
44
+ return Err(format!("DESKLINK_VPX_STATIC_DIR must be the libvpx install prefix containing include/vpx/vp8cx.h and lib/libvpx.a, not its lib directory: {}", prefix.display()).into());
45
+ }
46
+ }
47
+ if target_os == "macos" && std::env::var_os("DESKLINK_MACOS_CLI").is_some() {
48
+ println!("cargo:rustc-cfg=desklink_macos_cli");
49
+ }
50
+ if target_os == "linux" {
51
+ let static_vpx = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from);
52
+ let mut shim = cc::Build::new();
53
+ shim.file("native/vpx_shim.c")
54
+ .flag_if_supported("-Wno-unused-parameter");
55
+ if let Some(prefix) = &static_vpx {
56
+ shim.include(prefix.join("include"));
57
+ }
58
+ shim.compile("dlvpx");
59
+ match &static_vpx {
60
+ Some(prefix) => {
61
+ println!(
62
+ "cargo:rustc-link-search=native={}",
63
+ prefix.join("lib").display()
64
+ );
65
+ println!("cargo:rustc-link-lib=static=vpx");
66
+ }
67
+ None => println!("cargo:rustc-link-lib=vpx"),
68
+ }
38
69
  }
39
- shim.compile("dlvpx");
40
- match &static_vpx {
41
- Some(prefix) => {
70
+
71
+ if target_os == "linux" {
72
+ cc::Build::new()
73
+ .file("native/agent_overlay_x11.c")
74
+ .file("native/agent_overlay_wayland.c")
75
+ .compile("dlagentoverlay");
76
+ for lib in ["X11", "Xcursor", "wayland-client", "m"] {
77
+ println!("cargo:rustc-link-lib={lib}");
78
+ }
79
+ cc::Build::new()
80
+ .cpp(true)
81
+ .std("c++17")
82
+ .include("vendor/inputtino/src/uinput/include")
83
+ .file("native/inputtino_shim.cpp")
84
+ .compile("dlinputkey");
85
+ build_inputtino(&PathBuf::from(std::env::var("OUT_DIR")?))?;
86
+ } else if target_os == "macos" {
87
+ cc::Build::new()
88
+ .cpp(true)
89
+ .file("native/mac_stream.mm")
90
+ .flag("-fobjc-arc")
91
+ .compile("dlmacstream");
92
+ cc::Build::new()
93
+ .file("native/agent_overlay_mac.m")
94
+ .flag("-fobjc-arc")
95
+ .compile("dlagentoverlay");
96
+ for framework in [
97
+ "ScreenCaptureKit",
98
+ "CoreVideo",
99
+ "CoreMedia",
100
+ "Foundation",
101
+ "AppKit",
102
+ "QuartzCore",
103
+ ] {
104
+ println!("cargo:rustc-link-lib=framework={framework}");
105
+ }
106
+ if let Some(prefix) = std::env::var_os("DESKLINK_VPX_STATIC_DIR").map(PathBuf::from) {
107
+ cc::Build::new()
108
+ .file("native/vpx_shim.c")
109
+ .include(prefix.join("include"))
110
+ .compile("dlvpx");
42
111
  println!(
43
112
  "cargo:rustc-link-search=native={}",
44
113
  prefix.join("lib").display()
45
114
  );
46
115
  println!("cargo:rustc-link-lib=static=vpx");
116
+ println!("cargo:rustc-cfg=desklink_vpx");
117
+ }
118
+ println!("cargo:rustc-link-lib=framework=CoreGraphics");
119
+ println!("cargo:rustc-link-lib=framework=ApplicationServices");
120
+ println!("cargo:rustc-link-lib=framework=Carbon");
121
+ // The dev harness embeds its bundle identity for local TCC qualification.
122
+ // The published CLI must inherit the responsible app's TCC identity instead.
123
+ if std::env::var_os("DESKLINK_MACOS_CLI").is_none() {
124
+ println!("cargo:rustc-link-arg-bins=-sectcreate");
125
+ println!("cargo:rustc-link-arg-bins=__TEXT");
126
+ println!("cargo:rustc-link-arg-bins=__info_plist");
127
+ println!(
128
+ "cargo:rustc-link-arg-bins={}",
129
+ Path::new("Info.plist").canonicalize()?.display()
130
+ );
47
131
  }
48
- None => println!("cargo:rustc-link-lib=vpx"),
49
132
  }
50
-
51
- cc::Build::new()
52
- .cpp(true)
53
- .std("c++17")
54
- .include("vendor/inputtino/src/uinput/include")
55
- .file("native/inputtino_shim.cpp")
56
- .compile("dlinputkey");
57
- build_inputtino(&PathBuf::from(std::env::var("OUT_DIR")?))?;
58
133
  Ok(())
59
134
  }
60
135
 
@@ -0,0 +1,133 @@
1
+ #import <AppKit/AppKit.h>
2
+ #import <QuartzCore/QuartzCore.h>
3
+ #include <stdio.h>
4
+ #include <stdlib.h>
5
+ #include <unistd.h>
6
+
7
+ @interface AgentOverlayView : NSView {
8
+ NSPoint target, cursor;
9
+ CFTimeInterval clickAt, typeAt, endingAt;
10
+ BOOL positioned;
11
+ }
12
+ - (void)event:(char)kind x:(double)x y:(double)y;
13
+ - (void)end;
14
+ @end
15
+
16
+ @implementation AgentOverlayView
17
+ - (BOOL)isOpaque { return NO; }
18
+ - (void)event:(char)kind x:(double)x y:(double)y {
19
+ if (kind == 'S') { [self end]; return; }
20
+ if (x >= 0 && y >= 0) {
21
+ NSPoint point = NSMakePoint(x, self.bounds.size.height - y);
22
+ target = point;
23
+ if (!positioned) { cursor = point; positioned = YES; }
24
+ }
25
+ CFTimeInterval now = CACurrentMediaTime();
26
+ if (kind == 'C') clickAt = now;
27
+ if (kind == 'T') typeAt = now;
28
+ [self setNeedsDisplay:YES];
29
+ }
30
+ - (void)end { endingAt = CACurrentMediaTime(); }
31
+ - (void)tick {
32
+ if (endingAt && CACurrentMediaTime() - endingAt > 0.7) { [NSApp terminate:nil]; return; }
33
+ cursor.x += (target.x - cursor.x) * 0.24;
34
+ cursor.y += (target.y - cursor.y) * 0.24;
35
+ [self setNeedsDisplay:YES];
36
+ }
37
+ - (void)drawRect:(NSRect)rect {
38
+ [[NSColor clearColor] set]; NSRectFillUsingOperation(rect, NSCompositingOperationCopy);
39
+ CFTimeInterval now = CACurrentMediaTime();
40
+ CGFloat edge = endingAt ? fmax(0, 1 - (now-endingAt)/0.7) : 1;
41
+ if (edge > 0) {
42
+ NSRect bounds = self.bounds;
43
+ [[NSColor colorWithCalibratedRed:0.28 green:0.67 blue:0.96 alpha:0.75*edge] set];
44
+ NSBezierPath *border = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(bounds, 4, 4) xRadius:12 yRadius:12];
45
+ border.lineWidth = 5; [border stroke];
46
+ }
47
+ if (!positioned) return;
48
+ NSBezierPath *pointer = [NSBezierPath bezierPath];
49
+ [pointer moveToPoint:NSMakePoint(cursor.x, cursor.y)];
50
+ [pointer lineToPoint:NSMakePoint(cursor.x+2, cursor.y-24)];
51
+ [pointer lineToPoint:NSMakePoint(cursor.x+8, cursor.y-18)];
52
+ [pointer lineToPoint:NSMakePoint(cursor.x+15, cursor.y-31)];
53
+ [pointer lineToPoint:NSMakePoint(cursor.x+21, cursor.y-28)];
54
+ [pointer lineToPoint:NSMakePoint(cursor.x+14, cursor.y-15)];
55
+ [pointer lineToPoint:NSMakePoint(cursor.x+24, cursor.y-14)];
56
+ [pointer closePath];
57
+ [[NSColor colorWithCalibratedRed:0.16 green:0.60 blue:1 alpha:1] set]; [pointer fill];
58
+ [[NSColor whiteColor] set]; pointer.lineWidth = 2; [pointer stroke];
59
+ for (int i = 3; i >= 0; i--) {
60
+ CGFloat radius = 12 + i*5;
61
+ CGFloat alpha = 0.13 * (1.0 - i/5.0);
62
+ [[NSColor colorWithCalibratedRed:0.30 green:0.73 blue:1 alpha:alpha] set];
63
+ NSBezierPath *halo = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(cursor.x-radius, cursor.y-radius, radius*2, radius*2)];
64
+ halo.lineWidth = 1.5 + i; [halo stroke];
65
+ }
66
+ if (clickAt && now-clickAt < 0.55) {
67
+ CGFloat progress = (now-clickAt)/0.55;
68
+ CGFloat radius = 11 + 37*progress;
69
+ [[NSColor colorWithCalibratedRed:0.37 green:0.77 blue:1 alpha:0.45*(1-progress)] set];
70
+ NSBezierPath *ripple = [NSBezierPath bezierPathWithOvalInRect:NSMakeRect(target.x-radius,target.y-radius,2*radius,2*radius)];
71
+ ripple.lineWidth = 2.5*(1-progress)+0.5; [ripple stroke];
72
+ }
73
+ if (typeAt && now-typeAt < 0.4) {
74
+ CGFloat progress = (now-typeAt)/0.4;
75
+ CGFloat radius = 5 + progress*15;
76
+ [[NSColor colorWithCalibratedRed:0.50 green:0.83 blue:1 alpha:0.32*(1-progress)] set];
77
+ [[NSBezierPath bezierPathWithOvalInRect:NSMakeRect(target.x-radius,target.y-radius,2*radius,2*radius)] fill];
78
+ }
79
+ }
80
+ @end
81
+
82
+ // The dev recording path converts ScreenCaptureKit screenshots to packed BGRA.
83
+ unsigned char *desklink_image_bgra(CGImageRef image, size_t *length, size_t *stride) {
84
+ size_t width = CGImageGetWidth(image), height = CGImageGetHeight(image);
85
+ *stride = width * 4; *length = *stride * height;
86
+ unsigned char *pixels = malloc(*length);
87
+ if (!pixels) return NULL;
88
+ CGColorSpaceRef color = CGColorSpaceCreateDeviceRGB();
89
+ CGContextRef context = CGBitmapContextCreate(pixels, width, height, 8, *stride, color,
90
+ kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
91
+ CGColorSpaceRelease(color);
92
+ if (!context) { free(pixels); return NULL; }
93
+ CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
94
+ CGContextRelease(context);
95
+ return pixels;
96
+ }
97
+
98
+ int desklink_agent_overlay_main(int display_id) {
99
+ @autoreleasepool {
100
+ [NSApplication sharedApplication];
101
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
102
+ NSScreen *screen = nil;
103
+ for (NSScreen *candidate in [NSScreen screens]) {
104
+ if ([candidate.deviceDescription[@"NSScreenNumber"] intValue] == display_id) { screen = candidate; break; }
105
+ }
106
+ if (!screen) return 2;
107
+ NSWindow *window = [[NSWindow alloc] initWithContentRect:screen.frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO screen:screen];
108
+ window.opaque = NO; window.backgroundColor = NSColor.clearColor;
109
+ window.ignoresMouseEvents = YES; window.hasShadow = NO;
110
+ window.level = NSScreenSaverWindowLevel - 1;
111
+ window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorStationary;
112
+ AgentOverlayView *view = [[AgentOverlayView alloc] initWithFrame:NSMakeRect(0,0,screen.frame.size.width,screen.frame.size.height)];
113
+ window.contentView = view;
114
+ NSPoint mouse = [NSEvent mouseLocation];
115
+ [view event:'M' x:mouse.x-screen.frame.origin.x y:NSMaxY(screen.frame)-mouse.y];
116
+ [window orderFrontRegardless];
117
+ puts("READY"); fflush(stdout);
118
+ [NSTimer scheduledTimerWithTimeInterval:1.0/60 target:view selector:@selector(tick) userInfo:nil repeats:YES];
119
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
120
+ char line[128];
121
+ while (fgets(line, sizeof(line), stdin)) {
122
+ char kind; double x = 0, y = 0;
123
+ if (sscanf(line, "%c %lf %lf", &kind, &x, &y) >= 1) {
124
+ dispatch_async(dispatch_get_main_queue(), ^{ [view event:kind x:x y:y]; });
125
+ }
126
+ }
127
+ dispatch_async(dispatch_get_main_queue(), ^{ [view end]; });
128
+ });
129
+ [NSApp run];
130
+ [window close];
131
+ }
132
+ return 0;
133
+ }