1use tauri::{Manager, Runtime};
2use victauri_core::WindowState;
3
4pub trait WebviewBridge: Send + Sync {
8 fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String>;
14 fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState>;
16 fn list_window_labels(&self) -> Vec<String>;
18 fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String>;
25 fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String>;
31 fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String>;
37 fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String>;
43 fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String>;
49
50 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 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 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 fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
105 Err("backend access not available".to_string())
106 }
107
108 fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
114 Err("backend access not available".to_string())
115 }
116
117 fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
123 Err("backend access not available".to_string())
124 }
125
126 fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
132 Err("backend access not available".to_string())
133 }
134
135 #[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
158fn on_main<R, T, F>(app: &tauri::AppHandle<R>, what: &str, f: F) -> Result<T, String>
183where
184 R: Runtime,
185 T: Send + 'static,
186 F: FnOnce(&tauri::AppHandle<R>) -> T + Send + 'static,
187{
188 use std::sync::atomic::{AtomicBool, Ordering};
189 let (tx, rx) = std::sync::mpsc::channel();
190 let app_for_closure = app.clone();
191 let abandoned = std::sync::Arc::new(AtomicBool::new(false));
195 let abandoned_closure = abandoned.clone();
196 app.run_on_main_thread(move || {
197 if abandoned_closure.load(Ordering::Acquire) {
198 return;
199 }
200 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&app_for_closure)));
201 let _ = tx.send(result);
203 })
204 .map_err(|e| format!("failed to dispatch {what} to the main thread: {e}"))?;
205 let timeout = std::time::Duration::from_secs(10);
210 let received = match tokio::runtime::Handle::try_current().map(|h| h.runtime_flavor()) {
211 Ok(tokio::runtime::RuntimeFlavor::MultiThread) => {
212 tokio::task::block_in_place(|| rx.recv_timeout(timeout))
213 }
214 _ => rx.recv_timeout(timeout),
215 };
216 match received {
217 Ok(Ok(value)) => Ok(value),
218 Ok(Err(_panic)) => Err(format!("{what} panicked on the main thread")),
219 Err(e) => {
220 abandoned.store(true, Ordering::Release);
221 Err(format!("{what} did not complete on the main thread: {e}"))
222 }
223 }
224}
225
226impl<R: Runtime> WebviewBridge for tauri::AppHandle<R> {
227 fn eval_webview(&self, label: Option<&str>, script: &str) -> Result<(), String> {
228 let label = label.map(str::to_string);
229 let script = script.to_string();
230 on_main(self, "eval_webview", move |app| {
231 let windows = app.webview_windows();
232 let webview = find_window(&windows, label.as_deref())?;
233 webview.eval(&script).map_err(|e| e.to_string())
234 })?
235 }
236
237 fn get_window_states(&self, label: Option<&str>) -> Vec<WindowState> {
238 let label = label.map(str::to_string);
239 on_main(self, "get_window_states", move |app| {
240 let windows = app.webview_windows();
241 let mut states = Vec::new();
242
243 for (win_label, window) in &windows {
244 if let Some(filter) = label.as_deref()
245 && win_label != filter
246 {
247 continue;
248 }
249
250 let pos = window.outer_position().unwrap_or_default();
251 let size = window.inner_size().unwrap_or_default();
252
253 states.push(WindowState {
254 label: win_label.clone(),
255 title: window.title().unwrap_or_default(),
256 url: window.url().map(|u| u.to_string()).unwrap_or_default(),
257 visible: window.is_visible().unwrap_or(false),
258 focused: window.is_focused().unwrap_or(false),
259 maximized: window.is_maximized().unwrap_or(false),
260 minimized: window.is_minimized().unwrap_or(false),
261 fullscreen: window.is_fullscreen().unwrap_or(false),
262 position: (pos.x, pos.y),
263 size: (size.width, size.height),
264 });
265 }
266
267 states
268 })
269 .unwrap_or_else(|e| {
272 tracing::warn!("get_window_states: {e}");
273 Vec::new()
274 })
275 }
276
277 fn list_window_labels(&self) -> Vec<String> {
278 on_main(self, "list_window_labels", |app| {
279 app.webview_windows().keys().cloned().collect()
280 })
281 .unwrap_or_else(|e| {
282 tracing::warn!("list_window_labels: {e}");
283 Vec::new()
284 })
285 }
286
287 fn get_native_handle(&self, label: Option<&str>) -> Result<isize, String> {
288 let label = label.map(str::to_string);
289 on_main(self, "get_native_handle", move |app| {
290 use raw_window_handle::{HasWindowHandle, RawWindowHandle};
291
292 let windows = app.webview_windows();
293 let _webview = find_window(&windows, label.as_deref())?;
294 let handle = _webview.window_handle().map_err(|e| e.to_string())?;
295 match handle.as_raw() {
296 #[cfg(windows)]
297 RawWindowHandle::Win32(h) => Ok(h.hwnd.get()),
298 #[cfg(target_os = "macos")]
299 RawWindowHandle::AppKit(h) => {
300 macos_window_number(h.ns_view.as_ptr())
303 }
304 #[cfg(target_os = "linux")]
305 RawWindowHandle::Xlib(h) => Ok(h.window as isize),
306 #[cfg(target_os = "linux")]
307 RawWindowHandle::Xcb(h) => Ok(h.window.get() as isize),
308 _ => Err("unsupported window handle type on this platform".to_string()),
309 }
310 })?
311 }
312
313 #[cfg(windows)]
314 fn native_type_text(&self, label: Option<&str>, text: &str) -> Result<(), String> {
315 let hwnd = self.get_native_handle(label)?;
316 win_focus(hwnd);
317 win_send_text(text)
318 }
319
320 #[cfg(windows)]
321 fn native_key(&self, label: Option<&str>, key: &str) -> Result<(), String> {
322 let hwnd = self.get_native_handle(label)?;
323 win_focus(hwnd);
324 win_send_key(key)
325 }
326
327 #[cfg(windows)]
328 fn native_click(&self, label: Option<&str>, x: f64, y: f64) -> Result<(), String> {
329 let hwnd = self.get_native_handle(label)?;
330 win_focus(hwnd);
331 win_click(hwnd, x, y)
332 }
333
334 fn manage_window(&self, label: Option<&str>, action: &str) -> Result<String, String> {
335 let label = label.map(str::to_string);
336 let action = action.to_string();
337 on_main(self, "manage_window", move |app| {
338 let windows = app.webview_windows();
339 let window = find_window(&windows, label.as_deref())?;
340
341 match action.as_str() {
342 "minimize" => window.minimize().map_err(|e| e.to_string())?,
343 "unminimize" => window.unminimize().map_err(|e| e.to_string())?,
344 "maximize" => window.maximize().map_err(|e| e.to_string())?,
345 "unmaximize" => window.unmaximize().map_err(|e| e.to_string())?,
346 "close" => window.close().map_err(|e| e.to_string())?,
347 "focus" => window.set_focus().map_err(|e| e.to_string())?,
348 "show" => window.show().map_err(|e| e.to_string())?,
349 "hide" => window.hide().map_err(|e| e.to_string())?,
350 "fullscreen" => window.set_fullscreen(true).map_err(|e| e.to_string())?,
351 "unfullscreen" => window.set_fullscreen(false).map_err(|e| e.to_string())?,
352 "always_on_top" => window.set_always_on_top(true).map_err(|e| e.to_string())?,
353 "not_always_on_top" => {
354 window.set_always_on_top(false).map_err(|e| e.to_string())?;
355 }
356 _ => return Err(format!("unknown action: {action}")),
357 }
358
359 Ok(format!("{action} executed"))
360 })?
361 }
362
363 fn resize_window(&self, label: Option<&str>, width: u32, height: u32) -> Result<(), String> {
364 let label = label.map(str::to_string);
365 on_main(self, "resize_window", move |app| {
366 let windows = app.webview_windows();
367 let window = find_window(&windows, label.as_deref())?;
368
369 window
370 .set_size(tauri::LogicalSize::new(width, height))
371 .map_err(|e| e.to_string())
372 })?
373 }
374
375 fn move_window(&self, label: Option<&str>, x: i32, y: i32) -> Result<(), String> {
376 let label = label.map(str::to_string);
377 on_main(self, "move_window", move |app| {
378 let windows = app.webview_windows();
379 let window = find_window(&windows, label.as_deref())?;
380
381 window
382 .set_position(tauri::LogicalPosition::new(x, y))
383 .map_err(|e| e.to_string())
384 })?
385 }
386
387 fn set_window_title(&self, label: Option<&str>, title: &str) -> Result<(), String> {
388 let label = label.map(str::to_string);
389 let title = title.to_string();
390 on_main(self, "set_window_title", move |app| {
391 let windows = app.webview_windows();
392 let window = find_window(&windows, label.as_deref())?;
393
394 window.set_title(&title).map_err(|e| e.to_string())
395 })?
396 }
397
398 fn app_data_dir(&self) -> Result<std::path::PathBuf, String> {
399 self.path().app_data_dir().map_err(|e| e.to_string())
400 }
401
402 fn app_config_dir(&self) -> Result<std::path::PathBuf, String> {
403 self.path().app_config_dir().map_err(|e| e.to_string())
404 }
405
406 fn app_log_dir(&self) -> Result<std::path::PathBuf, String> {
407 self.path().app_log_dir().map_err(|e| e.to_string())
408 }
409
410 fn app_local_data_dir(&self) -> Result<std::path::PathBuf, String> {
411 self.path().app_local_data_dir().map_err(|e| e.to_string())
412 }
413
414 fn tauri_config(&self) -> serde_json::Value {
415 let config = self.config();
416
417 let windows: Vec<serde_json::Value> = config
418 .app
419 .windows
420 .iter()
421 .map(|w| {
422 serde_json::json!({
423 "label": w.label,
424 "title": w.title,
425 "url": format!("{}", w.url),
426 "width": w.width,
427 "height": w.height,
428 "visible": w.visible,
429 "resizable": w.resizable,
430 "fullscreen": w.fullscreen,
431 "decorations": w.decorations,
432 "transparent": w.transparent,
433 "always_on_top": w.always_on_top,
434 })
435 })
436 .collect();
437
438 let plugins: Vec<String> = config.plugins.0.keys().cloned().collect();
439
440 let security = serde_json::json!({
441 "csp": config.app.security.csp.as_ref().map(|c| format!("{c}")),
442 "freeze_prototype": config.app.security.freeze_prototype,
443 "capabilities": config.app.security.capabilities.iter().map(|c| {
444 match c {
445 tauri::utils::config::CapabilityEntry::Inlined(cap) => {
446 serde_json::json!({
447 "identifier": cap.identifier,
448 "description": cap.description,
449 "windows": cap.windows,
450 "webviews": cap.webviews,
451 "permissions": cap.permissions.iter().map(|p| format!("{p:?}")).collect::<Vec<_>>(),
452 "platforms": cap.platforms,
453 })
454 }
455 tauri::utils::config::CapabilityEntry::Reference(path) => {
456 serde_json::json!({ "reference": path })
457 }
458 }
459 }).collect::<Vec<_>>(),
460 });
461
462 serde_json::json!({
463 "identifier": config.identifier,
464 "product_name": config.product_name,
465 "version": config.version,
466 "windows": windows,
467 "plugins": plugins,
468 "security": security,
469 })
470 }
471}
472
473#[cfg(target_os = "macos")]
474#[allow(unsafe_code)]
475fn macos_window_number(ns_view: *mut std::ffi::c_void) -> Result<isize, String> {
476 unsafe extern "C" {
477 fn objc_msgSend(obj: *mut std::ffi::c_void, sel: *mut std::ffi::c_void) -> isize;
478 fn sel_registerName(name: *const std::ffi::c_char) -> *mut std::ffi::c_void;
479 }
480
481 if ns_view.is_null() {
482 return Err("null NSView handle".to_string());
483 }
484
485 unsafe {
489 let sel_window = sel_registerName(c"window".as_ptr());
490 let ns_window = objc_msgSend(ns_view, sel_window);
491 if ns_window == 0 {
492 return Err("NSView has no parent NSWindow".to_string());
493 }
494 let sel_window_number = sel_registerName(c"windowNumber".as_ptr());
495 let ns_window_ptr = ns_window as *mut std::ffi::c_void;
496 let window_number = objc_msgSend(ns_window_ptr, sel_window_number);
497 if window_number <= 0 {
498 return Err(format!("invalid CGWindowID: {window_number}"));
499 }
500 Ok(window_number)
501 }
502}
503
504#[cfg(windows)]
510fn win_hwnd(hwnd: isize) -> windows::Win32::Foundation::HWND {
511 windows::Win32::Foundation::HWND(hwnd as *mut core::ffi::c_void)
512}
513
514#[allow(unsafe_code)]
517#[cfg(windows)]
518fn win_focus(hwnd: isize) {
519 use windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow;
520 unsafe {
523 let _ = SetForegroundWindow(win_hwnd(hwnd));
524 }
525 std::thread::sleep(std::time::Duration::from_millis(40));
526}
527
528#[cfg(windows)]
529fn win_keyboard_input(
530 vk: u16,
531 scan: u16,
532 key_up: bool,
533 unicode: bool,
534) -> windows::Win32::UI::Input::KeyboardAndMouse::INPUT {
535 use windows::Win32::UI::Input::KeyboardAndMouse::{
536 INPUT, INPUT_0, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP,
537 KEYEVENTF_UNICODE, VIRTUAL_KEY,
538 };
539 let mut flags = KEYBD_EVENT_FLAGS(0);
540 if unicode {
541 flags |= KEYEVENTF_UNICODE;
542 }
543 if key_up {
544 flags |= KEYEVENTF_KEYUP;
545 }
546 INPUT {
547 r#type: INPUT_KEYBOARD,
548 Anonymous: INPUT_0 {
549 ki: KEYBDINPUT {
550 wVk: VIRTUAL_KEY(vk),
551 wScan: scan,
552 dwFlags: flags,
553 time: 0,
554 dwExtraInfo: 0,
555 },
556 },
557 }
558}
559
560#[allow(unsafe_code)]
562#[cfg(windows)]
563fn win_send_text(text: &str) -> Result<(), String> {
564 use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
565 let mut inputs: Vec<INPUT> = Vec::new();
566 for unit in text.encode_utf16() {
567 inputs.push(win_keyboard_input(0, unit, false, true));
568 inputs.push(win_keyboard_input(0, unit, true, true));
569 }
570 if inputs.is_empty() {
571 return Ok(());
572 }
573 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
574 let sent = unsafe { SendInput(&inputs, cb) } as usize;
576 if sent == inputs.len() {
577 Ok(())
578 } else {
579 Err(format!(
580 "SendInput delivered {sent}/{} key events",
581 inputs.len()
582 ))
583 }
584}
585
586#[cfg(windows)]
588fn win_vk_for_key(key: &str) -> Option<u16> {
589 use windows::Win32::UI::Input::KeyboardAndMouse as k;
590 let vk = match key {
591 "Enter" | "Return" => k::VK_RETURN,
592 "Tab" => k::VK_TAB,
593 "Escape" | "Esc" => k::VK_ESCAPE,
594 "Backspace" => k::VK_BACK,
595 "Delete" | "Del" => k::VK_DELETE,
596 "ArrowUp" | "Up" => k::VK_UP,
597 "ArrowDown" | "Down" => k::VK_DOWN,
598 "ArrowLeft" | "Left" => k::VK_LEFT,
599 "ArrowRight" | "Right" => k::VK_RIGHT,
600 "Home" => k::VK_HOME,
601 "End" => k::VK_END,
602 "PageUp" => k::VK_PRIOR,
603 "PageDown" => k::VK_NEXT,
604 "Space" | " " => k::VK_SPACE,
605 "F1" => k::VK_F1,
606 "F2" => k::VK_F2,
607 "F3" => k::VK_F3,
608 "F4" => k::VK_F4,
609 "F5" => k::VK_F5,
610 "F6" => k::VK_F6,
611 "F7" => k::VK_F7,
612 "F8" => k::VK_F8,
613 "F9" => k::VK_F9,
614 "F10" => k::VK_F10,
615 "F11" => k::VK_F11,
616 "F12" => k::VK_F12,
617 _ => return None,
618 };
619 Some(vk.0)
620}
621
622#[allow(unsafe_code)]
624#[cfg(windows)]
625fn win_send_key(key: &str) -> Result<(), String> {
626 use windows::Win32::UI::Input::KeyboardAndMouse::{INPUT, SendInput};
627 let inputs: Vec<INPUT> = if let Some(vk) = win_vk_for_key(key) {
628 vec![
629 win_keyboard_input(vk, 0, false, false),
630 win_keyboard_input(vk, 0, true, false),
631 ]
632 } else {
633 let mut chars = key.chars();
635 let (Some(c), None) = (chars.next(), chars.next()) else {
636 return Err(format!(
637 "unknown key '{key}' (use a named key or a single character)"
638 ));
639 };
640 let mut buf = [0u16; 2];
641 let mut v = Vec::new();
642 for unit in c.encode_utf16(&mut buf) {
643 v.push(win_keyboard_input(0, *unit, false, true));
644 v.push(win_keyboard_input(0, *unit, true, true));
645 }
646 v
647 };
648 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
649 let sent = unsafe { SendInput(&inputs, cb) } as usize;
651 if sent == inputs.len() {
652 Ok(())
653 } else {
654 Err(format!(
655 "SendInput delivered {sent}/{} key events",
656 inputs.len()
657 ))
658 }
659}
660
661#[allow(unsafe_code)]
664#[cfg(windows)]
665fn win_click(hwnd: isize, x: f64, y: f64) -> Result<(), String> {
666 use windows::Win32::Foundation::POINT;
667 use windows::Win32::Graphics::Gdi::ClientToScreen;
668 use windows::Win32::UI::HiDpi::GetDpiForWindow;
669 use windows::Win32::UI::Input::KeyboardAndMouse::{
670 INPUT, INPUT_0, INPUT_MOUSE, MOUSE_EVENT_FLAGS, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN,
671 MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_VIRTUALDESK, MOUSEINPUT, SendInput,
672 };
673 use windows::Win32::UI::WindowsAndMessaging::{
674 GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
675 SM_YVIRTUALSCREEN,
676 };
677 let h = win_hwnd(hwnd);
678 let (nx, ny) = unsafe {
681 let dpi = GetDpiForWindow(h);
682 let scale = if dpi == 0 { 1.0 } else { f64::from(dpi) / 96.0 };
683 let mut pt = POINT {
684 x: (x * scale) as i32,
685 y: (y * scale) as i32,
686 };
687 let _ = ClientToScreen(h, &mut pt);
688 let vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
689 let vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
690 let vw = GetSystemMetrics(SM_CXVIRTUALSCREEN);
691 let vh = GetSystemMetrics(SM_CYVIRTUALSCREEN);
692 if vw <= 1 || vh <= 1 {
693 return Err("virtual screen metrics unavailable".to_string());
694 }
695 let nx = ((f64::from(pt.x - vx)) * 65535.0 / f64::from(vw - 1)) as i32;
696 let ny = ((f64::from(pt.y - vy)) * 65535.0 / f64::from(vh - 1)) as i32;
697 (nx, ny)
698 };
699 let make = |flags: MOUSE_EVENT_FLAGS| INPUT {
700 r#type: INPUT_MOUSE,
701 Anonymous: INPUT_0 {
702 mi: MOUSEINPUT {
703 dx: nx,
704 dy: ny,
705 mouseData: 0,
706 dwFlags: flags,
707 time: 0,
708 dwExtraInfo: 0,
709 },
710 },
711 };
712 let base = MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
713 let inputs = [
714 make(base | MOUSEEVENTF_MOVE),
715 make(base | MOUSEEVENTF_LEFTDOWN),
716 make(base | MOUSEEVENTF_LEFTUP),
717 ];
718 let cb = i32::try_from(std::mem::size_of::<INPUT>()).unwrap_or(0);
719 let sent = unsafe { SendInput(&inputs, cb) } as usize;
721 if sent == inputs.len() {
722 Ok(())
723 } else {
724 Err(format!(
725 "SendInput delivered {sent}/{} mouse events",
726 inputs.len()
727 ))
728 }
729}