//! Tauri glue that does not depend on the app's own state: caller checks, shell events, tab //! webview lookups and actions, app-data JSON files, profile data folders and the Windows system //! menu. The app keeps its `#[tauri::command]` wrappers (so IPC names and capabilities stay the //! app's) and calls these. use crate::{ policy, profiles::{self, ProfileSettings}, site::SiteSpec, tabs::{self, TabAction, TabState}, }; use serde::Serialize; use tauri::{AppHandle, Emitter, EventTarget, Manager, Runtime, Webview}; use url::Url; /// Shell-only commands: the trusted local `shell` webview (debug builds also accept the dev server). pub fn shell_only(webview: &Webview) -> Result<(), String> { let url = webview.url().map_err(|_| "FORBIDDEN: cannot verify workspace origin")?; policy::require_local(webview.label(), &url, cfg!(debug_assertions)) } /// Tab-chrome commands: any tab webview, speaking only about itself. Returns its tab id. pub fn tab_only(webview: &Webview) -> Result { policy::tab_id(webview.label()) .map(str::to_owned) .ok_or_else(|| "FORBIDDEN: tab webview required".into()) } /// Site-only commands: a tab webview that is exactly on the site right now. Returns its URL (the /// webview's own, never the page's claim). pub fn site_tab_only(site: &SiteSpec, webview: &Webview) -> Result { let url = webview.url().map_err(|_| "FORBIDDEN: cannot verify origin")?; policy::require_site_tab(site, webview.label(), &url)?; Ok(url) } pub fn emit_shell(app: &AppHandle, event: &str, payload: S) { let _ = app.emit_to(EventTarget::webview(policy::SHELL_LABEL), event, payload); } pub fn emit_tab_state(app: &AppHandle, state: TabState) { emit_shell(app, tabs::EVENT_TAB_STATE, state); } pub fn tab_webview(app: &AppHandle, id: &str) -> Option> { app.get_webview(&policy::tab_label(id)) } /// Evaluates `script` in every tab that is on the site right now. pub fn eval_in_site_tabs(app: &AppHandle, site: &SiteSpec, script: &str) { for (label, webview) in app.webviews() { if policy::tab_id(&label).is_some() && webview.url().is_ok_and(|url| site.is_origin(&url)) { let _ = webview.eval(script); } } } pub fn data_file(app: &AppHandle, name: &str) -> Option { app.path().app_data_dir().ok().map(|dir| dir.join(name)) } pub fn read_json(app: &AppHandle, name: &str) -> Option { serde_json::from_str(&std::fs::read_to_string(data_file(app, name)?).ok()?).ok() } pub fn write_json(app: &AppHandle, name: &str, value: &T) -> Result<(), String> { let path = data_file(app, name).ok_or("STORAGE: no application data directory")?; let json = serde_json::to_string_pretty(value).map_err(|_| "STORAGE: serialization failed")?; std::fs::write(path, json).map_err(|error| format!("STORAGE: {error}")) } pub fn navigate_tab(app: &AppHandle, id: &str, url: &str) -> Result<(), String> { let url = tabs::parse_web_url(url)?; let tab = tab_webview(app, id).ok_or("TAB: not found")?; tab.navigate(url).map_err(|error| format!("TAB: {error}")) } pub fn tab_action(app: &AppHandle, id: &str, action: &str) -> Result<(), String> { let action = TabAction::parse(action)?; let tab = tab_webview(app, id).ok_or("TAB: not found")?; let result = match action { TabAction::Back => tab.eval("history.back()"), TabAction::Forward => tab.eval("history.forward()"), TabAction::Reload => tab.reload(), TabAction::Stop => tab.eval("window.stop()"), TabAction::Focus => tab.set_focus(), }; result.map_err(|error| format!("TAB: {error}")) } /// A browser shortcut pressed inside tab `id`, forwarded to the shell (`tab-shortcut`). pub fn forward_shortcut(app: &AppHandle, id: String, shortcut: String) -> Result<(), String> { if !tabs::valid_shortcut(&shortcut) { return Err("VALIDATION: unknown shortcut".into()); } #[derive(Clone, Serialize)] struct Shortcut { id: String, shortcut: String, } emit_shell(app, tabs::EVENT_TAB_SHORTCUT, Shortcut { id, shortcut }); Ok(()) } /// A link drag started/entered/ended in tab `id` (`link-drag`); the link opens only as a new tab. pub fn forward_link_drag(app: &AppHandle, id: String, phase: String, url: Option) -> Result<(), String> { let url = tabs::link_drag(&phase, url)?; #[derive(Clone, Serialize)] struct LinkDrag { id: String, phase: String, url: Option, } emit_shell(app, tabs::EVENT_LINK_DRAG, LinkDrag { id, phase, url }); Ok(()) } /// Deletes the WebView data folders (the logins) of removed profiles. pub fn remove_profile_dirs(app: &AppHandle, site: &SiteSpec, removed: &[String]) { let Ok(local) = app.path().app_local_data_dir() else { return }; for id in removed { if let Some(dir) = profiles::data_dir(&local, id) { if let Err(error) = std::fs::remove_dir_all(&dir) { if error.kind() != std::io::ErrorKind::NotFound { eprintln!("{}: could not remove profile data {}: {error}", site.product, dir.display()); } } } } } /// Deletes WebView data folders of profiles that no longer exist (see [`profiles::orphan_dirs`]). pub fn prune_profile_dirs(app: &AppHandle, settings: &ProfileSettings) { let Ok(local) = app.path().app_local_data_dir() else { return }; for dir in profiles::orphan_dirs(&local, settings) { let _ = std::fs::remove_dir_all(dir); } } /// Moves app-data and app-local data (WebView profile, i.e. the site login) from a previous bundle /// identifier. Never overwrites an existing new folder. pub fn migrate_identifier(app: &AppHandle, site: &SiteSpec, legacy_identifier: &str) { let dirs = [app.path().app_data_dir(), app.path().app_local_data_dir()]; for new_dir in dirs.into_iter().flatten() { let Some(parent) = new_dir.parent() else { continue }; let old_dir = parent.join(legacy_identifier); if old_dir.is_dir() && !new_dir.exists() { if let Err(error) = std::fs::rename(&old_dir, &new_dir) { eprintln!("{}: could not migrate {}: {error}", site.product, old_dir.display()); } } } } /// Shows the native window system menu (Restore, Move, Size, Minimize, Maximize, Close) at a /// screen position, for a right-click on the custom titlebar. No-op outside Windows. pub fn system_menu(webview: &Webview, x: f64, y: f64) -> Result<(), String> { if !x.is_finite() || !y.is_finite() { return Err("VALIDATION: non-finite position".into()); } #[cfg(windows)] { use windows::Win32::Foundation::{LPARAM, WPARAM}; use windows::Win32::UI::WindowsAndMessaging::{ GetSystemMenu, PostMessageW, SetForegroundWindow, TrackPopupMenu, TPM_RETURNCMD, TPM_RIGHTBUTTON, WM_SYSCOMMAND, }; let hwnd = webview.window().hwnd().map_err(|error| format!("WINDOW: {error}"))?; // SAFETY: plain Win32 calls on our own top-level window handle. unsafe { let menu = GetSystemMenu(hwnd, false); if menu.is_invalid() { return Ok(()); } let _ = SetForegroundWindow(hwnd); let command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON, x as i32, y as i32, None, hwnd, None); if command.0 != 0 { let _ = PostMessageW(Some(hwnd), WM_SYSCOMMAND, WPARAM(command.0 as usize), LPARAM(0)); } } } #[cfg(not(windows))] let _ = webview; Ok(()) }