Skip to main content

dioxus_inspector/
lib.rs

1//! # Dioxus Inspector
2//!
3//! HTTP bridge for inspecting and debugging Dioxus Desktop apps.
4//! Embed this in your app to enable MCP-based debugging from Claude Code.
5//!
6//! ## Quick Start
7//!
8//! ```rust,ignore
9//! use dioxus::prelude::*;
10//! use dioxus_inspector::{start_bridge, EvalResponse};
11//!
12//! fn main() {
13//!     dioxus::launch(app);
14//! }
15//!
16//! fn app() -> Element {
17//!     use_inspector_bridge(9999, "my-app");
18//!     rsx! { div { "Hello, inspector!" } }
19//! }
20//!
21//! fn use_inspector_bridge(port: u16, app_name: &str) {
22//!     use_hook(|| {
23//!         let mut eval_rx = start_bridge(port, app_name);
24//!         spawn(async move {
25//!             while let Some(cmd) = eval_rx.recv().await {
26//!                 let result = document::eval(&cmd.script).await;
27//!                 let response = match result {
28//!                     Ok(val) => EvalResponse::success(val.to_string()),
29//!                     Err(e) => EvalResponse::error(e.to_string()),
30//!                 };
31//!                 let _ = cmd.response_tx.send(response);
32//!             }
33//!         });
34//!     });
35//! }
36//! ```
37//!
38//! ## Architecture
39//!
40//! ```text
41//! Claude Code <--MCP--> dioxus-mcp <--HTTP--> dioxus-inspector <--eval--> WebView
42//! ```
43//!
44//! 1. Call [`start_bridge`] with a port and app name
45//! 2. Poll the returned receiver for [`EvalCommand`]s
46//! 3. Execute JavaScript via `document::eval()` and send responses back
47//!
48//! ## HTTP Endpoints
49//!
50//! | Endpoint | Method | Purpose |
51//! |----------|--------|---------|
52//! | `/status` | GET | App status, PID, uptime |
53//! | `/eval` | POST | Execute JavaScript in webview |
54//! | `/query` | POST | Query DOM by CSS selector |
55//! | `/dom` | GET | Get simplified DOM tree |
56//! | `/inspect` | POST | Element visibility analysis |
57//! | `/validate-classes` | POST | Check CSS class availability |
58//! | `/diagnose` | GET | Quick UI health check |
59//! | `/screenshot` | POST | Capture window (macOS only) |
60//! | `/resize` | POST | Resize window (requires app handling) |
61//!
62//! ## Platform Support
63//!
64//! - **Screenshot capture**: macOS only (uses Core Graphics)
65//! - **All other features**: Cross-platform
66
67mod handlers;
68mod screenshot;
69mod types;
70
71pub use types::{
72    EvalCommand, EvalRequest, EvalResponse, QueryRequest, ResizeRequest, ResizeResponse,
73    StatusResponse,
74};
75
76use axum::{routing::get, Router};
77use std::sync::Arc;
78use tokio::sync::mpsc;
79
80/// Shared state for the HTTP bridge.
81///
82/// This struct holds the internal state used by the Axum handlers.
83/// You don't need to interact with this directly.
84pub struct BridgeState {
85    /// The application name, used in status responses.
86    pub app_name: String,
87    /// Channel to send eval commands to the Dioxus app.
88    pub eval_tx: mpsc::Sender<EvalCommand>,
89    /// When the bridge was started, for uptime calculation.
90    pub started_at: std::time::Instant,
91    /// Process ID of the running application.
92    pub pid: u32,
93}
94
95/// Start the inspector HTTP bridge.
96///
97/// Returns a receiver that your Dioxus app should poll to execute JavaScript.
98/// The bridge listens on `127.0.0.1:{port}`.
99///
100/// # Example
101///
102/// ```rust,ignore
103/// let mut eval_rx = start_bridge(9999, "my-app");
104/// spawn(async move {
105///     while let Some(cmd) = eval_rx.recv().await {
106///         let result = document::eval(&cmd.script).await;
107///         let response = match result {
108///             Ok(val) => EvalResponse::success(val.to_string()),
109///             Err(e) => EvalResponse::error(e.to_string()),
110///         };
111///         let _ = cmd.response_tx.send(response);
112///     }
113/// });
114/// ```
115#[cfg(not(tarpaulin_include))]
116pub fn start_bridge(port: u16, app_name: impl Into<String>) -> mpsc::Receiver<EvalCommand> {
117    let (eval_tx, eval_rx) = mpsc::channel::<EvalCommand>(32);
118
119    let state = Arc::new(BridgeState {
120        app_name: app_name.into(),
121        eval_tx,
122        started_at: std::time::Instant::now(),
123        pid: std::process::id(),
124    });
125
126    let app = Router::new()
127        .route("/status", get(handlers::status))
128        .route("/eval", axum::routing::post(handlers::eval))
129        .route("/query", axum::routing::post(handlers::query))
130        .route("/dom", get(handlers::dom))
131        .route("/inspect", axum::routing::post(handlers::inspect))
132        .route(
133            "/validate-classes",
134            axum::routing::post(handlers::validate_classes),
135        )
136        .route("/diagnose", get(handlers::diagnose))
137        .route("/screenshot", axum::routing::post(handlers::screenshot))
138        .route("/resize", axum::routing::post(handlers::resize))
139        .with_state(state);
140
141    tokio::spawn(async move {
142        let addr = format!("127.0.0.1:{}", port);
143        let listener = match tokio::net::TcpListener::bind(&addr).await {
144            Ok(l) => l,
145            Err(e) => {
146                tracing::error!("Failed to bind inspector bridge on {}: {}", addr, e);
147                return;
148            }
149        };
150
151        tracing::info!("Inspector bridge listening on http://{}", addr);
152        let _ = axum::serve(listener, app).await;
153    });
154
155    eval_rx
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_bridge_state_creation() {
164        let (tx, _rx) = mpsc::channel(1);
165        let state = BridgeState {
166            app_name: "test".to_string(),
167            eval_tx: tx,
168            started_at: std::time::Instant::now(),
169            pid: 12345,
170        };
171        assert_eq!(state.app_name, "test");
172        assert_eq!(state.pid, 12345);
173    }
174}