lesser 0.1.0

A lesser pager (even less than less), for everyday use
# lesser — plan

A `less`-compatible pager in Rust. Drop-in replacement for `$PAGER` in everyday
use (git, man, systemd-journald). Targets Linux + macOS primarily, but should
work cross-platform.

## Vision

Cover the 80% of `less` that makes a pager *feel* native, skip the long tail of
features almost nobody uses. Where we can do better than 1980s `less` for free
(true-color, mouse scroll, nicer search highlight), do it. Where parity is the
constraint (env vars, flag bundles, key bindings), match `less` exactly.

## Ideas

Syntax highlighting (opt in via `--highlight rust` flag) using tree-sitter for
viewing code files.

## Constraints

- **Clean-room reimplementation.** Do not read or reference `less`'s source
  code. Behavior is derived from the man page, observed runtime behavior,
  public discussion, and our own reasoning. This applies to research, debugging,
  and edge-case clarification alike.

## Scope

### v1 must-haves

Without these, it doesn't function as `$PAGER`:
- Read from stdin (every tool that pipes into us)
- `-R` ANSI passthrough — colored `git diff` is the canary test
- `-F` quit-if-one-screen — `git log -1` printing two lines must not pop a
  pager
- `-X` no-init / skip alternate screen — `LESS=FRX` is git's default; users
  expect output to stay in scrollback
- SIGWINCH on resize
- Basic nav: `space`, `b`, `j`, `k`, `g`, `G`, `q`
- `/` regex search
- Parse bundled-flag env vars: `LESS=FRX`, `SYSTEMD_LESS=FRSXMK`. Unknown flags
  must be silently ignored, never cause errors.

### Soon after v1

- `-S` chop long lines (and a runtime toggle keybind)
- Backspace-overstrike decoding for `man` output: `_\bx` → underline, `x\bx`  bold
- `-K` quit on ctrl-c (systemd sets this)
- `?` reverse search, `n`/`N` next/prev match
- `<num>g` / `<num>G` for jump-to-line

### Explicit non-goals

Out of scope, do not implement unless re-prioritized:
- Marks, tags, `!` shell escape, `+cmd`
- `:n` / `:p` multi-file navigation
- `LESSOPEN` preprocessors
- Hex mode
- `less`'s elaborate position-prompt formatting

### Stretch / nice-to-have (post-MVP)

- True 24-bit color passthrough (less mangles this)
- Mouse wheel scroll
- Better search highlighting (all matches on screen, not just current)
- Follow mode (`F` in less, like `tail -f`)

## Architecture

Single crate, internal modules. Extract later only if something proves reusable
across `~/Projects/rustutils/`.

```
src/
  main.rs    -- arg parse, env merge, open input, hand to app
  flags.rs   -- Flags struct + LESS-bundle parser
  input.rs   -- Source enum (Stdin | File). /dev/tty is opened separately for keys.
  buffer.rs  -- LineBuffer: Vec<String> of input lines (v1: read fully into memory)
  render.rs  -- Viewport + draw(); ANSI-aware width and clipping
  search.rs  -- regex compile + match navigation
  app.rs     -- event loop, state machine, key dispatch, -F decision
```

### Key design points
- **Stdin vs TTY.** When stdin is piped (the data), keyboard input must come
  from `/dev/tty` directly. Forgetting this is the classic first-pager bug.
- **`-F` flow.** Don't enter alternate screen at startup. Buffer up to
  terminal-height lines first; if EOF before that, dump to stdout and exit.
  Only enter raw mode / alt-screen if we need to page. Interacts with `-X` (which
  says don't enter alt-screen at all).
- **In-memory buffer.** v1 holds the whole input as `Vec<String>`. Fine up to
  hundreds of MB. Add lazy/streaming + line-offset index later if/when a real
  perf complaint appears. Keep `LineBuffer`'s public surface small so swapping
  the backing store is cheap.
- **ANSI passthrough.** SGR escapes (`\x1b[...m`) take zero display columns.
  `-R` mode means: emit them verbatim, but skip them when computing line width
  for wrapping/`-S`/search highlight column math. `unicode-width` handles the
  rest.
- **Signals.** Use `crossterm`'s event stream, which yields `Event::Resize`
  (covers SIGWINCH) and key events for ctrl-c (covers `-K`). No `signal-hook`
  crate needed for v1.

## Dependencies

| Crate           | Purpose                                                            |
|-----------------|--------------------------------------------------------------------|
| `crossterm`     | Raw mode, alt screen, key events, resize events                    |
| `regex`         | `/` search                                                         |
| `clap` (derive) | CLI flag parsing; `LESS` env bundle merges into the same struct    |
| `unicode-width` | Display-column math for `-S`, wrapping, highlight alignment        |
| `anyhow`        | Error plumbing                                                     |

Considered and deferred:

- `signal-hook` — crossterm covers our needs.
- `memmap2` — only if huge-file `G` becomes a real complaint; can't mmap stdin anyway.
- `ratatui` — wrong abstraction; we want raw line output, not widgets.
- `bstr` — only if non-UTF-8 handling becomes a goal. v1 is UTF-8 + lossy decode.

## Roadmap

Rough order. Each step ends with something testable.

1. **Skeleton + flag parsing.** `cargo check` clean. `LESS=FRX lesser file.txt`
   parses correctly. *(done — current state)*
2. **Dump mode.** Read stdin/file → write to stdout. No paging yet. Verifies
   the input plumbing.
3. **`-F` short-circuit.** Buffer N lines; if EOF before screen height, dump
   and exit. Without this, step 4 breaks `git log -1`.
4. **Raw-mode paging.** Open `/dev/tty`, enter raw mode (skip alt-screen if
   `-X`), basic nav: `q`, `space`, `b`, `j`, `k`, `g`, `G`. Status line.
   SIGWINCH redraw.
5. **`-R` ANSI passthrough.** Pass SGR through; compute widths ignoring SGR.
   Test: `git diff --color=always | lesser -R`.
6. **`/` regex search.** Forward search, `n` next. Highlight current match.
7. **`-S` chop long lines** + runtime toggle.
8. **`-K` ctrl-c quit.**
9. **Backspace-overstrike** for `man`.

## Decisions to revisit

- **File backend strategy.** When does the in-memory `Vec<String>` actually
  hurt? Watch for it; don't preempt.
- **Multi-file (`:n`/`:p`).** Currently a non-goal. Reconsider only if needed
  for some specific tool integration.
- **Config file.** `less` has none worth speaking of. Probably stay
  zero-config; rely on env vars.
- **Workspace split.** If a second `rustutils` project wants the `LESS`-bundle
  parser or the ANSI-aware width util, extract then.

## Test strategy

- Unit tests per module where it's tractable (flag-bundle parsing,
  ANSI-skipping width, regex search positions).
- Integration: drive the binary with golden inputs (`git diff` capture, a `man`
  page capture, a one-line file for `-F`). Assertion-on-stdout where possible.
- Manual smoke tests on the canary commands before each milestone:
  - `git diff --color=always | lesser -R`
  - `git log -1 | lesser -F` (must not page)
  - `man ls | lesser` (overstrike decoding once implemented)
  - Resize the terminal mid-paging.
- Linux VM run-through before declaring a milestone done.
- Possible future: run `less`'s own test suite against `lesser` as a
  conformance check, once we're far enough along for it to surface real
  issues. Surface the clean-room interpretation (running vs. reading test
  sources) before relying on it.