@desklink/host 0.2.0 → 0.3.1
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/NOTICE +26 -0
- package/README.md +134 -56
- package/bin/desklink-host.mjs +13 -5
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +12 -4
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +35 -4
- 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/dist/resolveEngine.d.ts +7 -1
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +66 -14
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +15 -7
- package/docs/PROTOCOL.md +267 -27
- package/engine/Cargo.lock +47 -2
- package/engine/Cargo.toml +18 -13
- package/engine/build.rs +86 -4
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +145 -12
- package/engine/native/agent_overlay_x11.c +72 -0
- package/engine/native/nvenc_shim.c +453 -0
- package/engine/native/vaapi_shim.c +727 -0
- package/engine/native/vpx_shim.c +54 -4
- package/engine/native/vt_shim.c +448 -0
- package/engine/src/capture.rs +167 -58
- package/engine/src/capture_mac.rs +24 -14
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/convert.rs +286 -61
- package/engine/src/cursor.rs +118 -0
- package/engine/src/encoder.rs +122 -13
- package/engine/src/h264.rs +188 -0
- package/engine/src/h264_nvenc.rs +308 -0
- package/engine/src/h264_openh264.rs +735 -0
- package/engine/src/h264_vaapi.rs +263 -0
- package/engine/src/h264_vt.rs +368 -0
- package/engine/src/indicator.rs +104 -3
- package/engine/src/input_win.rs +91 -0
- package/engine/src/keymap_win.rs +39 -0
- package/engine/src/mac.rs +34 -9
- package/engine/src/main.rs +186 -26
- package/engine/src/peer.rs +466 -28
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +64 -2
- package/engine/src/session.rs +1252 -105
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +225 -9
- package/engine/src/x11_mac.rs +12 -0
- package/engine/vendor/libva/COPYING +19 -0
- package/engine/vendor/libva/va/va.h +5450 -0
- package/engine/vendor/libva/va/va_dec_av1.h +695 -0
- package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
- package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
- package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
- package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
- package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
- package/engine/vendor/libva/va/va_drm.h +61 -0
- package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
- package/engine/vendor/libva/va/va_enc_h264.h +659 -0
- package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
- package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
- package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
- package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
- package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
- package/engine/vendor/libva/va/va_fei.h +170 -0
- package/engine/vendor/libva/va/va_fei_h264.h +505 -0
- package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
- package/engine/vendor/libva/va/va_prot.h +525 -0
- package/engine/vendor/libva/va/va_version.h +87 -0
- package/engine/vendor/libva/va/va_vpp.h +1590 -0
- package/engine/vendor/nv-codec-headers/LICENSE +24 -0
- package/engine/vendor/nv-codec-headers/README +7 -0
- package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
- package/examples/reference-client.html +1 -1
- package/package.json +4 -3
package/engine/src/indicator.rs
CHANGED
|
@@ -13,7 +13,30 @@ pub struct Indicator {
|
|
|
13
13
|
position: std::sync::Arc<std::sync::Mutex<Option<(i64, i64)>>>,
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
#[cfg(target_os = "linux")]
|
|
17
|
+
fn point_wayland_executable() -> Result<std::path::PathBuf> {
|
|
18
|
+
#[cfg(test)]
|
|
19
|
+
if let Some(path) = std::env::var_os("DESKLINK_POINT_TEST_ENGINE") {
|
|
20
|
+
return Ok(path.into());
|
|
21
|
+
}
|
|
22
|
+
Ok(std::env::current_exe()?)
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
impl Indicator {
|
|
26
|
+
#[cfg(target_os = "linux")]
|
|
27
|
+
pub fn wayland_layer_shell_available() -> Result<bool> {
|
|
28
|
+
let mut command = Command::new(point_wayland_executable()?);
|
|
29
|
+
command.arg("point-layer-shell-probe");
|
|
30
|
+
match Self::spawn(&mut command, 1.0, 1.0) {
|
|
31
|
+
Ok(helper) => {
|
|
32
|
+
helper.stop();
|
|
33
|
+
Ok(true)
|
|
34
|
+
}
|
|
35
|
+
Err(error) if error.to_string().contains("layer_shell_unavailable") => Ok(false),
|
|
36
|
+
Err(error) => Err(error),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
#[cfg(target_os = "macos")]
|
|
18
41
|
pub fn start(display_id: u32, width: usize, height: usize) -> Result<Self> {
|
|
19
42
|
let executable = std::env::current_exe()?;
|
|
@@ -39,6 +62,77 @@ impl Indicator {
|
|
|
39
62
|
)
|
|
40
63
|
}
|
|
41
64
|
|
|
65
|
+
#[cfg(target_os = "linux")]
|
|
66
|
+
pub fn start_point(display: &str) -> Result<Self> {
|
|
67
|
+
Self::spawn(
|
|
68
|
+
Command::new(std::env::current_exe()?)
|
|
69
|
+
.arg("point-overlay")
|
|
70
|
+
.arg(display),
|
|
71
|
+
1.0,
|
|
72
|
+
1.0,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[cfg(target_os = "linux")]
|
|
77
|
+
pub fn start_point_wayland(
|
|
78
|
+
width: i32,
|
|
79
|
+
height: i32,
|
|
80
|
+
position: Option<(i32, i32)>,
|
|
81
|
+
) -> Result<Self> {
|
|
82
|
+
let (x, y) = position.unwrap_or_default();
|
|
83
|
+
Self::spawn(
|
|
84
|
+
Command::new(point_wayland_executable()?)
|
|
85
|
+
.arg("point-overlay-wayland")
|
|
86
|
+
.args([
|
|
87
|
+
width.to_string(),
|
|
88
|
+
height.to_string(),
|
|
89
|
+
i32::from(position.is_some()).to_string(),
|
|
90
|
+
x.to_string(),
|
|
91
|
+
y.to_string(),
|
|
92
|
+
]),
|
|
93
|
+
1.0,
|
|
94
|
+
1.0,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[cfg(target_os = "macos")]
|
|
99
|
+
pub fn start_point(display_id: u32, width: usize, height: usize) -> Result<Self> {
|
|
100
|
+
let (_, _, points_w, points_h) = crate::mac::display_geometry(display_id)
|
|
101
|
+
.context("the selected display is unavailable")?;
|
|
102
|
+
Self::spawn(
|
|
103
|
+
Command::new(std::env::current_exe()?)
|
|
104
|
+
.arg("point-overlay")
|
|
105
|
+
.arg(display_id.to_string()),
|
|
106
|
+
points_w / width as f64,
|
|
107
|
+
points_h / height as f64,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[cfg(all(test, target_os = "linux"))]
|
|
112
|
+
pub fn test_pid(&self) -> u32 {
|
|
113
|
+
self.child.as_ref().unwrap().id()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub fn point(&mut self, x: i64, y: i64, timeout_ms: u64, label: &str) -> Result<()> {
|
|
117
|
+
let input = self.input.as_mut().context("point overlay exited")?;
|
|
118
|
+
writeln!(
|
|
119
|
+
input,
|
|
120
|
+
"P {:.2} {:.2} {timeout_ms} {label}",
|
|
121
|
+
x as f64 * self.scale_x,
|
|
122
|
+
y as f64 * self.scale_y
|
|
123
|
+
)?;
|
|
124
|
+
Ok(())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Point markers have no fade: close/clear must remove them and reap the helper.
|
|
128
|
+
pub fn stop(mut self) {
|
|
129
|
+
self.input.take();
|
|
130
|
+
if let Some(mut child) = self.child.take() {
|
|
131
|
+
let _ = child.kill();
|
|
132
|
+
let _ = child.wait();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
42
136
|
#[cfg(target_os = "linux")]
|
|
43
137
|
pub fn start_wayland(width: usize, height: usize) -> Result<Self> {
|
|
44
138
|
let executable = std::env::current_exe()?;
|
|
@@ -69,13 +163,20 @@ impl Indicator {
|
|
|
69
163
|
let reader = std::thread::spawn(move || {
|
|
70
164
|
let mut ready = String::new();
|
|
71
165
|
let result = BufReader::new(stdout).read_line(&mut ready);
|
|
72
|
-
let _ = ready_tx.send(result.is_ok()
|
|
166
|
+
let _ = ready_tx.send(if result.is_ok() {
|
|
167
|
+
ready.trim().to_owned()
|
|
168
|
+
} else {
|
|
169
|
+
String::new()
|
|
170
|
+
});
|
|
73
171
|
});
|
|
74
|
-
|
|
172
|
+
let ready = ready_rx
|
|
173
|
+
.recv_timeout(std::time::Duration::from_secs(5))
|
|
174
|
+
.unwrap_or_default();
|
|
175
|
+
if ready != "READY" {
|
|
75
176
|
let _ = child.kill();
|
|
76
177
|
let _ = child.wait();
|
|
77
178
|
let _ = reader.join();
|
|
78
|
-
anyhow::bail!("agent indicator did not open its overlay within 5s");
|
|
179
|
+
anyhow::bail!("agent indicator did not open its overlay within 5s: {ready}");
|
|
79
180
|
}
|
|
80
181
|
let _ = reader.join();
|
|
81
182
|
Ok(Self {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
use anyhow::{bail, Result};
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
4
|
+
pub enum Button {
|
|
5
|
+
Left,
|
|
6
|
+
Middle,
|
|
7
|
+
Right,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
impl Button {
|
|
11
|
+
pub fn from_number(number: i64) -> Self {
|
|
12
|
+
match number {
|
|
13
|
+
2 => Self::Middle,
|
|
14
|
+
3 => Self::Right,
|
|
15
|
+
_ => Self::Left,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
21
|
+
pub struct HeldState {
|
|
22
|
+
keys: Vec<i16>,
|
|
23
|
+
buttons: Vec<Button>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl HeldState {
|
|
27
|
+
pub fn key(&mut self, code: i16, down: bool) {
|
|
28
|
+
if down {
|
|
29
|
+
if !self.keys.contains(&code) {
|
|
30
|
+
self.keys.push(code);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
self.keys.retain(|held| *held != code);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn button(&mut self, button: Button, down: bool) {
|
|
38
|
+
if down {
|
|
39
|
+
if !self.buttons.contains(&button) {
|
|
40
|
+
self.buttons.push(button);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
self.buttons.retain(|held| *held != button);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn release_plan(&mut self) -> (Vec<Button>, Vec<i16>) {
|
|
48
|
+
(
|
|
49
|
+
std::mem::take(&mut self.buttons),
|
|
50
|
+
std::mem::take(&mut self.keys),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[derive(Debug, Clone, serde::Serialize)]
|
|
56
|
+
pub struct InputUnavailable {
|
|
57
|
+
pub reason: String,
|
|
58
|
+
pub remedy: String,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn unavailable_reason() -> &'static str {
|
|
62
|
+
"Windows input is not implemented in this build."
|
|
63
|
+
}
|
|
64
|
+
pub fn probe() -> Result<(), InputUnavailable> {
|
|
65
|
+
Err(InputUnavailable {
|
|
66
|
+
reason: unavailable_reason().into(),
|
|
67
|
+
remedy: "Use a future Windows input-enabled build.".into(),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
// This private, unconstructible adapter cannot inject native input.
|
|
71
|
+
pub struct InputDevices {
|
|
72
|
+
_private: (),
|
|
73
|
+
}
|
|
74
|
+
impl InputDevices {
|
|
75
|
+
pub fn create_for_display(_width: i32, _height: i32, _display: u32) -> Result<Self> {
|
|
76
|
+
bail!("{}", unavailable_reason())
|
|
77
|
+
}
|
|
78
|
+
pub fn move_absolute(&mut self, _x: i64, _y: i64) {}
|
|
79
|
+
pub fn button(&mut self, _button: Button, _down: bool) {}
|
|
80
|
+
pub fn scroll(&mut self, _dx: f64, _dy: f64) {}
|
|
81
|
+
pub fn key(&mut self, _code: i16, _down: bool) -> Result<()> {
|
|
82
|
+
bail!("{}", unavailable_reason())
|
|
83
|
+
}
|
|
84
|
+
pub fn unicode_text(&mut self, _text: &str) -> Result<()> {
|
|
85
|
+
bail!("{}", unavailable_reason())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub fn native_keycode(_code: i16) -> Result<i16> {
|
|
90
|
+
bail!("{}", unavailable_reason())
|
|
91
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
|
|
3
|
+
// No key mapping is advertised until the native input lane is implemented.
|
|
4
|
+
pub struct Keystroke {
|
|
5
|
+
pub code: i16,
|
|
6
|
+
}
|
|
7
|
+
impl Keystroke {
|
|
8
|
+
pub fn modifiers(&self) -> Vec<i16> {
|
|
9
|
+
Vec::new()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn named_key(_name: &str) -> Option<Keystroke> {
|
|
14
|
+
None
|
|
15
|
+
}
|
|
16
|
+
pub fn modifier_key(_name: &str) -> Option<i16> {
|
|
17
|
+
None
|
|
18
|
+
}
|
|
19
|
+
pub fn keyboard_layout() -> &'static str {
|
|
20
|
+
"unknown"
|
|
21
|
+
}
|
|
22
|
+
pub struct LayoutNames;
|
|
23
|
+
impl LayoutNames {
|
|
24
|
+
pub fn from_environment() -> Self {
|
|
25
|
+
Self
|
|
26
|
+
}
|
|
27
|
+
pub fn identity(&self) -> String {
|
|
28
|
+
keyboard_layout().into()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
pub struct Layout;
|
|
32
|
+
impl Layout {
|
|
33
|
+
pub fn from_environment() -> Result<Self> {
|
|
34
|
+
Ok(Self)
|
|
35
|
+
}
|
|
36
|
+
pub fn keystroke_for_char(&self, _character: char) -> Option<Keystroke> {
|
|
37
|
+
None
|
|
38
|
+
}
|
|
39
|
+
}
|
package/engine/src/mac.rs
CHANGED
|
@@ -26,6 +26,7 @@ struct CGRect {
|
|
|
26
26
|
type DisplayId = u32;
|
|
27
27
|
|
|
28
28
|
unsafe extern "C" {
|
|
29
|
+
fn desklink_point_overlay_main(display_id: i32) -> i32;
|
|
29
30
|
fn desklink_agent_overlay_main(display_id: i32) -> i32;
|
|
30
31
|
fn desklink_image_bgra(image: *const c_void, length: *mut usize, stride: *mut usize)
|
|
31
32
|
-> *mut u8;
|
|
@@ -182,6 +183,7 @@ fn capabilities() -> Value {
|
|
|
182
183
|
"backends": ["screencapturekit"],
|
|
183
184
|
"formats": ["bgra"],
|
|
184
185
|
"cursor": "embedded",
|
|
186
|
+
"cursor_modes": ["embedded"],
|
|
185
187
|
"audio": false,
|
|
186
188
|
"displays": displays(),
|
|
187
189
|
"grant": if capture_granted { "granted" } else { "missing-screen-recording" },
|
|
@@ -327,7 +329,7 @@ fn encode_probe() -> Result<(), String> {
|
|
|
327
329
|
let mut encoder = crate::encoder::Encoder::new(640, 360, 1000, 30, 1)
|
|
328
330
|
.map_err(|error| format!("VP9 encoder initialization failed: {error:#}"))?;
|
|
329
331
|
let packet = encoder
|
|
330
|
-
.encode(&frame, true)
|
|
332
|
+
.encode(&frame, true, false)
|
|
331
333
|
.map_err(|error| format!("VP9 encoding failed: {error:#}"))?;
|
|
332
334
|
println!(
|
|
333
335
|
"{}",
|
|
@@ -463,14 +465,25 @@ fn indicator_demo(display_id: u32, path: &str) -> Result<(), String> {
|
|
|
463
465
|
fn record_agent_overlay(path: &str) -> Result<(), String> {
|
|
464
466
|
use std::io::Write;
|
|
465
467
|
let display = unsafe { CGMainDisplayID() };
|
|
466
|
-
let (w, h) = crate::convert::fit(
|
|
467
|
-
unsafe {
|
|
468
|
+
let (w, h) = crate::convert::fit(
|
|
469
|
+
unsafe { CGDisplayPixelsWide(display) },
|
|
470
|
+
unsafe { CGDisplayPixelsHigh(display) },
|
|
471
|
+
960,
|
|
472
|
+
640,
|
|
473
|
+
);
|
|
468
474
|
let mut file = std::fs::File::create(path).map_err(|error| error.to_string())?;
|
|
469
475
|
for _ in 0..30 {
|
|
470
476
|
let (pixels, sw, sh, stride) = capture_display(display)?;
|
|
471
|
-
let raw = crate::convert::to_bgrx(
|
|
472
|
-
|
|
473
|
-
|
|
477
|
+
let raw = crate::convert::to_bgrx(
|
|
478
|
+
&pixels,
|
|
479
|
+
sw,
|
|
480
|
+
sh,
|
|
481
|
+
stride,
|
|
482
|
+
crate::convert::PixelFormat::Bgra,
|
|
483
|
+
w,
|
|
484
|
+
h,
|
|
485
|
+
)
|
|
486
|
+
.ok_or_else(|| String::from("could not scale recording frame"))?;
|
|
474
487
|
file.write_all(&raw).map_err(|error| error.to_string())?;
|
|
475
488
|
std::thread::sleep(Duration::from_millis(100));
|
|
476
489
|
}
|
|
@@ -581,7 +594,12 @@ pub fn run() -> i32 {
|
|
|
581
594
|
let command = args.next().unwrap_or_else(|| "help".into());
|
|
582
595
|
if matches!(
|
|
583
596
|
command.as_str(),
|
|
584
|
-
"serve"
|
|
597
|
+
"serve"
|
|
598
|
+
| "capabilities"
|
|
599
|
+
| "capture-probe"
|
|
600
|
+
| "indicator-demo"
|
|
601
|
+
| "axi-record"
|
|
602
|
+
| "setup-input"
|
|
585
603
|
) {
|
|
586
604
|
#[cfg(not(desklink_macos_cli))]
|
|
587
605
|
if let Err(error) = ensure_disclaimed() {
|
|
@@ -595,10 +613,14 @@ pub fn run() -> i32 {
|
|
|
595
613
|
let engine = match std::env::current_exe() { Ok(path) => path, Err(error) => { eprintln!("engine path: {error}"); return 1; } };
|
|
596
614
|
let result = std::process::Command::new("/opt/homebrew/bin/node")
|
|
597
615
|
.arg(cli).arg("--bridge").args(args)
|
|
598
|
-
.env("DESKLINK_AXI_ENGINE", engine)
|
|
616
|
+
.env("DESKLINK_AXI_ENGINE", engine)
|
|
599
617
|
.status();
|
|
600
618
|
return match result { Ok(status) => status.code().unwrap_or(1), Err(error) => { eprintln!("axi bridge: {error}"); 1 } };
|
|
601
619
|
}
|
|
620
|
+
"point-overlay" => {
|
|
621
|
+
let display = args.next().and_then(|value| value.parse().ok()).unwrap_or(0);
|
|
622
|
+
return unsafe { desklink_point_overlay_main(display) };
|
|
623
|
+
}
|
|
602
624
|
"agent-overlay" => {
|
|
603
625
|
let display = args.next().and_then(|value| value.parse().ok()).unwrap_or(0);
|
|
604
626
|
return unsafe { desklink_agent_overlay_main(display) };
|
|
@@ -707,7 +729,10 @@ mod tests {
|
|
|
707
729
|
assert_eq!(value["protocol"], crate::protocol::PROTOCOL_VERSION);
|
|
708
730
|
assert_eq!(value["platform"], "macos");
|
|
709
731
|
assert_eq!(value["capture"]["mechanism"], "screencapturekit");
|
|
710
|
-
assert_eq!(value["encode"]["codecs"],
|
|
732
|
+
assert_eq!(value["encode"]["codecs"][0], "vp9", "VP9 is always offered");
|
|
733
|
+
// Every Mac this engine runs on has a hardware H.264 encoder.
|
|
734
|
+
assert_eq!(value["encode"]["codecs"][1], "h264");
|
|
735
|
+
assert_eq!(value["encode"]["h264"]["encoder"], "videotoolbox");
|
|
711
736
|
assert!(value["capture"]["displays"].is_array());
|
|
712
737
|
assert!(value["clipboard"]["read"].as_bool().unwrap());
|
|
713
738
|
assert!(matches!(
|