# tess

[![GitHub](https://img.shields.io/badge/github-codedeviate%2Ftess-181717?logo=github)](https://github.com/codedeviate/tess)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue?logo=opensourceinitiative)](LICENSE)
[![Rust edition 2021](<https://img.shields.io/badge/rust-2021_edition_(MSRV_1.85)-CE422B?logo=rust>)](https://www.rust-lang.org)
<br/>
[![Latest release](https://img.shields.io/github/v/release/codedeviate/tess?logo=semanticrelease&label=release&color=blue)](https://github.com/codedeviate/tess/releases/latest)
[![crates.io](https://img.shields.io/badge/crates.io-tess--cli-fc8d62?logo=rust)](https://crates.io/crates/tess-cli)
[![Homebrew](https://img.shields.io/badge/homebrew-codedeviate%2Fcli%2Ftess-fbb040?logo=homebrew)](https://github.com/codedeviate/homebrew-cli)

A `less`-style terminal pager for files, pipes, and live logs — with first-class support for **structured-log filtering**, **pretty-printing** of JSON/YAML/TOML/XML/HTML/CSV, **ANSI color passthrough** for piped tools that emit colored output, **wrap-aware scrolling** of long lines, **multi-file navigation** with `:n`/`:p`/`:e` colon commands, and **ctags/etags tag jumping** (`-t NAME`, `Ctrl-]`/`Ctrl-T`). Escape to the shell with `!cmd`, preprocess inputs (`$LESSOPEN` / `--preprocess`), and remap keys via `~/.config/tess/keys.toml`. Written in Rust. macOS + Linux.

```sh
tess /var/log/syslog                                # like `less`, but newer keys
git log | tess                                      # pipe-friendly
tess -f --tail 1000 huge.log                        # tail -f the last 1000 lines
tess --format apache-combined --filter status~^5 access.log
tess --prettify config.json                         # auto-detected JSON layout
```

---

## Why?

`less` is great, but it doesn't know what your data _is_. `tess` adds a small amount of structure-awareness without trying to be a log explorer:

- **Parse-aware filtering.** Declare a log format once (built-in or your own regex), then keep only the lines whose parsed `status`, `ip`, `level`, etc. match a predicate. Hide them entirely or just dim them so context is preserved.
- **Multi-line records.** Set `record_start` in formats.toml (or pass `--record-start REGEX`) to group continuation lines under their leading record — search, filter, and `--grep` then operate on whole records like PHP stack traces or Java exception dumps.
- **Pretty-printing.** Open a JSON / YAML / TOML / XML / HTML / CSV file and see it laid out, not minified — without leaving the pager. Toggle on/off with `Shift-P`.
- **Live + follow modes.** `--follow` for tail-style appended logs (background reader thread, doesn't fight your tty); `--live` for whole-file rewrites (editor saves, AI agents, build outputs).
- **Cheap on huge files.** mmap'd file source, lazy line indexing, `--tail N` reverse-scans only as far as needed.
- **Long-line scrolling that actually works.** `j` walks wrap-rows so the last 1000 chars of a 5000-char line are reachable; `J` / `K` jump by whole logical lines when you don't want to scroll through every wrap.

It is **not** a log search/correlation tool, structured query engine, or replacement for `lnav`. It's a single-file pager.

---

## Install

### Homebrew (macOS / Linuxbrew)

```sh
brew install codedeviate/cli/tess
```

The formula lives in the [`codedeviate/homebrew-cli`](https://github.com/codedeviate/homebrew-cli) tap and builds from source on first install.

### crates.io

```sh
cargo install tess-cli
```

The crate is published as **`tess-cli`** because the bare `tess` name was already taken (an unrelated parked crate). The installed binary is still `tess`.

### From source

```sh
tar -xzf tess-<version>.tar.gz
cd <extracted-dir>
cargo build --release
install -m 755 target/release/tess ~/.local/bin/tess
```

Requires **Rust 1.85+**. See [INSTALL.md](INSTALL.md) for full instructions, including a macOS 26 (Tahoe) gotcha where `cp` into `/usr/local/bin` can get a binary SIGKILLed by the kernel — `install` avoids it.

---

## Quick start

| Goal                                  | Command                                                       |
| ------------------------------------- | ------------------------------------------------------------- |
| View a file                           | `tess Cargo.toml`                                             |
| View piped output                     | `git log \| tess`                                             |
| Watch a log live                      | `tess -f /var/log/syslog`                                     |
| Watch a file get rewritten            | `tess --live src/main.rs`                                     |
| Pretty-print JSON / YAML / etc.       | `tess --prettify config.json`                                 |
| Show line numbers                     | `tess -N script.sh`                                           |
| Don't wrap long lines                 | `tess -S /etc/hosts`                                          |
| Last 1000 lines (cheap on huge files) | `tess --tail 1000 huge.log`                                   |
| `tail -f` last 1000                   | `tess -f --tail 1000 huge.log`                                |
| First 50 lines                        | `tess --head 50 file.txt`                                     |
| Apache 5xx errors                     | `tess --format apache-combined --filter status~^5 access.log` |
| Navigate multiple files               | `tess foo.log bar.log baz.log` then `:n` / `:p`               |

Run `tess --examples` for a curated list, or `tess --manual` for the full manual paged through `tess` itself.

---

## Key bindings

Vim-ish + `less`-ish hybrid. The full table is in [MANUAL.md](MANUAL.md); these are the ones you'll use:

| Key(s)                        | Action                                                                 |
| ----------------------------- | ---------------------------------------------------------------------- |
| `↓` `j` `e` `Ctrl-E` `Return` | Scroll down one screen line (walks through wrap rows)                  |
| `↑` `k` `y` `Ctrl-Y`          | Scroll up one screen line                                              |
| `J` / `K`                     | Jump to next / previous _logical_ line (skips wrap rows of long lines) |
| `Space` `f` `Ctrl-F` `PgDn`   | Page down                                                              |
| `b` `Ctrl-B` `PgUp`           | Page up                                                                |
| `g` / `G`                     | Go to top / bottom                                                     |
| `/` _pattern_ `Enter`         | Forward regex search; `Esc` cancels                                    |
| `?` _pattern_ `Enter`         | Backward regex search                                                  |
| `n` / `N`                     | Repeat last search forward / backward                                  |
| `-N` / `-S` / `Shift-F`       | Toggle line numbers / chop / follow mode                               |
| `Shift-P`                     | Toggle pretty-print on/off                                             |
| `Shift-R`                     | Force reload from disk (with `--live`)                                 |
| `:n` / `:p` / `:e <path>`     | Next file / previous file / open new file                              |
| `Ctrl-]` / `Ctrl-T`           | Tag jump / pop tag stack                                               |
| `q` `Q` `Ctrl-C`              | Quit                                                                   |

Pressing `/<Enter>` (or `?<Enter>`) with an empty pattern repeats the last search in the typed direction, the way `less` does it.

---

## Plain-text grep

If you don't have (or don't need) a parsed format, `--grep PATTERN` filters
by regex against the raw line. Repeatable, AND-combined.

```sh
tess --grep error app.log
tess --grep error --grep '^\[' app.log    # both patterns must match
tess --grep error --dim app.log           # show all, dim non-matches
```

`--grep` composes with `--filter` when both are set (line must match both):

```sh
tess --format apache-combined --filter status=500 --grep timeout access.log
```

---

## Structured log filtering

Three formats ship built-in: `apache-common`, `apache-combined`, `nginx-combined`. Drop a TOML file at `~/.config/tess/formats.toml` to define your own:

```toml
[format.app]
pattern = '^(?P<ts>\S+) (?P<level>\w+) (?P<msg>.+)$'
```

Then:

```sh
tess --format app --filter 'level!=DEBUG' app.log         # hide DEBUG lines
tess --format app --filter 'level=ERROR' --dim app.log    # keep DEBUG dimmed for context
```

Operators: `=` (exact), `!=` (exact ≠), `~` (regex), `!~` (regex ≠). Multiple `--filter` flags AND together. Quote arguments with `'…'` in interactive shells so `!` doesn't trigger history expansion.

Run `tess --list-formats` to see what's available, including your custom ones.

---

## Status line

```text
<source>  <top>-<bottom>/<total>  <pct>%  +<wrap>/<wraps>  [<format>]  [filter|dim]  [/<search>]  [pretty:<type>]  (L)  (F)
```

Each segment appears only when relevant. The `+12/50` indicator surfaces wrap-row position when scrolled inside a long wrapping line, so `j` is visibly making progress.

---

## Project status

Pre-1.0; the CLI is settling but small breakage at minor-version boundaries is permitted (and called out in commit messages). Daily-driver-quality on macOS and Linux. **No Windows support** — the stdin / `/dev/tty` plumbing is Unix-specific.

Architecture notes and module layout: [CLAUDE.md](CLAUDE.md). Deferred features and intentional non-goals: [OUT-OF-SCOPE.md](OUT-OF-SCOPE.md).

---

## Building from source

```sh
cargo build --release                  # binary at target/release/tess
cargo test -- --test-threads=1         # unit + integration tests (PTY tests need serial)
cargo bench                            # criterion baselines (HTML in target/criterion/)
cargo run -- Cargo.toml                # quick interactive run
ls -la | cargo run --release           # piped stdin
```

---

## License

MIT — see [LICENSE](LICENSE).
