@pheem49/mint 1.9.0 → 1.10.0
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/Cargo.lock +55 -3
- package/Cargo.toml +2 -1
- package/Release_Note.md +285 -140
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/actions.rs +233 -0
- package/crates/mint-cli/src/agent.rs +287 -43
- package/crates/mint-cli/src/gmail.rs +22 -2
- package/crates/mint-cli/src/interactive.rs +2888 -0
- package/crates/mint-cli/src/main.rs +336 -2854
- package/crates/mint-cli/src/markdown.rs +294 -0
- package/crates/mint-cli/src/onboard.rs +44 -1
- package/crates/mint-cli/src/updater.rs +48 -6
- package/crates/mint-core/Cargo.toml +1 -0
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +376 -13
- package/crates/mint-core/src/browser.rs +629 -101
- package/crates/mint-core/src/channels.rs +105 -6
- package/crates/mint-core/src/chat.rs +52 -4
- package/crates/mint-core/src/code_tools.rs +29 -11
- package/crates/mint-core/src/config.rs +118 -0
- package/crates/mint-core/src/image_gen.rs +294 -33
- package/crates/mint-core/src/lib.rs +5 -2
- package/crates/mint-core/src/mcp.rs +119 -6
- package/crates/mint-core/src/orchestration.rs +133 -11
- package/crates/mint-core/src/pictures.rs +64 -1
- package/crates/mint-core/src/skills.rs +82 -6
- package/crates/mint-core/src/video_gen.rs +235 -0
- package/package.json +8 -2
- package/shared-modules/luajit/luajit.json +2 -2
- package/skills-lock.json +41 -0
- package/src/renderer/index-web.html +1 -1
- package/src/renderer/index.html +1 -1
- package/src/renderer/shared/constants/slashCommands.ts +27 -0
- package/src/renderer/shared/platform.ts +3 -0
- package/src/renderer/shared/types.ts +4 -0
- package/src/renderer/shared/utils/approval.tsx +86 -7
- package/src/renderer/shared/utils/markdown.tsx +217 -88
- package/src/renderer/shared/utils/slashCommandProcessor.ts +239 -0
- package/src/renderer/src/components/ChatPanel.tsx +461 -64
- package/src/renderer/src/components/DashboardSidebar.tsx +11 -2
- package/src/renderer/src/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src/components/MintDashboard.tsx +150 -5
- package/src/renderer/src/components/PicturesLibrary.tsx +156 -15
- package/src/renderer/src/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src/components/Settings/GeneralTab.tsx +762 -634
- package/src/renderer/src/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src/components/SettingsWindow.tsx +38 -2
- package/src/renderer/src/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src/css/image-studio.css +106 -0
- package/src/renderer/src/css/settings.css +158 -0
- package/src/renderer/src/css/styles.css +352 -1
- package/src/renderer/src/css/veo-studio.css +626 -0
- package/src/renderer/src/index.css +1 -0
- package/src/renderer/src/tauri.ts +144 -17
- package/src/renderer/src-web/components/ChatPanel.tsx +360 -88
- package/src/renderer/src-web/components/DashboardSidebar.tsx +16 -1
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +87 -4
- package/src/renderer/src-web/components/MintDashboard.tsx +137 -4
- package/src/renderer/src-web/components/PicturesLibrary.tsx +155 -15
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +316 -18
- package/src/renderer/src-web/components/Settings/GeneralTab.tsx +762 -629
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +304 -70
- package/src/renderer/src-web/components/Settings/ThemeTab.tsx +6 -1
- package/src/renderer/src-web/components/SettingsWindow.tsx +35 -2
- package/src/renderer/src-web/components/VeoStudioPanel.tsx +549 -0
- package/src/renderer/src-web/css/image-studio.css +106 -0
- package/src/renderer/src-web/css/settings.css +158 -0
- package/src/renderer/src-web/css/styles.css +353 -1
- package/src/renderer/src-web/css/veo-studio.css +650 -0
- package/src/renderer/src-web/index.css +1 -0
- package/src/renderer/src-web/tauri.ts +135 -16
- package/src-tauri/src/lib.rs +114 -8
- package/src-tauri/src/webhooks.rs +45 -31
- package/src-tauri/src/workflows.rs +1 -0
- package/src-tauri/tauri.conf.json +1 -1
- package/temp_gridgeist/AGENTS.md +8 -0
- package/temp_gridgeist/CHANGELOG.md +130 -0
- package/temp_gridgeist/LICENSE +21 -0
- package/temp_gridgeist/README.md +323 -0
- package/temp_gridgeist/README.th.md +325 -0
- package/temp_gridgeist/skills/gridgeist/SKILL.md +88 -0
- package/temp_gridgeist/skills/gridgeist/agents/openai.yaml +9 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist-small.svg +10 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.png +0 -0
- package/temp_gridgeist/skills/gridgeist/assets/gridgeist.svg +18 -0
- package/temp_gridgeist/skills/gridgeist/references/design-language.md +88 -0
- package/temp_gridgeist/skills/gridgeist/references/review-checklist.md +128 -0
- package/temp_gridgeist/skills/gridgeist/references/system-contract.md +109 -0
|
@@ -4,6 +4,10 @@ use serde::Serialize;
|
|
|
4
4
|
use serde_json::{Value, json};
|
|
5
5
|
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
|
6
6
|
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Logging
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
7
11
|
fn log_action(action: &str, details: &str) {
|
|
8
12
|
if let Some(config_dir) = dirs::config_dir() {
|
|
9
13
|
let log_dir = config_dir.join("mint");
|
|
@@ -21,6 +25,10 @@ fn log_action(action: &str, details: &str) {
|
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Public types
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
24
32
|
#[derive(Debug, Clone, Serialize)]
|
|
25
33
|
#[serde(rename_all = "camelCase")]
|
|
26
34
|
pub struct BrowserTab {
|
|
@@ -29,6 +37,10 @@ pub struct BrowserTab {
|
|
|
29
37
|
pub url: String,
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Public API — navigation / reading
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
32
44
|
pub async fn list_tabs(config: &MintConfig) -> Result<Vec<BrowserTab>, String> {
|
|
33
45
|
if !is_browser_running(config).await {
|
|
34
46
|
return Err("Browser automation is not running. Please run 'mint auto' first.".to_string());
|
|
@@ -110,6 +122,15 @@ pub async fn read_page_text(config: &MintConfig) -> Result<String, String> {
|
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Public API — click / type (selector-based, with native CDP events)
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/// Click a CSS selector element using native CDP mouse events.
|
|
130
|
+
/// Supports:
|
|
131
|
+
/// - Standard CSS selectors: `button.submit`, `#id`, `[attr=val]`
|
|
132
|
+
/// - Text match: `text=Login`, `contains=Submit`
|
|
133
|
+
/// - XPath: `xpath=//button[@type='submit']`
|
|
113
134
|
pub async fn click(config: &MintConfig, selector: &str) -> Result<String, String> {
|
|
114
135
|
let selector = selector.trim();
|
|
115
136
|
if selector.is_empty() || selector.len() > 500 {
|
|
@@ -121,36 +142,85 @@ pub async fn click(config: &MintConfig, selector: &str) -> Result<String, String
|
|
|
121
142
|
}
|
|
122
143
|
log_action("CLICK", &format!("Clicking element '{selector}'"));
|
|
123
144
|
ensure_page_open(config).await?;
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
|
|
146
|
+
// Ensure overlay exists before doing any visual interaction
|
|
147
|
+
inject_overlay(config).await;
|
|
148
|
+
|
|
149
|
+
// Build a JS expression that finds the element using selector or text/xpath
|
|
150
|
+
let find_expr = selector_to_js_find(selector);
|
|
151
|
+
|
|
152
|
+
let expression = format!(
|
|
153
|
+
r#"(() => {{
|
|
154
|
+
{find_expr}
|
|
155
|
+
if (!el) return null;
|
|
156
|
+
const r = el.getBoundingClientRect();
|
|
157
|
+
return JSON.stringify({{ x: r.left + r.width / 2, y: r.top + r.height / 2 }});
|
|
158
|
+
}})()"#
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
let coord_result = cdp_call_raw(
|
|
126
162
|
config,
|
|
127
163
|
"Runtime.evaluate",
|
|
128
|
-
json!({
|
|
129
|
-
"expression": format!(
|
|
130
|
-
"(() => {{ const element = document.querySelector({selector_json}); if (!element) return 'not-found'; element.click(); return 'clicked'; }})()"
|
|
131
|
-
),
|
|
132
|
-
"returnByValue": true
|
|
133
|
-
}),
|
|
164
|
+
json!({ "expression": expression, "returnByValue": true }),
|
|
134
165
|
)
|
|
135
|
-
.await
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
.await;
|
|
167
|
+
|
|
168
|
+
if let Ok(response) = coord_result {
|
|
169
|
+
let val = &response["result"]["result"];
|
|
170
|
+
if let Some(val_str) = val["value"].as_str() {
|
|
171
|
+
if let Ok(parsed) = serde_json::from_str::<Value>(val_str) {
|
|
172
|
+
if let (Some(x), Some(y)) = (parsed["x"].as_f64(), parsed["y"].as_f64()) {
|
|
173
|
+
log_action(
|
|
174
|
+
"CLICK",
|
|
175
|
+
&format!("Coordinates ({x:.0},{y:.0}) for '{selector}'"),
|
|
176
|
+
);
|
|
177
|
+
let result = mouse_click(config, x, y, "left").await?;
|
|
178
|
+
log_action(
|
|
179
|
+
"CLICK_SUCCESS",
|
|
180
|
+
&format!("Clicked '{selector}' at ({x:.0},{y:.0})"),
|
|
181
|
+
);
|
|
182
|
+
return Ok(result);
|
|
151
183
|
}
|
|
152
184
|
}
|
|
153
185
|
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Fallback: JS .click() directly
|
|
189
|
+
let click_expr = format!(
|
|
190
|
+
r#"(() => {{
|
|
191
|
+
{find_expr}
|
|
192
|
+
if (!el) return 'not-found';
|
|
193
|
+
el.scrollIntoView({{behavior:'instant',block:'center'}});
|
|
194
|
+
el.click();
|
|
195
|
+
return 'clicked';
|
|
196
|
+
}})()"#
|
|
197
|
+
);
|
|
198
|
+
match cdp_call(
|
|
199
|
+
config,
|
|
200
|
+
"Runtime.evaluate",
|
|
201
|
+
json!({ "expression": click_expr, "returnByValue": true }),
|
|
202
|
+
)
|
|
203
|
+
.await
|
|
204
|
+
{
|
|
205
|
+
Ok(response) => match response["result"]["result"]["value"].as_str() {
|
|
206
|
+
Some("clicked") => {
|
|
207
|
+
log_action(
|
|
208
|
+
"CLICK_SUCCESS",
|
|
209
|
+
&format!("JS-fallback clicked '{selector}'"),
|
|
210
|
+
);
|
|
211
|
+
Ok("clicked".into())
|
|
212
|
+
}
|
|
213
|
+
Some("not-found") => {
|
|
214
|
+
let err = format!("element not found for selector: {selector}");
|
|
215
|
+
log_action("CLICK_ERROR", &err);
|
|
216
|
+
Err(err)
|
|
217
|
+
}
|
|
218
|
+
_ => {
|
|
219
|
+
let err = response_error(&response);
|
|
220
|
+
log_action("CLICK_ERROR", &err);
|
|
221
|
+
Err(err)
|
|
222
|
+
}
|
|
223
|
+
},
|
|
154
224
|
Err(e) => {
|
|
155
225
|
log_action("CLICK_ERROR", &format!("Websocket error: {e}"));
|
|
156
226
|
Err(e)
|
|
@@ -158,6 +228,8 @@ pub async fn click(config: &MintConfig, selector: &str) -> Result<String, String
|
|
|
158
228
|
}
|
|
159
229
|
}
|
|
160
230
|
|
|
231
|
+
/// Type text into a CSS selector element using native CDP keyboard events.
|
|
232
|
+
/// Focuses the element first (via click), then sends Input.insertText.
|
|
161
233
|
pub async fn type_text(config: &MintConfig, selector: &str, text: &str) -> Result<String, String> {
|
|
162
234
|
let selector = selector.trim();
|
|
163
235
|
if selector.is_empty() || selector.len() > 500 {
|
|
@@ -167,55 +239,403 @@ pub async fn type_text(config: &MintConfig, selector: &str, text: &str) -> Resul
|
|
|
167
239
|
);
|
|
168
240
|
return Err("browser selector must contain between 1 and 500 characters".into());
|
|
169
241
|
}
|
|
170
|
-
log_action("TYPE", &format!("Typing
|
|
242
|
+
log_action("TYPE", &format!("Typing into '{selector}'"));
|
|
171
243
|
ensure_page_open(config).await?;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
244
|
+
|
|
245
|
+
// Focus the element by clicking it
|
|
246
|
+
let _ = click(config, selector).await;
|
|
247
|
+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
|
248
|
+
|
|
249
|
+
// Native keyboard input
|
|
250
|
+
type_text_native(config, text).await
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Public API — native mouse control
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/// Move the real browser mouse cursor to (x, y) via CDP Input.dispatchMouseEvent.
|
|
258
|
+
/// Also ensures overlay exists and updates cursor position.
|
|
259
|
+
pub async fn mouse_move(config: &MintConfig, x: f64, y: f64) -> Result<String, String> {
|
|
260
|
+
log_action("MOUSE_MOVE", &format!("Moving mouse to ({x:.0},{y:.0})"));
|
|
261
|
+
ensure_page_open(config).await?;
|
|
262
|
+
|
|
263
|
+
// Ensure overlay exists
|
|
264
|
+
inject_overlay(config).await;
|
|
265
|
+
|
|
266
|
+
match cdp_call_raw(
|
|
267
|
+
config,
|
|
268
|
+
"Input.dispatchMouseEvent",
|
|
269
|
+
json!({
|
|
270
|
+
"type": "mouseMoved",
|
|
271
|
+
"x": x,
|
|
272
|
+
"y": y,
|
|
273
|
+
"button": "none",
|
|
274
|
+
"buttons": 0,
|
|
275
|
+
"clickCount": 0,
|
|
276
|
+
"modifiers": 0
|
|
277
|
+
}),
|
|
278
|
+
)
|
|
279
|
+
.await
|
|
280
|
+
{
|
|
281
|
+
Ok(_) => {
|
|
282
|
+
// Update the visual cursor overlay position
|
|
283
|
+
let cursor_script = format!(
|
|
284
|
+
r#"(() => {{
|
|
285
|
+
const c = document.getElementById('mint-cursor-overlay');
|
|
286
|
+
if (c) {{ c.style.left = '{x}px'; c.style.top = '{y}px'; }}
|
|
287
|
+
}})()"#
|
|
288
|
+
);
|
|
289
|
+
let _ = cdp_call_raw(
|
|
290
|
+
config,
|
|
291
|
+
"Runtime.evaluate",
|
|
292
|
+
json!({ "expression": cursor_script, "returnByValue": false }),
|
|
293
|
+
)
|
|
294
|
+
.await;
|
|
295
|
+
log_action("MOUSE_MOVE_SUCCESS", &format!("Moved to ({x:.0},{y:.0})"));
|
|
296
|
+
Ok(format!("mouse moved to ({x:.0},{y:.0})"))
|
|
297
|
+
}
|
|
298
|
+
Err(e) => {
|
|
299
|
+
log_action("MOUSE_MOVE_ERROR", &format!("Failed: {e}"));
|
|
300
|
+
Err(e)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Click at absolute screen coordinates (x, y) using native CDP mouse events.
|
|
306
|
+
/// Dispatches mouseMoved → mousePressed → mouseReleased with animation feedback.
|
|
307
|
+
pub async fn mouse_click(
|
|
308
|
+
config: &MintConfig,
|
|
309
|
+
x: f64,
|
|
310
|
+
y: f64,
|
|
311
|
+
button: &str,
|
|
312
|
+
) -> Result<String, String> {
|
|
313
|
+
log_action(
|
|
314
|
+
"MOUSE_CLICK",
|
|
315
|
+
&format!("Clicking ({x:.0},{y:.0}) btn={button}"),
|
|
316
|
+
);
|
|
317
|
+
ensure_page_open(config).await?;
|
|
318
|
+
|
|
319
|
+
let button_str = match button {
|
|
320
|
+
"right" => "right",
|
|
321
|
+
"middle" => "middle",
|
|
322
|
+
_ => "left",
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Move cursor to target first
|
|
326
|
+
let _ = mouse_move(config, x, y).await;
|
|
327
|
+
tokio::time::sleep(std::time::Duration::from_millis(60)).await;
|
|
328
|
+
|
|
329
|
+
// Cursor click animation
|
|
330
|
+
let anim = r#"(() => {
|
|
331
|
+
const c = document.getElementById('mint-cursor-overlay');
|
|
332
|
+
if (c) {
|
|
333
|
+
c.style.transition = 'transform 0.08s ease, opacity 0.08s ease';
|
|
334
|
+
c.style.transform = 'scale(0.82)';
|
|
335
|
+
c.style.opacity = '0.65';
|
|
336
|
+
setTimeout(() => { c.style.transform = 'scale(1)'; c.style.opacity = '1'; }, 120);
|
|
337
|
+
}
|
|
338
|
+
})()"#;
|
|
339
|
+
let _ = cdp_call_raw(
|
|
175
340
|
config,
|
|
176
341
|
"Runtime.evaluate",
|
|
342
|
+
json!({ "expression": anim, "returnByValue": false }),
|
|
343
|
+
)
|
|
344
|
+
.await;
|
|
345
|
+
|
|
346
|
+
// mousePressed
|
|
347
|
+
cdp_call_raw(
|
|
348
|
+
config,
|
|
349
|
+
"Input.dispatchMouseEvent",
|
|
177
350
|
json!({
|
|
178
|
-
"
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
351
|
+
"type": "mousePressed",
|
|
352
|
+
"x": x, "y": y,
|
|
353
|
+
"button": button_str,
|
|
354
|
+
"buttons": 1,
|
|
355
|
+
"clickCount": 1,
|
|
356
|
+
"modifiers": 0
|
|
357
|
+
}),
|
|
358
|
+
)
|
|
359
|
+
.await
|
|
360
|
+
.map_err(|e| {
|
|
361
|
+
log_action("MOUSE_CLICK_ERROR", &format!("mousePressed: {e}"));
|
|
362
|
+
e
|
|
363
|
+
})?;
|
|
364
|
+
|
|
365
|
+
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
|
366
|
+
|
|
367
|
+
// mouseReleased
|
|
368
|
+
cdp_call_raw(
|
|
369
|
+
config,
|
|
370
|
+
"Input.dispatchMouseEvent",
|
|
371
|
+
json!({
|
|
372
|
+
"type": "mouseReleased",
|
|
373
|
+
"x": x, "y": y,
|
|
374
|
+
"button": button_str,
|
|
375
|
+
"buttons": 0,
|
|
376
|
+
"clickCount": 1,
|
|
377
|
+
"modifiers": 0
|
|
378
|
+
}),
|
|
379
|
+
)
|
|
380
|
+
.await
|
|
381
|
+
.map_err(|e| {
|
|
382
|
+
log_action("MOUSE_CLICK_ERROR", &format!("mouseReleased: {e}"));
|
|
383
|
+
e
|
|
384
|
+
})?;
|
|
385
|
+
|
|
386
|
+
log_action(
|
|
387
|
+
"MOUSE_CLICK_SUCCESS",
|
|
388
|
+
&format!("Clicked ({x:.0},{y:.0}) {button_str}"),
|
|
389
|
+
);
|
|
390
|
+
Ok(format!(
|
|
391
|
+
"clicked at ({x:.0},{y:.0}) with {button_str} button"
|
|
392
|
+
))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Public API — native keyboard control
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/// Type text using CDP Input.insertText (native, supports any Unicode text).
|
|
400
|
+
pub async fn type_text_native(config: &MintConfig, text: &str) -> Result<String, String> {
|
|
401
|
+
log_action(
|
|
402
|
+
"TYPE",
|
|
403
|
+
&format!("Native typing: '{}'", &text[..text.len().min(60)]),
|
|
404
|
+
);
|
|
405
|
+
ensure_page_open(config).await?;
|
|
406
|
+
|
|
407
|
+
match cdp_call_raw(config, "Input.insertText", json!({ "text": text })).await {
|
|
408
|
+
Ok(_) => {
|
|
409
|
+
log_action(
|
|
410
|
+
"TYPE_SUCCESS",
|
|
411
|
+
&format!("Typed {} characters natively", text.len()),
|
|
412
|
+
);
|
|
413
|
+
Ok(format!("typed {} characters", text.len()))
|
|
414
|
+
}
|
|
415
|
+
Err(e) => {
|
|
416
|
+
log_action("TYPE_ERROR", &format!("Native type failed: {e}"));
|
|
417
|
+
Err(e)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// Press a single key using CDP Input.dispatchKeyEvent.
|
|
423
|
+
///
|
|
424
|
+
/// Supported special keys: Enter, Tab, Escape, Backspace, Delete,
|
|
425
|
+
/// ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End,
|
|
426
|
+
/// PageUp, PageDown, Space, F1–F12.
|
|
427
|
+
/// Also accepts single printable characters.
|
|
428
|
+
pub async fn key_press(config: &MintConfig, key: &str) -> Result<String, String> {
|
|
429
|
+
log_action("KEY_PRESS", &format!("Pressing key '{key}'"));
|
|
430
|
+
ensure_page_open(config).await?;
|
|
431
|
+
|
|
432
|
+
let (key_code, code) = key_to_cdp_params(key);
|
|
433
|
+
|
|
434
|
+
// keyDown
|
|
435
|
+
cdp_call_raw(
|
|
436
|
+
config,
|
|
437
|
+
"Input.dispatchKeyEvent",
|
|
438
|
+
json!({
|
|
439
|
+
"type": "keyDown",
|
|
440
|
+
"key": key,
|
|
441
|
+
"code": code,
|
|
442
|
+
"windowsVirtualKeyCode": key_code,
|
|
443
|
+
"nativeVirtualKeyCode": key_code,
|
|
444
|
+
"modifiers": 0,
|
|
445
|
+
"isSystemKey": false,
|
|
446
|
+
"location": 0
|
|
447
|
+
}),
|
|
448
|
+
)
|
|
449
|
+
.await
|
|
450
|
+
.map_err(|e| {
|
|
451
|
+
log_action("KEY_PRESS_ERROR", &format!("keyDown failed: {e}"));
|
|
452
|
+
e
|
|
453
|
+
})?;
|
|
454
|
+
|
|
455
|
+
// char event for printable single characters
|
|
456
|
+
if key.chars().count() == 1 && !key.chars().next().map(|c| c.is_control()).unwrap_or(true) {
|
|
457
|
+
let _ = cdp_call_raw(
|
|
458
|
+
config,
|
|
459
|
+
"Input.dispatchKeyEvent",
|
|
460
|
+
json!({
|
|
461
|
+
"type": "char",
|
|
462
|
+
"key": key,
|
|
463
|
+
"code": code,
|
|
464
|
+
"windowsVirtualKeyCode": key_code,
|
|
465
|
+
"modifiers": 0
|
|
466
|
+
}),
|
|
467
|
+
)
|
|
468
|
+
.await;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
|
|
472
|
+
|
|
473
|
+
// keyUp
|
|
474
|
+
cdp_call_raw(
|
|
475
|
+
config,
|
|
476
|
+
"Input.dispatchKeyEvent",
|
|
477
|
+
json!({
|
|
478
|
+
"type": "keyUp",
|
|
479
|
+
"key": key,
|
|
480
|
+
"code": code,
|
|
481
|
+
"windowsVirtualKeyCode": key_code,
|
|
482
|
+
"nativeVirtualKeyCode": key_code,
|
|
483
|
+
"modifiers": 0,
|
|
484
|
+
"isSystemKey": false,
|
|
485
|
+
"location": 0
|
|
486
|
+
}),
|
|
487
|
+
)
|
|
488
|
+
.await
|
|
489
|
+
.map_err(|e| {
|
|
490
|
+
log_action("KEY_PRESS_ERROR", &format!("keyUp failed: {e}"));
|
|
491
|
+
e
|
|
492
|
+
})?;
|
|
493
|
+
|
|
494
|
+
log_action("KEY_PRESS_SUCCESS", &format!("Pressed '{key}'"));
|
|
495
|
+
Ok(format!("pressed key '{key}'"))
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
// Public API — screenshot
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
/// Capture a PNG screenshot of the current browser page.
|
|
503
|
+
/// Returns base64-encoded PNG string.
|
|
504
|
+
pub async fn screenshot(config: &MintConfig) -> Result<String, String> {
|
|
505
|
+
log_action("SCREENSHOT", "Capturing screenshot");
|
|
506
|
+
ensure_page_open(config).await?;
|
|
507
|
+
|
|
508
|
+
match cdp_call_raw(
|
|
509
|
+
config,
|
|
510
|
+
"Page.captureScreenshot",
|
|
511
|
+
json!({
|
|
512
|
+
"format": "png",
|
|
513
|
+
"fromSurface": true,
|
|
514
|
+
"captureBeyondViewport": false
|
|
189
515
|
}),
|
|
190
516
|
)
|
|
191
517
|
.await
|
|
192
518
|
{
|
|
193
|
-
Ok(response) =>
|
|
194
|
-
Some("
|
|
519
|
+
Ok(response) => {
|
|
520
|
+
if let Some(data) = response["result"]["data"].as_str() {
|
|
195
521
|
log_action(
|
|
196
|
-
"
|
|
197
|
-
&format!("
|
|
522
|
+
"SCREENSHOT_SUCCESS",
|
|
523
|
+
&format!("Captured {} base64 bytes", data.len()),
|
|
198
524
|
);
|
|
199
|
-
Ok(
|
|
200
|
-
}
|
|
201
|
-
Some("not-found") => {
|
|
202
|
-
let err = format!("browser selector not found: {selector}");
|
|
203
|
-
log_action("TYPE_ERROR", &err);
|
|
204
|
-
Err(err)
|
|
205
|
-
}
|
|
206
|
-
_ => {
|
|
525
|
+
Ok(data.to_owned())
|
|
526
|
+
} else {
|
|
207
527
|
let err = response_error(&response);
|
|
208
|
-
log_action("
|
|
528
|
+
log_action("SCREENSHOT_ERROR", &format!("Failed: {err}"));
|
|
209
529
|
Err(err)
|
|
210
530
|
}
|
|
211
|
-
}
|
|
531
|
+
}
|
|
212
532
|
Err(e) => {
|
|
213
|
-
log_action("
|
|
533
|
+
log_action("SCREENSHOT_ERROR", &format!("Websocket error: {e}"));
|
|
214
534
|
Err(e)
|
|
215
535
|
}
|
|
216
536
|
}
|
|
217
537
|
}
|
|
218
538
|
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
// Public API — element coordinates
|
|
541
|
+
// ---------------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
/// Get the viewport-relative center (x, y) of an element by CSS selector.
|
|
544
|
+
/// Supports text=, contains=, xpath= prefixes in addition to CSS selectors.
|
|
545
|
+
pub async fn get_element_coordinates(
|
|
546
|
+
config: &MintConfig,
|
|
547
|
+
selector: &str,
|
|
548
|
+
) -> Result<(f64, f64), String> {
|
|
549
|
+
let find_expr = selector_to_js_find(selector);
|
|
550
|
+
let expression = format!(
|
|
551
|
+
r#"(() => {{
|
|
552
|
+
{find_expr}
|
|
553
|
+
if (!el) return null;
|
|
554
|
+
const r = el.getBoundingClientRect();
|
|
555
|
+
return JSON.stringify({{ x: r.left + r.width / 2, y: r.top + r.height / 2 }});
|
|
556
|
+
}})()"#
|
|
557
|
+
);
|
|
558
|
+
match cdp_call_raw(
|
|
559
|
+
config,
|
|
560
|
+
"Runtime.evaluate",
|
|
561
|
+
json!({ "expression": expression, "returnByValue": true }),
|
|
562
|
+
)
|
|
563
|
+
.await
|
|
564
|
+
{
|
|
565
|
+
Ok(response) => {
|
|
566
|
+
let val = &response["result"]["result"];
|
|
567
|
+
if val["type"] == "null" || val["value"].is_null() {
|
|
568
|
+
return Err(format!("element not found: {selector}"));
|
|
569
|
+
}
|
|
570
|
+
if let Some(val_str) = val["value"].as_str() {
|
|
571
|
+
let parsed: Value = serde_json::from_str(val_str).map_err(|e| e.to_string())?;
|
|
572
|
+
let x = parsed["x"].as_f64().ok_or("missing x")?;
|
|
573
|
+
let y = parsed["y"].as_f64().ok_or("missing y")?;
|
|
574
|
+
Ok((x, y))
|
|
575
|
+
} else {
|
|
576
|
+
Err(response_error(&response))
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
Err(e) => Err(e),
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/// Convert a selector (CSS / text= / contains= / xpath=) into a JS let statement:
|
|
584
|
+
/// `let el = <expression>;`
|
|
585
|
+
fn selector_to_js_find(selector: &str) -> String {
|
|
586
|
+
if let Some(text) = selector.strip_prefix("text=") {
|
|
587
|
+
// Exact text match (trimmed)
|
|
588
|
+
let escaped = text.replace('\\', "\\\\").replace('`', "\\`");
|
|
589
|
+
format!(
|
|
590
|
+
"const el = Array.from(document.querySelectorAll('*')).find(\
|
|
591
|
+
e => e.childElementCount === 0 && e.textContent.trim() === `{escaped}`) \
|
|
592
|
+
|| Array.from(document.querySelectorAll('*')).find(\
|
|
593
|
+
e => e.textContent.trim() === `{escaped}`);"
|
|
594
|
+
)
|
|
595
|
+
} else if let Some(text) = selector.strip_prefix("contains=") {
|
|
596
|
+
// Partial text match
|
|
597
|
+
let escaped = text.replace('\\', "\\\\").replace('`', "\\`");
|
|
598
|
+
format!(
|
|
599
|
+
"const el = Array.from(document.querySelectorAll('*')).find(\
|
|
600
|
+
e => e.childElementCount === 0 && e.textContent.includes(`{escaped}`)) \
|
|
601
|
+
|| Array.from(document.querySelectorAll('*')).find(\
|
|
602
|
+
e => e.textContent.includes(`{escaped}`));"
|
|
603
|
+
)
|
|
604
|
+
} else if let Some(xpath) = selector.strip_prefix("xpath=") {
|
|
605
|
+
let escaped = xpath.replace('\\', "\\\\").replace('"', "\\\"");
|
|
606
|
+
format!(
|
|
607
|
+
"const el = document.evaluate(`{escaped}`, document, null, \
|
|
608
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;"
|
|
609
|
+
)
|
|
610
|
+
} else {
|
|
611
|
+
// Standard CSS selector — wrap in try/catch so invalid selectors don't throw
|
|
612
|
+
let escaped = selector.replace('\\', "\\\\").replace('`', "\\`");
|
|
613
|
+
format!(
|
|
614
|
+
"let el; try {{ el = document.querySelector(`{escaped}`); }} \
|
|
615
|
+
catch(e) {{ \
|
|
616
|
+
el = Array.from(document.querySelectorAll('*')).find(\
|
|
617
|
+
e => e.textContent.trim() === `{escaped}`); \
|
|
618
|
+
}}"
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// Inject the green aura + animated cursor overlay into the current page.
|
|
624
|
+
/// Best-effort: errors are silently ignored.
|
|
625
|
+
async fn inject_overlay(config: &MintConfig) {
|
|
626
|
+
let script = build_overlay_script();
|
|
627
|
+
let _ = cdp_call_raw(
|
|
628
|
+
config,
|
|
629
|
+
"Runtime.evaluate",
|
|
630
|
+
json!({ "expression": script, "returnByValue": false }),
|
|
631
|
+
)
|
|
632
|
+
.await;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
// Browser lifecycle
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
|
|
219
639
|
pub async fn is_browser_running(config: &MintConfig) -> bool {
|
|
220
640
|
let endpoint = config
|
|
221
641
|
.extra
|
|
@@ -256,7 +676,7 @@ pub async fn spawn_automation_browser(config: &MintConfig) -> Result<(), String>
|
|
|
256
676
|
];
|
|
257
677
|
|
|
258
678
|
let mut spawned = false;
|
|
259
|
-
let executables = if cfg!(target_os = "windows") {
|
|
679
|
+
let executables: Vec<&str> = if cfg!(target_os = "windows") {
|
|
260
680
|
vec!["chrome.exe", "chromium.exe", "msedge.exe"]
|
|
261
681
|
} else if cfg!(target_os = "macos") {
|
|
262
682
|
vec![
|
|
@@ -290,7 +710,8 @@ pub async fn spawn_automation_browser(config: &MintConfig) -> Result<(), String>
|
|
|
290
710
|
|
|
291
711
|
if !spawned {
|
|
292
712
|
return Err(format!(
|
|
293
|
-
"Could not find or spawn browser '{browser_name}' with remote debugging.
|
|
713
|
+
"Could not find or spawn browser '{browser_name}' with remote debugging. \
|
|
714
|
+
Please verify it is installed."
|
|
294
715
|
));
|
|
295
716
|
}
|
|
296
717
|
|
|
@@ -328,6 +749,68 @@ pub async fn ensure_page_open(config: &MintConfig) -> Result<(), String> {
|
|
|
328
749
|
Ok(())
|
|
329
750
|
}
|
|
330
751
|
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
// Overlay injection script
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
|
|
756
|
+
fn build_overlay_script() -> &'static str {
|
|
757
|
+
r##"
|
|
758
|
+
(() => {
|
|
759
|
+
// ── Green aura border ────────────────────────────────────────────────
|
|
760
|
+
if (!document.getElementById('mint-browser-aura')) {
|
|
761
|
+
const style = document.createElement('style');
|
|
762
|
+
style.id = 'mint-browser-aura-style';
|
|
763
|
+
style.textContent = `
|
|
764
|
+
@keyframes mint-pulse {
|
|
765
|
+
0% { box-shadow: inset 0 0 15px rgba(16,185,129,0.4); border-color: rgba(16,185,129,0.6); }
|
|
766
|
+
100% { box-shadow: inset 0 0 30px rgba(16,185,129,0.8); border-color: rgba(16,185,129,1); }
|
|
767
|
+
}
|
|
768
|
+
`;
|
|
769
|
+
document.head.appendChild(style);
|
|
770
|
+
|
|
771
|
+
const aura = document.createElement('div');
|
|
772
|
+
aura.id = 'mint-browser-aura';
|
|
773
|
+
aura.style.cssText = `
|
|
774
|
+
position:fixed; top:0; left:0; width:100vw; height:100vh;
|
|
775
|
+
border:5px solid rgba(16,185,129,0.6);
|
|
776
|
+
pointer-events:none; z-index:2147483646;
|
|
777
|
+
box-sizing:border-box;
|
|
778
|
+
animation:mint-pulse 1.5s infinite alternate;
|
|
779
|
+
`;
|
|
780
|
+
document.body.appendChild(aura);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ── Animated mouse cursor overlay ─────────────────────────────────────
|
|
784
|
+
if (!document.getElementById('mint-cursor-overlay')) {
|
|
785
|
+
const cursor = document.createElement('div');
|
|
786
|
+
cursor.id = 'mint-cursor-overlay';
|
|
787
|
+
cursor.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
|
|
788
|
+
<defs>
|
|
789
|
+
<filter id="mint-cs" x="-30%" y="-30%" width="160%" height="160%">
|
|
790
|
+
<feDropShadow dx="1" dy="2" stdDeviation="2.5" flood-color="rgba(0,0,0,0.5)"/>
|
|
791
|
+
</filter>
|
|
792
|
+
</defs>
|
|
793
|
+
<path d="M5 2 L5 22 L9.5 17 L13.5 24.5 L16 23 L12 15.5 L19 15.5 Z"
|
|
794
|
+
fill="white" stroke="#10b981" stroke-width="1.6"
|
|
795
|
+
stroke-linejoin="round" filter="url(#mint-cs)"/>
|
|
796
|
+
</svg>`;
|
|
797
|
+
cursor.style.cssText = `
|
|
798
|
+
position:fixed; left:0; top:0; pointer-events:none;
|
|
799
|
+
z-index:2147483647;
|
|
800
|
+
transition:left 0.06s ease-out, top 0.06s ease-out, transform 0.08s ease, opacity 0.08s ease;
|
|
801
|
+
transform-origin:5px 2px;
|
|
802
|
+
`;
|
|
803
|
+
document.body.appendChild(cursor);
|
|
804
|
+
}
|
|
805
|
+
})()
|
|
806
|
+
"##
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ---------------------------------------------------------------------------
|
|
810
|
+
// CDP helpers
|
|
811
|
+
// ---------------------------------------------------------------------------
|
|
812
|
+
|
|
813
|
+
/// Full CDP call — injects the aura+cursor overlay before the actual call.
|
|
331
814
|
async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Value, String> {
|
|
332
815
|
let page = fetch_pages(config)
|
|
333
816
|
.await?
|
|
@@ -339,55 +822,22 @@ async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Va
|
|
|
339
822
|
.ok_or("Chrome DevTools page does not expose a websocket URL")?;
|
|
340
823
|
let (mut socket, _) = connect_async(socket_url)
|
|
341
824
|
.await
|
|
342
|
-
.map_err(|
|
|
343
|
-
|
|
825
|
+
.map_err(|e| format!("unable to connect to Chrome DevTools websocket: {e}"))?;
|
|
826
|
+
|
|
827
|
+
let is_overlay = method == "Runtime.evaluate"
|
|
344
828
|
&& params["expression"]
|
|
345
829
|
.as_str()
|
|
346
830
|
.map(|s| s.contains("mint-browser-aura"))
|
|
347
831
|
.unwrap_or(false);
|
|
348
832
|
|
|
349
|
-
if !
|
|
350
|
-
let
|
|
351
|
-
(() => {
|
|
352
|
-
if (document.getElementById('mint-browser-aura')) return;
|
|
353
|
-
const aura = document.createElement('div');
|
|
354
|
-
aura.id = 'mint-browser-aura';
|
|
355
|
-
|
|
356
|
-
const style = document.createElement('style');
|
|
357
|
-
style.id = 'mint-browser-aura-style';
|
|
358
|
-
style.textContent = `
|
|
359
|
-
@keyframes mint-pulse {
|
|
360
|
-
0% { box-shadow: inset 0 0 15px rgba(16, 185, 129, 0.4); border-color: rgba(16, 185, 129, 0.6); }
|
|
361
|
-
100% { box-shadow: inset 0 0 30px rgba(16, 185, 129, 0.8); border-color: rgba(16, 185, 129, 1); }
|
|
362
|
-
}
|
|
363
|
-
`;
|
|
364
|
-
document.head.appendChild(style);
|
|
365
|
-
|
|
366
|
-
aura.style.cssText = `
|
|
367
|
-
position: fixed;
|
|
368
|
-
top: 0;
|
|
369
|
-
left: 0;
|
|
370
|
-
width: 100vw;
|
|
371
|
-
height: 100vh;
|
|
372
|
-
border: 5px solid rgba(16, 185, 129, 0.6);
|
|
373
|
-
pointer-events: none;
|
|
374
|
-
z-index: 2147483647;
|
|
375
|
-
box-sizing: border-box;
|
|
376
|
-
animation: mint-pulse 1.5s infinite alternate;
|
|
377
|
-
`;
|
|
378
|
-
document.body.appendChild(aura);
|
|
379
|
-
})()
|
|
380
|
-
"#;
|
|
381
|
-
|
|
833
|
+
if !is_overlay {
|
|
834
|
+
let overlay = build_overlay_script();
|
|
382
835
|
let _ = socket
|
|
383
836
|
.send(Message::Text(
|
|
384
837
|
json!({
|
|
385
838
|
"id": 999,
|
|
386
839
|
"method": "Runtime.evaluate",
|
|
387
|
-
"params": {
|
|
388
|
-
"expression": aura_script,
|
|
389
|
-
"returnByValue": true
|
|
390
|
-
}
|
|
840
|
+
"params": { "expression": overlay, "returnByValue": false }
|
|
391
841
|
})
|
|
392
842
|
.to_string()
|
|
393
843
|
.into(),
|
|
@@ -402,13 +852,50 @@ async fn cdp_call(config: &MintConfig, method: &str, params: Value) -> Result<Va
|
|
|
402
852
|
.into(),
|
|
403
853
|
))
|
|
404
854
|
.await
|
|
405
|
-
.map_err(|
|
|
855
|
+
.map_err(|e| e.to_string())?;
|
|
856
|
+
|
|
406
857
|
while let Some(message) = socket.next().await {
|
|
407
|
-
let message = message.map_err(|
|
|
858
|
+
let message = message.map_err(|e| e.to_string())?;
|
|
408
859
|
let Message::Text(raw) = message else {
|
|
409
860
|
continue;
|
|
410
861
|
};
|
|
411
|
-
let value: Value = serde_json::from_str(&raw).map_err(|
|
|
862
|
+
let value: Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
|
|
863
|
+
if value["id"] == 1 {
|
|
864
|
+
return Ok(value);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
Err("Chrome DevTools websocket closed before returning a response".into())
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/// Lightweight CDP call — no overlay injection. Used for Input.* and Page.captureScreenshot.
|
|
871
|
+
async fn cdp_call_raw(config: &MintConfig, method: &str, params: Value) -> Result<Value, String> {
|
|
872
|
+
let page = fetch_pages(config)
|
|
873
|
+
.await?
|
|
874
|
+
.into_iter()
|
|
875
|
+
.find(|page| page["type"] == "page")
|
|
876
|
+
.ok_or("Chrome DevTools did not report an open browser page")?;
|
|
877
|
+
let socket_url = page["webSocketDebuggerUrl"]
|
|
878
|
+
.as_str()
|
|
879
|
+
.ok_or("Chrome DevTools page does not expose a websocket URL")?;
|
|
880
|
+
let (mut socket, _) = connect_async(socket_url)
|
|
881
|
+
.await
|
|
882
|
+
.map_err(|e| format!("unable to connect to Chrome DevTools websocket: {e}"))?;
|
|
883
|
+
|
|
884
|
+
socket
|
|
885
|
+
.send(Message::Text(
|
|
886
|
+
json!({ "id": 1, "method": method, "params": params })
|
|
887
|
+
.to_string()
|
|
888
|
+
.into(),
|
|
889
|
+
))
|
|
890
|
+
.await
|
|
891
|
+
.map_err(|e| e.to_string())?;
|
|
892
|
+
|
|
893
|
+
while let Some(message) = socket.next().await {
|
|
894
|
+
let message = message.map_err(|e| e.to_string())?;
|
|
895
|
+
let Message::Text(raw) = message else {
|
|
896
|
+
continue;
|
|
897
|
+
};
|
|
898
|
+
let value: Value = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
|
|
412
899
|
if value["id"] == 1 {
|
|
413
900
|
return Ok(value);
|
|
414
901
|
}
|
|
@@ -436,12 +923,13 @@ async fn fetch_pages_endpoint(endpoint: &str) -> Result<Vec<Value>, String> {
|
|
|
436
923
|
.get(endpoint)
|
|
437
924
|
.send()
|
|
438
925
|
.await
|
|
439
|
-
.map_err(|
|
|
926
|
+
.map_err(|e| format!("unable to reach Chrome DevTools at {endpoint}: {e}"))?
|
|
440
927
|
.error_for_status()
|
|
441
|
-
.map_err(|
|
|
928
|
+
.map_err(|e| e.to_string())?
|
|
442
929
|
.json()
|
|
443
930
|
.await
|
|
444
|
-
.map_err(|
|
|
931
|
+
.map_err(|e| e.to_string())?;
|
|
932
|
+
|
|
445
933
|
value
|
|
446
934
|
.as_array()
|
|
447
935
|
.cloned()
|
|
@@ -454,3 +942,43 @@ fn response_error(response: &Value) -> String {
|
|
|
454
942
|
.unwrap_or("Chrome DevTools returned an unexpected response")
|
|
455
943
|
.to_owned()
|
|
456
944
|
}
|
|
945
|
+
|
|
946
|
+
// ---------------------------------------------------------------------------
|
|
947
|
+
// Key-to-CDP mapping
|
|
948
|
+
// ---------------------------------------------------------------------------
|
|
949
|
+
|
|
950
|
+
/// Returns (windowsVirtualKeyCode, code string) for a key name.
|
|
951
|
+
fn key_to_cdp_params(key: &str) -> (i32, &'static str) {
|
|
952
|
+
match key {
|
|
953
|
+
"Enter" | "\n" | "\r" => (13, "Enter"),
|
|
954
|
+
"Tab" | "\t" => (9, "Tab"),
|
|
955
|
+
"Escape" | "Esc" => (27, "Escape"),
|
|
956
|
+
"Backspace" => (8, "Backspace"),
|
|
957
|
+
"Delete" | "Del" => (46, "Delete"),
|
|
958
|
+
"ArrowUp" | "Up" => (38, "ArrowUp"),
|
|
959
|
+
"ArrowDown" | "Down" => (40, "ArrowDown"),
|
|
960
|
+
"ArrowLeft" | "Left" => (37, "ArrowLeft"),
|
|
961
|
+
"ArrowRight" | "Right" => (39, "ArrowRight"),
|
|
962
|
+
"Home" => (36, "Home"),
|
|
963
|
+
"End" => (35, "End"),
|
|
964
|
+
"PageUp" => (33, "PageUp"),
|
|
965
|
+
"PageDown" => (34, "PageDown"),
|
|
966
|
+
"Space" | " " => (32, "Space"),
|
|
967
|
+
"F1" => (112, "F1"),
|
|
968
|
+
"F2" => (113, "F2"),
|
|
969
|
+
"F3" => (114, "F3"),
|
|
970
|
+
"F4" => (115, "F4"),
|
|
971
|
+
"F5" => (116, "F5"),
|
|
972
|
+
"F6" => (117, "F6"),
|
|
973
|
+
"F7" => (118, "F7"),
|
|
974
|
+
"F8" => (119, "F8"),
|
|
975
|
+
"F9" => (120, "F9"),
|
|
976
|
+
"F10" => (121, "F10"),
|
|
977
|
+
"F11" => (122, "F11"),
|
|
978
|
+
"F12" => (123, "F12"),
|
|
979
|
+
_ => {
|
|
980
|
+
let code = key.chars().next().map(|c| c as i32).unwrap_or(0);
|
|
981
|
+
(code, "Unidentified")
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|