tauri_plugin_debug_bridge/
lib.rs1use 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#[derive(Debug, Deserialize, Default)]
23pub struct Config {
24 pub port: Option<u16>,
26}
27
28pub type PendingResults = Arc<Mutex<HashMap<String, oneshot::Sender<EvalResult>>>>;
30
31#[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
39pub struct BridgeState<R: Runtime> {
41 pub app: AppHandle<R>,
42 pub pending: PendingResults,
43}
44
45#[derive(Serialize)]
47struct HealthResponse {
48 status: &'static str,
49 plugin: &'static str,
50 version: &'static str,
51}
52
53#[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
74fn build_router<R: Runtime>(state: Arc<Mutex<BridgeState<R>>>) -> Router {
76 Router::new()
77 .route("/health", get(health))
79 .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 .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 .route("/events/emit", post(events::emit::<R>))
93 .route("/events/list", get(events::list::<R>))
94 .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
109pub 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 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}