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}