@mediar-ai/terminator 0.20.6

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/src/desktop.rs ADDED
@@ -0,0 +1,520 @@
1
+ use crate::types::{Monitor, MonitorScreenshotPair};
2
+ use crate::Selector;
3
+ use crate::{
4
+ map_error, CommandOutput, Element, Locator, ScreenshotResult, TreeBuildConfig, UINode,
5
+ };
6
+ use napi::bindgen_prelude::Either;
7
+ use napi_derive::napi;
8
+ use std::sync::Once;
9
+ use terminator::Desktop as TerminatorDesktop;
10
+
11
+ /// Main entry point for desktop automation.
12
+ #[napi(js_name = "Desktop")]
13
+ pub struct Desktop {
14
+ inner: TerminatorDesktop,
15
+ }
16
+
17
+ #[allow(clippy::needless_pass_by_value)]
18
+ #[napi]
19
+ impl Desktop {
20
+ /// Create a new Desktop automation instance with configurable options.
21
+ ///
22
+ /// @param {boolean} [useBackgroundApps=false] - Enable background apps support.
23
+ /// @param {boolean} [activateApp=false] - Enable app activation support.
24
+ /// @param {string} [logLevel] - Logging level (e.g., 'info', 'debug', 'warn', 'error').
25
+ /// @returns {Desktop} A new Desktop automation instance.
26
+ #[napi(constructor)]
27
+ pub fn new(
28
+ use_background_apps: Option<bool>,
29
+ activate_app: Option<bool>,
30
+ log_level: Option<String>,
31
+ ) -> Self {
32
+ let use_background_apps = use_background_apps.unwrap_or(false);
33
+ let activate_app = activate_app.unwrap_or(false);
34
+ let log_level = log_level.unwrap_or_else(|| "warn".to_string());
35
+ static INIT: Once = Once::new();
36
+ INIT.call_once(|| {
37
+ let _ = tracing_subscriber::fmt()
38
+ .with_env_filter(log_level)
39
+ .with_ansi(false) // Disable ANSI color codes for cleaner output
40
+ .try_init();
41
+ });
42
+ let desktop = TerminatorDesktop::new(use_background_apps, activate_app)
43
+ .expect("Failed to create Desktop instance");
44
+ Desktop { inner: desktop }
45
+ }
46
+
47
+ /// Get the root UI element of the desktop.
48
+ ///
49
+ /// @returns {Element} The root UI element.
50
+ #[napi]
51
+ pub fn root(&self) -> Element {
52
+ let root = self.inner.root();
53
+ Element::from(root)
54
+ }
55
+
56
+ /// Get a list of all running applications.
57
+ ///
58
+ /// @returns {Array<Element>} List of application UI elements.
59
+ #[napi]
60
+ pub fn applications(&self) -> napi::Result<Vec<Element>> {
61
+ self.inner
62
+ .applications()
63
+ .map(|apps| apps.into_iter().map(Element::from).collect())
64
+ .map_err(map_error)
65
+ }
66
+
67
+ /// Get a running application by name.
68
+ ///
69
+ /// @param {string} name - The name of the application to find.
70
+ /// @returns {Element} The application UI element.
71
+ #[napi]
72
+ pub fn application(&self, name: String) -> napi::Result<Element> {
73
+ self.inner
74
+ .application(&name)
75
+ .map(Element::from)
76
+ .map_err(map_error)
77
+ }
78
+
79
+ /// Open an application by name.
80
+ ///
81
+ /// @param {string} name - The name of the application to open.
82
+ #[napi]
83
+ pub fn open_application(&self, name: String) -> napi::Result<Element> {
84
+ self.inner
85
+ .open_application(&name)
86
+ .map(Element::from)
87
+ .map_err(map_error)
88
+ }
89
+
90
+ /// Activate an application by name.
91
+ ///
92
+ /// @param {string} name - The name of the application to activate.
93
+ #[napi]
94
+ pub fn activate_application(&self, name: String) -> napi::Result<()> {
95
+ self.inner.activate_application(&name).map_err(map_error)
96
+ }
97
+
98
+ /// (async) Run a shell command.
99
+ ///
100
+ /// @param {string} [windowsCommand] - Command to run on Windows.
101
+ /// @param {string} [unixCommand] - Command to run on Unix.
102
+ /// @returns {Promise<CommandOutput>} The command output.
103
+ #[napi]
104
+ pub async fn run_command(
105
+ &self,
106
+ windows_command: Option<String>,
107
+ unix_command: Option<String>,
108
+ ) -> napi::Result<CommandOutput> {
109
+ self.inner
110
+ .run_command(windows_command.as_deref(), unix_command.as_deref())
111
+ .await
112
+ .map(|r| CommandOutput {
113
+ exit_status: r.exit_status,
114
+ stdout: r.stdout,
115
+ stderr: r.stderr,
116
+ })
117
+ .map_err(map_error)
118
+ }
119
+
120
+ /// (async) Execute a shell command using GitHub Actions-style syntax.
121
+ ///
122
+ /// @param {string} command - The command to run (can be single or multi-line).
123
+ /// @param {string} [shell] - Optional shell to use (defaults to PowerShell on Windows, bash on Unix).
124
+ /// @param {string} [workingDirectory] - Optional working directory for the command.
125
+ /// @returns {Promise<CommandOutput>} The command output.
126
+ #[napi]
127
+ pub async fn run(
128
+ &self,
129
+ command: String,
130
+ shell: Option<String>,
131
+ working_directory: Option<String>,
132
+ ) -> napi::Result<CommandOutput> {
133
+ self.inner
134
+ .run(
135
+ command.as_str(),
136
+ shell.as_deref(),
137
+ working_directory.as_deref(),
138
+ )
139
+ .await
140
+ .map(|r| CommandOutput {
141
+ exit_status: r.exit_status,
142
+ stdout: r.stdout,
143
+ stderr: r.stderr,
144
+ })
145
+ .map_err(map_error)
146
+ }
147
+
148
+ /// (async) Perform OCR on an image file.
149
+ ///
150
+ /// @param {string} imagePath - Path to the image file.
151
+ /// @returns {Promise<string>} The extracted text.
152
+ #[napi]
153
+ pub async fn ocr_image_path(&self, image_path: String) -> napi::Result<String> {
154
+ self.inner
155
+ .ocr_image_path(&image_path)
156
+ .await
157
+ .map_err(map_error)
158
+ }
159
+
160
+ /// (async) Perform OCR on a screenshot.
161
+ ///
162
+ /// @param {ScreenshotResult} screenshot - The screenshot to process.
163
+ /// @returns {Promise<string>} The extracted text.
164
+ #[napi]
165
+ pub async fn ocr_screenshot(&self, screenshot: ScreenshotResult) -> napi::Result<String> {
166
+ let rust_screenshot = terminator::ScreenshotResult {
167
+ image_data: screenshot.image_data,
168
+ width: screenshot.width,
169
+ height: screenshot.height,
170
+ monitor: screenshot.monitor.map(|m| terminator::Monitor {
171
+ id: m.id,
172
+ name: m.name,
173
+ is_primary: m.is_primary,
174
+ width: m.width,
175
+ height: m.height,
176
+ x: m.x,
177
+ y: m.y,
178
+ scale_factor: m.scale_factor,
179
+ work_area: None,
180
+ }),
181
+ };
182
+ self.inner
183
+ .ocr_screenshot(&rust_screenshot)
184
+ .await
185
+ .map_err(map_error)
186
+ }
187
+
188
+ /// (async) Get the currently focused browser window.
189
+ ///
190
+ /// @returns {Promise<Element>} The current browser window element.
191
+ #[napi]
192
+ pub async fn get_current_browser_window(&self) -> napi::Result<Element> {
193
+ self.inner
194
+ .get_current_browser_window()
195
+ .await
196
+ .map(Element::from)
197
+ .map_err(map_error)
198
+ }
199
+
200
+ /// Create a locator for finding UI elements.
201
+ ///
202
+ /// @param {string | Selector} selector - The selector.
203
+ /// @returns {Locator} A locator for finding elements.
204
+ #[napi]
205
+ pub fn locator(
206
+ &self,
207
+ #[napi(ts_arg_type = "string | Selector")] selector: Either<String, &Selector>,
208
+ ) -> napi::Result<Locator> {
209
+ use napi::bindgen_prelude::Either::*;
210
+ let sel_rust: terminator::selector::Selector = match selector {
211
+ A(sel_str) => sel_str.as_str().into(),
212
+ B(sel_obj) => sel_obj.inner.clone(),
213
+ };
214
+ let loc = self.inner.locator(sel_rust);
215
+ Ok(Locator::from(loc))
216
+ }
217
+
218
+ /// (async) Get the currently focused window.
219
+ ///
220
+ /// @returns {Promise<Element>} The current window element.
221
+ #[napi]
222
+ pub async fn get_current_window(&self) -> napi::Result<Element> {
223
+ self.inner
224
+ .get_current_window()
225
+ .await
226
+ .map(Element::from)
227
+ .map_err(map_error)
228
+ }
229
+
230
+ /// (async) Get the currently focused application.
231
+ ///
232
+ /// @returns {Promise<Element>} The current application element.
233
+ #[napi]
234
+ pub async fn get_current_application(&self) -> napi::Result<Element> {
235
+ self.inner
236
+ .get_current_application()
237
+ .await
238
+ .map(Element::from)
239
+ .map_err(map_error)
240
+ }
241
+
242
+ /// Get the currently focused element.
243
+ ///
244
+ /// @returns {Element} The focused element.
245
+ #[napi]
246
+ pub fn focused_element(&self) -> napi::Result<Element> {
247
+ self.inner
248
+ .focused_element()
249
+ .map(Element::from)
250
+ .map_err(map_error)
251
+ }
252
+
253
+ /// Open a URL in a browser.
254
+ ///
255
+ /// @param {string} url - The URL to open.
256
+ /// @param {string} [browser] - The browser to use. Can be "Default", "Chrome", "Firefox", "Edge", "Brave", "Opera", "Vivaldi", or a custom browser path.
257
+ #[napi]
258
+ pub fn open_url(&self, url: String, browser: Option<String>) -> napi::Result<Element> {
259
+ let browser_enum = browser.map(|b| match b.to_lowercase().as_str() {
260
+ "default" => terminator::Browser::Default,
261
+ "chrome" => terminator::Browser::Chrome,
262
+ "firefox" => terminator::Browser::Firefox,
263
+ "edge" => terminator::Browser::Edge,
264
+ "brave" => terminator::Browser::Brave,
265
+ "opera" => terminator::Browser::Opera,
266
+ "vivaldi" => terminator::Browser::Vivaldi,
267
+ custom => terminator::Browser::Custom(custom.to_string()),
268
+ });
269
+ self.inner
270
+ .open_url(&url, browser_enum)
271
+ .map(Element::from)
272
+ .map_err(map_error)
273
+ }
274
+
275
+ /// Open a file with its default application.
276
+ ///
277
+ /// @param {string} filePath - Path to the file to open.
278
+ #[napi]
279
+ pub fn open_file(&self, file_path: String) -> napi::Result<()> {
280
+ self.inner.open_file(&file_path).map_err(map_error)
281
+ }
282
+
283
+ /// Activate a browser window by title.
284
+ ///
285
+ /// @param {string} title - The window title to match.
286
+ #[napi]
287
+ pub fn activate_browser_window_by_title(&self, title: String) -> napi::Result<()> {
288
+ self.inner
289
+ .activate_browser_window_by_title(&title)
290
+ .map_err(map_error)
291
+ }
292
+
293
+ /// Get the UI tree for a window identified by process ID and optional title.
294
+ ///
295
+ /// @param {number} pid - Process ID of the target application.
296
+ /// @param {string} [title] - Optional window title filter.
297
+ /// @param {TreeBuildConfig} [config] - Optional configuration for tree building.
298
+ /// @returns {UINode} Complete UI tree starting from the identified window.
299
+ #[napi]
300
+ pub fn get_window_tree(
301
+ &self,
302
+ pid: u32,
303
+ title: Option<String>,
304
+ config: Option<TreeBuildConfig>,
305
+ ) -> napi::Result<UINode> {
306
+ let rust_config = config.map(|c| c.into());
307
+ self.inner
308
+ .get_window_tree(pid, title.as_deref(), rust_config)
309
+ .map(UINode::from)
310
+ .map_err(map_error)
311
+ }
312
+
313
+ // ============== NEW MONITOR METHODS ==============
314
+
315
+ /// (async) List all available monitors/displays.
316
+ ///
317
+ /// @returns {Promise<Array<Monitor>>} List of monitor information.
318
+ #[napi]
319
+ pub async fn list_monitors(&self) -> napi::Result<Vec<Monitor>> {
320
+ self.inner
321
+ .list_monitors()
322
+ .await
323
+ .map(|monitors| monitors.into_iter().map(Monitor::from).collect())
324
+ .map_err(map_error)
325
+ }
326
+
327
+ /// (async) Get the primary monitor.
328
+ ///
329
+ /// @returns {Promise<Monitor>} Primary monitor information.
330
+ #[napi]
331
+ pub async fn get_primary_monitor(&self) -> napi::Result<Monitor> {
332
+ self.inner
333
+ .get_primary_monitor()
334
+ .await
335
+ .map(Monitor::from)
336
+ .map_err(map_error)
337
+ }
338
+
339
+ /// (async) Get the monitor containing the currently focused window.
340
+ ///
341
+ /// @returns {Promise<Monitor>} Active monitor information.
342
+ #[napi]
343
+ pub async fn get_active_monitor(&self) -> napi::Result<Monitor> {
344
+ self.inner
345
+ .get_active_monitor()
346
+ .await
347
+ .map(Monitor::from)
348
+ .map_err(map_error)
349
+ }
350
+
351
+ /// (async) Get a monitor by its ID.
352
+ ///
353
+ /// @param {string} id - The monitor ID to find.
354
+ /// @returns {Promise<Monitor>} Monitor information.
355
+ #[napi]
356
+ pub async fn get_monitor_by_id(&self, id: String) -> napi::Result<Monitor> {
357
+ self.inner
358
+ .get_monitor_by_id(&id)
359
+ .await
360
+ .map(Monitor::from)
361
+ .map_err(map_error)
362
+ }
363
+
364
+ /// (async) Get a monitor by its name.
365
+ ///
366
+ /// @param {string} name - The monitor name to find.
367
+ /// @returns {Promise<Monitor>} Monitor information.
368
+ #[napi]
369
+ pub async fn get_monitor_by_name(&self, name: String) -> napi::Result<Monitor> {
370
+ self.inner
371
+ .get_monitor_by_name(&name)
372
+ .await
373
+ .map(Monitor::from)
374
+ .map_err(map_error)
375
+ }
376
+
377
+ /// (async) Capture a screenshot of a specific monitor.
378
+ ///
379
+ /// @param {Monitor} monitor - The monitor to capture.
380
+ /// @returns {Promise<ScreenshotResult>} The screenshot data.
381
+ #[napi]
382
+ pub async fn capture_monitor(&self, monitor: Monitor) -> napi::Result<ScreenshotResult> {
383
+ let rust_monitor = terminator::Monitor {
384
+ id: monitor.id,
385
+ name: monitor.name,
386
+ is_primary: monitor.is_primary,
387
+ width: monitor.width,
388
+ height: monitor.height,
389
+ x: monitor.x,
390
+ y: monitor.y,
391
+ scale_factor: monitor.scale_factor,
392
+ work_area: None,
393
+ };
394
+ self.inner
395
+ .capture_monitor(&rust_monitor)
396
+ .await
397
+ .map(|r| ScreenshotResult {
398
+ width: r.width,
399
+ height: r.height,
400
+ image_data: r.image_data,
401
+ monitor: r.monitor.map(Monitor::from),
402
+ })
403
+ .map_err(map_error)
404
+ }
405
+
406
+ /// (async) Capture screenshots of all monitors.
407
+ ///
408
+ /// @returns {Promise<Array<{monitor: Monitor, screenshot: ScreenshotResult}>>} Array of monitor and screenshot pairs.
409
+ #[napi]
410
+ pub async fn capture_all_monitors(&self) -> napi::Result<Vec<MonitorScreenshotPair>> {
411
+ self.inner
412
+ .capture_all_monitors()
413
+ .await
414
+ .map(|results| {
415
+ results
416
+ .into_iter()
417
+ .map(|(monitor, screenshot)| MonitorScreenshotPair {
418
+ monitor: Monitor::from(monitor),
419
+ screenshot: ScreenshotResult {
420
+ width: screenshot.width,
421
+ height: screenshot.height,
422
+ image_data: screenshot.image_data,
423
+ monitor: screenshot.monitor.map(Monitor::from),
424
+ },
425
+ })
426
+ .collect()
427
+ })
428
+ .map_err(map_error)
429
+ }
430
+
431
+ /// (async) Get all window elements for a given application name.
432
+ ///
433
+ /// @param {string} name - The name of the application whose windows will be retrieved.
434
+ /// @returns {Promise<Array<Element>>} A list of window elements belonging to the application.
435
+ #[napi]
436
+ pub async fn windows_for_application(&self, name: String) -> napi::Result<Vec<Element>> {
437
+ self.inner
438
+ .windows_for_application(&name)
439
+ .await
440
+ .map(|windows| windows.into_iter().map(Element::from).collect())
441
+ .map_err(map_error)
442
+ }
443
+
444
+ // ============== ADDITIONAL MISSING METHODS ==============
445
+
446
+ /// (async) Get the UI tree for all open applications in parallel.
447
+ ///
448
+ /// @returns {Promise<Array<UINode>>} List of UI trees for all applications.
449
+ #[napi]
450
+ pub async fn get_all_applications_tree(&self) -> napi::Result<Vec<UINode>> {
451
+ self.inner
452
+ .get_all_applications_tree()
453
+ .await
454
+ .map(|trees| trees.into_iter().map(UINode::from).collect())
455
+ .map_err(map_error)
456
+ }
457
+
458
+ /// (async) Press a key globally.
459
+ ///
460
+ /// @param {string} key - The key to press (e.g., "Enter", "Ctrl+C", "F1").
461
+ #[napi]
462
+ pub async fn press_key(&self, key: String) -> napi::Result<()> {
463
+ self.inner.press_key(&key).await.map_err(map_error)
464
+ }
465
+
466
+ /// (async) Execute JavaScript in the currently focused browser tab.
467
+ /// Automatically finds the active browser window and executes the script.
468
+ ///
469
+ /// @param {string} script - The JavaScript code to execute in browser context.
470
+ /// @returns {Promise<string>} The result of script execution.
471
+ #[napi]
472
+ pub async fn execute_browser_script(&self, script: String) -> napi::Result<String> {
473
+ self.inner
474
+ .execute_browser_script(&script)
475
+ .await
476
+ .map_err(map_error)
477
+ }
478
+
479
+ /// (async) Delay execution for a specified number of milliseconds.
480
+ /// Useful for waiting between actions to ensure UI stability.
481
+ ///
482
+ /// @param {number} delayMs - Delay in milliseconds.
483
+ /// @returns {Promise<void>}
484
+ #[napi]
485
+ pub async fn delay(&self, delay_ms: u32) -> napi::Result<()> {
486
+ self.inner.delay(delay_ms as u64).await.map_err(map_error)
487
+ }
488
+
489
+ /// Navigate to a URL in a browser.
490
+ /// This is the recommended method for browser navigation - more reliable than
491
+ /// manually manipulating the address bar with keyboard/mouse actions.
492
+ ///
493
+ /// @param {string} url - URL to navigate to
494
+ /// @param {string | null} browser - Optional browser name ('Chrome', 'Firefox', 'Edge', 'Brave', 'Opera', 'Vivaldi', or 'Default')
495
+ /// @returns {Promise<Element>} The browser window element
496
+ #[napi]
497
+ pub fn navigate_browser(&self, url: String, browser: Option<String>) -> napi::Result<Element> {
498
+ let browser_enum = browser.map(|b| match b.as_str() {
499
+ "Chrome" => terminator::Browser::Chrome,
500
+ "Firefox" => terminator::Browser::Firefox,
501
+ "Edge" => terminator::Browser::Edge,
502
+ "Brave" => terminator::Browser::Brave,
503
+ "Opera" => terminator::Browser::Opera,
504
+ "Vivaldi" => terminator::Browser::Vivaldi,
505
+ "Default" => terminator::Browser::Default,
506
+ custom => terminator::Browser::Custom(custom.to_string()),
507
+ });
508
+
509
+ let element = self.inner.open_url(&url, browser_enum).map_err(map_error)?;
510
+ Ok(Element { inner: element })
511
+ }
512
+
513
+ /// (async) Set the zoom level to a specific percentage.
514
+ ///
515
+ /// @param {number} percentage - The zoom percentage (e.g., 100 for 100%, 150 for 150%, 50 for 50%).
516
+ #[napi]
517
+ pub async fn set_zoom(&self, percentage: u32) -> napi::Result<()> {
518
+ self.inner.set_zoom(percentage).await.map_err(map_error)
519
+ }
520
+ }