# 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
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
| `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.