@desklink/host 0.5.2 → 0.5.4

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 (77) hide show
  1. package/README.md +45 -18
  2. package/bin/desklink-host.mjs +5 -1
  3. package/dist/bridge.d.ts +7 -0
  4. package/dist/bridge.d.ts.map +1 -1
  5. package/dist/bridge.js +55 -7
  6. package/dist/bridge.js.map +1 -1
  7. package/dist/engineProcess.d.ts +9 -2
  8. package/dist/engineProcess.d.ts.map +1 -1
  9. package/dist/engineProcess.js +26 -15
  10. package/dist/engineProcess.js.map +1 -1
  11. package/dist/protocol.d.ts +1 -1
  12. package/dist/protocol.d.ts.map +1 -1
  13. package/dist/protocol.js +1 -1
  14. package/dist/protocol.js.map +1 -1
  15. package/docs/BOUNDARY.md +4 -1
  16. package/docs/PROTOCOL.md +20 -5
  17. package/engine/Cargo.lock +6 -8
  18. package/engine/Cargo.toml +13 -1
  19. package/engine/src/clipboard.rs +0 -34
  20. package/engine/src/convert.rs +0 -170
  21. package/engine/src/cursor.rs +0 -23
  22. package/engine/src/input.rs +0 -65
  23. package/engine/src/input_mac.rs +0 -11
  24. package/engine/src/keeper.rs +0 -137
  25. package/engine/src/keymap.rs +0 -107
  26. package/engine/src/keymap_mac.rs +0 -25
  27. package/engine/src/mac.rs +0 -28
  28. package/engine/src/main.rs +3 -2
  29. package/engine/src/peer.rs +231 -1
  30. package/engine/src/portal.rs +0 -18
  31. package/engine/src/protocol.rs +0 -162
  32. package/engine/src/session.rs +37 -1
  33. package/engine/src/x11.rs +0 -13
  34. package/engine/vendor/rtc-ice/Cargo.toml +137 -0
  35. package/engine/vendor/rtc-ice/DESKLINK-PATCH.md +59 -0
  36. package/engine/vendor/rtc-ice/LICENSE-APACHE +201 -0
  37. package/engine/vendor/rtc-ice/LICENSE-MIT +21 -0
  38. package/engine/vendor/rtc-ice/README.md +30 -0
  39. package/engine/vendor/rtc-ice/examples/ping_pong.rs +376 -0
  40. package/engine/vendor/rtc-ice/src/agent/agent_config.rs +126 -0
  41. package/engine/vendor/rtc-ice/src/agent/agent_proto.rs +172 -0
  42. package/engine/vendor/rtc-ice/src/agent/agent_selector.rs +566 -0
  43. package/engine/vendor/rtc-ice/src/agent/agent_stats.rs +292 -0
  44. package/engine/vendor/rtc-ice/src/agent/agent_test.rs +3388 -0
  45. package/engine/vendor/rtc-ice/src/agent/mod.rs +1662 -0
  46. package/engine/vendor/rtc-ice/src/attributes/control/control_test.rs +168 -0
  47. package/engine/vendor/rtc-ice/src/attributes/control/mod.rs +142 -0
  48. package/engine/vendor/rtc-ice/src/attributes/mod.rs +6 -0
  49. package/engine/vendor/rtc-ice/src/attributes/priority/mod.rs +37 -0
  50. package/engine/vendor/rtc-ice/src/attributes/priority/priority_test.rs +39 -0
  51. package/engine/vendor/rtc-ice/src/attributes/use_candidate/mod.rs +33 -0
  52. package/engine/vendor/rtc-ice/src/attributes/use_candidate/use_candidate_test.rs +19 -0
  53. package/engine/vendor/rtc-ice/src/candidate/candidate_host.rs +53 -0
  54. package/engine/vendor/rtc-ice/src/candidate/candidate_pair.rs +215 -0
  55. package/engine/vendor/rtc-ice/src/candidate/candidate_pair_test.rs +201 -0
  56. package/engine/vendor/rtc-ice/src/candidate/candidate_peer_reflexive.rs +49 -0
  57. package/engine/vendor/rtc-ice/src/candidate/candidate_relay.rs +53 -0
  58. package/engine/vendor/rtc-ice/src/candidate/candidate_relay_test.rs +114 -0
  59. package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive.rs +53 -0
  60. package/engine/vendor/rtc-ice/src/candidate/candidate_server_reflexive_test.rs +91 -0
  61. package/engine/vendor/rtc-ice/src/candidate/candidate_test.rs +540 -0
  62. package/engine/vendor/rtc-ice/src/candidate/mod.rs +679 -0
  63. package/engine/vendor/rtc-ice/src/lib.rs +85 -0
  64. package/engine/vendor/rtc-ice/src/mdns/mdns_test.rs +150 -0
  65. package/engine/vendor/rtc-ice/src/mdns/mod.rs +69 -0
  66. package/engine/vendor/rtc-ice/src/network_type/mod.rs +162 -0
  67. package/engine/vendor/rtc-ice/src/network_type/network_type_test.rs +95 -0
  68. package/engine/vendor/rtc-ice/src/rand/mod.rs +91 -0
  69. package/engine/vendor/rtc-ice/src/rand/rand_test.rs +115 -0
  70. package/engine/vendor/rtc-ice/src/state/mod.rs +106 -0
  71. package/engine/vendor/rtc-ice/src/state/state_test.rs +46 -0
  72. package/engine/vendor/rtc-ice/src/stats/mod.rs +166 -0
  73. package/engine/vendor/rtc-ice/src/tcp_type/mod.rs +47 -0
  74. package/engine/vendor/rtc-ice/src/tcp_type/tcp_type_test.rs +18 -0
  75. package/engine/vendor/rtc-ice/src/url/mod.rs +266 -0
  76. package/engine/vendor/rtc-ice/src/url/url_test.rs +142 -0
  77. package/package.json +8 -3
package/docs/BOUNDARY.md CHANGED
@@ -141,7 +141,10 @@ address, port, code or certificate reaches the user, and the engine's session is
141
141
  started by the host that already serves the terminal. A consumer with no channel
142
142
  of its own is not left to invent one — `desklink-host bridge` re-serves the same
143
143
  protocol over a WebSocket and serves a reference client that needs no build step,
144
- so a stranger's first run is one command and one URL.
144
+ so a stranger's first run is one command and one URL. Its bounded reattach window
145
+ is in-memory only: no durable store, new pairing authority, lease renewal or
146
+ restart-after-process-exit guarantee. Consumers close revoked pairing sessions
147
+ explicitly; a missing signaling carrier is not itself revocation.
145
148
 
146
149
  The same substitution is therefore available to both: the packages carry no muxr
147
150
  concept, and muxr's contribution is one file that adapts its own channel to the
package/docs/PROTOCOL.md CHANGED
@@ -346,7 +346,16 @@ re-offers — a fresh offer with a new ICE generation — as a new
346
346
  `session.description` event, which the client answers as usual. The session
347
347
  and its generation survive; only the ICE generation turns over. Fresh
348
348
  candidates from the restart trickle through `session.candidate` on both sides
349
- exactly as the initial ones do.
349
+ exactly as the initial ones do. If a local offer is still unanswered, the
350
+ engine re-emits that pending offer instead: queued restarts share its ICE
351
+ credentials until the answer arrives.
352
+
353
+ The WebSocket bridge retains the session after its last signaling socket leaves
354
+ for `reattachMs` (default 30000 ms; CLI `--reattach-ms`; 0 closes immediately).
355
+ A token-authenticated socket attaching within that window cancels the deadline;
356
+ it may restart ICE with the held session id without a new `session.open`.
357
+ This does not replay offers or automatically reconnect a client, extend its
358
+ lease, or guarantee recovery within 2 seconds. Engine-side ends remain terminal.
350
359
 
351
360
  ```jsonc
352
361
  {"id":7,"method":"session.restart_ice","params":{"session_id":"…","generation":1}}
@@ -421,9 +430,13 @@ The session ended on the engine's side; there is nothing further to drain.
421
430
  | `error` | The engine could not keep serving it, such as an encoder refusing a frame | end and report |
422
431
 
423
432
  A session the consumer closes or replaces (`session.close`, a new
424
- `session.open`, the consumer leaving) sends no `session.revoked`: the consumer
425
- asked for it, and signaling sees `session.state` with `capture` `ended`. Only
426
- its control channel is told, with code `closed` (below).
433
+ `session.open`, engine stdin EOF, or the bridge's detach deadline) sends no
434
+ `session.revoked`: signaling sees `session.state` with `capture` `ended`. Only
435
+ its control channel is told, with code `closed` (below). A signaling socket
436
+ leaving alone does not end the session during the bridge's reattach window.
437
+ If the held session is revoked while detached, the bridge remembers its last
438
+ `session.revoked`, replays it once to the next authenticated attacher, then
439
+ forgets it. In particular, a spent lease cannot be revived by reattachment.
427
440
 
428
441
  A code a client does not know is terminal. Engines before this field sent no
429
442
  `code`; treat its absence as terminal too.
@@ -906,4 +919,6 @@ anything to show.
906
919
  and input; audio is a different product decision.
907
920
  - It does not arbitrate between two controllers. One session, one controller.
908
921
  - It does not auto-reconnect with authority it no longer holds: after a lease
909
- expiry or owner disconnect it requires a fresh `session.open`.
922
+ expiry or an engine-side end it requires a fresh `session.open`. A brief
923
+ signaling-carrier loss within the bridge's reattach window is not owner
924
+ revocation; the application must close revoked pairing sessions explicitly.
package/engine/Cargo.lock CHANGED
@@ -536,7 +536,7 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
536
536
 
537
537
  [[package]]
538
538
  name = "desklink-host"
539
- version = "0.5.2"
539
+ version = "0.5.4"
540
540
  dependencies = [
541
541
  "anyhow",
542
542
  "ashpd",
@@ -1942,8 +1942,6 @@ dependencies = [
1942
1942
  [[package]]
1943
1943
  name = "rtc-ice"
1944
1944
  version = "0.21.0"
1945
- source = "registry+https://github.com/rust-lang/crates.io-index"
1946
- checksum = "4f437cc58721fdb3f0a2ab8b90660d3c19ea2817bc4e2117b2d80f3f6cfba5f3"
1947
1945
  dependencies = [
1948
1946
  "bytes",
1949
1947
  "crc",
@@ -2024,9 +2022,9 @@ dependencies = [
2024
2022
 
2025
2023
  [[package]]
2026
2024
  name = "rtc-sctp"
2027
- version = "0.21.0"
2025
+ version = "0.21.1"
2028
2026
  source = "registry+https://github.com/rust-lang/crates.io-index"
2029
- checksum = "161e3eb55695d4ce7492a690b6a27dc43bfeee43fd5d4af0423de1ace49456c1"
2027
+ checksum = "2eb430b379d90e7984306036d1646454a26b300c9c3256009eb8f7e0615e90b9"
2030
2028
  dependencies = [
2031
2029
  "bytes",
2032
2030
  "crc32c",
@@ -2051,9 +2049,9 @@ dependencies = [
2051
2049
 
2052
2050
  [[package]]
2053
2051
  name = "rtc-shared"
2054
- version = "0.21.0"
2052
+ version = "0.21.1"
2055
2053
  source = "registry+https://github.com/rust-lang/crates.io-index"
2056
- checksum = "24d32c3e91a0eec814e093f415f5848f5c3c497d8b1140965bf26f81101691ee"
2054
+ checksum = "1250e81796ac6d3d67bd04ecdb891b8371ff97c8a2ce0a5a1c1321f2e12b657b"
2057
2055
  dependencies = [
2058
2056
  "bitflags 1.3.2",
2059
2057
  "bytes",
@@ -2426,7 +2424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2426
2424
  checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
2427
2425
  dependencies = [
2428
2426
  "fastrand",
2429
- "getrandom 0.4.3",
2427
+ "getrandom 0.3.4",
2430
2428
  "once_cell",
2431
2429
  "rustix",
2432
2430
  "windows-sys 0.61.2",
package/engine/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "desklink-host"
3
- version = "0.5.2"
3
+ version = "0.5.4"
4
4
  edition = "2021"
5
5
  rust-version = "1.97.1"
6
6
  license = "Apache-2.0"
@@ -21,6 +21,10 @@ anyhow = "1"
21
21
  tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "io-std", "sync", "time", "signal"] }
22
22
  libc = "0.2"
23
23
  bytes = "1"
24
+ # rtc-sctp 0.21.1 is required through this stack: 0.21.0 replaces an
25
+ # expired-but-unhandled T3 retransmission deadline, so the data channel stalls
26
+ # on the new nominated pair after an ICE restart and the client reopens the
27
+ # session. Cargo.lock pins 0.21.1; drop the pin when a later release carries it.
24
28
  webrtc = { version = "0.21", default-features = false, features = ["runtime-tokio", "crypto-ring"] }
25
29
  rtc = { version = "0.21", default-features = false, features = ["crypto-ring"] }
26
30
  log = "0.4"
@@ -64,3 +68,11 @@ cc = "1"
64
68
  [profile.release]
65
69
  lto = "thin"
66
70
  opt-level = 2
71
+
72
+ # The engine needs an ICE restart that does not interrupt media already flowing
73
+ # on the current selected pair (roam persistence, < 200 ms gap). rtc-ice 0.21.0
74
+ # clears the selected pair and every candidate pair in `Agent::apply_restart`,
75
+ # and exposes no setting to keep them, so a patched copy is vendored here. See
76
+ # vendor/rtc-ice/DESKLINK-PATCH.md for what changed and how to drop it.
77
+ [patch.crates-io]
78
+ rtc-ice = { path = "vendor/rtc-ice" }
@@ -270,37 +270,3 @@ pub fn x11_read(display: &str) -> Result<(String, bool)> {
270
270
  std::thread::sleep(std::time::Duration::from_millis(5));
271
271
  }
272
272
  }
273
-
274
- #[cfg(test)]
275
- mod tests {
276
- use super::*;
277
-
278
- #[test]
279
- fn an_oversized_clipboard_is_cut_cleanly_and_reported() {
280
- let mut raw = vec![b'a'; 4];
281
- raw.extend_from_slice("\u{e9}".as_bytes());
282
- raw.extend_from_slice(b"zzzz");
283
- let (text, truncated) =
284
- bounded_text(&raw, 5).expect("a character-boundary cut is valid text");
285
- assert!(truncated, "a cut past the limit must be reported");
286
- assert_eq!(
287
- text, "aaaa",
288
- "the partial character is dropped, not invented"
289
- );
290
-
291
- // A byte that is not valid UTF-8 before the cut is not a partial
292
- // character; the whole read is refused.
293
- let mut invalid = vec![b'a'; 4];
294
- invalid.push(0xff);
295
- invalid.extend_from_slice(b"zzzz");
296
- assert!(bounded_text(&invalid, 5).is_err());
297
-
298
- let (text, truncated) = bounded_text(b"hello", 5).unwrap();
299
- assert!(!truncated);
300
- assert_eq!(text, "hello");
301
-
302
- let (text, truncated) = bounded_text(b"hello world", 5).unwrap();
303
- assert!(truncated);
304
- assert_eq!(text, "hello");
305
- }
306
- }
@@ -408,173 +408,3 @@ fn pack(
408
408
  (*u, *v) = chroma(sum(0), sum(1), sum(2));
409
409
  }
410
410
  }
411
-
412
- #[cfg(test)]
413
- mod tests {
414
- use super::*;
415
-
416
- #[test]
417
- fn downscale_keeps_average_colour_and_plane_sizes() {
418
- // 4x4 opaque red desktop in BGRx, downscaled to 2x2.
419
- let mut src = vec![0u8; 4 * 16];
420
- for px in src.chunks_exact_mut(4) {
421
- px[0] = 0; // B
422
- px[1] = 0; // G
423
- px[2] = 255; // R
424
- px[3] = 255; // X
425
- }
426
- let out = to_i420(&src, 4, 4, 16, PixelFormat::Bgrx, 2, 2).unwrap();
427
- assert_eq!(out.data.len(), 4 + 2 * 1);
428
- assert!(
429
- out.y_plane().iter().all(|&y| y > 70),
430
- "red luma should be high"
431
- );
432
- assert!(out.u_plane().iter().all(|&u| u < 128), "red has low Cb");
433
- assert!(out.v_plane().iter().all(|&v| v > 200), "red has high Cr");
434
- }
435
-
436
- #[test]
437
- fn odd_destination_is_clamped_to_whole_chroma_blocks() {
438
- let src = vec![255u8; 4 * 16];
439
- let out = to_i420(&src, 4, 4, 16, PixelFormat::Bgrx, 3, 3).unwrap();
440
- assert_eq!((out.width, out.height), (2, 2));
441
- assert_eq!(out.data.len(), 4 + 2 * 1);
442
- }
443
-
444
- #[test]
445
- fn a_stream_a_few_pixels_off_the_encoders_size_is_converted_to_exactly_that_size() {
446
- // A fractional-scaled display reports one size to the portal and
447
- // streams another; the encoder only accepts its own.
448
- let src = vec![200u8; 4 * 1704 * 1066];
449
- let out = to_i420(&src, 1704, 1066, 1704 * 4, PixelFormat::Bgrx, 1706, 1066).unwrap();
450
- assert_eq!((out.width, out.height), (1706, 1066));
451
- let out = to_i420(&src, 1704, 1066, 1704 * 4, PixelFormat::Bgrx, 1702, 1064).unwrap();
452
- assert_eq!((out.width, out.height), (1702, 1064));
453
- }
454
-
455
- #[test]
456
- fn still_pixels_keep_exact_source_colours_before_encoding() {
457
- let src = [1, 2, 3, 0, 4, 5, 6, 0];
458
- assert_eq!(
459
- to_bgrx(&src, 2, 1, 8, PixelFormat::Bgrx, 2, 1).unwrap(),
460
- src
461
- );
462
- assert_eq!(
463
- to_bgrx(&src, 2, 1, 8, PixelFormat::Rgbx, 2, 1).unwrap(),
464
- [3, 2, 1, 255, 6, 5, 4, 255]
465
- );
466
- }
467
-
468
- /// The per-pixel conversion this module used to run, kept as the definition
469
- /// the fast paths must reproduce bit for bit.
470
- fn reference(
471
- src: &[u8],
472
- sw: usize,
473
- sh: usize,
474
- stride: usize,
475
- format: PixelFormat,
476
- dw: usize,
477
- dh: usize,
478
- ) -> Vec<u8> {
479
- let (bpp, o) = layout(format).unwrap();
480
- let rgb = |x: usize, y: usize| {
481
- let p = &src[y * stride + x * bpp..];
482
- (p[o[0]] as i32, p[o[1]] as i32, p[o[2]] as i32)
483
- };
484
- let sample = |dx: usize, dy: usize| {
485
- let x0 = dx * sw / dw;
486
- let x1 = ((dx + 1) * sw).div_ceil(dw).clamp(x0 + 1, sw);
487
- let y0 = dy * sh / dh;
488
- let y1 = ((dy + 1) * sh).div_ceil(dh).clamp(y0 + 1, sh);
489
- let (mut r, mut g, mut b) = (0, 0, 0);
490
- for y in y0..y1 {
491
- for x in x0..x1 {
492
- let p = rgb(x, y);
493
- (r, g, b) = (r + p.0, g + p.1, b + p.2);
494
- }
495
- }
496
- let n = ((x1 - x0) * (y1 - y0)) as i32;
497
- (r / n, g / n, b / n)
498
- };
499
- let (mut y, mut u, mut v) = (vec![0; dw * dh], vec![], vec![]);
500
- for cy in 0..dh / 2 {
501
- for cx in 0..dw / 2 {
502
- let px = [(0, 0), (1, 0), (0, 1), (1, 1)]
503
- .map(|(i, j)| (cx * 2 + i, cy * 2 + j, sample(cx * 2 + i, cy * 2 + j)));
504
- for (x, yy, p) in px {
505
- y[yy * dw + x] = luma(p.0, p.1, p.2);
506
- }
507
- let s = |f: fn(&(i32, i32, i32)) -> i32| {
508
- (px.iter().map(|p| f(&p.2)).sum::<i32>() + 2) >> 2
509
- };
510
- let (cu, cv) = chroma(s(|p| p.0), s(|p| p.1), s(|p| p.2));
511
- u.push(cu);
512
- v.push(cv);
513
- }
514
- }
515
- [y, u, v].concat()
516
- }
517
-
518
- #[test]
519
- fn every_path_matches_the_per_pixel_definition_exactly() {
520
- let mut seed = 0x9e3779b9u32;
521
- let mut noise = |n: usize| {
522
- (0..n)
523
- .map(|_| {
524
- seed ^= seed << 13;
525
- seed ^= seed >> 17;
526
- seed ^= seed << 5;
527
- seed as u8
528
- })
529
- .collect::<Vec<u8>>()
530
- };
531
- let formats = [
532
- PixelFormat::Bgrx,
533
- PixelFormat::Rgba,
534
- PixelFormat::Bgr,
535
- PixelFormat::Rgb,
536
- ];
537
- // Native, a few pixels off, box downscales, and boxes near and past the
538
- // largest area the reciprocal table covers.
539
- for (sw, sh, dw, dh) in [
540
- (256, 126, 4, 2),
541
- (400, 200, 4, 2),
542
- (64, 36, 64, 36),
543
- (67, 41, 66, 40),
544
- (100, 60, 38, 22),
545
- (90, 50, 92, 50),
546
- (33, 17, 8, 6),
547
- ] {
548
- for format in formats {
549
- let bpp = layout(format).unwrap().0;
550
- let stride = sw * bpp + 12;
551
- let src = noise(stride * sh);
552
- let fast = to_i420(&src, sw, sh, stride, format, dw, dh).unwrap();
553
- assert!(
554
- fast.data == reference(&src, sw, sh, stride, format, dw & !1, dh & !1),
555
- "{format:?} {sw}x{sh} -> {dw}x{dh}"
556
- );
557
- }
558
- }
559
- }
560
-
561
- #[test]
562
- fn padded_bgra_rows_remain_aligned_in_stills_and_video() {
563
- // ScreenCaptureKit pads 3420 BGRA pixels from 13680 to 13696 bytes/row.
564
- let src = [
565
- 0, 0, 255, 255, 0, 0, 255, 255, 0, 255, 0, 0, // red row + green padding
566
- 255, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 0, // blue row + green padding
567
- ];
568
- assert_eq!(
569
- to_bgrx(&src, 2, 2, 12, PixelFormat::Bgra, 2, 2).unwrap(),
570
- [src[0..8].to_vec(), src[12..20].to_vec()].concat()
571
- );
572
- let video = to_i420(&src, 2, 2, 12, PixelFormat::Bgra, 2, 2).unwrap();
573
- assert_eq!(video.y_plane(), &[82, 82, 41, 41]);
574
- }
575
-
576
- #[test]
577
- fn unsupported_format_is_reported_not_guessed() {
578
- assert!(to_i420(&vec![0u8; 64], 4, 4, 16, PixelFormat::Unsupported, 2, 2).is_none());
579
- }
580
- }
@@ -32,26 +32,3 @@ impl Reporter {
32
32
  });
33
33
  }
34
34
  }
35
-
36
- #[cfg(test)]
37
- mod tests {
38
- use super::*;
39
- #[test]
40
- fn reports_initial_motion_and_visibility_but_not_unchanged_polls() {
41
- let (tx, rx) = std::sync::mpsc::channel();
42
- let mut reporter = Reporter::new(Box::new(move |p| {
43
- tx.send(p).unwrap();
44
- }));
45
- reporter.update(10, 20, true);
46
- reporter.update(10, 20, true);
47
- reporter.update(30, 40, true);
48
- reporter.update(30, 40, false);
49
- let notices: Vec<_> = rx.try_iter().collect();
50
- assert_eq!(notices.len(), 3);
51
- assert_eq!((notices[1].x, notices[1].y), (30, 40));
52
- assert!(!notices[2].visible);
53
- assert!(notices
54
- .windows(2)
55
- .all(|p| p[0].timestamp_us <= p[1].timestamp_us));
56
- }
57
- }
@@ -300,68 +300,3 @@ impl Drop for InputDevices {
300
300
  }
301
301
  }
302
302
  }
303
-
304
- #[cfg(test)]
305
- mod tests {
306
- use super::*;
307
-
308
- #[test]
309
- fn button_numbers_map_to_the_expected_pointer_buttons() {
310
- assert_eq!(Button::from_number(1), Button::Left);
311
- assert_eq!(Button::from_number(2), Button::Middle);
312
- assert_eq!(Button::from_number(3), Button::Right);
313
- assert_eq!(Button::from_number(9), Button::Left);
314
- }
315
-
316
- #[test]
317
- fn held_state_releases_exactly_what_this_session_pressed() {
318
- let mut held = HeldState::default();
319
- held.key(29, true); // left ctrl
320
- held.key(46, true); // c
321
- held.button(Button::Left, true);
322
- // A repeat press is the same press, not a second one, so the plan still
323
- // releases it exactly once.
324
- held.key(29, true);
325
-
326
- let (buttons, keys) = held.release_plan();
327
- assert_eq!(buttons, vec![Button::Left]);
328
- assert_eq!(keys, vec![29, 46]);
329
- assert!(
330
- held.release_plan().1.is_empty(),
331
- "releasing twice releases nothing"
332
- );
333
- }
334
-
335
- #[test]
336
- fn an_explicit_release_leaves_only_what_is_still_pressed() {
337
- let mut held = HeldState::default();
338
- held.key(29, true);
339
- held.key(46, true);
340
- held.key(46, false);
341
- let (buttons, keys) = held.release_plan();
342
- assert!(buttons.is_empty());
343
- assert_eq!(keys, vec![29]);
344
- }
345
-
346
- /// Proving the backend is really available here is the point: if this host
347
- /// cannot create the devices, the engine must say so rather than offer a
348
- /// control surface that does nothing.
349
- #[test]
350
- fn this_host_can_create_and_destroy_the_virtual_devices() {
351
- // This touches the real desktop when uinput is writable. Ordinary
352
- // suites must never create devices on the owner's active session.
353
- if std::env::var("DESKLINK_TEST_UINPUT").as_deref() != Ok("1") {
354
- return;
355
- }
356
- match InputDevices::create(2560, 1440) {
357
- Ok(_devices) => {}
358
- Err(error) => {
359
- let unavailable = probe().unwrap_err();
360
- assert!(
361
- error.to_string().contains(&unavailable.reason),
362
- "a creation failure must be the reported availability problem"
363
- );
364
- }
365
- }
366
- }
367
- }
@@ -299,14 +299,3 @@ impl InputDevices {
299
299
  }
300
300
  }
301
301
  }
302
-
303
- #[cfg(test)]
304
- mod tests {
305
- use super::*;
306
-
307
- #[test]
308
- fn command_key_uses_quartz_command_flag() {
309
- assert_eq!(modifier_flag(keycode::LEFT_META), 0x100000);
310
- assert_eq!(modifier_flag(0), 0);
311
- }
312
- }
@@ -483,140 +483,3 @@ fn fatal(out: &mut impl Write, error: &str) -> Result<()> {
483
483
  write_line(out, &serde_json::json!({ "error": error }).to_string())?;
484
484
  Err(anyhow!("{error}"))
485
485
  }
486
-
487
- #[cfg(test)]
488
- mod tests {
489
- use super::*;
490
-
491
- #[test]
492
- fn content_fills_the_screen_and_helpers_are_parked() {
493
- assert_eq!(kiosk_geometry(1240, 760, 1280, 800), (0, 0, 1280, 800));
494
- // Exactly at the limit is content; one pixel under is not.
495
- assert_eq!(kiosk_geometry(120, 120, 1280, 800), (0, 0, 1280, 800));
496
- assert_eq!(
497
- kiosk_geometry(119, 800, 1280, 800),
498
- (i32::from(PARKED_XY), i32::from(PARKED_XY), 119, 800)
499
- );
500
- assert_eq!(
501
- kiosk_geometry(1000, 24, 1280, 800),
502
- (i32::from(PARKED_XY), i32::from(PARKED_XY), 1000, 24)
503
- );
504
- }
505
-
506
- #[test]
507
- fn wm_class_reads_the_class_not_the_instance() {
508
- assert_eq!(
509
- decode_wm_class(b"google-chrome\0Google-chrome\0"),
510
- Some(String::from("Google-chrome"))
511
- );
512
- assert_eq!(decode_wm_class(b"instance-only\0"), None);
513
- assert_eq!(decode_wm_class(b""), None);
514
- }
515
-
516
- #[test]
517
- fn titles_come_from_either_name_property_and_blank_is_none() {
518
- assert_eq!(
519
- decode_title("Pricing — Acme Store"),
520
- Some(String::from("Pricing — Acme Store"))
521
- );
522
- assert_eq!(
523
- decode_title("PPF probe - Google Chrome"),
524
- Some(String::from("PPF probe - Google Chrome"))
525
- );
526
- assert_eq!(decode_title(" "), None);
527
- assert_eq!(decode_title(""), None);
528
- assert_eq!(
529
- decode_title(&latin1(b"Gr\xf6\xdfe")),
530
- Some(String::from("Gr\u{00f6}\u{00df}e"))
531
- );
532
- }
533
-
534
- #[test]
535
- fn the_report_line_is_exactly_the_promised_shape() {
536
- let line = window_line(&[KeeperWindow {
537
- id: 0x0040_0001,
538
- title: Some(String::from("PPF probe - Google Chrome")),
539
- class: Some(String::from("Google-chrome")),
540
- pid: Some(1003571),
541
- width: 1280,
542
- height: 800,
543
- }]);
544
- let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
545
- assert_eq!(
546
- parsed,
547
- serde_json::json!({
548
- "windows": [{
549
- "id": 0x40_0001_u64,
550
- "title": "PPF probe - Google Chrome",
551
- "class": "Google-chrome",
552
- "pid": 1003571,
553
- "width": 1280,
554
- "height": 800,
555
- }]
556
- })
557
- );
558
- assert_eq!(window_line(&[]), r#"{"windows":[]}"#);
559
- }
560
-
561
- #[test]
562
- fn missing_fields_are_null_not_absent() {
563
- let line = window_line(&[KeeperWindow {
564
- id: 7,
565
- title: None,
566
- class: None,
567
- pid: None,
568
- width: 100,
569
- height: 100,
570
- }]);
571
- let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
572
- let window = &parsed["windows"][0];
573
- assert_eq!(window["title"], serde_json::Value::Null);
574
- assert_eq!(window["class"], serde_json::Value::Null);
575
- assert_eq!(window["pid"], serde_json::Value::Null);
576
- assert!(window.get("width").is_some());
577
- }
578
-
579
- #[test]
580
- fn an_unopenable_display_is_refused_in_one_error_line() {
581
- let mut out = Vec::new();
582
- assert!(keep(Some("not a display"), &mut out).is_err());
583
- let line: serde_json::Value =
584
- serde_json::from_str(&String::from_utf8(out).unwrap()).unwrap();
585
- assert!(line["error"]
586
- .as_str()
587
- .is_some_and(|error| !error.is_empty()));
588
- }
589
-
590
- #[test]
591
- fn a_refused_keeper_says_why_in_its_error_line() {
592
- let mut out = Vec::new();
593
- let error = fatal(&mut out, "another window manager").unwrap_err();
594
- assert_eq!(error.to_string(), "another window manager");
595
- assert_eq!(
596
- String::from_utf8(out).unwrap(),
597
- "{\"error\":\"another window manager\"}\n"
598
- );
599
- }
600
-
601
- #[test]
602
- fn the_display_flag_has_one_spelling() {
603
- let flag = |args: &[&str]| {
604
- display_argument(
605
- &args
606
- .iter()
607
- .map(|arg| String::from(*arg))
608
- .collect::<Vec<_>>(),
609
- )
610
- };
611
- assert_eq!(flag(&["--display", ":42"]), Some(String::from(":42")));
612
- assert_eq!(
613
- flag(&["--display", ":42", "--x"]),
614
- Some(String::from(":42"))
615
- );
616
- assert_eq!(flag(&[]), None);
617
- assert_eq!(flag(&["--display"]), None);
618
- // The undocumented `--display=:42` spelling is not a display: `keep`
619
- // refuses it rather than guessing what was meant.
620
- assert_eq!(flag(&["--display=:42"]), None);
621
- }
622
- }