Skip to main content

tauri_plugin_debug_bridge/
lib.rs

1use std::{collections::HashMap, sync::Arc};
2
3use axum::{
4    Router,
5    response::Json,
6    routing::{get, post},
7};
8use serde::{Deserialize, Serialize};
9use tauri::{
10    AppHandle, Manager, Runtime,
11    plugin::{Builder, TauriPlugin},
12};
13use tokio::sync::{Mutex, oneshot};
14use tower_http::cors::CorsLayer;
15
16mod backend;
17mod events;
18mod logs;
19mod webview;
20
21/// Plugin configuration, read from tauri.conf.json plugin section.
22#[derive(Debug, Deserialize, Default)]
23pub struct Config {
24    /// Port for the debug HTTP/WS server. Defaults to 9229.
25    pub port: Option<u16>,
26}
27
28/// Pending JS evaluation results, keyed by request ID.
29pub type PendingResults = Arc<Mutex<HashMap<String, oneshot::Sender<EvalResult>>>>;
30
31/// Result from a JS evaluation in the webview.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct EvalResult {
34    pub success: bool,
35    pub value: Option<serde_json::Value>,
36    pub error: Option<String>,
37}
38
39/// Shared state accessible to all axum route handlers.
40pub struct BridgeState<R: Runtime> {
41    pub app: AppHandle<R>,
42    pub pending: PendingResults,
43}
44
45/// Health check response.
46#[derive(Serialize)]
47struct HealthResponse {
48    status: &'static str,
49    plugin: &'static str,
50    version: &'static str,
51}
52
53/// Tauri command: receives JS eval results from the webview.
54/// Called by injected JS via `window.__TAURI__.invoke('plugin:debug-bridge|eval_callback', ...)`.
55#[tauri::command]
56async fn eval_callback(
57    pending: tauri::State<'_, PendingResults>,
58    id: String,
59    success: bool,
60    value: Option<serde_json::Value>,
61    error: Option<String>,
62) -> Result<(), String> {
63    let mut map = pending.lock().await;
64    if let Some(tx) = map.remove(&id) {
65        let _ = tx.send(EvalResult {
66            success,
67            value,
68            error,
69        });
70    }
71    Ok(())
72}
73
74/// Build the axum router with all debug bridge routes.
75fn build_router<R: Runtime>(state: Arc<Mutex<BridgeState<R>>>) -> Router {
76    Router::new()
77        // Health
78        .route("/health", get(health))
79        // Webview
80        .route("/eval", post(webview::webview_eval::<R>))
81        .route("/screenshot", get(webview::screenshot::<R>))
82        .route("/snapshot", get(webview::snapshot::<R>))
83        .route("/click", post(webview::click::<R>))
84        .route("/fill", post(webview::fill::<R>))
85        // Backend
86        .route("/invoke", post(backend::invoke::<R>))
87        .route("/commands", get(backend::commands::<R>))
88        .route("/state", get(backend::state::<R>))
89        .route("/windows", get(backend::windows::<R>))
90        .route("/config", get(backend::config::<R>))
91        // Events
92        .route("/events/emit", post(events::emit::<R>))
93        .route("/events/list", get(events::list::<R>))
94        // Logs (WebSocket)
95        .route("/logs", get(logs::logs_ws::<R>))
96        .route("/console", get(logs::console_ws::<R>))
97        .layer(CorsLayer::permissive())
98        .with_state(state)
99}
100
101async fn health() -> Json<HealthResponse> {
102    Json(HealthResponse {
103        status: "ok",
104        plugin: "tauri-plugin-debug-bridge",
105        version: env!("CARGO_PKG_VERSION"),
106    })
107}
108
109/// Initialize the debug bridge plugin.
110///
111/// ```rust,no_run
112/// // In your Tauri app's lib.rs:
113/// #[cfg(feature = "debug")]
114/// app.plugin(tauri_plugin_debug_bridge::init());
115/// ```
116pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
117    let pending: PendingResults = Arc::new(Mutex::new(HashMap::new()));
118
119    Builder::<R, Option<Config>>::new("debug-bridge")
120        .invoke_handler(tauri::generate_handler![eval_callback])
121        .setup(move |app, api| {
122            let port = api.config().as_ref().and_then(|c| c.port).unwrap_or(9229);
123
124            // Share pending results with both the Tauri command and axum handlers.
125            app.manage(pending.clone());
126
127            let state = Arc::new(Mutex::new(BridgeState {
128                app: app.clone(),
129                pending,
130            }));
131
132            let router = build_router(state);
133
134            tauri::async_runtime::spawn(async move {
135                let addr = format!("127.0.0.1:{port}");
136                tracing::info!("debug-bridge listening on http://{addr}");
137                let listener = tokio::net::TcpListener::bind(&addr)
138                    .await
139                    .expect("failed to bind debug-bridge port");
140                axum::serve(listener, router)
141                    .await
142                    .expect("debug-bridge server error");
143            });
144
145            Ok(())
146        })
147        .build()
148}