danger-write 0.1.5

A writing app that erases everything you've written if you stop typing
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`danger-write` is a single-binary terminal writing app (Rust + ratatui). You keep
typing; if you go idle longer than the idle limit, the text fades to gray and then
dissolves away — game over. Reach the goal (a time limit or word count) to survive
and unlock copying your text to the clipboard.

All logic lives in `src/main.rs` (~550 lines). There are no modules, no test files,
and no lib target — the whole program is one binary crate.

## Commands

```bash
cargo build --release        # binary at target/release/danger-write
cargo run -- -w 250          # run with args (note the `--` before app flags)
cargo clippy                 # lint
cargo fmt                    # format

# Regenerate the demo GIF after a visual/behavior change (requires charmbracelet/vhs):
cargo build --release && vhs demo.tape   # writes demo.gif
```

There is no test suite. `cargo test` currently does nothing.

## Architecture

The program is a single synchronous render loop in `run()` — no async, no threads.
Each iteration: compute frame delta → `terminal.draw()` → `event::poll(50ms)` for a
keystroke → `app.tick()`. The 50ms poll timeout is what keeps fades/timers/effects
animating while the user isn't pressing keys.

**State machine.** Everything hangs off the `Phase` enum on `App`:
- `Writing` → keys append to `app.text`; each keystroke calls `touch()` to reset the idle clock.
- `Won` → goal reached, text frozen; `c` copies, `q` quits.
- `Dying` → idle limit exceeded; the `tachyonfx` dissolve effect is destroying the on-screen text. Input is ignored here.
- `Dead` → text wiped, game-over banner shown; `r` restarts, `q` quits.

Transitions happen only in `tick()` (goal check, idle-timeout → `Dying`, and
`Dying` → `Dead` once the effect reports `done()`). `run()`'s match on `app.phase`
decides which keys are live in each phase. `Ctrl+C` always quits, regardless of phase.

**Key details worth knowing before editing:**
- The dissolve animation (`fx::dissolve`) needs the text to still be on screen while it plays, so `tick()` sets `phase = Dying` but does **not** clear `app.text` — the text is cleared only when entering `Dead`. `draw_body` renders the effect over `inner` during `Dying`.
- Fading is manual color interpolation, not an fx: `fade_color()` and `border_color()` lerp RGB from normal toward gray/red over `fade_window` (the last 80% of `idle_limit`). Safe text uses `Color::Reset` so it matches the user's terminal theme rather than a hardcoded white.
- Scrolling is hand-rolled: `wrap_lines()` wraps text to the body width so the code knows the exact visual row count, then `draw_body` renders only the last screenful (keeps the cursor `` visible). Don't replace this with ratatui's built-in wrap without also fixing the scroll-to-bottom behavior.
- Clipboard copy (`copy()` / `pipe_to_command()`) deliberately shells out to whatever CLI tool exists (`wl-copy`/`xclip`/`xsel`/`pbcopy`/`clip`) instead of using a clipboard crate — native crates drop the Linux selection when the process exits. It does not `wait()` on the child because those tools daemonize to keep serving the selection. `clip`/`clip.exe` get CRLF-normalized payloads.

**CLI.** `parse_args()` is a hand-written arg parser (no clap). Flags: `-t/--time`
(minutes, default 5), `-w/--words`, `-i/--idle` (seconds, default 3). `-w` overrides
`-t` if both are given (last goal wins). Help text is the `HELP` const.

## Conventions

- Rust 2024 edition. Keep the single-file structure and the existing `// --- section ---` comment banners.
- The README's flag table, the `HELP` const, and `parse_args()` must stay in sync — update all three when changing CLI options.