# lupa 🔍
[](https://crates.io/crates/lupa)
[](https://docs.rs/lupa)
[](https://github.com/NameOfShadow/lupa/blob/main/LICENSE)
**Interactive object inspector for Rust** – a drop‑in replacement for `dbg!` that opens a local web UI **and** a terminal interface (TUI) where you can explore your structs as a collapsible tree, diff snapshots, and search fields – all with zero configuration.
## Web UI
**Snapshots tab** – collapsible tree view with syntax highlighting

**Diffs tab** – line‑by‑line diff view

## TUI
**Snapshots panel** – navigate with ↑/↓, expand with Enter

**Diffs panel** – coloured diff lines

## Features
- **Zero setup** – just add `lupa` to your `Cargo.toml` and start using `inspect!`.
- **Web inspector** – automatic HTTP + WebSocket server on `localhost:7777` with a modern, reactive UI.
- **Terminal inspector** – full‑screen TUI with navigation, syntax highlighting, and diff view.
- **Snapshot diffing** – capture “before” and “after” states and see exactly what changed line by line.
- **Works everywhere** – in async runtimes (tokio, async‑std), in CLI tools, in web servers – anywhere Rust runs.
- **Lightweight** – only pulls in dependencies you need via feature flags (`web` enabled by default, `tui` optional).
## Quick start
Add `lupa` to your `Cargo.toml`:
```toml
[dependencies]
lupa = "0.1"
```
Then replace `dbg!` with `inspect!`:
```rust
use lupa::{inspect, snapshot, snapshot_diff};
#[derive(Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let mut user = User { name: "Alice".into(), age: 30 };
inspect!(user); // snapshot appears in web UI
let before = snapshot!(user); // capture before state
user.age = 31;
inspect!(user); // second snapshot
snapshot_diff!(before, user); // diff shown in inspector
// Keep the web server alive (or use run() / run_mode())
lupa::keep_alive();
}
```
Open [http://localhost:7777](http://localhost:7777) in your browser – you’ll see both snapshots and the diff.
## Using the TUI (terminal interface)
Enable the `tui` feature:
```toml
lupa = { version = "0.1", features = ["tui"] }
```
Then run the terminal inspector instead of the web server:
```rust
lupa::run_mode(lupa::RunMode::Tui).unwrap();
```
Key bindings:
- `↑` / `↓` – move selection
- `Tab` – switch between Snapshots and Diffs panels
- `Enter` – expand / collapse the detail view
- `PgUp` / `PgDn` – scroll
- `q` / `Esc` / `Ctrl+C` – quit
## Runtime mode selection
When both `web` and `tui` features are enabled, you can choose which interface(s) to run:
```rust
use lupa::RunMode;
// Only web server (blocks until Ctrl+C)
lupa::run_mode(RunMode::Web).unwrap();
// Only TUI (blocks until user quits)
lupa::run_mode(RunMode::Tui).unwrap();
// Both: web server in background, TUI in foreground
lupa::run_mode(RunMode::Both).unwrap();
```
The default `lupa::run()` automatically picks `Web` if only `web` is enabled, `Tui` if only `tui` is enabled, and `Both` if both are enabled.
## Integration with web frameworks (Axum, Actix, etc.)
Because `lupa` stores all data in a global thread‑safe state, you can call `inspect!` from any thread, including inside async request handlers. Here is a complete example using **Axum**:
```rust
use axum::extract::State;
use axum::{routing::get, Router};
use lupa::{inspect, snapshot, snapshot_diff};
use std::sync::{Arc, Mutex};
use tokio::net::TcpListener;
struct AppState {
counter: u32,
}
async fn increment(State(state): State<Arc<Mutex<AppState>>>) -> String {
let mut s = state.lock().unwrap();
let before = snapshot!(s.counter);
s.counter += 1;
inspect!(s.counter);
snapshot_diff!(before, s.counter);
format!("Counter is now {}", s.counter)
}
#[tokio::main]
async fn main() {
let state = Arc::new(Mutex::new(AppState { counter: 0 }));
let app = Router::new()
.route("/inc", get(increment))
.with_state(state);
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
lupa::run_mode(lupa::RunMode::Tui).unwrap();
}
```
Every HTTP request that hits `/inc` will record a new snapshot and diff in the lupa inspector.
## Macros reference
| `inspect!(expr)` | Takes a debug‑printable expression, captures a snapshot, and sends it to the inspector. Returns a reference to the value. |
| `snapshot!(expr)` | Captures a snapshot and returns it as a `lupa::Snapshot` value for later diffing. |
| `snapshot_diff!(snapshot, expr)` | Compares an old snapshot with a new expression, computes a line diff, and sends it to the inspector. |
All macros automatically capture the source file path, line number, and the expression text as a label.
## Feature flags
| `web` | ✅ | Enables the HTTP + WebSocket server and the web UI. |
| `tui` | ❌ | Enables the terminal interface (ratatui + crossterm). |
You can disable default features to use only the TUI:
```toml
lupa = { version = "0.1", default-features = false, features = ["tui"] }
```
## Configuration
- **Port**: set the environment variable `LUPA_PORT` to change the HTTP port (the WebSocket port becomes `LUPA_PORT + 1`). Default is `7777`.
## How it works
1. `inspect!` and `snapshot!` capture the `Debug` representation of your value (`{:#?}`) together with the file, line, and a label.
2. All snapshots are stored in a global, thread‑safe `InspectorState` (append‑only).
3. The web server serves the static UI and provides a JSON API for past snapshots/diffs.
4. A WebSocket connection pushes every new snapshot/diff to all connected browsers in real time.
## License
Licensed under the MIT license. See [LICENSE](LICENSE) for details.
---
*Happy inspecting!*