lupa 0.1.0

Interactive object inspector for Rust — web UI + TUI + snapshot diffing
Documentation
//! # lupa
//!
//! Interactive object inspector for Rust.
//!
//! Drop-in replacement for `dbg!` that opens a local web UI **and** a TUI
//! where you can explore your structs as a collapsible tree, diff snapshots,
//! and search fields — all with zero configuration.
//!
//! ## Quick start
//!
//! ```rust,ignore
//! use lupa::{inspect, snapshot, snapshot_diff};
//!
//! let mut user = User::new("Alice");
//! inspect!(user);          // opens http://localhost:7777
//!
//! let s1 = snapshot!(user);
//! user.add_role(Role::Admin);
//! inspect!(user);
//! snapshot_diff!(s1, user); // diff tab shows what changed
//!
//! lupa::keep_alive();      // blocks until Ctrl+C
//! ```
//!
//! ## Feature flags
//!
//! - `web` (enabled by default): provides the HTTP + WebSocket server and the
//!   web‑based inspector interface.
//! - `tui`: enables the terminal‑based inspector (ratatui + crossterm).
//!
//! With only `web` the program starts the web server and blocks (or runs
//! `keep_alive`). With only `tui` it runs the terminal interface. With both
//! features you can choose the mode at runtime via [`RunMode`].

#[cfg(feature = "web")]
pub mod server;

#[cfg(feature = "tui")]
pub mod tui;

pub mod __internal;
pub mod diff;
pub mod state;

pub use lupa_macros::{inspect, snapshot, snapshot_diff};
pub use state::Snapshot;

#[cfg(feature = "tui")]
pub use tui::run as run_tui;

/// Runtime selection of which inspector interface(s) to activate.
///
/// Used with [`run_mode`] when both `web` and `tui` features are enabled.
#[derive(Debug, Clone, Copy)]
pub enum RunMode {
    /// Start only the web server (HTTP + WebSocket). Blocks until Ctrl+C.
    Web,
    /// Start only the TUI (terminal interface). Blocks until the user quits (`q` or Esc).
    Tui,
    /// Start both: the web server runs in a background thread, the TUI runs
    /// in the main thread. The web server’s startup message is suppressed to
    /// keep the TUI clean.
    Both,
}

/// Automatically chooses a [`RunMode`] based on the enabled feature flags.
///
/// - If only `web` is enabled → [`RunMode::Web`]
/// - If only `tui` is enabled → [`RunMode::Tui`]
/// - If both are enabled → [`RunMode::Both`]
/// - If neither is enabled → returns an error.
///
/// This is the default entry point. For explicit control, use [`run_mode`].
pub fn run() -> std::io::Result<()> {
    match (cfg!(feature = "web"), cfg!(feature = "tui")) {
        (true, false) => run_mode(RunMode::Web),
        (false, true) => run_mode(RunMode::Tui),
        (true, true) => run_mode(RunMode::Both),
        (false, false) => Err(std::io::Error::new(
            std::io::ErrorKind::Other,
            "at least one of 'web' or 'tui' features must be enabled",
        )),
    }
}

/// Starts the inspector with the explicitly requested [`RunMode`].
///
/// This function respects the enabled feature flags:
/// - Requesting `Web` when `web` is not enabled returns an error.
/// - Requesting `Tui` when `tui` is not enabled returns an error.
/// - Requesting `Both` requires both features to be enabled.
///
/// # Examples
///
/// ```rust,ignore
/// // When both features are enabled, you can pick only the TUI:
/// lupa::run_mode(lupa::RunMode::Tui).unwrap();
/// ```
pub fn run_mode(mode: RunMode) -> std::io::Result<()> {
    match mode {
        RunMode::Web => {
            #[cfg(feature = "web")]
            {
                keep_alive();
                Ok(())
            }
            #[cfg(not(feature = "web"))]
            Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "web feature is not enabled",
            ))
        }
        RunMode::Tui => {
            #[cfg(feature = "tui")]
            {
                tui::run()
            }
            #[cfg(not(feature = "tui"))]
            Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "tui feature is not enabled",
            ))
        }
        RunMode::Both => {
            #[cfg(all(feature = "web", feature = "tui"))]
            {
                let _ = &*server::INSPECTOR_SERVER; // start web server in background
                tui::run() // TUI in main thread
            }
            #[cfg(not(all(feature = "web", feature = "tui")))]
            Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                "both web and tui features are required for Both mode",
            ))
        }
    }
}

/// Blocks the current thread, keeping the web server alive until Ctrl+C.
///
/// This function is only available when the `web` feature is enabled.
/// It starts the web server (if not already running) and then waits for
/// a SIGINT (Unix) or Ctrl+C (Windows). It prints a simple message and
/// exits gracefully.
///
/// You normally do not need to call this directly – [`run`] or [`run_mode`]
/// with `RunMode::Web` or `RunMode::Both` will call it for you.
#[cfg(feature = "web")]
pub fn keep_alive() {
    let _ = &*server::INSPECTOR_SERVER;
    let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
    let r = running.clone();
    ctrlc_install(move || r.store(false, std::sync::atomic::Ordering::SeqCst));
    eprintln!("lupa: press Ctrl+C to exit…");
    while running.load(std::sync::atomic::Ordering::SeqCst) {
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
    eprintln!("lupa: bye!");
}

// ─── Ctrl+C handler (platform‑specific, no extra dependencies) ───

#[cfg(unix)]
fn ctrlc_install(f: impl Fn() + Send + Sync + 'static) {
    use std::os::raw::c_int;
    unsafe extern "C" {
        fn signal(sig: c_int, handler: extern "C" fn(c_int)) -> extern "C" fn(c_int);
    }
    static CB: std::sync::OnceLock<Box<dyn Fn() + Send + Sync>> = std::sync::OnceLock::new();
    CB.set(Box::new(f)).ok();
    extern "C" fn handler(_: c_int) {
        if let Some(cb) = CB.get() { cb(); }
    }
    unsafe { signal(2, handler); }
}

#[cfg(windows)]
fn ctrlc_install(f: impl Fn() + Send + 'static) {
    let _ = f;
}

#[cfg(not(any(unix, windows)))]
fn ctrlc_install(f: impl Fn() + Send + 'static) {
    let _ = f;
}