summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-06 09:42:43 +0300
committerPaul Buetow <paul@buetow.org>2026-05-06 09:42:43 +0300
commitd78a2530da91b76625b71c2aeaf3293abc6c3a4b (patch)
tree5d59a7a1014955564b9f2c30decaf6be257e61cf
parentfbb7c9a9ad8d03d5d095ac441a58b37537e0ab8d (diff)
move demo/ to docs/tutorial/, commit assets, consolidate TUI docs
- demo/ renamed to docs/tutorial/ (tapes, scripts, TUTORIAL.md) - docs/tutorial/assets/ added to git (51 MB of GIFs + PNGs); removed /demo/assets/ from .gitignore so images render on Codeberg - docs/tui-reference.md removed; its hotkey tables merged into a new "Hotkey Quick Reference" section at the end of TUTORIAL.md - TUTORIAL.md: updated install section (mage buildDocker, no GOTOOLCHAIN=auto), fixed README relative path (../../README.md), updated internal tapes/scripts/assets path prose - README.md: updated all demo/ image paths and links to docs/tutorial/; TUI and recording-modes links now point to TUTORIAL.md anchors - AGENTS.md: updated demo/ references to docs/tutorial/ - Magefile.go: updated demoDir/demoTapesDir/demoScriptsDir/demoRunTape/ demoSudoKeepers constants to docs/tutorial/ paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--.gitignore1
-rw-r--r--AGENTS.md4
-rw-r--r--Magefile.go20
-rw-r--r--README.md14
-rw-r--r--docs/tui-reference.md175
-rw-r--r--docs/tutorial/TUTORIAL.md (renamed from demo/TUTORIAL.md)63
-rw-r--r--docs/tutorial/assets/00-hero-flamegraph.pngbin0 -> 156731 bytes
-rw-r--r--docs/tutorial/assets/00-logo.pngbin0 -> 332693 bytes
-rw-r--r--docs/tutorial/assets/01-launch.gifbin0 -> 4244461 bytes
-rw-r--r--docs/tutorial/assets/02-overview-tab.gifbin0 -> 263106 bytes
-rw-r--r--docs/tutorial/assets/03-syscalls-tab.gifbin0 -> 1091628 bytes
-rw-r--r--docs/tutorial/assets/04-files-tab.gifbin0 -> 1577094 bytes
-rw-r--r--docs/tutorial/assets/05-processes-tab.gifbin0 -> 1034718 bytes
-rw-r--r--docs/tutorial/assets/06-latency-gaps-tab.gifbin0 -> 300466 bytes
-rw-r--r--docs/tutorial/assets/07-stream-live.gifbin0 -> 7063227 bytes
-rw-r--r--docs/tutorial/assets/08-stream-pause-filter.gifbin0 -> 5796596 bytes
-rw-r--r--docs/tutorial/assets/09-stream-regex-search.gifbin0 -> 5367913 bytes
-rw-r--r--docs/tutorial/assets/10-stream-csv-export.gifbin0 -> 4671995 bytes
-rw-r--r--docs/tutorial/assets/11-pid-tid-probe.gifbin0 -> 3584783 bytes
-rw-r--r--docs/tutorial/assets/12-parquet-recording.gifbin0 -> 2353504 bytes
-rw-r--r--docs/tutorial/assets/13-tui-flamegraph.gifbin0 -> 6333590 bytes
-rw-r--r--docs/tutorial/assets/13a-order-by-process.pngbin0 -> 231292 bytes
-rw-r--r--docs/tutorial/assets/13b-order-by-path.pngbin0 -> 218540 bytes
-rw-r--r--docs/tutorial/assets/13c-order-by-syscall.pngbin0 -> 255276 bytes
-rw-r--r--docs/tutorial/assets/13d-order-by-pid.pngbin0 -> 183662 bytes
-rw-r--r--docs/tutorial/assets/13e-order-by-process-paths.pngbin0 -> 214308 bytes
-rw-r--r--docs/tutorial/assets/14-headless-modes.gifbin0 -> 4791550 bytes
-rw-r--r--docs/tutorial/assets/screenshot-dashboard.pngbin0 -> 173558 bytes
-rw-r--r--docs/tutorial/assets/screenshot-files-grouped.pngbin0 -> 207344 bytes
-rw-r--r--docs/tutorial/assets/screenshot-flamegraph.pngbin0 -> 171887 bytes
-rw-r--r--docs/tutorial/assets/screenshot-latency.pngbin0 -> 105203 bytes
-rw-r--r--docs/tutorial/assets/screenshot-overview.pngbin0 -> 167438 bytes
-rw-r--r--docs/tutorial/assets/screenshot-pidpicker-open.pngbin0 -> 122673 bytes
-rw-r--r--docs/tutorial/assets/screenshot-pidpicker.pngbin0 -> 122249 bytes
-rw-r--r--docs/tutorial/assets/screenshot-probes.pngbin0 -> 157287 bytes
-rw-r--r--docs/tutorial/assets/screenshot-processes.pngbin0 -> 191556 bytes
-rw-r--r--docs/tutorial/assets/screenshot-recording-active.pngbin0 -> 188855 bytes
-rw-r--r--docs/tutorial/assets/screenshot-stream-export.pngbin0 -> 28883 bytes
-rw-r--r--docs/tutorial/assets/screenshot-stream-filtered.pngbin0 -> 198074 bytes
-rw-r--r--docs/tutorial/assets/screenshot-stream-live.pngbin0 -> 29593 bytes
-rw-r--r--docs/tutorial/assets/screenshot-stream-search.pngbin0 -> 169878 bytes
-rw-r--r--docs/tutorial/assets/screenshot-syscalls.pngbin0 -> 226128 bytes
-rw-r--r--docs/tutorial/assets/screenshot-tidpicker.pngbin0 -> 121954 bytes
-rwxr-xr-xdocs/tutorial/scripts/run-tape.sh (renamed from demo/scripts/run-tape.sh)0
-rwxr-xr-xdocs/tutorial/scripts/sudo-keepalive.sh (renamed from demo/scripts/sudo-keepalive.sh)0
-rwxr-xr-xdocs/tutorial/scripts/workload.sh (renamed from demo/scripts/workload.sh)0
-rw-r--r--docs/tutorial/tapes/01-launch.tape (renamed from demo/tapes/01-launch.tape)0
-rw-r--r--docs/tutorial/tapes/02-overview-tab.tape (renamed from demo/tapes/02-overview-tab.tape)0
-rw-r--r--docs/tutorial/tapes/03-syscalls-tab.tape (renamed from demo/tapes/03-syscalls-tab.tape)0
-rw-r--r--docs/tutorial/tapes/04-files-tab.tape (renamed from demo/tapes/04-files-tab.tape)0
-rw-r--r--docs/tutorial/tapes/05-processes-tab.tape (renamed from demo/tapes/05-processes-tab.tape)0
-rw-r--r--docs/tutorial/tapes/06-latency-gaps-tab.tape (renamed from demo/tapes/06-latency-gaps-tab.tape)0
-rw-r--r--docs/tutorial/tapes/07-stream-live.tape (renamed from demo/tapes/07-stream-live.tape)0
-rw-r--r--docs/tutorial/tapes/08-stream-pause-filter.tape (renamed from demo/tapes/08-stream-pause-filter.tape)0
-rw-r--r--docs/tutorial/tapes/09-stream-regex-search.tape (renamed from demo/tapes/09-stream-regex-search.tape)0
-rw-r--r--docs/tutorial/tapes/10-stream-csv-export.tape (renamed from demo/tapes/10-stream-csv-export.tape)0
-rw-r--r--docs/tutorial/tapes/11-pid-tid-probe.tape (renamed from demo/tapes/11-pid-tid-probe.tape)0
-rw-r--r--docs/tutorial/tapes/12-parquet-recording.tape (renamed from demo/tapes/12-parquet-recording.tape)0
-rw-r--r--docs/tutorial/tapes/13-tui-flamegraph.tape (renamed from demo/tapes/13-tui-flamegraph.tape)0
-rw-r--r--docs/tutorial/tapes/14-headless-modes.tape (renamed from demo/tapes/14-headless-modes.tape)0
60 files changed, 72 insertions, 205 deletions
diff --git a/.gitignore b/.gitignore
index 0649d46..86a6136 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,3 @@ perltidy.ERR
/.cursor/
REVIEW-COMMENTS.md
/.serena/
-/demo/assets/
diff --git a/AGENTS.md b/AGENTS.md
index 11a3618..3eceece 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -26,14 +26,14 @@ mage bench # Run benchmarks
mage prReview # Run PR review baseline: world + benchProf
mage clean # Clean build artifacts
mage world # Clean + generate + test + build (recommended reset path)
-mage demo # Regen demo/ GIFs + screenshots (needs vhs+ttyd, sudo -v warmed)
+mage demo # Regen docs/tutorial/ GIFs + screenshots (needs vhs+ttyd, sudo -v warmed)
TAPE=07-stream-live mage demoOne # Regen one demo tape only
mage installDemoTools # One-time: install vhs (go install) and ttyd (dnf)
```
## Demo Pipeline
-`demo/` holds the reproducible TUI demo: 14 [VHS](https://github.com/charmbracelet/vhs) `.tape` files under `demo/tapes/` drive every dashboard tab and headless mode, write GIFs and PNGs into `demo/assets/`, and the resulting tutorial is `demo/TUTORIAL.md`. Background workload is generated by `demo/scripts/workload.sh`. `mage demo` is fully headless (no real terminal window) — safe to run in the background while editing code; the only foreground requirement is one `sudo -v` to pre-warm the sudo timestamp.
+`docs/tutorial/` holds the reproducible TUI demo: 14 [VHS](https://github.com/charmbracelet/vhs) `.tape` files under `docs/tutorial/tapes/` drive every dashboard tab and headless mode, write GIFs and PNGs into `docs/tutorial/assets/`, and the resulting tutorial is `docs/tutorial/TUTORIAL.md`. Background workload is generated by `docs/tutorial/scripts/workload.sh`. `mage demo` is fully headless (no real terminal window) — safe to run in the background while editing code; the only foreground requirement is one `sudo -v` to pre-warm the sudo timestamp.
## Code Generation
diff --git a/Magefile.go b/Magefile.go
index e9458b5..e675a8a 100644
--- a/Magefile.go
+++ b/Magefile.go
@@ -1159,18 +1159,18 @@ func runParquetChecks(dir, file string) error {
// --- Demo (VHS-driven TUI recordings) ---------------------------------------
//
-// Demo regenerates GIFs and PNG screenshots under demo/assets/ by running every
-// VHS tape under demo/tapes/. Each tape is wrapped by demo/scripts/run-tape.sh
-// which spins up the background workload generator. A sudo keep-alive loop in
-// the background of `mage demo` keeps the sudo timestamp warm so no tape ever
-// blocks on a password prompt.
+// Demo regenerates GIFs and PNG screenshots under docs/tutorial/assets/ by
+// running every VHS tape under docs/tutorial/tapes/. Each tape is wrapped by
+// docs/tutorial/scripts/run-tape.sh which spins up the background workload
+// generator. A sudo keep-alive loop keeps the sudo timestamp warm so no tape
+// ever blocks on a password prompt.
const (
- demoDir = "demo"
- demoTapesDir = "demo/tapes"
- demoScriptsDir = "demo/scripts"
- demoRunTape = "demo/scripts/run-tape.sh"
- demoSudoKeepers = "demo/scripts/sudo-keepalive.sh"
+ demoDir = "docs/tutorial"
+ demoTapesDir = "docs/tutorial/tapes"
+ demoScriptsDir = "docs/tutorial/scripts"
+ demoRunTape = "docs/tutorial/scripts/run-tape.sh"
+ demoSudoKeepers = "docs/tutorial/scripts/sudo-keepalive.sh"
)
// Demo regenerates every demo asset (full ~14-tape run, ~10 minutes).
diff --git a/README.md b/README.md
index f031fc6..739e681 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-> **🚧 PRE-ALPHA SOFTWARE:** This project is in a pre-alpha state and is intended for my own personal use only. Use at your own risk.
-
# I/O Riot NG (aka ior)
<img src=assets/ior-small.png />
@@ -14,13 +12,13 @@ This works only on Linux!
## Demo
-A short guided tour with animated GIFs of every major surface lives in [`demo/TUTORIAL.md`](./demo/TUTORIAL.md). Two teasers:
+A short guided tour with animated GIFs of every major surface lives in [`docs/tutorial/TUTORIAL.md`](./docs/tutorial/TUTORIAL.md). Two teasers:
-<img src=demo/assets/01-launch.gif width=720 alt="Cold start: PID picker, then the dashboard appears" />
+<img src=docs/tutorial/assets/01-launch.gif width=720 alt="Cold start: PID picker, then the dashboard appears" />
-<img src=demo/assets/13-tui-flamegraph.gif width=720 alt="Live in-TUI flamegraph rebuilding from real workload" />
+<img src=docs/tutorial/assets/13-tui-flamegraph.gif width=720 alt="Live in-TUI flamegraph rebuilding from real workload" />
-The demo is fully reproducible: `mage installDemoTools` once, then `sudo -v && mage demo` regenerates every GIF and screenshot. See the [tutorial](./demo/TUTORIAL.md) for the full walkthrough.
+The demo is fully reproducible: `mage installDemoTools` once, then `sudo -v && mage demo` regenerates every GIF and screenshot. See the [tutorial](./docs/tutorial/TUTORIAL.md) for the full walkthrough.
## Requirements
@@ -80,7 +78,7 @@ explanation.
Press **H** inside the dashboard to toggle the built-in help panel. Tabs are
reachable with **tab/shift+tab** or number keys **1–6**. Full hotkey reference:
-[docs/tui-reference.md](./docs/tui-reference.md).
+[docs/tutorial/TUTORIAL.md](./docs/tutorial/TUTORIAL.md#hotkey-quick-reference).
## Recording Modes
@@ -94,4 +92,4 @@ reachable with **tab/shift+tab** or number keys **1–6**. Full hotkey reference
| Parquet recording | press `R` in TUI, or `-parquet <file>` | streaming Parquet file |
Full details and the `.ior.zst` vs Parquet trade-off:
-[docs/tui-reference.md](./docs/tui-reference.md).
+[docs/tutorial/TUTORIAL.md](./docs/tutorial/TUTORIAL.md#recording-for-offline-analysis).
diff --git a/docs/tui-reference.md b/docs/tui-reference.md
deleted file mode 100644
index d6a7266..0000000
--- a/docs/tui-reference.md
+++ /dev/null
@@ -1,175 +0,0 @@
-# TUI Reference
-
-## TUI Flamegraphs
-
-Flamegraphs are available only inside the TUI dashboard.
-Use `-fields` to change the stack order and `-count` to choose the metric.
-The default stack order is `comm,path,tracepoint` (bottom to top).
-
-## Recording Modes
-
-`ior` has four distinct output flows:
-
-| Mode | How to use it | What it writes | Filter behavior |
-| --- | --- | --- | --- |
-| TUI dashboard | default startup | nothing continuously; data stays in memory unless you export | current TUI/global filters drive what you see |
-| TUI CSV snapshot export | press `e` in the dashboard | one `ior-stream-<timestamp>.csv` snapshot of the current filtered stream view | exports only the currently filtered in-memory rows |
-| Headless `.ior.zst` export | start with `-flamegraph -name <name>` | one aggregated native trace artifact written at shutdown | no TUI filter stack; this is the native trace/integration workflow |
-| Parquet recording | press `R` in the TUI, or start with `-parquet <file>` | a streaming Parquet file of traced syscall rows | TUI mode records rows that pass the active TUI filter; headless `-parquet` records all traced rows |
-
-Important distinction:
-
-- `.ior.zst` output is an aggregated native artifact, not a row-by-row event log.
-- CSV export is a point-in-time snapshot of the ring buffer.
-- Parquet recording is a streaming capture from start to stop.
-- The ring buffer is capped, so CSV export is not a replacement for Parquet recording or `.ior.zst` output.
-
-### Headless Native `.ior.zst` Output
-
-Use `-flamegraph` when you want the native `ior` trace artifact instead of a streaming row log:
-
-```shell
-sudo ./ior -flamegraph -name trace-run -duration 60
-```
-
-Native `.ior.zst` behavior:
-
-- writes one `*.ior.zst` file when the run ends
-- stores aggregated counters for repeated syscall/path/process combinations
-- is intended for `ior`'s native flamegraph and integration-style workflows
-- does not preserve one output row per traced syscall
-
-### TUI Parquet Recording
-
-Start a recording from the dashboard with `R`.
-
-- First `R`: open a filename prompt (`ior-recording-<timestamp>.parquet` by default).
-- `Enter`: start recording to that file.
-- Second `R`: stop and finalize the active Parquet file.
-- Recording stops automatically when you quit the TUI or reselect PID/TID/session scope.
-
-Lifecycle details:
-
-- TUI recording uses the active TUI global filter at emission time.
-- If a filter change restarts tracing, the recorder stays alive and continues writing matching rows after the restart.
-- The dashboard footer shows the active recording path or the last recording error.
-
-### Headless Parquet Recording
-
-Use `-parquet` to skip the TUI and stream traced syscall rows directly to a Parquet file:
-
-```shell
-sudo ./ior -parquet trace.parquet -duration 60
-```
-
-Headless Parquet mode behavior:
-
-- skips the TUI completely
-- records all traced rows
-- rejects content filters such as `-comm`, `-path`, `-pid`, and `-tid`
-- cannot be combined with `-plain`, `-flamegraph`, `--testflames`, or `--testliveflames`
-
-Use headless mode when you want a full recording, and TUI mode when you want interactive filtering plus optional start/stop recording from the dashboard.
-
-### Choosing Between `.ior.zst` and Parquet
-
-| Question | Native `.ior.zst` | Parquet |
-| --- | --- | --- |
-| Data shape | aggregated counters | one row per traced syscall |
-| Write pattern | collect in memory, write one compressed artifact at the end | stream rows continuously while recording |
-| Best for | `ior`-native trace artifacts, flamegraph workflows, integration assertions | offline analysis in other tools, long captures, preserving per-event detail |
-| Relative write cost | usually lower because repeated events are folded together before file write | usually higher because each traced row is serialized |
-| Detail retained | loses original row order and per-event granularity | keeps per-event timing and syscall fields |
-
-Rule of thumb:
-
-- choose `.ior.zst` when you want the native `ior` artifact and do not need every traced syscall row preserved
-- choose Parquet when you want a full event stream for downstream analysis outside `ior`
-
-## TUI Navigation
-
-The TUI has an in-screen help panel (toggle with **H**) that lists all available
-keys. Use it to discover shortcuts without consulting this document.
-
-Dashboard tabs:
-
-- **tab** / **shift+tab** — next / previous tab
-- **1** — Overview
-- **2** — Syscalls
-- **3** — Files
-- **4** — Processes
-- **5** — Latency+Gaps
-- **6** — Stream
-
-The TUI has two key scopes:
-
-- Global hotkeys: available from any dashboard screen.
-- Dashboard hotkeys: behavior that depends on the active tab (especially `6:Stream`).
-
-### Global Hotkeys
-
-- `tab` / `shift+tab`: cycle tabs.
-- `1`–`6`: jump to tab by number (`7` is an alias for `6`).
-- `e`: export filtered stream rows to CSV (`ior-stream-<timestamp>.csv`).
-- `R`: start or stop Parquet recording.
-- `p`: re-open process selector (PID selection flow).
-- `t`: open TID selector flow.
-- `o`: open probe selection/toggling dialog.
-- `r`: refresh dashboard snapshot.
-- `H`: toggle bottom help sections on/off.
-- `q` or `ctrl+c`: quit.
-
-### Dashboard / Tab-Specific Hotkeys
-
-- `d` in `3:Files`: toggle directory-grouped files view.
-- `s` in sortable tabs (`2:Syscalls`, `3:Files`, `4:Processes`): sort by selected column.
-- `S` in sortable tabs: reverse-sort by selected column.
-- `j/k` or `up/down` in list tabs: scroll list.
-
-`left/right` and `h/l` do not switch tabs. In `6:Stream` paused mode they move the selected column.
-
-### 6:Stream Hotkeys and Behavior
-
-`6:Stream` has two modes:
-
-- Live mode (`paused=false`): rows update continuously.
-- Pause mode (`paused=true`): selection/cell/filter/search/export workflows are enabled.
-
-Core controls:
-
-- `space`: toggle live/pause.
-- `g`/`G`: jump to top/tail.
-- `c`: clear stream filters.
-- `f`: open advanced filter modal.
-- `j/k` or `up/down`: move selected row (pause) or scroll (live).
-- `left/right` or `h/l`: move selected column in pause mode.
-
-#### Enter-Based Filter Stack (Pause Mode)
-
-In pause mode, `enter` on the selected cell pushes a filter onto a stack and
-immediately re-filters the current ring buffer snapshot. Filters are stackable.
-
-- String columns use case-insensitive substring match:
- - `Comm` → `comm~<value>`
- - `Syscall` → `syscall~<value>`
- - `File` → `file~<value>`
-- Numeric exact match: `PID`, `TID`, `FD`, `Ret`, `Bytes`
-- Numeric threshold (`>=`): `Latency` → `latency>=selected_value`, `Gap` → `gap>=selected_value`
-
-`esc` in pause mode pops the most recent filter (LIFO); repeated `esc` undoes
-all stacked filters.
-
-#### Regex Search (Pause Mode)
-
-- `/`: search forward; `?`: search backward.
-- Search checks all stream columns and wraps around the ring buffer.
-- `n` / `N`: next / previous match.
-
-#### Stream CSV Export (Pause Mode)
-
-- `x`: quick export filtered stream rows to CSV.
-- `X`: export with filename prompt.
-- `E`: open last stream-exported CSV in foreground editor (`EDITOR` → `VISUAL` → `SUDO_EDITOR` → `hx` → `vi`).
-
-`e` (global) exports a fresh filtered snapshot even outside paused mode; `x`/`X`
-export the exact paused view.
diff --git a/demo/TUTORIAL.md b/docs/tutorial/TUTORIAL.md
index 5940d05..e53013a 100644
--- a/demo/TUTORIAL.md
+++ b/docs/tutorial/TUTORIAL.md
@@ -28,22 +28,23 @@ Every GIF in this document is regenerated from a [VHS](https://github.com/charmb
## Installing ior
-See the [main README](../README.md) for full install steps. In short:
+See the [main README](../../README.md) for full install steps. The quickest path from any Docker-capable Linux host:
```shell
git clone https://codeberg.org/snonux/ior ~/git/ior
-git clone https://github.com/aquasecurity/libbpfgo ~/git/libbpfgo
-sudo dnf install -y golang clang bpftool elfutils-libelf-devel zlib-static glibc-static libzstd-static
-git -C ~/git/libbpfgo checkout v0.9.2-libbpf-1.5.1
-git -C ~/git/libbpfgo submodule update --init --recursive
-make -C ~/git/libbpfgo libbpfgo-static
cd ~/git/ior
-env GOTOOLCHAIN=auto mage all
+mage buildDocker # builds inside a Rocky 9 container, ~15 min on first run
+```
+
+For a native build (libbpfgo must be cloned alongside the repo first — see the README):
+
+```shell
+mage all
```
ior needs `CAP_BPF`, so every invocation below uses `sudo`.
-The build dance only has to happen once: the resulting `ior` binary is fully statically linked (libbpf, libelf, libzstd, zlib are baked in) **and** the embedded BPF object uses CO-RE, so libbpf relocates field offsets against the target kernel's BTF at load time. Build it on one box, then `scp ior` to any other Linux host with a BTF-enabled kernel and run it there. See the [Compile once, run everywhere](../README.md#compile-once-run-everywhere) section in the main README for details.
+The build dance only has to happen once: the resulting `ior` binary is fully statically linked and uses CO-RE, so the same binary runs on any BTF-enabled Linux kernel without recompilation. See the [Compile once, run everywhere](../../README.md#compile-once-run-everywhere) section for details.
## First launch: the PID picker
@@ -197,4 +198,48 @@ Or rebuild a single tape after editing it:
TAPE=07-stream-live mage demoOne
```
-Tapes live in [`demo/tapes/`](./tapes), the background workload that drives them is [`demo/scripts/workload.sh`](./scripts/workload.sh), and the resulting assets land in [`demo/assets/`](./assets). VHS records headlessly under `ttyd` + Chromium — no real terminal window opens, so `mage demo` is safe to run in the background while you keep working.
+Tapes live in [`tapes/`](./tapes), the background workload that drives them is [`scripts/workload.sh`](./scripts/workload.sh), and the resulting assets land in [`assets/`](./assets). VHS records headlessly under `ttyd` + Chromium — no real terminal window opens, so `mage demo` is safe to run in the background while you keep working.
+
+## Hotkey Quick Reference
+
+### Global keys
+
+| Key | Action |
+|-----|--------|
+| `tab` / `shift+tab` | next / previous tab |
+| `1`–`6` (`7` = alias for `6`) | jump to tab by number |
+| `H` | toggle bottom help panel |
+| `e` | export filtered stream snapshot to CSV |
+| `R` | start / stop Parquet recording |
+| `p` | re-open PID picker |
+| `t` | open TID picker |
+| `o` | open probe selection dialog |
+| `r` | refresh dashboard snapshot |
+| `q` / `ctrl+c` | quit |
+
+### Tab-specific keys (`2:Syscalls`, `3:Files`, `4:Processes`)
+
+| Key | Action |
+|-----|--------|
+| `s` | sort by selected column (default direction) |
+| `S` | reverse-sort by selected column |
+| `j`/`k` or `↑`/`↓` | scroll list |
+| `d` (Files only) | toggle directory grouping |
+
+### Stream tab (`7:Stream`)
+
+| Key | Action |
+|-----|--------|
+| `space` | toggle live / pause mode |
+| `g` / `G` | jump to top / tail |
+| `j`/`k` or `↑`/`↓` | move row (pause) / scroll (live) |
+| `←`/`→` or `h`/`l` | move selected column (pause only) |
+| `enter` | push cell value as filter (pause) |
+| `esc` | pop most recent filter (LIFO) |
+| `c` | clear all stream filters |
+| `f` | open advanced filter modal |
+| `/` / `?` | regex search forward / backward |
+| `n` / `N` | next / previous search match |
+| `x` | quick CSV export of paused view |
+| `X` | CSV export with filename prompt |
+| `E` | open last CSV export in `$EDITOR` |
diff --git a/docs/tutorial/assets/00-hero-flamegraph.png b/docs/tutorial/assets/00-hero-flamegraph.png
new file mode 100644
index 0000000..d409e79
--- /dev/null
+++ b/docs/tutorial/assets/00-hero-flamegraph.png
Binary files differ
diff --git a/docs/tutorial/assets/00-logo.png b/docs/tutorial/assets/00-logo.png
new file mode 100644
index 0000000..d2da30c
--- /dev/null
+++ b/docs/tutorial/assets/00-logo.png
Binary files differ
diff --git a/docs/tutorial/assets/01-launch.gif b/docs/tutorial/assets/01-launch.gif
new file mode 100644
index 0000000..978b95e
--- /dev/null
+++ b/docs/tutorial/assets/01-launch.gif
Binary files differ
diff --git a/docs/tutorial/assets/02-overview-tab.gif b/docs/tutorial/assets/02-overview-tab.gif
new file mode 100644
index 0000000..56f0e98
--- /dev/null
+++ b/docs/tutorial/assets/02-overview-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/03-syscalls-tab.gif b/docs/tutorial/assets/03-syscalls-tab.gif
new file mode 100644
index 0000000..47c4029
--- /dev/null
+++ b/docs/tutorial/assets/03-syscalls-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/04-files-tab.gif b/docs/tutorial/assets/04-files-tab.gif
new file mode 100644
index 0000000..af18828
--- /dev/null
+++ b/docs/tutorial/assets/04-files-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/05-processes-tab.gif b/docs/tutorial/assets/05-processes-tab.gif
new file mode 100644
index 0000000..a6beed1
--- /dev/null
+++ b/docs/tutorial/assets/05-processes-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/06-latency-gaps-tab.gif b/docs/tutorial/assets/06-latency-gaps-tab.gif
new file mode 100644
index 0000000..b0d9c81
--- /dev/null
+++ b/docs/tutorial/assets/06-latency-gaps-tab.gif
Binary files differ
diff --git a/docs/tutorial/assets/07-stream-live.gif b/docs/tutorial/assets/07-stream-live.gif
new file mode 100644
index 0000000..af742bd
--- /dev/null
+++ b/docs/tutorial/assets/07-stream-live.gif
Binary files differ
diff --git a/docs/tutorial/assets/08-stream-pause-filter.gif b/docs/tutorial/assets/08-stream-pause-filter.gif
new file mode 100644
index 0000000..7821cf7
--- /dev/null
+++ b/docs/tutorial/assets/08-stream-pause-filter.gif
Binary files differ
diff --git a/docs/tutorial/assets/09-stream-regex-search.gif b/docs/tutorial/assets/09-stream-regex-search.gif
new file mode 100644
index 0000000..73e068b
--- /dev/null
+++ b/docs/tutorial/assets/09-stream-regex-search.gif
Binary files differ
diff --git a/docs/tutorial/assets/10-stream-csv-export.gif b/docs/tutorial/assets/10-stream-csv-export.gif
new file mode 100644
index 0000000..aa5bb3b
--- /dev/null
+++ b/docs/tutorial/assets/10-stream-csv-export.gif
Binary files differ
diff --git a/docs/tutorial/assets/11-pid-tid-probe.gif b/docs/tutorial/assets/11-pid-tid-probe.gif
new file mode 100644
index 0000000..f7f96c7
--- /dev/null
+++ b/docs/tutorial/assets/11-pid-tid-probe.gif
Binary files differ
diff --git a/docs/tutorial/assets/12-parquet-recording.gif b/docs/tutorial/assets/12-parquet-recording.gif
new file mode 100644
index 0000000..b9baab3
--- /dev/null
+++ b/docs/tutorial/assets/12-parquet-recording.gif
Binary files differ
diff --git a/docs/tutorial/assets/13-tui-flamegraph.gif b/docs/tutorial/assets/13-tui-flamegraph.gif
new file mode 100644
index 0000000..71aece3
--- /dev/null
+++ b/docs/tutorial/assets/13-tui-flamegraph.gif
Binary files differ
diff --git a/docs/tutorial/assets/13a-order-by-process.png b/docs/tutorial/assets/13a-order-by-process.png
new file mode 100644
index 0000000..8a469bf
--- /dev/null
+++ b/docs/tutorial/assets/13a-order-by-process.png
Binary files differ
diff --git a/docs/tutorial/assets/13b-order-by-path.png b/docs/tutorial/assets/13b-order-by-path.png
new file mode 100644
index 0000000..3518412
--- /dev/null
+++ b/docs/tutorial/assets/13b-order-by-path.png
Binary files differ
diff --git a/docs/tutorial/assets/13c-order-by-syscall.png b/docs/tutorial/assets/13c-order-by-syscall.png
new file mode 100644
index 0000000..3eb5058
--- /dev/null
+++ b/docs/tutorial/assets/13c-order-by-syscall.png
Binary files differ
diff --git a/docs/tutorial/assets/13d-order-by-pid.png b/docs/tutorial/assets/13d-order-by-pid.png
new file mode 100644
index 0000000..0d56f12
--- /dev/null
+++ b/docs/tutorial/assets/13d-order-by-pid.png
Binary files differ
diff --git a/docs/tutorial/assets/13e-order-by-process-paths.png b/docs/tutorial/assets/13e-order-by-process-paths.png
new file mode 100644
index 0000000..ccd450a
--- /dev/null
+++ b/docs/tutorial/assets/13e-order-by-process-paths.png
Binary files differ
diff --git a/docs/tutorial/assets/14-headless-modes.gif b/docs/tutorial/assets/14-headless-modes.gif
new file mode 100644
index 0000000..cf245f4
--- /dev/null
+++ b/docs/tutorial/assets/14-headless-modes.gif
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-dashboard.png b/docs/tutorial/assets/screenshot-dashboard.png
new file mode 100644
index 0000000..0eee11f
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-dashboard.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-files-grouped.png b/docs/tutorial/assets/screenshot-files-grouped.png
new file mode 100644
index 0000000..b2a206a
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-files-grouped.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-flamegraph.png b/docs/tutorial/assets/screenshot-flamegraph.png
new file mode 100644
index 0000000..ae79335
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-flamegraph.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-latency.png b/docs/tutorial/assets/screenshot-latency.png
new file mode 100644
index 0000000..43b6cd8
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-latency.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-overview.png b/docs/tutorial/assets/screenshot-overview.png
new file mode 100644
index 0000000..eac3138
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-overview.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-pidpicker-open.png b/docs/tutorial/assets/screenshot-pidpicker-open.png
new file mode 100644
index 0000000..22a7b93
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-pidpicker-open.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-pidpicker.png b/docs/tutorial/assets/screenshot-pidpicker.png
new file mode 100644
index 0000000..4b8f025
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-pidpicker.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-probes.png b/docs/tutorial/assets/screenshot-probes.png
new file mode 100644
index 0000000..df0664a
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-probes.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-processes.png b/docs/tutorial/assets/screenshot-processes.png
new file mode 100644
index 0000000..c1873d2
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-processes.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-recording-active.png b/docs/tutorial/assets/screenshot-recording-active.png
new file mode 100644
index 0000000..e50d588
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-recording-active.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-stream-export.png b/docs/tutorial/assets/screenshot-stream-export.png
new file mode 100644
index 0000000..921330c
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-stream-export.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-stream-filtered.png b/docs/tutorial/assets/screenshot-stream-filtered.png
new file mode 100644
index 0000000..7b85470
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-stream-filtered.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-stream-live.png b/docs/tutorial/assets/screenshot-stream-live.png
new file mode 100644
index 0000000..a71338c
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-stream-live.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-stream-search.png b/docs/tutorial/assets/screenshot-stream-search.png
new file mode 100644
index 0000000..5243f18
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-stream-search.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-syscalls.png b/docs/tutorial/assets/screenshot-syscalls.png
new file mode 100644
index 0000000..7a63ce3
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-syscalls.png
Binary files differ
diff --git a/docs/tutorial/assets/screenshot-tidpicker.png b/docs/tutorial/assets/screenshot-tidpicker.png
new file mode 100644
index 0000000..baf08c7
--- /dev/null
+++ b/docs/tutorial/assets/screenshot-tidpicker.png
Binary files differ
diff --git a/demo/scripts/run-tape.sh b/docs/tutorial/scripts/run-tape.sh
index c22e2e3..c22e2e3 100755
--- a/demo/scripts/run-tape.sh
+++ b/docs/tutorial/scripts/run-tape.sh
diff --git a/demo/scripts/sudo-keepalive.sh b/docs/tutorial/scripts/sudo-keepalive.sh
index 4ae1e27..4ae1e27 100755
--- a/demo/scripts/sudo-keepalive.sh
+++ b/docs/tutorial/scripts/sudo-keepalive.sh
diff --git a/demo/scripts/workload.sh b/docs/tutorial/scripts/workload.sh
index ebbf58b..ebbf58b 100755
--- a/demo/scripts/workload.sh
+++ b/docs/tutorial/scripts/workload.sh
diff --git a/demo/tapes/01-launch.tape b/docs/tutorial/tapes/01-launch.tape
index 1e2b558..1e2b558 100644
--- a/demo/tapes/01-launch.tape
+++ b/docs/tutorial/tapes/01-launch.tape
diff --git a/demo/tapes/02-overview-tab.tape b/docs/tutorial/tapes/02-overview-tab.tape
index a16a0b8..a16a0b8 100644
--- a/demo/tapes/02-overview-tab.tape
+++ b/docs/tutorial/tapes/02-overview-tab.tape
diff --git a/demo/tapes/03-syscalls-tab.tape b/docs/tutorial/tapes/03-syscalls-tab.tape
index 376f632..376f632 100644
--- a/demo/tapes/03-syscalls-tab.tape
+++ b/docs/tutorial/tapes/03-syscalls-tab.tape
diff --git a/demo/tapes/04-files-tab.tape b/docs/tutorial/tapes/04-files-tab.tape
index 49adfee..49adfee 100644
--- a/demo/tapes/04-files-tab.tape
+++ b/docs/tutorial/tapes/04-files-tab.tape
diff --git a/demo/tapes/05-processes-tab.tape b/docs/tutorial/tapes/05-processes-tab.tape
index ad5f64e..ad5f64e 100644
--- a/demo/tapes/05-processes-tab.tape
+++ b/docs/tutorial/tapes/05-processes-tab.tape
diff --git a/demo/tapes/06-latency-gaps-tab.tape b/docs/tutorial/tapes/06-latency-gaps-tab.tape
index b19ae58..b19ae58 100644
--- a/demo/tapes/06-latency-gaps-tab.tape
+++ b/docs/tutorial/tapes/06-latency-gaps-tab.tape
diff --git a/demo/tapes/07-stream-live.tape b/docs/tutorial/tapes/07-stream-live.tape
index 9d7ad4f..9d7ad4f 100644
--- a/demo/tapes/07-stream-live.tape
+++ b/docs/tutorial/tapes/07-stream-live.tape
diff --git a/demo/tapes/08-stream-pause-filter.tape b/docs/tutorial/tapes/08-stream-pause-filter.tape
index 416830c..416830c 100644
--- a/demo/tapes/08-stream-pause-filter.tape
+++ b/docs/tutorial/tapes/08-stream-pause-filter.tape
diff --git a/demo/tapes/09-stream-regex-search.tape b/docs/tutorial/tapes/09-stream-regex-search.tape
index b643740..b643740 100644
--- a/demo/tapes/09-stream-regex-search.tape
+++ b/docs/tutorial/tapes/09-stream-regex-search.tape
diff --git a/demo/tapes/10-stream-csv-export.tape b/docs/tutorial/tapes/10-stream-csv-export.tape
index c1f4935..c1f4935 100644
--- a/demo/tapes/10-stream-csv-export.tape
+++ b/docs/tutorial/tapes/10-stream-csv-export.tape
diff --git a/demo/tapes/11-pid-tid-probe.tape b/docs/tutorial/tapes/11-pid-tid-probe.tape
index caccc48..caccc48 100644
--- a/demo/tapes/11-pid-tid-probe.tape
+++ b/docs/tutorial/tapes/11-pid-tid-probe.tape
diff --git a/demo/tapes/12-parquet-recording.tape b/docs/tutorial/tapes/12-parquet-recording.tape
index c6f3399..c6f3399 100644
--- a/demo/tapes/12-parquet-recording.tape
+++ b/docs/tutorial/tapes/12-parquet-recording.tape
diff --git a/demo/tapes/13-tui-flamegraph.tape b/docs/tutorial/tapes/13-tui-flamegraph.tape
index 7572159..7572159 100644
--- a/demo/tapes/13-tui-flamegraph.tape
+++ b/docs/tutorial/tapes/13-tui-flamegraph.tape
diff --git a/demo/tapes/14-headless-modes.tape b/docs/tutorial/tapes/14-headless-modes.tape
index ce894f6..ce894f6 100644
--- a/demo/tapes/14-headless-modes.tape
+++ b/docs/tutorial/tapes/14-headless-modes.tape