Skip to main content

macos_agent/
model.rs

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}