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
# External programs (editors, pagers, subprocesses)

When your app spawns an interactive program that needs the **real TTY** (Neovim, `less`, a password prompt, etc.), the framework must temporarily give up:

- raw mode and the alternate screen (fullscreen apps),
- mouse and focus-tracking sequences,
- and, in **fullscreen** mode, the background thread that reads stdin for crossterm events.

Otherwise the subprocess and the TUI will **fight over stdin and the display** (garbled input, cursor blink on top of the editor, incomplete redraw after exit).

This page describes the supported APIs and the follow-up repaint behavior for **nested** components.

For **non-interactive** commands where the TUI should keep running and consume
stdout/stderr as data, use the native-only `process` helpers instead of terminal
handoff.

---

## `terminal_handoff` (crate root)

```rust
use std::io;
use tui_lipan::terminal_handoff::{
    resume_after_external_process,
    suspend_for_external_process,
};
```

| Function | Role |
|----------|------|
| `suspend_for_external_process(surface_mode)` | Pause the fullscreen stdin reader (when applicable), leave interactive terminal state so the child can use the TTY. |
| `resume_after_external_process(surface_mode, mouse_enabled)` | Restore raw mode, alternate screen (if not inline), mouse capture if it was enabled, resume the reader, and **request a host redraw** on the next frame (ratatui buffer clear + full draw). |

The framework consumes that request on the next tick: it promotes the frame to **full** render, runs [`Terminal::clear`](https://docs.rs/ratatui/latest/ratatui/terminal/struct.Terminal.html#method.clear) to reset the back buffer, and drops incremental scroll snapshots so the UI matches the TTY again. You can still call [`Context::request_full_repaint()`](components.md#context-methods) for other cases where the host display may be stale.

**Stale stdin:** Before the event reader is unpaused, `resume_after_external_process` drains the crossterm event queue and, on Unix, `tcflush(TCIFLUSH)` on stdin so CSI/OSC/DA tails and other mode-switch bytes are not delivered as fake key input to the focused widget.

**Parameters must match the running app:**

- `surface_mode` - the `SurfaceMode` configured on the `App` (Fullscreen, InlineEphemeral, or InlineTranscript).
- `mouse_enabled` - same as [`Context::mouse_capture_enabled`]components.md#context-methods **at the time you suspend** (pass through to `resume_after_external_process` so mouse state is restored correctly).

**Keyboard enhancement (Kitty protocol):** suspend/resume does **not** push or pop keyboard-enhancement flags; the long-lived `TerminalGuard` still owns that. Only terminal modes needed for a typical full-screen subprocess are toggled.

Always pair suspend and resume. Prefer an RAII guard in your own code so resume runs on panic or early return:

```rust
struct Handoff {
    surface_mode: SurfaceMode,
    mouse_enabled: bool,
}

impl Drop for Handoff {
    fn drop(&mut self) {
        let _ = resume_after_external_process(
            self.surface_mode,
            self.mouse_enabled,
        );
    }
}

fn run_editor(
    surface_mode: SurfaceMode,
    mouse_enabled: bool,
) -> io::Result<()> {
    suspend_for_external_process(surface_mode)?;
    let _guard = Handoff {
        surface_mode,
        mouse_enabled,
    };
    // spawn / wait on editor...
    Ok(())
}
```

---

## Streaming non-interactive processes (`process`)

`tui_lipan::process` is available only on native targets
(`#[cfg(not(target_arch = "wasm32"))]`). It does not expose crossterm, ratatui,
or PTY types, and it is separate from the `terminal` feature/LSP integrations.

Use it for commands such as `rg`, formatters, compilers, or small shell helpers
whose stdout/stderr should become component messages:

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

enum Msg {
    Proc(ProcessEvent),
}

// Inside update():
let command = ProcessSpec::new("sh")
    .args(["-c", "printf out; printf err >&2"])
    .command(Msg::Proc);

Update::command_only(command)
```

`ProcessEvent::Stdout(Vec<u8>)` and `ProcessEvent::Stderr(Vec<u8>)` may arrive
in chunks. `ProcessEvent::Exited(ProcessExitStatus)` is sent after both output
streams are drained. `ProcessEvent::Error(Arc<str>)` reports spawn/pipe/wait
errors without exposing `std::process::ExitStatus` in the public API.

Stdout and stderr are drained concurrently, so a child that writes heavily to
both streams will not deadlock on a full pipe. The helper is for streaming data,
not for programs that need terminal control; use `terminal_handoff` for editors,
pagers, shells, password prompts, and other interactive programs.

**Cancellation:** unkeyed `.command(...)` tasks normally run to completion. Use
`.command_keyed(key, TaskPolicy::LatestOnly, ...)` or `process_command_keyed`
when newer work should cancel a running process. Keyed process commands observe
the background cancellation token; on cancellation they kill the child, drain
stdout/stderr, and suppress stale component messages. For manual use,
`stream_process_until` accepts a cancellation predicate with the same
kill-and-drain behavior.

---

## Run blocking work on the **UI thread**

[`Command::spawn`](components.md#commands-async--background-work) and `Link::command(...)` run closures on a **worker thread**. That is wrong for `terminal_handoff`: the main thread still holds the ratatui terminal and keeps drawing.

Use **`Command::new`** so suspend → subprocess → resume runs on the **same thread** as the event loop:

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

// Inside update():
let link = ctx.link().clone();
let surface_mode = ctx.surface_mode();
let mouse_enabled = ctx.mouse_capture_enabled();
let initial = ctx.state.draft.clone();

Update {
    dirty: false,
    command: Some(Command::new(move || {
        match run_my_editor(&initial, surface_mode, mouse_enabled) {
            Ok(text) => link.send(Msg::EditorDone(text)),
            Err(e) => link.send(Msg::EditorFailed(e)),
        }
    })),
}
```

Use `link.send(...)` inside the closure to push follow-up messages; they are processed in the same message drain as other updates.

---

## Force a **full** redraw after handoff

When a nested child returns [`Update::full()`](components.md#the-update-return-type), the runtime may schedule a **layout-only** reconcile for that scope. After the host terminal was repainted by another process, that is often **not enough** to refresh the entire frame.

Call **`Context::request_full_repaint()`** from the message handler that runs **after** the external program exits (success or failure), before or alongside your usual state updates:

```rust
Msg::EditorDone(text) => {
    ctx.request_full_repaint();
    ctx.state.draft = text;
    Update::full()
}
```

On the next loop iteration the runner promotes the frame to a **full** render (full reconcile + draw), not only a nested layout pass.

---

## Summary checklist

1. Use `suspend_for_external_process` / `resume_after_external_process` with correct `surface_mode` and `mouse_enabled`.
2. Run that sequence on the UI thread via `Command::new`, not `Command::spawn` / `link.command`.
3. After returning to the TUI, call `ctx.request_full_repaint()` when a full frame repaint is required (especially for nested components).