tui-lipan 0.1.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
# Quick Start

## Introduction

**tui-lipan** is an opinionated, component-based TUI framework for Rust, inspired by React and Elm.

Key characteristics:
- **Declarative UI** - builder API + `ui!` macro (with full autocomplete), plus optional `rsx!`.
- **Component model** - properties, local state, and message-based updates.
- **Flexbox-like layout** - sensible defaults, no raw coordinate math.
- **Rich widget set** - Frames, Tabs, Lists, Inputs, Tables, Modals, and more.

## Import Map

```rust
// Recommended: start here for typical app-author code
use tui_lipan::prelude::*;
```

The prelude is intentionally curated for app authors. It re-exports the common
component/runtime types, styling primitives, macros, and a broad set of
user-facing widgets and widget event types. For framework internals or unusual
helpers, prefer explicit imports from `tui_lipan`.

Representative `prelude::*` re-exports:

| Symbol | Category |
|--------|----------|
| `App`, `AppRunner`, `ContrastPolicy`, `TextAreaNewlineBinding` | App runner |
| `Component`, `Context`, `Update`, `Command`, `Breakpoint`, `KeyUpdate`, `TaskPolicy` | Component trait |
| `Element`, `IntoElement`, `Key` | Tree primitives |
| `Callback`, `CommandLink`, `KeyHandler`, `Link` | Messaging |
| `KeyCode`, `KeyEvent`, `KeyMods`, `MouseEvent`, `MouseMoveEvent` | Events |
| `KeyBinding`, `KeyBindings` | Common keybinding types |
| `Style`, `Color`, `Length`, `Padding`, `Align`, `Justify`, `BorderStyle`, `BorderEdges`, `CaretShape` | Styling |
| `RichText`, `Span`, `Edge`, `Rect`, `Size`, `ScrollbarConfig`, `ScrollbarVariant` | Layout & text types |
| `Theme`, `ColorGradient`, `GradientDirection`, `GradientRange`, `VisualEffect`, `RippleRadius`, `RetroPreset` | Themes & effects |
| `ClipboardConfig`, `PasteShiftInsertBehavior` | Clipboard config |
| `TextEditor`, `TextInput`, `TextEditEvent`, `TextEditKind` | Text editing |
| `OverlayId`, `OverlayScope`, `ToastHandle`, `ToastPlacement` | Overlays |
| `App`, `CommandEntry`, `CommandRegistry` | App commands |
| `child`, `mockup!`, `rsx!`, `ui!` | Macros & helpers |
| `VStack`, `HStack`, `ZStack`, `Canvas`, `Frame`, `Button`, `Text`, `Input`, `List`, `Tabs`, `Table`, `Modal`, `TextArea`, `Tree`, `DocumentView`, `FileTree`, `Animated`, `AsciiCanvas` | Common and advanced widgets |

The prelude no longer re-exports broad internal modules like `core`, `utils`,
or `widgets::*` wholesale.

Extra imports **not** in `prelude::*`:

```rust
// Clipboard image support (requires feature "image" or "clipboard-images")
use tui_lipan::{ImageContent, ImageFormat, ClipboardProvider, ClipboardError};

// Lower-level framework or specialized APIs
use tui_lipan::NodeId;
```

## Feature Flags

```toml
[dependencies]
tui-lipan = { version = "*", features = ["image", "big-text"] }
```

| Feature | Default | What it enables |
|---------|---------|-----------------|
| `clipboard` | **Yes** | System clipboard via arboard (X11/Wayland/macOS/Windows) |
| `devtools` | No | In-app DevTools overlay (`F12` by default, rebindable) with frame stats and debug log console; controllable from `Context` and configurable via `DevToolsConfig` |
| `ui-snapshot-json` | No | JSON export for `UiSnapshot::to_json()` (markdown export is always available) |
| `ui-snapshot-png` | No | Font-backed PNG export for `UiSnapshot::to_png()` / `to_png_default()` and `CapturedFrame::to_png()` |
| `clipboard-images` | No | Image clipboard read/write (without `Image` rendering widget) |
| `big-text` | No | Large ASCII/pixel text via FIGlet and pixel fonts - `BigText` |
| `diff-view` | No | Side-by-side/unified diff viewer - `DiffView` |
| `image` | No | Protocol-aware image rendering (Kitty, iTerm2, Sixel, halfblocks) with PNG/JPEG/GIF/WebP codecs - includes `clipboard-images` |
| `image-full-formats` | No | Restores the broad `image` crate default codec set for `image`, `clipboard-images`, or `ui-snapshot-png` builds |
| `markdown` | No | Markdown formatter for `DocumentView` + markdown preview example |
| `profiling-tracing` | No | `tracing` spans/events around render loop and `DocumentView` formatting/reconcile hot paths |
| `syntax-syntect` | No | Syntax highlighting in `TextArea`, `DocumentView`, and `DiffView` via syntect |
| `terminal` | No | Embedded PTY / terminal viewport - `Terminal`, `ManagedTerminal` |
| `terminal-serde` | No | Serde derives for terminal snapshot leaf style/mouse types used by external, versioned snapshot transports; includes `terminal` |
| `theme-reload` | No | Live reload of TOML theme files without restarting the app - see [Styling](styling.md) |
| `web` | No | Browser/WASM backend - see [Web / WASM Backend](web-backend.md) |

### Profiling with `tracing`

Enable instrumentation:

```toml
tui-lipan = { version = "*", features = ["markdown", "profiling-tracing"] }
```

Then install any standard `tracing` subscriber in your app binary (for example
`tracing-subscriber`, `tracing-tracy`, or OpenTelemetry exporters). tui-lipan emits
spans/events for frame loop, draw, and `DocumentView` formatting/reconcile hot paths.

To **disable** clipboard (e.g. for minimal no-system-dep builds):

```toml
tui-lipan = { version = "*", default-features = false }
```

For smaller shipping binaries, build app artifacts with the size-optimized profile:

```bash
cargo build --profile release-size --no-default-features
```

Use the normal `release` profile when runtime throughput is more important than artifact size.

Examples requiring specific features:

| Example | Required feature |
|---------|-----------------|
| `big_text`, `figlet_editor` | `big-text` |
| `diff_hub` | `diff-view` |
| `image`, `image_modes`, `messenger` | `image` |
| `markdown_hub` | `markdown` |
| `markdown_editor_sync` | `markdown`, `syntax-syntect` |
| `terminal_filetree_devtools` | `terminal` |
| `devtools` | `devtools` |
| `theme_hot_reload` | `theme-reload` |

With `devtools` enabled, the built-in panel uses fixed default dimensions per tab; use `Context` (`show_devtools`, `hide_devtools`, `toggle_devtools`) for visibility.

### DevTools runtime configuration

When the `devtools` feature is enabled, you can opt out of individual subsystems at app start time:

```rust
use tui_lipan::prelude::*;

App::new()
    .devtools_config(DevToolsConfig {
        logs: true,    // ingest debug_log! lines into the DevTools log panel
        metrics: true, // collect per-frame stats (FPS, reconcile/draw times, node count)
        show_framework_logs: false, // hide tui-lipan's own internal log lines by default
    })
    .mount(MyApp)
    .run()
```

- `logs: false` removes the `debug_log!` → devtools sink path entirely (the macro still respects `TUI_LIPAN_DEBUG=1` env logging).
- `metrics: false` skips frame timing and tree-size collection; the panel will show "No frame metrics yet".
- `show_framework_logs: false` starts the Logs tab with tui-lipan's own framework
  noise (key events, dirty tracking, etc.) hidden, leaving only your app's
  `debug_log!` lines. Toggle it live with the **tui-lipan** button in the Logs tab.
- `logs`, `metrics`, and `show_framework_logs` all default to `true`, so
  `features = ["devtools"]` behaves exactly as before.

In the Logs tab you can also copy the selected row to the clipboard with `Ctrl+C`,
or by activating a row (double-click / `Enter`).

Subsystem cost — what each toggle controls:

| Subsystem | When `true` (default)                                                                                        | When to turn off                                                                |
| --------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| `logs`    | Every `debug_log!` allocates a `String` and pushes a `DevLogEntry` onto a bounded ring buffer (small).       | Hot loops calling `debug_log!` thousands of times per frame, or to drop the formatting cost when the panel is never opened. |
| `metrics` | Per-frame timing samples + node-tree size snapshot collected on every render; small fixed-size ring buffer.  | Profiling renders against a `release` build where you don't want sampling overhead, or shipping a build with the feature compiled in but the panel unused. |

Note: `debug_log!` works in `--release` builds when the `devtools` feature is enabled — there is no `debug_assertions` gate. Lines are emitted to the panel regardless of profile, so guard hot paths yourself if you need zero overhead in release.

## Minimal Example

```rust
use tui_lipan::prelude::*;

struct Counter;

#[derive(Default)]
struct State {
    count: i32,
}

#[derive(Clone)]
enum Msg {
    Increment,
    Decrement,
}

impl Component for Counter {
    type Message = Msg;
    type Properties = ();
    type State = State;

    fn create_state(&self, _props: &Self::Properties) -> Self::State {
        State::default()
    }

    fn view(&self, ctx: &Context<Self>) -> Element {
        rsx! {
            VStack {
                gap: 1,
                padding: 2,

                Text { content: format!("Count: {}", ctx.state.count) }

                HStack {
                    gap: 1,
                    Button { label: "-", on_click: ctx.link().callback(|_| Msg::Decrement) }
                    Button { label: "+", on_click: ctx.link().callback(|_| Msg::Increment) }
                }
            }
        }
    }

    fn update(&mut self, msg: Msg, ctx: &mut Context<Self>) -> Update {
        match msg {
            Msg::Increment => ctx.state.count += 1,
            Msg::Decrement => ctx.state.count -= 1,
        }
        Update::full()  // (needs_redraw, optional_command)
    }
}

fn main() -> tui_lipan::Result<()> {
    App::new()
        .title("Counter")
        .mount(Counter)
        .run()
}
```

## Fast Prototyping with `mockup!`

Skip all `Component` boilerplate for layout previews:

```rust
use tui_lipan::prelude::*;

fn main() -> tui_lipan::Result<()> {
    mockup!("Dashboard Preview", {
        HStack::new()
            .gap(1)
            .child(
                Frame::new()
                    .title("Sidebar")
                    .border(true)
                    .width(Length::Px(30))
                    .child(List::new().items([
                        ListItem::new("Dashboard"),
                        ListItem::new("Settings"),
                        ListItem::new("Logs"),
                    ]).selected(0)),
            )
            .child(
                Frame::new()
                    .title("Content")
                    .border(true)
                    .padding(1)
                    .child(Text::new("Hello from mockup!")),
            )
    })
}
```

**Key behaviors:**
- Press `Esc` or `q` to quit.
- The body expression is auto-wrapped in `.into()` - return any widget builder directly.
- Interactive widgets (List, Tabs, Inputs) still respond to focus and mouse.
- The closure uses `move` capture, so local data is accessible.

**Using `Mockup` adapter directly:**

```rust
App::new()
    .title("My Layout")
    .mount(Mockup::new(|| {
        Frame::new().title("Panel").border(true)
            .child(Text::new("World")).into()  // closure must return Element
    }))
    .run()
```

### Mockup → App Workflow

Extract views as plain functions reusable in both mockups and real components:

```rust
fn sidebar(items: &[&str], selected: usize) -> Element {
    Frame::new().title("Nav").border(true)
        .width(Length::Px(28))
        .child(List::new().items(items.iter().map(|s| ListItem::new(*s))).selected(selected))
        .into()
}

// Step 1: preview with mockup
fn main() -> tui_lipan::Result<()> {
    let items = vec!["Home", "Settings", "Logs"];
    mockup!("Preview", { sidebar(&items, 0) })
}

// Step 2: reuse in real component - zero rewrite
fn view(&self, ctx: &Context<Self>) -> Element {
    sidebar(&ctx.state.nav_items, ctx.state.selected)
}
```

## App Configuration

```rust
App::new()
    .title("My App")           // Optional outer chrome frame
    .theme(Theme::one_dark())  // Optional theme override
    .system_theme()            // Optional: derive theme from host terminal colors
    .inline_ephemeral(8)       // Optional: inline mode (8 terminal rows)
    .mouse(true)               // Mouse capture (default: true in fullscreen, false in inline)
    .scroll_wheel_multiplier(3) // Optional: lines per wheel tick (default: 1)
    .toast_placement(ToastPlacement::BottomEnd)
    .keymap_path("/path/to/keymap.conf")  // see docs/keybindings.md
    .clipboard_config(ClipboardConfig { .. })
    .contrast_policy(ContrastPolicy::Wcag)
    .terminal_bg(query_host_colors().map(|c| c.bg))  // enables Opacity through Color::Reset
    .live_host_terminal_colors(true)  // opt-in runner-managed live host palette refresh
    .mount(Root)
    .exit_view(|_component, ctx| {
        Text::new(format!("Final count: {}", ctx.state.count)).into()
    })
    .run()
```

`ScrollView::scroll_wheel_multiplier(...)`,
`TextArea::scroll_wheel_multiplier(...)`, and
`DocumentView::scroll_wheel_multiplier(...)` override the app-wide wheel
multiplier for a specific widget.

> **`terminal_bg` / live host colors**: `ColorTransform::Opacity` blends foreground colors toward the resolved cell background. When the cell background is `Color::Reset` (terminal default) there is no RGB to blend toward, so opacity has no effect. Calling `.terminal_bg(query_host_colors().map(|c| c.bg))` before `run()` provides the terminal's actual default background color and enables correct opacity blending for static apps. Use `.system_theme()` to opt into a framework-wide theme derived from live host colors, or `.live_host_terminal_colors(true)` when app code wants to read `ctx.host_terminal_colors()` and build its own tokens. The runner probes once at startup, refreshes on terminal focus gained, services `ctx.request_host_terminal_color_refresh()`, and never polls continuously. Refreshed host backgrounds update `terminal_bg` automatically. Omitting both leaves opacity unchanged on reset-background cells.

> **`exit_view`**: Attach this on `AppRunner<C>` after `.mount(...)` when you want a final one-shot element rendered to stdout after the TUI exits. The callback runs before unmount, so component state is still available in `ctx.state`. This is useful for persisting a session summary or logo in terminal scrollback.

## Development Workflow

1. **Define State** - `struct State { ... }` with `#[derive(Default)]`
2. **Define Messages** - `enum Msg { ... }` with `#[derive(Clone)]`
3. **Implement Component** - `create_state`, `update`, `view`
4. **Run** - `App::new().mount(Root).run()`

## Debugging

### Debug logging

Enable debug output with environment variables:

```sh
TUI_LIPAN_DEBUG=1 cargo run                         # Print to stderr
TUI_LIPAN_DEBUG_FILE=/tmp/tui.log cargo run          # Also append to file
```

Use the `debug_log!` macro in your own code to emit messages through the same channel:

```rust
use tui_lipan::debug_log;

debug_log!("Current state: {:?}", ctx.state);
```

### Layout snapshot diagnostics

When content vanishes in tests or mockups, first check the viewport and sizing:
- `TestBackend` starts at an 80x24 viewport unless you call `set_viewport(...)`; use a fixed viewport for reproducible snapshots.
- `Mockup` renders at the live terminal size, so a layout can change when the terminal is narrow or short.
- `VStack`, `HStack`, and `Frame` default to `Length::Flex(1)` on both axes; fixed headers, footers, and side bars usually need `Length::Px(...)`.
- Capture with `UiSnapshotOptions::diagnostic()` to include zero-area nodes, spacers, and dividers; markdown snapshots flag zero-size widgets as `zero-area`.

### Mouse event diagnostics

The `tui_lipan::debug` module exposes counters for diagnosing mouse event throughput:

```rust
use tui_lipan::debug;

let count = debug::mouse_events_processed();  // Total mouse events since start
debug::reset_mouse_events();                  // Reset counter to zero
```