cosh_tools/computer/
errors.rs1use xa11y::Error;
17
18pub fn render(tool: &str, ctx: &str, err: &Error) -> String {
28 let head = format!("{tool}: {ctx}: ");
29 match err {
30 Error::PermissionDenied { instructions } => format!(
31 "{head}permission denied — {instructions} After granting, retry the \
32 same call; the consent applies to the whole terminal session"
33 ),
34 Error::AccessibilityNotEnabled { app, instructions } => {
35 format!("{head}accessibility not enabled for {app} — {instructions}")
36 }
37 Error::SelectorNotMatched {
38 selector,
39 diagnosis,
40 } => {
41 let mut out = format!(
42 "{head}no element matched `{selector}` — re-capture with \
43 computer_snapshot and match an element that exists NOW \
44 (role[name='…'] is stable; a bare :nth index goes stale \
45 after any UI change)"
46 );
47 out.push_str(&diagnosis_suffix(diagnosis.as_deref()));
48 out
49 }
50 Error::ElementStale { selector } => format!(
51 "{head}element `{selector}` went stale (the UI changed between \
52 capture and action) — re-snapshot and act on the fresh tree"
53 ),
54 Error::ActionNotSupported { action, role } => format!(
55 "{head}action `{action}` is not supported on a {role} element — \
56 try the action appropriate to its role instead (`press` on \
57 buttons, `toggle` on check boxes, `show_menu` on menu items), \
58 or act on a parent container"
59 ),
60 Error::TextValueNotSupported => format!(
61 "{head}this element does not accept text input through the \
62 accessibility bridge — for a slider/spinner use `set_numeric_value` \
63 with `numeric_value`, otherwise click it first (computer_control, \
64 the real click is the one reliable focus mover) and type with a \
65 keyboard step instead"
66 ),
67 Error::Timeout { elapsed, diagnosis } => {
68 let mut out = format!(
69 "{head}timed out after {elapsed:.1?} — if the target is slow \
70 to appear, raise `timeout_ms` or add a `wait` step before \
71 this one"
72 );
73 out.push_str(&diagnosis_suffix(diagnosis.as_deref()));
74 out
75 }
76 Error::InvalidSelector { selector, message } => {
77 format!(
78 "{head}selector `{selector}` is invalid: {message} — fix the selector syntax and retry (no platform call was made)"
79 )
80 }
81 Error::InvalidActionData { message } => {
82 format!(
83 "{head}invalid action data: {message} — fix the call arguments and retry (no platform call was made)"
84 )
85 }
86 Error::InvalidConfig { message } => {
87 format!(
88 "{head}invalid configuration: {message} — fix the environment setting and restart the session"
89 )
90 }
91 Error::NoElementBounds => format!(
92 "{head}the element matched but has no on-screen bounds — it may \
93 sit in a collapsed container or be virtual; scroll it into \
94 view (computer_act `scroll_into_view`) or act on a visible \
95 ancestor instead"
96 ),
97 Error::Unsupported { feature } => format!(
98 "{head}`{feature}` has no implementation on this platform/session — a \
99 capability limit, not a fixable call error. Input synthesis and \
100 screen capture both depend on the session type (native X11, or \
101 Wayland with the right portal grants), so pick an approach that \
102 fits what this session exposes"
103 ),
104 Error::Platform { code, message } => format!(
105 "{head}platform error ({code}): {message} — a provider-level failure \
106 whose meaning depends on the operation (an accessibility-backend \
107 fault, a capture or image-processing failure, or an argument the \
108 backend rejected); one retry is reasonable, but if unchanged \
109 arguments fail the same way, stop and report it to the user \
110 instead of looping"
111 ),
112 other => format!(
116 "{head}{} — re-capture with computer_snapshot to check the \
117 current state before retrying",
118 other
119 ),
120 }
121}
122
123pub fn render_app_miss(tool: &str, err: &Error) -> String {
129 let out = render(tool, "resolve application", err);
130 match err {
131 Error::SelectorNotMatched { .. } => format!(
132 "{out} — a name that matches no APPLICATION may belong to a \
133 transient shell surface (Quick Settings, Notification Center, \
134 a shell menu): those never appear as applications. Target them \
135 with `surface` instead (e.g. `\"flyout\"` for an open flyout, \
136 `\"menu_bar\"` for menus)"
137 ),
138 _ => out,
139 }
140}
141
142fn diagnosis_suffix(diagnosis: Option<&xa11y::Diagnosis>) -> String {
147 match diagnosis {
148 None => String::new(),
149 Some(d) => {
150 let rendered = d.to_string();
151 if rendered.starts_with(';') {
152 format!(" {rendered}")
153 } else {
154 format!("; {rendered}")
155 }
156 }
157 }
158}