use tauri::{Manager, Runtime};
use victauri_core::WindowState;
pub trait WebviewBridge: Send + Sync {
fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String>;
fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState>;
fn list_window_labels(&self) -> Vec<String>;
fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String>;
fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String>;
fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String>;
fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String>;
fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String>;
fn native_type_text(&self, _label: Option<&str>, _text: &str) -> Result<(), String> {
Err(
"native (trusted) keyboard input is not implemented on this platform; \
use synthetic input via the `input` tool without `trusted`"
.to_string(),
)
}
fn native_key(&self, _label: Option<&str>, _key: &str) -> Result<(), String> {
Err(
"native (trusted) key input is not implemented on this platform; \
use synthetic input via the `input` tool without `trusted`"
.to_string(),
)
}
fn native_click(&self, _label: Option<&str>, _x: f64, _y: f64) -> Result<(), String> {
Err(
"native (trusted) mouse input is not implemented on this platform; \
use synthetic input via the `interact` tool"
.to_string(),
)
}
fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
Err("backend access not available".to_string())
}
fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
Err("backend access not available".to_string())
}
fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
Err("backend access not available".to_string())
}
fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
Err("backend access not available".to_string())
}
#[must_use]
fn tauri_config(&self) -> serde_json::Value {
serde_json::Value::Null
}
}
fn find_window<'a, R: Runtime>(
windows: &'a std::collections::HashMap<String, tauri::WebviewWindow<R>>,
label: Option<&str>,
) -> Result<&'a tauri::WebviewWindow<R>, String> {
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()),
}
}
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 = find_window(&windows, label)?;
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> {
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
let windows = self.webview_windows();
let _webview = find_window(&windows, label)?;
let handle = _webview.window_handle().map_err(|e| e.to_string())?;
match handle.as_raw() {
#[cfg(windows)]
RawWindowHandle::Win32(h) => Ok(h.hwnd.get()),
#[cfg(target_os = "macos")]
RawWindowHandle::AppKit(h) => {
macos_window_number(h.ns_view.as_ptr())
}
#[cfg(target_os = "linux")]
RawWindowHandle::Xlib(h) => Ok(h.window as isize),
#[cfg(target_os = "linux")]
RawWindowHandle::Xcb(h) => Ok(h.window.get() as isize),
_ => Err("unsupported window handle type on this platform".to_string()),
}
}
#[cfg(windows)]
fn native_type_text(&self, label: Option<&str>, text: &str) -> Result<(), String> {
let hwnd = self.get_native_handle(label)?;
win_focus(hwnd);
win_send_text(text)
}
#[cfg(windows)]
fn native_key(&self, label: Option<&str>, key: &str) -> Result<(), String> {
let hwnd = self.get_native_handle(label)?;
win_focus(hwnd);
win_send_key(key)
}
#[cfg(windows)]
fn native_click(&self, label: Option<&str>, x: f64, y: f64) -> Result<(), String> {
let hwnd = self.get_native_handle(label)?;
win_focus(hwnd);
win_click(hwnd, x, y)
}
fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String> {
let windows = self.webview_windows();
let window = find_window(&windows, label)?;
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 = find_window(&windows, label)?;
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 = find_window(&windows, label)?;
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 = find_window(&windows, label)?;
window.set_title(title).map_err(|e| e.to_string())
}
fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
self.path().app_data_dir().map_err(|e| e.to_string())
}
fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
self.path().app_config_dir().map_err(|e| e.to_string())
}
fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
self.path().app_log_dir().map_err(|e| e.to_string())
}
fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
self.path().app_local_data_dir().map_err(|e| e.to_string())
}
fn tauri_config(&self) -> serde_json::Value {
let config = self.config();
let windows: Vec<serde_json::Value> = config
.app
.windows
.iter()
.map(|w| {
serde_json::json!({
"label": w.label,
"title": w.title,
"url": format!("{}", w.url),
"width": w.width,
"height": w.height,
"visible": w.visible,
"resizable": w.resizable,
"fullscreen": w.fullscreen,
"decorations": w.decorations,
"transparent": w.transparent,
"always_on_top": w.always_on_top,
})
})
.collect();
let plugins: Vec<String> = config.plugins.0.keys().cloned().collect();
let security = serde_json::json!({
"csp": config.app.security.csp.as_ref().map(|c| format!("{c}")),
"freeze_prototype": config.app.security.freeze_prototype,
"capabilities": config.app.security.capabilities.iter().map(|c| {
match c {
tauri::utils::config::CapabilityEntry::Inlined(cap) => {
serde_json::json!({
"identifier": cap.identifier,
"description": cap.description,
"windows": cap.windows,
"webviews": cap.webviews,
"permissions": cap.permissions.iter().map(|p| format!("{p:?}")).collect::<Vec<_>>(),
"platforms": cap.platforms,
})
}
tauri::utils::config::CapabilityEntry::Reference(path) => {
serde_json::json!({ "reference": path })
}
}
}).collect::<Vec<_>>(),
});
serde_json::json!({
"identifier": config.identifier,
"product_name": config.product_name,
"version": config.version,
"windows": windows,
"plugins": plugins,
"security": security,
})
}
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn macos_window_number(ns_view: *mut std::ffi::c_void) -> Result<isize, String> {
unsafe extern "C" {
fn objc_msgSend(obj: *mut std::ffi::c_void, sel: *mut std::ffi::c_void) -> isize;
fn sel_registerName(name: *const std::ffi::c_char) -> *mut std::ffi::c_void;
}
if ns_view.is_null() {
return Err("null NSView handle".to_string());
}
unsafe {
let sel_window = sel_registerName(c"window".as_ptr());
let ns_window = objc_msgSend(ns_view, sel_window);
if ns_window == 0 {
return Err("NSView has no parent NSWindow".to_string());
}
let sel_window_number = sel_registerName(c"windowNumber".as_ptr());
let ns_window_ptr = ns_window as *mut std::ffi::c_void;
let window_number = objc_msgSend(ns_window_ptr, sel_window_number);
if window_number <= 0 {
return Err(format!("invalid CGWindowID: {window_number}"));
}
Ok(window_number)
}
}
#[cfg(windows)]
fn win_hwnd(hwnd: isize) -> windows::Win32::Foundation::HWND {
windows::Win32::Foundation::HWND(hwnd as *mut core::ffi::c_void)
}
#[allow(unsafe_code)]
#[cfg(windows)]
fn win_focus(hwnd: isize) {
use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
unsafe {
let _ = SetForegroundWindow(win_hwnd(hwnd));
}
std::thread::sleep(std::time::Duration::from_millis(40));
}
#[cfg(windows)]
fn win_keyboard_input(
vk: u16,
scan: u16,
key_up: bool,
unicode: bool,
) -> windows::Win32::UI::Input::KeyboardAndMouse::INPUT {
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP,
KEYEVENTF_UNICODE, VIRTUAL_KEY,
};
let mut flags = KEYBD_EVENT_FLAGS(0);
if unicode {
flags |= KEYEVENTF_UNICODE;
}
if key_up {
flags |= KEYEVENTF_KEYUP;
}
INPUT {
r#type: INPUT_KEYBOARD,
Anonymous: INPUT_0 {
ki: KEYBDINPUT {
wVk: VIRTUAL_KEY(vk),
wScan: scan,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
}
}
#[allow(unsafe_code)]
#[cfg(windows)]
fn win_send_text(text: &str) -> Result<(), String> {
use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
let mut inputs: Vec<INPUT> = Vec::new();
for unit in text.encode_utf16() {
inputs.push(win_keyboard_input(0, unit, false, true));
inputs.push(win_keyboard_input(0, unit, true, true));
}
if inputs.is_empty() {
return Ok(());
}
let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
let sent = unsafe { SendInput(&inputs, cb) } as usize;
if sent == inputs.len() {
Ok(())
} else {
Err(format!(
"SendInput delivered {sent}/{} key events",
inputs.len()
))
}
}
#[cfg(windows)]
fn win_vk_for_key(key: &str) -> Option<u16> {
use windows::Win32::UI::Input::KeyboardAndMouse as k;
let vk = match key {
"Enter" | "Return" => k::VK_RETURN,
"Tab" => k::VK_TAB,
"Escape" | "Esc" => k::VK_ESCAPE,
"Backspace" => k::VK_BACK,
"Delete" | "Del" => k::VK_DELETE,
"ArrowUp" | "Up" => k::VK_UP,
"ArrowDown" | "Down" => k::VK_DOWN,
"ArrowLeft" | "Left" => k::VK_LEFT,
"ArrowRight" | "Right" => k::VK_RIGHT,
"Home" => k::VK_HOME,
"End" => k::VK_END,
"PageUp" => k::VK_PRIOR,
"PageDown" => k::VK_NEXT,
"Space" | " " => k::VK_SPACE,
"F1" => k::VK_F1,
"F2" => k::VK_F2,
"F3" => k::VK_F3,
"F4" => k::VK_F4,
"F5" => k::VK_F5,
"F6" => k::VK_F6,
"F7" => k::VK_F7,
"F8" => k::VK_F8,
"F9" => k::VK_F9,
"F10" => k::VK_F10,
"F11" => k::VK_F11,
"F12" => k::VK_F12,
_ => return None,
};
Some(vk.0)
}
#[allow(unsafe_code)]
#[cfg(windows)]
fn win_send_key(key: &str) -> Result<(), String> {
use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
let inputs: Vec<INPUT> = if let Some(vk) = win_vk_for_key(key) {
vec![
win_keyboard_input(vk, 0, false, false),
win_keyboard_input(vk, 0, true, false),
]
} else {
let mut chars = key.chars();
let (Some(c), None) = (chars.next(), chars.next()) else {
return Err(format!(
"unknown key '{key}' (use a named key or a single character)"
));
};
let mut buf = [0u16; 2];
let mut v = Vec::new();
for unit in c.encode_utf16(&mut buf) {
v.push(win_keyboard_input(0, *unit, false, true));
v.push(win_keyboard_input(0, *unit, true, true));
}
v
};
let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
let sent = unsafe { SendInput(&inputs, cb) } as usize;
if sent == inputs.len() {
Ok(())
} else {
Err(format!(
"SendInput delivered {sent}/{} key events",
inputs.len()
))
}
}
#[allow(unsafe_code)]
#[cfg(windows)]
fn win_click(hwnd: isize, x: f64, y: f64) -> Result<(), String> {
use windows::Win32::Foundation::POINT;
use windows::Win32::Graphics::Gdi::ClientToScreen;
use windows::Win32::UI::HiDpi::GetDpiForWindow;
use windows::Win32::UI::Input::KeyboardAndMouse::{
INPUT, INPUT_0, INPUT_MOUSE, MOUSE_EVENT_FLAGS, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN,
MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_VIRTUALDESK, MOUSEINPUT, SendInput,
};
use windows::Win32::UI::WindowsAndMessaging::{
GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
SM_YVIRTUALSCREEN,
};
let h = win_hwnd(hwnd);
let (nx, ny) = unsafe {
let dpi = GetDpiForWindow(h);
let scale = if dpi == 0 { 1.0 } else { f64::from(dpi) / 96.0 };
let mut pt = POINT {
x: (x * scale) as i32,
y: (y * scale) as i32,
};
let _ = ClientToScreen(h, &mut pt);
let vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
let vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
let vw = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let vh = GetSystemMetrics(SM_CYVIRTUALSCREEN);
if vw <= 1 || vh <= 1 {
return Err("virtual screen metrics unavailable".to_string());
}
let nx = ((f64::from(pt.x - vx)) * 65535.0 / f64::from(vw - 1)) as i32;
let ny = ((f64::from(pt.y - vy)) * 65535.0 / f64::from(vh - 1)) as i32;
(nx, ny)
};
let make = |flags: MOUSE_EVENT_FLAGS| INPUT {
r#type: INPUT_MOUSE,
Anonymous: INPUT_0 {
mi: MOUSEINPUT {
dx: nx,
dy: ny,
mouseData: 0,
dwFlags: flags,
time: 0,
dwExtraInfo: 0,
},
},
};
let base = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
let inputs = [
make(base | MOUSEEVENTF_MOVE),
make(base | MOUSEEVENTF_LEFTDOWN),
make(base | MOUSEEVENTF_LEFTUP),
];
let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
let sent = unsafe { SendInput(&inputs, cb) } as usize;
if sent == inputs.len() {
Ok(())
} else {
Err(format!(
"SendInput delivered {sent}/{} mouse events",
inputs.len()
))
}
}