1use clap::ValueEnum;
2use screen_record::types::{PermissionState, PermissionStatusSchema};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::error::{CliError, ErrorCategory};
7use crate::screen_record_adapter::{AppInfo, WindowInfo};
8
9#[derive(Debug, Clone, Serialize)]
10pub struct SuccessEnvelope<T>
11where
12 T: Serialize,
13{
14 pub schema_version: u8,
15 pub ok: bool,
16 pub command: &'static str,
17 pub result: T,
18}
19
20impl<T> SuccessEnvelope<T>
21where
22 T: Serialize,
23{
24 pub fn new(command: &'static str, result: T) -> Self {
25 Self {
26 schema_version: 1,
27 ok: true,
28 command,
29 result,
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize)]
35pub struct ErrorEnvelope {
36 pub schema_version: u8,
37 pub ok: bool,
38 pub error: ErrorResult,
39}
40
41impl ErrorEnvelope {
42 pub fn from_error(err: &CliError) -> Self {
43 Self {
44 schema_version: 1,
45 ok: false,
46 error: ErrorResult::from(err),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Serialize)]
52pub struct ErrorResult {
53 pub category: &'static str,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub operation: Option<String>,
56 pub message: String,
57 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub hints: Vec<String>,
59}
60
61impl From<&CliError> for ErrorResult {
62 fn from(err: &CliError) -> Self {
63 let category = match err.category() {
64 ErrorCategory::Usage => "usage",
65 ErrorCategory::Runtime => "runtime",
66 };
67 Self {
68 category,
69 operation: err.operation().map(str::to_string),
70 message: err.message().to_string(),
71 hints: err.hints().to_vec(),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
77#[serde(rename_all = "snake_case")]
78pub enum PermissionStateResult {
79 Ready,
80 Blocked,
81 Unknown,
82}
83
84impl From<PermissionState> for PermissionStateResult {
85 fn from(value: PermissionState) -> Self {
86 match value {
87 PermissionState::Ready => Self::Ready,
88 PermissionState::Blocked => Self::Blocked,
89 PermissionState::Unknown => Self::Unknown,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
95pub struct PermissionStatusResult {
96 pub screen_recording: PermissionStateResult,
97 pub accessibility: PermissionStateResult,
98 pub automation: PermissionStateResult,
99 pub ready: bool,
100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub hints: Vec<String>,
102}
103
104impl From<&PermissionStatusSchema> for PermissionStatusResult {
105 fn from(value: &PermissionStatusSchema) -> Self {
106 Self {
107 screen_recording: value.screen_recording.into(),
108 accessibility: value.accessibility.into(),
109 automation: value.automation.into(),
110 ready: value.ready,
111 hints: value.hints.clone(),
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize)]
117pub struct ActionMeta {
118 pub action_id: String,
119 pub elapsed_ms: u64,
120 pub dry_run: bool,
121 pub retries: u8,
122 pub attempts_used: u8,
123 pub timeout_ms: u64,
124}
125
126#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
127pub struct ActionPolicyResult {
128 pub dry_run: bool,
129 pub retries: u8,
130 pub retry_delay_ms: u64,
131 pub timeout_ms: u64,
132}
133
134#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
135pub struct WindowRow {
136 pub window_id: u32,
137 pub owner_name: String,
138 pub window_title: String,
139 pub x: i32,
140 pub y: i32,
141 pub width: i32,
142 pub height: i32,
143 pub on_screen: bool,
144 pub active: bool,
145 pub owner_pid: i32,
146 pub z_order: usize,
147}
148
149impl From<&WindowInfo> for WindowRow {
150 fn from(window: &WindowInfo) -> Self {
151 Self {
152 window_id: window.id,
153 owner_name: window.owner_name.clone(),
154 window_title: window.title.clone(),
155 x: window.bounds.x,
156 y: window.bounds.y,
157 width: window.bounds.width,
158 height: window.bounds.height,
159 on_screen: window.on_screen,
160 active: window.active,
161 owner_pid: window.owner_pid,
162 z_order: window.z_order,
163 }
164 }
165}
166
167impl WindowRow {
168 pub fn tsv_line(&self) -> String {
169 format!(
170 "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
171 self.window_id,
172 normalize_tsv_field(&self.owner_name),
173 normalize_tsv_field(&self.window_title),
174 self.x,
175 self.y,
176 self.width,
177 self.height,
178 if self.on_screen { "true" } else { "false" }
179 )
180 }
181}
182
183#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
184pub struct AppRow {
185 pub app_name: String,
186 pub pid: i32,
187 pub bundle_id: String,
188}
189
190impl From<&AppInfo> for AppRow {
191 fn from(app: &AppInfo) -> Self {
192 Self {
193 app_name: app.name.clone(),
194 pid: app.pid,
195 bundle_id: app.bundle_id.clone(),
196 }
197 }
198}
199
200impl AppRow {
201 pub fn tsv_line(&self) -> String {
202 format!(
203 "{}\t{}\t{}",
204 normalize_tsv_field(&self.app_name),
205 self.pid,
206 normalize_tsv_field(&self.bundle_id)
207 )
208 }
209}
210
211#[derive(Debug, Clone, Serialize)]
212pub struct ListWindowsResult {
213 pub windows: Vec<WindowRow>,
214}
215
216#[derive(Debug, Clone, Serialize)]
217pub struct ListAppsResult {
218 pub apps: Vec<AppRow>,
219}
220
221#[derive(Debug, Clone, Serialize)]
222pub struct ScreenshotResult {
223 pub path: String,
224 pub target: WindowRow,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 pub selector: Option<ScreenshotSelectorResult>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub if_changed: Option<IfChangedResult>,
229}
230
231#[derive(Debug, Clone, Serialize)]
232pub struct WaitResult {
233 pub condition: &'static str,
234 pub attempts: u32,
235 pub elapsed_ms: u64,
236 pub terminal_status: &'static str,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub matched_count: Option<usize>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub selector_explain: Option<AxSelectorExplain>,
241}
242
243#[derive(Debug, Clone, Serialize)]
244pub struct ScreenshotSelectorResult {
245 pub node_id: String,
246 pub matched_count: usize,
247 pub padding: i32,
248 pub frame: AxFrame,
249 pub capture_region: AxFrame,
250}
251
252#[derive(Debug, Clone, Serialize)]
253pub struct IfChangedResult {
254 pub changed: bool,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub baseline_hash: Option<String>,
257 pub current_hash: String,
258 pub threshold: u32,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub captured_path: Option<String>,
261}
262
263#[derive(Debug, Clone, Serialize)]
264pub struct WindowActivateResult {
265 pub selected_app: String,
266 pub selected_window_id: Option<u32>,
267 pub wait_ms: Option<u64>,
268 pub policy: ActionPolicyResult,
269 pub meta: ActionMeta,
270}
271
272#[derive(Debug, Clone, Serialize)]
273pub struct InputClickResult {
274 pub x: i32,
275 pub y: i32,
276 pub button: &'static str,
277 pub count: u8,
278 pub policy: ActionPolicyResult,
279 pub meta: ActionMeta,
280}
281
282#[derive(Debug, Clone, Serialize)]
283pub struct InputTypeResult {
284 pub text_length: usize,
285 pub enter: bool,
286 pub delay_ms: Option<u64>,
287 pub policy: ActionPolicyResult,
288 pub meta: ActionMeta,
289}
290
291#[derive(Debug, Clone, Serialize)]
292pub struct InputHotkeyResult {
293 pub mods: Vec<String>,
294 pub key: String,
295 pub policy: ActionPolicyResult,
296 pub meta: ActionMeta,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
300pub struct AxFrame {
301 pub x: f64,
302 pub y: f64,
303 pub width: f64,
304 pub height: f64,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
308pub struct AxNode {
309 pub node_id: String,
310 pub role: String,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub subrole: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub title: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub identifier: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
318 pub value_preview: Option<String>,
319 #[serde(default)]
320 pub enabled: bool,
321 #[serde(default)]
322 pub focused: bool,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub frame: Option<AxFrame>,
325 #[serde(default, skip_serializing_if = "Vec::is_empty")]
326 pub actions: Vec<String>,
327 #[serde(default, skip_serializing_if = "Vec::is_empty")]
328 pub path: Vec<String>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
332pub struct AxTarget {
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub session_id: Option<String>,
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub app: Option<String>,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub bundle_id: Option<String>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub window_title_contains: Option<String>,
341}
342
343#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)]
344#[serde(rename_all = "kebab-case")]
345#[value(rename_all = "kebab-case")]
346pub enum AxMatchStrategy {
347 #[default]
348 Contains,
349 Exact,
350 Prefix,
351 Suffix,
352 Regex,
353}
354
355#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ValueEnum)]
356#[serde(rename_all = "kebab-case")]
357#[value(rename_all = "kebab-case")]
358pub enum AxClickFallbackStage {
359 AxPress,
360 AxConfirm,
361 FrameCenter,
362 Coordinate,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
366pub struct AxSelectorExplainStage {
367 pub stage: String,
368 pub before_count: usize,
369 pub after_count: usize,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
373pub struct AxSelectorExplain {
374 pub strategy: AxMatchStrategy,
375 pub total_candidates: usize,
376 pub matched_count: usize,
377 pub selected_count: usize,
378 pub stage_results: Vec<AxSelectorExplainStage>,
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub selected_node_id: Option<String>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
384pub struct AxSelector {
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub node_id: Option<String>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub role: Option<String>,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub title_contains: Option<String>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub identifier_contains: Option<String>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub value_contains: Option<String>,
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub subrole: Option<String>,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 pub focused: Option<bool>,
399 #[serde(skip_serializing_if = "Option::is_none")]
400 pub enabled: Option<bool>,
401 #[serde(skip_serializing_if = "Option::is_none")]
402 pub nth: Option<usize>,
403 #[serde(default, skip_serializing_if = "is_match_strategy_contains")]
404 pub match_strategy: AxMatchStrategy,
405 #[serde(default, skip_serializing_if = "is_false")]
406 pub explain: bool,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
410pub struct AxListRequest {
411 #[serde(default)]
412 pub target: AxTarget,
413 #[serde(skip_serializing_if = "Option::is_none")]
414 pub role: Option<String>,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 pub title_contains: Option<String>,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub identifier_contains: Option<String>,
419 #[serde(skip_serializing_if = "Option::is_none")]
420 pub value_contains: Option<String>,
421 #[serde(skip_serializing_if = "Option::is_none")]
422 pub subrole: Option<String>,
423 #[serde(skip_serializing_if = "Option::is_none")]
424 pub focused: Option<bool>,
425 #[serde(skip_serializing_if = "Option::is_none")]
426 pub enabled: Option<bool>,
427 #[serde(skip_serializing_if = "Option::is_none")]
428 pub max_depth: Option<u32>,
429 #[serde(skip_serializing_if = "Option::is_none")]
430 pub limit: Option<usize>,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
434pub struct AxClickRequest {
435 #[serde(default)]
436 pub target: AxTarget,
437 #[serde(default)]
438 pub selector: AxSelector,
439 #[serde(default)]
440 pub allow_coordinate_fallback: bool,
441 #[serde(default)]
442 pub reselect_before_click: bool,
443 #[serde(default, skip_serializing_if = "Vec::is_empty")]
444 pub fallback_order: Vec<AxClickFallbackStage>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
448pub struct AxTypeRequest {
449 #[serde(default)]
450 pub target: AxTarget,
451 #[serde(default)]
452 pub selector: AxSelector,
453 pub text: String,
454 #[serde(default)]
455 pub clear_first: bool,
456 #[serde(default)]
457 pub submit: bool,
458 #[serde(default)]
459 pub paste: bool,
460 #[serde(default)]
461 pub allow_keyboard_fallback: bool,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
465pub struct AxListResult {
466 pub nodes: Vec<AxNode>,
467 #[serde(default, skip_serializing_if = "Vec::is_empty")]
468 pub warnings: Vec<String>,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
472pub struct AxClickResult {
473 #[serde(skip_serializing_if = "Option::is_none")]
474 pub node_id: Option<String>,
475 pub matched_count: usize,
476 pub action: String,
477 #[serde(default)]
478 pub used_coordinate_fallback: bool,
479 #[serde(skip_serializing_if = "Option::is_none")]
480 pub fallback_x: Option<i32>,
481 #[serde(skip_serializing_if = "Option::is_none")]
482 pub fallback_y: Option<i32>,
483 #[serde(default, skip_serializing_if = "Vec::is_empty")]
484 pub fallback_order: Vec<AxClickFallbackStage>,
485 #[serde(default, skip_serializing_if = "Vec::is_empty")]
486 pub attempted_stages: Vec<AxClickFallbackStage>,
487 #[serde(skip_serializing_if = "Option::is_none")]
488 pub selector_explain: Option<AxSelectorExplain>,
489 #[serde(skip_serializing_if = "Option::is_none")]
490 pub gates: Option<AxGateResult>,
491 #[serde(skip_serializing_if = "Option::is_none")]
492 pub postconditions: Option<AxPostconditionResult>,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
496pub struct AxTypeResult {
497 #[serde(skip_serializing_if = "Option::is_none")]
498 pub node_id: Option<String>,
499 pub matched_count: usize,
500 pub applied_via: String,
501 pub text_length: usize,
502 #[serde(default)]
503 pub submitted: bool,
504 #[serde(default)]
505 pub used_keyboard_fallback: bool,
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub selector_explain: Option<AxSelectorExplain>,
508 #[serde(skip_serializing_if = "Option::is_none")]
509 pub gates: Option<AxGateResult>,
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub postconditions: Option<AxPostconditionResult>,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
515pub struct AxGateCheckResult {
516 pub gate: String,
517 pub terminal_status: String,
518 pub attempts: u32,
519 pub elapsed_ms: u64,
520 #[serde(skip_serializing_if = "Option::is_none")]
521 pub matched_count: Option<usize>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
525pub struct AxGateResult {
526 pub timeout_ms: u64,
527 pub poll_ms: u64,
528 #[serde(default, skip_serializing_if = "Vec::is_empty")]
529 pub checks: Vec<AxGateCheckResult>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
533pub struct AxPostconditionCheckResult {
534 pub check: String,
535 pub terminal_status: String,
536 pub attempts: u32,
537 pub elapsed_ms: u64,
538 #[serde(skip_serializing_if = "Option::is_none")]
539 pub attribute: Option<String>,
540 pub expected: Value,
541 #[serde(skip_serializing_if = "Option::is_none")]
542 pub observed: Option<Value>,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
546pub struct AxPostconditionResult {
547 pub timeout_ms: u64,
548 pub poll_ms: u64,
549 #[serde(default, skip_serializing_if = "Vec::is_empty")]
550 pub checks: Vec<AxPostconditionCheckResult>,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
554pub struct DebugBundleArtifactEntry {
555 pub id: String,
556 pub path: String,
557 pub ok: bool,
558 #[serde(skip_serializing_if = "Option::is_none")]
559 pub error: Option<String>,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
563pub struct DebugBundleResult {
564 pub output_dir: String,
565 pub artifact_index_path: String,
566 pub partial_failure: bool,
567 #[serde(default, skip_serializing_if = "Vec::is_empty")]
568 pub artifacts: Vec<DebugBundleArtifactEntry>,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
572pub struct AxAttrGetRequest {
573 #[serde(default)]
574 pub target: AxTarget,
575 #[serde(default)]
576 pub selector: AxSelector,
577 pub name: String,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
581pub struct AxAttrSetRequest {
582 #[serde(default)]
583 pub target: AxTarget,
584 #[serde(default)]
585 pub selector: AxSelector,
586 pub name: String,
587 pub value: Value,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
591pub struct AxActionPerformRequest {
592 #[serde(default)]
593 pub target: AxTarget,
594 #[serde(default)]
595 pub selector: AxSelector,
596 pub name: String,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
600pub struct AxAttrGetResult {
601 #[serde(skip_serializing_if = "Option::is_none")]
602 pub node_id: Option<String>,
603 pub matched_count: usize,
604 pub name: String,
605 pub value: Value,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
609pub struct AxAttrSetResult {
610 #[serde(skip_serializing_if = "Option::is_none")]
611 pub node_id: Option<String>,
612 pub matched_count: usize,
613 pub name: String,
614 pub applied: bool,
615 pub value_type: String,
616}
617
618#[derive(Debug, Clone, Serialize)]
619pub struct AxAttrSetCommandResult {
620 #[serde(flatten)]
621 pub detail: AxAttrSetResult,
622 pub policy: ActionPolicyResult,
623 pub meta: ActionMeta,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
627pub struct AxActionPerformResult {
628 #[serde(skip_serializing_if = "Option::is_none")]
629 pub node_id: Option<String>,
630 pub matched_count: usize,
631 pub name: String,
632 pub performed: bool,
633}
634
635#[derive(Debug, Clone, Serialize)]
636pub struct AxActionPerformCommandResult {
637 #[serde(flatten)]
638 pub detail: AxActionPerformResult,
639 pub policy: ActionPolicyResult,
640 pub meta: ActionMeta,
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
644pub struct AxSessionStartRequest {
645 #[serde(default)]
646 pub target: AxTarget,
647 #[serde(skip_serializing_if = "Option::is_none")]
648 pub session_id: Option<String>,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
652pub struct AxSessionStopRequest {
653 pub session_id: String,
654}
655
656#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
657pub struct AxSessionInfo {
658 pub session_id: String,
659 #[serde(skip_serializing_if = "Option::is_none")]
660 pub app: Option<String>,
661 #[serde(skip_serializing_if = "Option::is_none")]
662 pub bundle_id: Option<String>,
663 #[serde(skip_serializing_if = "Option::is_none")]
664 pub pid: Option<i32>,
665 #[serde(skip_serializing_if = "Option::is_none")]
666 pub window_title_contains: Option<String>,
667 pub created_at_ms: u64,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
671pub struct AxSessionStartResult {
672 #[serde(flatten)]
673 pub session: AxSessionInfo,
674 pub created: bool,
675}
676
677#[derive(Debug, Clone, Serialize)]
678pub struct AxSessionStartCommandResult {
679 #[serde(flatten)]
680 pub detail: AxSessionStartResult,
681 pub policy: ActionPolicyResult,
682 pub meta: ActionMeta,
683}
684
685#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
686pub struct AxSessionListResult {
687 pub sessions: Vec<AxSessionInfo>,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
691pub struct AxSessionStopResult {
692 pub session_id: String,
693 pub removed: bool,
694}
695
696#[derive(Debug, Clone, Serialize)]
697pub struct AxSessionStopCommandResult {
698 #[serde(flatten)]
699 pub detail: AxSessionStopResult,
700 pub policy: ActionPolicyResult,
701 pub meta: ActionMeta,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
705pub struct AxWatchStartRequest {
706 pub session_id: String,
707 #[serde(default)]
708 pub events: Vec<String>,
709 #[serde(default)]
710 pub max_buffer: usize,
711 #[serde(skip_serializing_if = "Option::is_none")]
712 pub watch_id: Option<String>,
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
716pub struct AxWatchPollRequest {
717 pub watch_id: String,
718 #[serde(default)]
719 pub limit: usize,
720 #[serde(default)]
721 pub drain: bool,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
725pub struct AxWatchStopRequest {
726 pub watch_id: String,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
730pub struct AxWatchEvent {
731 pub watch_id: String,
732 pub event: String,
733 pub at_ms: u64,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub role: Option<String>,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 pub title: Option<String>,
738 #[serde(skip_serializing_if = "Option::is_none")]
739 pub identifier: Option<String>,
740 #[serde(skip_serializing_if = "Option::is_none")]
741 pub pid: Option<i32>,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
745pub struct AxWatchStartResult {
746 pub watch_id: String,
747 pub session_id: String,
748 pub events: Vec<String>,
749 pub max_buffer: usize,
750 pub started: bool,
751}
752
753#[derive(Debug, Clone, Serialize)]
754pub struct AxWatchStartCommandResult {
755 #[serde(flatten)]
756 pub detail: AxWatchStartResult,
757 pub policy: ActionPolicyResult,
758 pub meta: ActionMeta,
759}
760
761#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
762pub struct AxWatchPollResult {
763 pub watch_id: String,
764 pub events: Vec<AxWatchEvent>,
765 pub dropped: usize,
766 pub running: bool,
767}
768
769#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
770pub struct AxWatchStopResult {
771 pub watch_id: String,
772 pub stopped: bool,
773 pub drained: usize,
774}
775
776#[derive(Debug, Clone, Serialize)]
777pub struct AxWatchStopCommandResult {
778 #[serde(flatten)]
779 pub detail: AxWatchStopResult,
780 pub policy: ActionPolicyResult,
781 pub meta: ActionMeta,
782}
783
784#[derive(Debug, Clone, Serialize)]
785pub struct AxClickCommandResult {
786 #[serde(flatten)]
787 pub detail: AxClickResult,
788 pub policy: ActionPolicyResult,
789 pub meta: ActionMeta,
790}
791
792#[derive(Debug, Clone, Serialize)]
793pub struct AxTypeCommandResult {
794 #[serde(flatten)]
795 pub detail: AxTypeResult,
796 pub policy: ActionPolicyResult,
797 pub meta: ActionMeta,
798}
799
800#[derive(Debug, Clone, Serialize)]
801pub struct ScenarioStepResult {
802 pub step_id: String,
803 pub ok: bool,
804 pub exit_code: i32,
805 pub elapsed_ms: u64,
806 #[serde(skip_serializing_if = "Option::is_none")]
807 pub operation: Option<String>,
808 #[serde(skip_serializing_if = "Option::is_none")]
809 pub ax_path: Option<String>,
810 #[serde(skip_serializing_if = "Option::is_none")]
811 pub fallback_used: Option<bool>,
812 #[serde(skip_serializing_if = "String::is_empty")]
813 pub stdout: String,
814 #[serde(skip_serializing_if = "String::is_empty")]
815 pub stderr: String,
816}
817
818#[derive(Debug, Clone, Serialize)]
819pub struct ScenarioRunResult {
820 pub file: String,
821 pub total_steps: usize,
822 pub passed_steps: usize,
823 pub failed_steps: usize,
824 #[serde(skip_serializing_if = "Option::is_none")]
825 pub first_failed_step_id: Option<String>,
826 pub steps: Vec<ScenarioStepResult>,
827}
828
829#[derive(Debug, Clone, Serialize)]
830pub struct ProfileValidateResult {
831 pub file: String,
832 pub valid: bool,
833 #[serde(default, skip_serializing_if = "Vec::is_empty")]
834 pub issues: Vec<String>,
835}
836
837#[derive(Debug, Clone, Serialize)]
838pub struct ProfileInitResult {
839 pub path: String,
840 pub profile_name: String,
841}
842
843#[derive(Debug, Clone, Serialize)]
844pub struct InputSourceCurrentResult {
845 pub current: String,
846}
847
848#[derive(Debug, Clone, Serialize)]
849pub struct InputSourceSwitchResult {
850 pub previous: String,
851 pub current: String,
852 pub switched: bool,
853}
854
855fn normalize_tsv_field(value: &str) -> String {
856 value
857 .chars()
858 .map(|ch| {
859 if ch == '\t' || ch == '\n' || ch == '\r' {
860 ' '
861 } else {
862 ch
863 }
864 })
865 .collect()
866}
867
868fn is_false(value: &bool) -> bool {
869 !*value
870}
871
872fn is_match_strategy_contains(value: &AxMatchStrategy) -> bool {
873 *value == AxMatchStrategy::Contains
874}
875
876#[cfg(test)]
877mod tests {
878 use pretty_assertions::assert_eq;
879
880 use super::normalize_tsv_field;
881
882 #[test]
883 fn normalize_tsv_field_replaces_control_whitespace() {
884 assert_eq!(normalize_tsv_field("A\tB\nC\rD"), "A B C D");
885 }
886}