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    fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String>;
8    /// Retrieve the state of one or all windows (position, size, visibility, focus, URL).
9    fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState>;
10    /// Return the labels of all open webview windows.
11    fn list_window_labels(&self) -> Vec<String>;
12    /// Return the platform-native window handle (HWND on Windows) for screenshot capture.
13    fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String>;
14    /// Perform a window management action (minimize, maximize, close, show, hide, etc.).
15    fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String>;
16    /// Set the logical size of a window in device-independent pixels.
17    fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String>;
18    /// Set the logical position of a window in device-independent pixels.
19    fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String>;
20    /// Set the title bar text of a window.
21    fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String>;
22}
23
24impl<R: Runtime> WebviewBridge for tauri::AppHandle<R> {
25    fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String> {
26        let windows = self.webview_windows();
27        let webview = match label {
28            Some(l) => windows
29                .get(l)
30                .ok_or_else(|| format!("window not found: {l}"))?,
31            None => windows
32                .get("main")
33                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
34                .or_else(|| windows.values().next())
35                .ok_or_else(|| "no webview available".to_string())?,
36        };
37        webview.eval(script).map_err(|e| e.to_string())
38    }
39
40    fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState> {
41        let windows = self.webview_windows();
42        let mut states = Vec::new();
43
44        for (win_label, window) in &windows {
45            if let Some(filter) = label
46                && win_label != filter
47            {
48                continue;
49            }
50
51            let pos = window.outer_position().unwrap_or_default();
52            let size = window.inner_size().unwrap_or_default();
53
54            states.push(WindowState {
55                label: win_label.clone(),
56                title: window.title().unwrap_or_default(),
57                url: window.url().map(|u| u.to_string()).unwrap_or_default(),
58                visible: window.is_visible().unwrap_or(false),
59                focused: window.is_focused().unwrap_or(false),
60                maximized: window.is_maximized().unwrap_or(false),
61                minimized: window.is_minimized().unwrap_or(false),
62                fullscreen: window.is_fullscreen().unwrap_or(false),
63                position: (pos.x, pos.y),
64                size: (size.width, size.height),
65            });
66        }
67
68        states
69    }
70
71    fn list_window_labels(&self) -> Vec<String> {
72        self.webview_windows().keys().cloned().collect()
73    }
74
75    fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String> {
76        let windows = self.webview_windows();
77        let _webview = match label {
78            Some(l) => windows
79                .get(l)
80                .ok_or_else(|| format!("window not found: {l}"))?,
81            None => windows
82                .get("main")
83                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
84                .or_else(|| windows.values().next())
85                .ok_or_else(|| "no webview available".to_string())?,
86        };
87
88        #[cfg(windows)]
89        {
90            use raw_window_handle::{HasWindowHandle, RawWindowHandle};
91            let handle = _webview.window_handle().map_err(|e| e.to_string())?;
92            match handle.as_raw() {
93                RawWindowHandle::Win32(h) => Ok(h.hwnd.get()),
94                _ => Err("unexpected window handle type".to_string()),
95            }
96        }
97
98        #[cfg(not(windows))]
99        {
100            Err("native handle not yet supported on this platform".to_string())
101        }
102    }
103
104    fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String> {
105        let windows = self.webview_windows();
106        let window = match label {
107            Some(l) => windows
108                .get(l)
109                .ok_or_else(|| format!("window not found: {l}"))?,
110            None => windows
111                .get("main")
112                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
113                .or_else(|| windows.values().next())
114                .ok_or_else(|| "no window available".to_string())?,
115        };
116
117        match action {
118            "minimize" => window.minimize().map_err(|e| e.to_string())?,
119            "unminimize" => window.unminimize().map_err(|e| e.to_string())?,
120            "maximize" => window.maximize().map_err(|e| e.to_string())?,
121            "unmaximize" => window.unmaximize().map_err(|e| e.to_string())?,
122            "close" => window.close().map_err(|e| e.to_string())?,
123            "focus" => window.set_focus().map_err(|e| e.to_string())?,
124            "show" => window.show().map_err(|e| e.to_string())?,
125            "hide" => window.hide().map_err(|e| e.to_string())?,
126            "fullscreen" => window.set_fullscreen(true).map_err(|e| e.to_string())?,
127            "unfullscreen" => window.set_fullscreen(false).map_err(|e| e.to_string())?,
128            "always_on_top" => window.set_always_on_top(true).map_err(|e| e.to_string())?,
129            "not_always_on_top" => window.set_always_on_top(false).map_err(|e| e.to_string())?,
130            _ => return Err(format!("unknown action: {action}")),
131        }
132
133        Ok(format!("{action} executed"))
134    }
135
136    fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String> {
137        let windows = self.webview_windows();
138        let window = match label {
139            Some(l) => windows
140                .get(l)
141                .ok_or_else(|| format!("window not found: {l}"))?,
142            None => windows
143                .get("main")
144                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
145                .or_else(|| windows.values().next())
146                .ok_or_else(|| "no window available".to_string())?,
147        };
148
149        window
150            .set_size(tauri::LogicalSize::new(width, height))
151            .map_err(|e| e.to_string())
152    }
153
154    fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String> {
155        let windows = self.webview_windows();
156        let window = match label {
157            Some(l) => windows
158                .get(l)
159                .ok_or_else(|| format!("window not found: {l}"))?,
160            None => windows
161                .get("main")
162                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
163                .or_else(|| windows.values().next())
164                .ok_or_else(|| "no window available".to_string())?,
165        };
166
167        window
168            .set_position(tauri::LogicalPosition::new(x, y))
169            .map_err(|e| e.to_string())
170    }
171
172    fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String> {
173        let windows = self.webview_windows();
174        let window = match label {
175            Some(l) => windows
176                .get(l)
177                .ok_or_else(|| format!("window not found: {l}"))?,
178            None => windows
179                .get("main")
180                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
181                .or_else(|| windows.values().next())
182                .ok_or_else(|| "no window available".to_string())?,
183        };
184
185        window.set_title(title).map_err(|e| e.to_string())
186    }
187}