victauri-plugin 0.1.0

Tauri plugin for Victauri — embedded MCP server with full-stack introspection
Documentation
use tauri::{Manager, Runtime};
use victauri_core::WindowState;

/// Runtime-erased interface for webview access, allowing the MCP server to interact with Tauri windows without generic parameters.
pub trait WebviewBridge: Send + Sync {
    /// Execute JavaScript in the target webview (defaults to "main" or first visible window).
    fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String>;
    /// Retrieve the state of one or all windows (position, size, visibility, focus, URL).
    fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState>;
    /// Return the labels of all open webview windows.
    fn list_window_labels(&self) -> Vec<String>;
    /// Return the platform-native window handle (HWND on Windows) for screenshot capture.
    fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String>;
    /// Perform a window management action (minimize, maximize, close, show, hide, etc.).
    fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String>;
    /// Set the logical size of a window in device-independent pixels.
    fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String>;
    /// Set the logical position of a window in device-independent pixels.
    fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String>;
    /// Set the title bar text of a window.
    fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String>;
}

impl<R: Runtime> WebviewBridge for tauri::AppHandle<R> {
    fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String> {
        let windows = self.webview_windows();
        let webview = match label {
            Some(l) => windows
                .get(l)
                .ok_or_else(|| format!("window not found: {l}"))?,
            None => windows
                .get("main")
                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
                .or_else(|| windows.values().next())
                .ok_or_else(|| "no webview available".to_string())?,
        };
        webview.eval(script).map_err(|e| e.to_string())
    }

    fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState> {
        let windows = self.webview_windows();
        let mut states = Vec::new();

        for (win_label, window) in &windows {
            if let Some(filter) = label
                && win_label != filter
            {
                continue;
            }

            let pos = window.outer_position().unwrap_or_default();
            let size = window.inner_size().unwrap_or_default();

            states.push(WindowState {
                label: win_label.clone(),
                title: window.title().unwrap_or_default(),
                url: window.url().map(|u| u.to_string()).unwrap_or_default(),
                visible: window.is_visible().unwrap_or(false),
                focused: window.is_focused().unwrap_or(false),
                maximized: window.is_maximized().unwrap_or(false),
                minimized: window.is_minimized().unwrap_or(false),
                fullscreen: window.is_fullscreen().unwrap_or(false),
                position: (pos.x, pos.y),
                size: (size.width, size.height),
            });
        }

        states
    }

    fn list_window_labels(&self) -> Vec<String> {
        self.webview_windows().keys().cloned().collect()
    }

    fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String> {
        let windows = self.webview_windows();
        let _webview = match label {
            Some(l) => windows
                .get(l)
                .ok_or_else(|| format!("window not found: {l}"))?,
            None => windows
                .get("main")
                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
                .or_else(|| windows.values().next())
                .ok_or_else(|| "no webview available".to_string())?,
        };

        #[cfg(windows)]
        {
            use raw_window_handle::{HasWindowHandle, RawWindowHandle};
            let handle = _webview.window_handle().map_err(|e| e.to_string())?;
            match handle.as_raw() {
                RawWindowHandle::Win32(h) => Ok(h.hwnd.get()),
                _ => Err("unexpected window handle type".to_string()),
            }
        }

        #[cfg(not(windows))]
        {
            Err("native handle not yet supported on this platform".to_string())
        }
    }

    fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String> {
        let windows = self.webview_windows();
        let window = match label {
            Some(l) => windows
                .get(l)
                .ok_or_else(|| format!("window not found: {l}"))?,
            None => windows
                .get("main")
                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
                .or_else(|| windows.values().next())
                .ok_or_else(|| "no window available".to_string())?,
        };

        match action {
            "minimize" => window.minimize().map_err(|e| e.to_string())?,
            "unminimize" => window.unminimize().map_err(|e| e.to_string())?,
            "maximize" => window.maximize().map_err(|e| e.to_string())?,
            "unmaximize" => window.unmaximize().map_err(|e| e.to_string())?,
            "close" => window.close().map_err(|e| e.to_string())?,
            "focus" => window.set_focus().map_err(|e| e.to_string())?,
            "show" => window.show().map_err(|e| e.to_string())?,
            "hide" => window.hide().map_err(|e| e.to_string())?,
            "fullscreen" => window.set_fullscreen(true).map_err(|e| e.to_string())?,
            "unfullscreen" => window.set_fullscreen(false).map_err(|e| e.to_string())?,
            "always_on_top" => window.set_always_on_top(true).map_err(|e| e.to_string())?,
            "not_always_on_top" => window.set_always_on_top(false).map_err(|e| e.to_string())?,
            _ => return Err(format!("unknown action: {action}")),
        }

        Ok(format!("{action} executed"))
    }

    fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String> {
        let windows = self.webview_windows();
        let window = match label {
            Some(l) => windows
                .get(l)
                .ok_or_else(|| format!("window not found: {l}"))?,
            None => windows
                .get("main")
                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
                .or_else(|| windows.values().next())
                .ok_or_else(|| "no window available".to_string())?,
        };

        window
            .set_size(tauri::LogicalSize::new(width, height))
            .map_err(|e| e.to_string())
    }

    fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String> {
        let windows = self.webview_windows();
        let window = match label {
            Some(l) => windows
                .get(l)
                .ok_or_else(|| format!("window not found: {l}"))?,
            None => windows
                .get("main")
                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
                .or_else(|| windows.values().next())
                .ok_or_else(|| "no window available".to_string())?,
        };

        window
            .set_position(tauri::LogicalPosition::new(x, y))
            .map_err(|e| e.to_string())
    }

    fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String> {
        let windows = self.webview_windows();
        let window = match label {
            Some(l) => windows
                .get(l)
                .ok_or_else(|| format!("window not found: {l}"))?,
            None => windows
                .get("main")
                .or_else(|| windows.values().find(|w| w.is_visible().unwrap_or(false)))
                .or_else(|| windows.values().next())
                .ok_or_else(|| "no window available".to_string())?,
        };

        window.set_title(title).map_err(|e| e.to_string())
    }
}