Skip to main content

victauri_plugin/
bridge.rs

1use tauri::{Manager, Runtime};
2use victauri_core::WindowState;
3
4/// Runtime-erased interface for webview and backend access, allowing the MCP
5/// server to interact with Tauri windows and the application backend without
6/// generic parameters.
7pub trait WebviewBridge: Send + Sync {
8    /// Execute JavaScript in the target webview (defaults to "main" or first visible window).
9    ///
10    /// # Errors
11    ///
12    /// Returns an error string if no matching window is found or the eval fails.
13    fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String>;
14    /// Retrieve the state of one or all windows (position, size, visibility, focus, URL).
15    fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState>;
16    /// Return the labels of all open webview windows.
17    fn list_window_labels(&self) -> Vec<String>;
18    /// Return the platform-native window handle for screenshot capture.
19    /// Windows: `HWND`, macOS: `CGWindowID` (window number), Linux: `X11` window ID.
20    ///
21    /// # Errors
22    ///
23    /// Returns an error string if no matching window is found or the handle type is unsupported.
24    fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String>;
25    /// Perform a window management action (minimize, maximize, close, show, hide, etc.).
26    ///
27    /// # Errors
28    ///
29    /// Returns an error string if no matching window is found or the action fails.
30    fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String>;
31    /// Set the logical size of a window in device-independent pixels.
32    ///
33    /// # Errors
34    ///
35    /// Returns an error string if no matching window is found or the resize fails.
36    fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String>;
37    /// Set the logical position of a window in device-independent pixels.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error string if no matching window is found or the move fails.
42    fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String>;
43    /// Set the title bar text of a window.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error string if no matching window is found or the title change fails.
48    fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String>;
49
50    // ── Native (OS-level, trusted) input ───────────────────────────────────
51    //
52    // These deliver real OS input events (`isTrusted: true`), unlike the JS
53    // bridge's synthetic events. They are needed for app handlers that gate on
54    // `event.isTrusted` and for user-activation-gated browser APIs. Default
55    // implementations return an error so platforms without support degrade
56    // gracefully (callers fall back to synthetic input).
57
58    /// Type Unicode text as trusted OS keyboard input into the focused element
59    /// of the target window. The element must already hold focus.
60    ///
61    /// # Errors
62    /// Returns an error if not supported on this platform or the window is missing.
63    fn native_type_text(&self, _label: Option<&str>, _text: &str) -> Result<(), String> {
64        Err(
65            "native (trusted) keyboard input is not implemented on this platform; \
66             use synthetic input via the `input` tool without `trusted`"
67                .to_string(),
68        )
69    }
70
71    /// Press a single named key (e.g. `Enter`, `Tab`, `Escape`, `ArrowDown`) as
72    /// trusted OS keyboard input to the focused element of the target window.
73    ///
74    /// # Errors
75    /// Returns an error if not supported on this platform or the key is unknown.
76    fn native_key(&self, _label: Option<&str>, _key: &str) -> Result<(), String> {
77        Err(
78            "native (trusted) key input is not implemented on this platform; \
79             use synthetic input via the `input` tool without `trusted`"
80                .to_string(),
81        )
82    }
83
84    /// Click at logical (CSS-pixel) coordinates within the target window's
85    /// content area, as a trusted OS mouse event.
86    ///
87    /// # Errors
88    /// Returns an error if not supported on this platform or the window is missing.
89    fn native_click(&self, _label: Option<&str>, _x: f64, _y: f64) -> Result<(), String> {
90        Err(
91            "native (trusted) mouse input is not implemented on this platform; \
92             use synthetic input via the `interact` tool"
93                .to_string(),
94        )
95    }
96
97    // ── Backend Access ─────────────────────────────────────────────────────
98
99    /// Return the app's per-user data directory (e.g. `~/.local/share/<app>/`).
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if the path cannot be resolved.
104    fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
105        Err("backend access not available".to_string())
106    }
107
108    /// Return the app's per-user config directory (e.g. `~/.config/<app>/`).
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the path cannot be resolved.
113    fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
114        Err("backend access not available".to_string())
115    }
116
117    /// Return the app's log directory.
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the path cannot be resolved.
122    fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
123        Err("backend access not available".to_string())
124    }
125
126    /// Return the app's local data directory.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if the path cannot be resolved.
131    fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
132        Err("backend access not available".to_string())
133    }
134
135    /// Return the Tauri app configuration as JSON.
136    #[must_use]
137    fn tauri_config(&self) -> serde_json::Value {
138        serde_json::Value::Null
139    }
140}
141
142fn find_window<'a, R: Runtime>(
143    windows: &'a std::collections::HashMap<String, tauri::WebviewWindow<R>>,
144    label: Option<&str>,
145) -> Result<&'a tauri::WebviewWindow<R>, String> {
146    match label {
147        Some(l) => windows
148            .get(l)
149            .ok_or_else(|| format!("window not found: {l}")),
150        None => windows
151            .get("main")
152            .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
153            .or_else(|| windows.values().next())
154            .ok_or_else(|| "no window available".to_string()),
155    }
156}
157
158impl<R: Runtime> WebviewBridge for tauri::AppHandle<R> {
159    fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String> {
160        let windows = self.webview_windows();
161        let webview = find_window(&windows, label)?;
162        webview.eval(script).map_err(|e| e.to_string())
163    }
164
165    fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState> {
166        let windows = self.webview_windows();
167        let mut states = Vec::new();
168
169        for (win_label, window) in &windows {
170            if let Some(filter) = label
171                && win_label != filter
172            {
173                continue;
174            }
175
176            let pos = window.outer_position().unwrap_or_default();
177            let size = window.inner_size().unwrap_or_default();
178
179            states.push(WindowState {
180                label: win_label.clone(),
181                title: window.title().unwrap_or_default(),
182                url: window.url().map(|u| u.to_string()).unwrap_or_default(),
183                visible: window.is_visible().unwrap_or(false),
184                focused: window.is_focused().unwrap_or(false),
185                maximized: window.is_maximized().unwrap_or(false),
186                minimized: window.is_minimized().unwrap_or(false),
187                fullscreen: window.is_fullscreen().unwrap_or(false),
188                position: (pos.x, pos.y),
189                size: (size.width, size.height),
190            });
191        }
192
193        states
194    }
195
196    fn list_window_labels(&self) -> Vec<String> {
197        self.webview_windows().keys().cloned().collect()
198    }
199
200    fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String> {
201        use raw_window_handle::{HasWindowHandle, RawWindowHandle};
202
203        let windows = self.webview_windows();
204        let _webview = find_window(&windows, label)?;
205        let handle = _webview.window_handle().map_err(|e| e.to_string())?;
206        match handle.as_raw() {
207            #[cfg(windows)]
208            RawWindowHandle::Win32(h) => Ok(h.hwnd.get()),
209            #[cfg(target_os = "macos")]
210            RawWindowHandle::AppKit(h) => {
211                // CGWindowListCreateImage needs CGWindowID (the window number),
212                // not the NSView pointer. Extract via Objective-C runtime.
213                macos_window_number(h.ns_view.as_ptr())
214            }
215            #[cfg(target_os = "linux")]
216            RawWindowHandle::Xlib(h) => Ok(h.window as isize),
217            #[cfg(target_os = "linux")]
218            RawWindowHandle::Xcb(h) => Ok(h.window.get() as isize),
219            _ => Err("unsupported window handle type on this platform".to_string()),
220        }
221    }
222
223    #[cfg(windows)]
224    fn native_type_text(&self, label: Option<&str>, text: &str) -> Result<(), String> {
225        let hwnd = self.get_native_handle(label)?;
226        win_focus(hwnd);
227        win_send_text(text)
228    }
229
230    #[cfg(windows)]
231    fn native_key(&self, label: Option<&str>, key: &str) -> Result<(), String> {
232        let hwnd = self.get_native_handle(label)?;
233        win_focus(hwnd);
234        win_send_key(key)
235    }
236
237    #[cfg(windows)]
238    fn native_click(&self, label: Option<&str>, x: f64, y: f64) -> Result<(), String> {
239        let hwnd = self.get_native_handle(label)?;
240        win_focus(hwnd);
241        win_click(hwnd, x, y)
242    }
243
244    fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String> {
245        let windows = self.webview_windows();
246        let window = find_window(&windows, label)?;
247
248        match action {
249            "minimize" => window.minimize().map_err(|e| e.to_string())?,
250            "unminimize" => window.unminimize().map_err(|e| e.to_string())?,
251            "maximize" => window.maximize().map_err(|e| e.to_string())?,
252            "unmaximize" => window.unmaximize().map_err(|e| e.to_string())?,
253            "close" => window.close().map_err(|e| e.to_string())?,
254            "focus" => window.set_focus().map_err(|e| e.to_string())?,
255            "show" => window.show().map_err(|e| e.to_string())?,
256            "hide" => window.hide().map_err(|e| e.to_string())?,
257            "fullscreen" => window.set_fullscreen(true).map_err(|e| e.to_string())?,
258            "unfullscreen" => window.set_fullscreen(false).map_err(|e| e.to_string())?,
259            "always_on_top" => window.set_always_on_top(true).map_err(|e| e.to_string())?,
260            "not_always_on_top" => window.set_always_on_top(false).map_err(|e| e.to_string())?,
261            _ => return Err(format!("unknown action: {action}")),
262        }
263
264        Ok(format!("{action} executed"))
265    }
266
267    fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String> {
268        let windows = self.webview_windows();
269        let window = find_window(&windows, label)?;
270
271        window
272            .set_size(tauri::LogicalSize::new(width, height))
273            .map_err(|e| e.to_string())
274    }
275
276    fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String> {
277        let windows = self.webview_windows();
278        let window = find_window(&windows, label)?;
279
280        window
281            .set_position(tauri::LogicalPosition::new(x, y))
282            .map_err(|e| e.to_string())
283    }
284
285    fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String> {
286        let windows = self.webview_windows();
287        let window = find_window(&windows, label)?;
288
289        window.set_title(title).map_err(|e| e.to_string())
290    }
291
292    fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
293        self.path().app_data_dir().map_err(|e| e.to_string())
294    }
295
296    fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
297        self.path().app_config_dir().map_err(|e| e.to_string())
298    }
299
300    fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
301        self.path().app_log_dir().map_err(|e| e.to_string())
302    }
303
304    fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
305        self.path().app_local_data_dir().map_err(|e| e.to_string())
306    }
307
308    fn tauri_config(&self) -> serde_json::Value {
309        let config = self.config();
310
311        let windows: Vec<serde_json::Value> = config
312            .app
313            .windows
314            .iter()
315            .map(|w| {
316                serde_json::json!({
317                    "label": w.label,
318                    "title": w.title,
319                    "url": format!("{}", w.url),
320                    "width": w.width,
321                    "height": w.height,
322                    "visible": w.visible,
323                    "resizable": w.resizable,
324                    "fullscreen": w.fullscreen,
325                    "decorations": w.decorations,
326                    "transparent": w.transparent,
327                    "always_on_top": w.always_on_top,
328                })
329            })
330            .collect();
331
332        let plugins: Vec<String> = config.plugins.0.keys().cloned().collect();
333
334        let security = serde_json::json!({
335            "csp": config.app.security.csp.as_ref().map(|c| format!("{c}")),
336            "freeze_prototype": config.app.security.freeze_prototype,
337            "capabilities": config.app.security.capabilities.iter().map(|c| {
338                match c {
339                    tauri::utils::config::CapabilityEntry::Inlined(cap) => {
340                        serde_json::json!({
341                            "identifier": cap.identifier,
342                            "description": cap.description,
343                            "windows": cap.windows,
344                            "webviews": cap.webviews,
345                            "permissions": cap.permissions.iter().map(|p| format!("{p:?}")).collect::<Vec<_>>(),
346                            "platforms": cap.platforms,
347                        })
348                    }
349                    tauri::utils::config::CapabilityEntry::Reference(path) => {
350                        serde_json::json!({ "reference": path })
351                    }
352                }
353            }).collect::<Vec<_>>(),
354        });
355
356        serde_json::json!({
357            "identifier": config.identifier,
358            "product_name": config.product_name,
359            "version": config.version,
360            "windows": windows,
361            "plugins": plugins,
362            "security": security,
363        })
364    }
365}
366
367#[cfg(target_os = "macos")]
368#[allow(unsafe_code)]
369fn macos_window_number(ns_view: *mut std::ffi::c_void) -> Result<isize, String> {
370    unsafe extern "C" {
371        fn objc_msgSend(obj: *mut std::ffi::c_void, sel: *mut std::ffi::c_void) -> isize;
372        fn sel_registerName(name: *const std::ffi::c_char) -> *mut std::ffi::c_void;
373    }
374
375    if ns_view.is_null() {
376        return Err("null NSView handle".to_string());
377    }
378
379    // SAFETY: `ns_view` is a valid NSView pointer obtained from Tauri's
380    // `with_webview` callback; null was checked above. `objc_msgSend` and
381    // `sel_registerName` are stable Objective-C runtime ABI.
382    unsafe {
383        let sel_window = sel_registerName(c"window".as_ptr());
384        let ns_window = objc_msgSend(ns_view, sel_window);
385        if ns_window == 0 {
386            return Err("NSView has no parent NSWindow".to_string());
387        }
388        let sel_window_number = sel_registerName(c"windowNumber".as_ptr());
389        let ns_window_ptr = ns_window as *mut std::ffi::c_void;
390        let window_number = objc_msgSend(ns_window_ptr, sel_window_number);
391        if window_number <= 0 {
392            return Err(format!("invalid CGWindowID: {window_number}"));
393        }
394        Ok(window_number)
395    }
396}
397
398// ── Windows native (trusted) input helpers ─────────────────────────────────
399//
400// These deliver real OS input via SendInput, producing events with
401// `isTrusted: true` (unlike the JS bridge's synthetic events).
402
403#[cfg(windows)]
404fn win_hwnd(hwnd: isize) -> windows::Win32::Foundation::HWND {
405    windows::Win32::Foundation::HWND(hwnd as *mut core::ffi::c_void)
406}
407
408/// Bring the target window to the foreground so input is routed to it, then
409/// give the OS a brief moment to apply focus.
410#[allow(unsafe_code)]
411#[cfg(windows)]
412fn win_focus(hwnd: isize) {
413    use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
414    // SAFETY: hwnd comes from Tauri's window handle; SetForegroundWindow is safe
415    // to call with any HWND (returns false if it fails).
416    unsafe {
417        let _ = SetForegroundWindow(win_hwnd(hwnd));
418    }
419    std::thread::sleep(std::time::Duration::from_millis(40));
420}
421
422#[cfg(windows)]
423fn win_keyboard_input(
424    vk: u16,
425    scan: u16,
426    key_up: bool,
427    unicode: bool,
428) -> windows::Win32::UI::Input::KeyboardAndMouse::INPUT {
429    use windows::Win32::UI::Input::KeyboardAndMouse::{
430        INPUT, INPUT_0, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP,
431        KEYEVENTF_UNICODE, VIRTUAL_KEY,
432    };
433    let mut flags = KEYBD_EVENT_FLAGS(0);
434    if unicode {
435        flags |= KEYEVENTF_UNICODE;
436    }
437    if key_up {
438        flags |= KEYEVENTF_KEYUP;
439    }
440    INPUT {
441        r#type: INPUT_KEYBOARD,
442        Anonymous: INPUT_0 {
443            ki: KEYBDINPUT {
444                wVk: VIRTUAL_KEY(vk),
445                wScan: scan,
446                dwFlags: flags,
447                time: 0,
448                dwExtraInfo: 0,
449            },
450        },
451    }
452}
453
454/// Type Unicode text via `SendInput` (`KEYEVENTF_UNICODE` per UTF-16 code unit).
455#[allow(unsafe_code)]
456#[cfg(windows)]
457fn win_send_text(text: &str) -> Result<(), String> {
458    use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
459    let mut inputs: Vec<INPUT> = Vec::new();
460    for unit in text.encode_utf16() {
461        inputs.push(win_keyboard_input(0, unit, false, true));
462        inputs.push(win_keyboard_input(0, unit, true, true));
463    }
464    if inputs.is_empty() {
465        return Ok(());
466    }
467    let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
468    // SAFETY: `inputs` is a valid slice of properly-initialized INPUT structs.
469    let sent = unsafe { SendInput(&inputs, cb) } as usize;
470    if sent == inputs.len() {
471        Ok(())
472    } else {
473        Err(format!(
474            "SendInput delivered {sent}/{} key events",
475            inputs.len()
476        ))
477    }
478}
479
480/// Map a named key (Playwright-style) to a Win32 virtual-key code.
481#[cfg(windows)]
482fn win_vk_for_key(key: &str) -> Option<u16> {
483    use windows::Win32::UI::Input::KeyboardAndMouse as k;
484    let vk = match key {
485        "Enter" | "Return" => k::VK_RETURN,
486        "Tab" => k::VK_TAB,
487        "Escape" | "Esc" => k::VK_ESCAPE,
488        "Backspace" => k::VK_BACK,
489        "Delete" | "Del" => k::VK_DELETE,
490        "ArrowUp" | "Up" => k::VK_UP,
491        "ArrowDown" | "Down" => k::VK_DOWN,
492        "ArrowLeft" | "Left" => k::VK_LEFT,
493        "ArrowRight" | "Right" => k::VK_RIGHT,
494        "Home" => k::VK_HOME,
495        "End" => k::VK_END,
496        "PageUp" => k::VK_PRIOR,
497        "PageDown" => k::VK_NEXT,
498        "Space" | " " => k::VK_SPACE,
499        "F1" => k::VK_F1,
500        "F2" => k::VK_F2,
501        "F3" => k::VK_F3,
502        "F4" => k::VK_F4,
503        "F5" => k::VK_F5,
504        "F6" => k::VK_F6,
505        "F7" => k::VK_F7,
506        "F8" => k::VK_F8,
507        "F9" => k::VK_F9,
508        "F10" => k::VK_F10,
509        "F11" => k::VK_F11,
510        "F12" => k::VK_F12,
511        _ => return None,
512    };
513    Some(vk.0)
514}
515
516/// Press and release a named key, or a single printable character, via `SendInput`.
517#[allow(unsafe_code)]
518#[cfg(windows)]
519fn win_send_key(key: &str) -> Result<(), String> {
520    use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
521    let inputs: Vec<INPUT> = if let Some(vk) = win_vk_for_key(key) {
522        vec![
523            win_keyboard_input(vk, 0, false, false),
524            win_keyboard_input(vk, 0, true, false),
525        ]
526    } else {
527        // Single printable character → send as Unicode.
528        let mut chars = key.chars();
529        let (Some(c), None) = (chars.next(), chars.next()) else {
530            return Err(format!(
531                "unknown key '{key}' (use a named key or a single character)"
532            ));
533        };
534        let mut buf = [0u16; 2];
535        let mut v = Vec::new();
536        for unit in c.encode_utf16(&mut buf) {
537            v.push(win_keyboard_input(0, *unit, false, true));
538            v.push(win_keyboard_input(0, *unit, true, true));
539        }
540        v
541    };
542    let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
543    // SAFETY: valid slice of initialized INPUT structs.
544    let sent = unsafe { SendInput(&inputs, cb) } as usize;
545    if sent == inputs.len() {
546        Ok(())
547    } else {
548        Err(format!(
549            "SendInput delivered {sent}/{} key events",
550            inputs.len()
551        ))
552    }
553}
554
555/// Click at logical (CSS-pixel) coordinates within the window's content area
556/// via an absolute-positioned `SendInput` mouse sequence (move + down + up).
557#[allow(unsafe_code)]
558#[cfg(windows)]
559fn win_click(hwnd: isize, x: f64, y: f64) -> Result<(), String> {
560    use windows::Win32::Foundation::POINT;
561    use windows::Win32::Graphics::Gdi::ClientToScreen;
562    use windows::Win32::UI::HiDpi::GetDpiForWindow;
563    use windows::Win32::UI::Input::KeyboardAndMouse::{
564        INPUT, INPUT_0, INPUT_MOUSE, MOUSE_EVENT_FLAGS, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN,
565        MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_VIRTUALDESK, MOUSEINPUT, SendInput,
566    };
567    use windows::Win32::UI::WindowsAndMessaging::{
568        GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
569        SM_YVIRTUALSCREEN,
570    };
571    let h = win_hwnd(hwnd);
572    // SAFETY: GetDpiForWindow/GetSystemMetrics/ClientToScreen are safe to call
573    // with a valid HWND; ClientToScreen writes into our stack POINT.
574    let (nx, ny) = unsafe {
575        let dpi = GetDpiForWindow(h);
576        let scale = if dpi == 0 { 1.0 } else { f64::from(dpi) / 96.0 };
577        let mut pt = POINT {
578            x: (x * scale) as i32,
579            y: (y * scale) as i32,
580        };
581        let _ = ClientToScreen(h, &mut pt);
582        let vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
583        let vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
584        let vw = GetSystemMetrics(SM_CXVIRTUALSCREEN);
585        let vh = GetSystemMetrics(SM_CYVIRTUALSCREEN);
586        if vw <= 1 || vh <= 1 {
587            return Err("virtual screen metrics unavailable".to_string());
588        }
589        let nx = ((f64::from(pt.x - vx)) * 65535.0 / f64::from(vw - 1)) as i32;
590        let ny = ((f64::from(pt.y - vy)) * 65535.0 / f64::from(vh - 1)) as i32;
591        (nx, ny)
592    };
593    let make = |flags: MOUSE_EVENT_FLAGS| INPUT {
594        r#type: INPUT_MOUSE,
595        Anonymous: INPUT_0 {
596            mi: MOUSEINPUT {
597                dx: nx,
598                dy: ny,
599                mouseData: 0,
600                dwFlags: flags,
601                time: 0,
602                dwExtraInfo: 0,
603            },
604        },
605    };
606    let base = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
607    let inputs = [
608        make(base | MOUSEEVENTF_MOVE),
609        make(base | MOUSEEVENTF_LEFTDOWN),
610        make(base | MOUSEEVENTF_LEFTUP),
611    ];
612    let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
613    // SAFETY: valid slice of initialized INPUT structs.
614    let sent = unsafe { SendInput(&inputs, cb) } as usize;
615    if sent == inputs.len() {
616        Ok(())
617    } else {
618        Err(format!(
619            "SendInput delivered {sent}/{} mouse events",
620            inputs.len()
621        ))
622    }
623}