Skip to main content

victauri_plugin/
bridge.rs

1use tauri::{Manager, Runtime};
2use victauri_core::WindowState;
3
4/// Runtime-erased interface for webview access, allowing the MCP server to interact with Tauri windows without generic parameters.
5pub trait WebviewBridge: Send + Sync {
6    /// Execute JavaScript in the target webview (defaults to "main" or first visible window).
7    ///
8    /// # Errors
9    ///
10    /// Returns an error string if no matching window is found or the eval fails.
11    fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String>;
12    /// Retrieve the state of one or all windows (position, size, visibility, focus, URL).
13    fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState>;
14    /// Return the labels of all open webview windows.
15    fn list_window_labels(&self) -> Vec<String>;
16    /// Return the platform-native window handle for screenshot capture.
17    /// Windows: `HWND`, macOS: `CGWindowID` (window number), Linux: `X11` window ID.
18    ///
19    /// # Errors
20    ///
21    /// Returns an error string if no matching window is found or the handle type is unsupported.
22    fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String>;
23    /// Perform a window management action (minimize, maximize, close, show, hide, etc.).
24    ///
25    /// # Errors
26    ///
27    /// Returns an error string if no matching window is found or the action fails.
28    fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String>;
29    /// Set the logical size of a window in device-independent pixels.
30    ///
31    /// # Errors
32    ///
33    /// Returns an error string if no matching window is found or the resize fails.
34    fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String>;
35    /// Set the logical position of a window in device-independent pixels.
36    ///
37    /// # Errors
38    ///
39    /// Returns an error string if no matching window is found or the move fails.
40    fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String>;
41    /// Set the title bar text of a window.
42    ///
43    /// # Errors
44    ///
45    /// Returns an error string if no matching window is found or the title change fails.
46    fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String>;
47}
48
49fn find_window<'a, R: Runtime>(
50    windows: &'a std::collections::HashMap<String, tauri::WebviewWindow<R>>,
51    label: Option<&str>,
52) -> Result<&'a tauri::WebviewWindow<R>, String> {
53    match label {
54        Some(l) => windows
55            .get(l)
56            .ok_or_else(|| format!("window not found: {l}")),
57        None => windows
58            .get("main")
59            .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
60            .or_else(|| windows.values().next())
61            .ok_or_else(|| "no window available".to_string()),
62    }
63}
64
65impl<R: Runtime> WebviewBridge for tauri::AppHandle<R> {
66    fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String> {
67        let windows = self.webview_windows();
68        let webview = find_window(&windows, label)?;
69        webview.eval(script).map_err(|e| e.to_string())
70    }
71
72    fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState> {
73        let windows = self.webview_windows();
74        let mut states = Vec::new();
75
76        for (win_label, window) in &windows {
77            if let Some(filter) = label
78                && win_label != filter
79            {
80                continue;
81            }
82
83            let pos = window.outer_position().unwrap_or_default();
84            let size = window.inner_size().unwrap_or_default();
85
86            states.push(WindowState {
87                label: win_label.clone(),
88                title: window.title().unwrap_or_default(),
89                url: window.url().map(|u| u.to_string()).unwrap_or_default(),
90                visible: window.is_visible().unwrap_or(false),
91                focused: window.is_focused().unwrap_or(false),
92                maximized: window.is_maximized().unwrap_or(false),
93                minimized: window.is_minimized().unwrap_or(false),
94                fullscreen: window.is_fullscreen().unwrap_or(false),
95                position: (pos.x, pos.y),
96                size: (size.width, size.height),
97            });
98        }
99
100        states
101    }
102
103    fn list_window_labels(&self) -> Vec<String> {
104        self.webview_windows().keys().cloned().collect()
105    }
106
107    fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String> {
108        use raw_window_handle::{HasWindowHandle, RawWindowHandle};
109
110        let windows = self.webview_windows();
111        let _webview = find_window(&windows, label)?;
112        let handle = _webview.window_handle().map_err(|e| e.to_string())?;
113        match handle.as_raw() {
114            #[cfg(windows)]
115            RawWindowHandle::Win32(h) => Ok(h.hwnd.get()),
116            #[cfg(target_os = "macos")]
117            RawWindowHandle::AppKit(h) => {
118                // CGWindowListCreateImage needs CGWindowID (the window number),
119                // not the NSView pointer. Extract via Objective-C runtime.
120                macos_window_number(h.ns_view.as_ptr())
121            }
122            #[cfg(target_os = "linux")]
123            RawWindowHandle::Xlib(h) => Ok(h.window as isize),
124            #[cfg(target_os = "linux")]
125            RawWindowHandle::Xcb(h) => Ok(h.window.get() as isize),
126            _ => Err("unsupported window handle type on this platform".to_string()),
127        }
128    }
129
130    fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String> {
131        let windows = self.webview_windows();
132        let window = find_window(&windows, label)?;
133
134        match action {
135            "minimize" => window.minimize().map_err(|e| e.to_string())?,
136            "unminimize" => window.unminimize().map_err(|e| e.to_string())?,
137            "maximize" => window.maximize().map_err(|e| e.to_string())?,
138            "unmaximize" => window.unmaximize().map_err(|e| e.to_string())?,
139            "close" => window.close().map_err(|e| e.to_string())?,
140            "focus" => window.set_focus().map_err(|e| e.to_string())?,
141            "show" => window.show().map_err(|e| e.to_string())?,
142            "hide" => window.hide().map_err(|e| e.to_string())?,
143            "fullscreen" => window.set_fullscreen(true).map_err(|e| e.to_string())?,
144            "unfullscreen" => window.set_fullscreen(false).map_err(|e| e.to_string())?,
145            "always_on_top" => window.set_always_on_top(true).map_err(|e| e.to_string())?,
146            "not_always_on_top" => window.set_always_on_top(false).map_err(|e| e.to_string())?,
147            _ => return Err(format!("unknown action: {action}")),
148        }
149
150        Ok(format!("{action} executed"))
151    }
152
153    fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String> {
154        let windows = self.webview_windows();
155        let window = find_window(&windows, label)?;
156
157        window
158            .set_size(tauri::LogicalSize::new(width, height))
159            .map_err(|e| e.to_string())
160    }
161
162    fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String> {
163        let windows = self.webview_windows();
164        let window = find_window(&windows, label)?;
165
166        window
167            .set_position(tauri::LogicalPosition::new(x, y))
168            .map_err(|e| e.to_string())
169    }
170
171    fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String> {
172        let windows = self.webview_windows();
173        let window = find_window(&windows, label)?;
174
175        window.set_title(title).map_err(|e| e.to_string())
176    }
177}
178
179#[cfg(target_os = "macos")]
180#[allow(unsafe_code)]
181fn macos_window_number(ns_view: *mut std::ffi::c_void) -> Result<isize, String> {
182    unsafe extern "C" {
183        fn objc_msgSend(obj: *mut std::ffi::c_void, sel: *mut std::ffi::c_void) -> isize;
184        fn sel_registerName(name: *const std::ffi::c_char) -> *mut std::ffi::c_void;
185    }
186
187    if ns_view.is_null() {
188        return Err("null NSView handle".to_string());
189    }
190
191    // SAFETY: `ns_view` is a valid NSView pointer obtained from Tauri's
192    // `with_webview` callback; null was checked above. `objc_msgSend` and
193    // `sel_registerName` are stable Objective-C runtime ABI.
194    unsafe {
195        let sel_window = sel_registerName(c"window".as_ptr());
196        let ns_window = objc_msgSend(ns_view, sel_window);
197        if ns_window == 0 {
198            return Err("NSView has no parent NSWindow".to_string());
199        }
200        let sel_window_number = sel_registerName(c"windowNumber".as_ptr());
201        let ns_window_ptr = ns_window as *mut std::ffi::c_void;
202        let window_number = objc_msgSend(ns_window_ptr, sel_window_number);
203        if window_number <= 0 {
204            return Err(format!("invalid CGWindowID: {window_number}"));
205        }
206        Ok(window_number)
207    }
208}