Margin — Project Blueprint
Historical document. This is the founding plan captured before Margin’s first release. It is preserved as project history and is not a current specification. See the root
README.mdfor the product,ROADMAP.mdfor direction,docs/architecture.mdfor current architecture, anddocs/adr/for decisions. Availability claims and future-tense statements below are intentionally not maintained.
A fast, keyboard-first terminal diff viewer for reviewing Git changes, patches, and AI-authored code. Written in Rust. Single binary. Starts instantly.
This document is the complete founding blueprint: analysis of the prior art (Hunk), product positioning, MVP scope, architecture, repo layout, process, and a step-by-step path from first commit to public launch.
The actual project foundation lives in the repository root — a compiling
Cargo workspace with governance docs, CI, and Architecture Decision Records
in adr/ that lock in every major technical
decision below with context and alternatives considered.
Naming note: margin is available on crates.io (verified 2026-06-10).
Verify GitHub org/repo and Homebrew formula availability before committing to it.
Backup candidates also free on crates.io: onceover, perusal, mull (collides
with the mull mutation-testing project — avoid). The rest of this document uses
Margin / binary margin.
1. What makes Hunk useful (honest analysis)
Hunk (modem-dev/hunk, MIT, TypeScript/Bun on OpenTUI + Pierre diffs) earned its
audience by getting a few things genuinely right:
- Review-first framing. It’s not a pager or a prettifier — it treats a changeset as a thing you review, with a sidebar of files and a review stream. That single framing decision separates it from delta/diff-so-fancy (stream prettifiers) and difftastic (a better diff algorithm).
- Mirrors Git’s mental model.
hunk diff,hunk show HEAD~1,hunk patch -,hunk pager— zero new concepts to learn. You can drop it intocore.pagerand it composes with what you already do. - The AI-review wedge. Inline agent annotations and a live agent-session workflow (“open Hunk, point your agent’s skill at the session”). It’s the first diff tool positioned for the human half of the human–agent loop, and that’s why it took off now.
- Responsive layout. Auto split/stack based on terminal width — a small thing that makes it feel like a designed product, not a curses demo.
- Meets users where they are. Watch mode, mouse support, Jujutsu and Sapling auto-detection, themes via TOML, npm + brew + nix packaging.
- Tasteful defaults. Syntax highlighting, line numbers, good themes out of the box. No configuration required to get the screenshot experience.
Its structural weaknesses (our opening):
- Runtime weight. Node 18+ / Bun, npm global install, a session-broker daemon for agent workflows. Startup and memory are JS-class, and the install story excludes people who don’t have Node.
- View-only. You can look at a hunk but you cannot act on it — no stage, unstage, or discard. After reviewing, you switch tools to do anything.
- No search, no structural diff, weaker giant-diff story. Their own feature table concedes structural diffing; lockfile-heavy agent diffs are exactly where TUI diff viewers fall over.
- Complexity creep. Daemon + session broker + embeddable component API at v0.15 is a lot of surface area for a young project.
2. Features to take inspiration from (not code)
Take the ideas, reimplement from scratch:
| Idea | Why it matters |
|---|---|
Git-verb CLI (diff, show, pager, stdin patch) | Zero learning curve; composes with git config |
| File sidebar + review stream | The core “review a changeset” interaction |
| Auto split/stack responsive layout | Feels designed; works in narrow tmux panes |
| Watch mode with auto-reload | Perfect for “agent is editing right now” |
| Pager mode with non-TTY passthrough | Safe to set as core.pager permanently |
| TOML config + custom themes inheriting a base | Low-effort personalization |
| Untracked files included in working-tree diff | Agents create new files constantly; git diff hides them |
| Honest feature-comparison table in README | Builds trust, frames the category |
| Examples directory with runnable demo diffs | Best onboarding device a diff tool can have |
Explicitly not copying: their branding/visual identity, OpenTUI component API, the agent daemon/session-broker architecture (right idea, too heavy too early), and the npm distribution model.
3. Where Margin improves beyond Hunk
- Single static Rust binary.
brew install margin,cargo install margin, orcurl | sh. No runtime. Cold start target: < 50 ms to first paint on a 100-file diff. This alone is a reason to switch. - Act on the diff. Stage, unstage, and discard at hunk (later: line) granularity, inside the review flow. Review → decide → done, one tool. This is the headline feature Hunk lacks and the killer workflow for AI changes: accept/reject an agent’s edits hunk by hunk.
- Search and jump.
/regex search across the changeset,n/Nbetween matches, fuzzy file picker. Table stakes from vim/less that review TUIs keep forgetting. - Review state. Mark files viewed (GitHub-style), persists per
(repo, diff-id)so re-running after a rebase keeps your place. - Giant-diff performance as a feature. Lazy syntax highlighting,
virtualized rendering, auto-collapse of lockfiles/generated files
(
linguist-generated, configurable globs). Benchmarked in CI; the README publishes the numbers. - Structured output for tooling.
margin --jsonemits the parsed changeset and (later) review decisions. Agents and scripts consume Margin without scraping a TTY. Our agent story is files and exit codes, not a daemon — simpler, scriptable, debuggable. - Terminal citizenship. Correct behavior in tmux/ssh/16-color terminals,
NO_COLOR, light/dark detection, non-TTY passthrough, graceful wide-char handling. Boring, and the difference between a toy and a tool. - Review notes that go somewhere. Per-hunk notes exported as Markdown — paste into a PR or feed back to the agent that wrote the code.
What we deliberately skip (scope discipline): structural/AST diffing (difftastic owns it; revisit post-1.0), commit graph/log browsing (lazygit/gitui own it), merge-conflict resolution (separate product), embeddable UI component.
4. MVP feature set (v0.1.0)
Principle: the best read-only diff review experience in a terminal, period. Writing operations land in v0.2 — the MVP must be flawless at viewing first, because trust in a tool that can discard your code starts with trust in how it displays your code.
Inputs
margin/margin diff— working tree vs HEAD, untracked files includedmargin diff --staged— index vs HEADmargin show [rev],margin diff A..B,margin diff A B -- path/(revision args delegated to git semantics)margin diff fileA fileB— two arbitrary filesgit diff | margin/margin patch -— any unified diff on stdinmargin pager—core.pagermode; passes input through unchanged when stdout is not a TTY
UI
- File sidebar (toggle
b): status glyph, +/- counts, viewed checkmarks - Unified and side-by-side views;
autopicks by width; togglev - Syntax highlighting (syntect) + intra-line word-level diff highlighting
- Line numbers, hunk headers, file headers with old/new paths and mode changes
- Collapse/expand files (
zaper file,zAall); auto-collapse generated files - Renames, binary files, mode changes, submodules rendered sensibly
- Help overlay (
?), command footer, 4 built-in themes + ANSI-16 fallback
Navigation (keyboard-first; vim grammar)
j/kline,J/Khunk,]/[orTabfile,gg/G,Ctrl-d/u/search (regex),n/Nnext/prev matchffuzzy file pickermmark file viewed (session-scoped in MVP),rreload,qquitwtoggle wrap,1..4theme quick-switch
Quality bars (release blockers, tested in CI)
- < 50 ms to first paint, 100-file / 10k-line diff (M-series Mac, release build)
- 250k-line diff scrolls at 60 fps; highlighting is lazy and never blocks input
- Works on macOS, Linux, Windows Terminal; tmux and ssh verified
- Zero panics on the patch-corpus fuzz set; non-UTF-8 content degrades, never crashes
Explicitly deferred
v0.2: stage/unstage/discard hunks, watch mode, persistent viewed-state.
v0.3: review notes + Markdown export, --json, gh PR integration, jj support.
Later: mouse, line-level staging, interdiff, custom keymaps, MCP server (maybe).
5. Tech stack and architecture
Every row below is locked in by an ADR in adr/ — change the ADR
to change the decision.
| Concern | Choice | ADR |
|---|---|---|
| Language | Rust (stable, MSRV = stable−2) | 0001 |
| TUI | ratatui + crossterm | 0002 |
| App pattern | Elm architecture, pure core | 0003 |
| Code layout | 4-crate Cargo workspace, DiffSource boundary | 0004 |
| Git | git2 behind a trait | 0005 |
| Highlight / intra-line | syntect + similar | 0006 |
| CLI | git-verb subcommands, pager passthrough guarantee | 0007 |
| Config | TOML, XDG + repo-local | 0008 |
| Errors | thiserror in libs, anyhow at edge, no-panic policy | 0009 |
| Testing | unit + insta snapshots + temp-repo integration + corpus + fuzz + bench | 0010 |
| Release | cargo-dist, Conventional Commits, git-cliff | 0011 |
| License | MIT OR Apache-2.0 | 0012 |
Why not TypeScript/OpenTUI like Hunk: we’d inherit exactly the weaknesses we’re positioning against (runtime, startup, install). Why not Go/bubbletea: viable, but ratatui’s ecosystem (syntect/similar/git2/insta) is a better fit and Rust attracts the contributor base that builds tools like this (delta, gitui, bat).
Architecture: Cargo workspace, Elm-shaped app
margin/ # workspace root
├── crates/
│ ├── margin-core/ # PURE: no I/O, no TUI deps
│ │ ├── model.rs # Changeset → FileDiff → Hunk → Line (+ intra-line spans)
│ │ ├── patch.rs # unified-diff parser (fuzzed)
│ │ ├── intraline.rs # word-level diff via `similar`
│ │ └── collapse.rs # generated-file detection, lockfile heuristics
│ ├── margin-vcs/ # DiffSource trait + implementations
│ │ ├── source.rs # trait DiffSource { fn load(&self) -> Changeset; fn id(&self) -> DiffId }
│ │ ├── git.rs # git2: worktree/staged/rev-range/show (+ apply in v0.2)
│ │ ├── files.rs # two-file comparison
│ │ └── patch_input.rs # stdin/patch-file
│ ├── margin-tui/ # rendering + interaction, depends on core only
│ │ ├── app.rs # Model (AppState), Msg, update() — Elm architecture
│ │ ├── view/ # sidebar, unified, side_by_side, help, picker, search
│ │ ├── highlight.rs # lazy syntect cache (highlight on first visibility)
│ │ ├── keymap.rs # Msg ← key event mapping (custom keymaps later = data change)
│ │ └── theme.rs # built-ins + custom-from-TOML
│ └── margin/ # the binary
│ ├── main.rs # clap → pick DiffSource → run TUI or passthrough
│ └── config.rs # config discovery/merge
Load-bearing decisions:
margin-coreis pure and I/O-free. The diff model, parser, and intra-line logic are plain functions over data — trivially unit-testable, fuzzable, and reusable (the future--jsonoutput is just serializing this model).DiffSourceis the only seam to the outside world. Git, files, and stdin all produce the sameChangeset. Watch mode (v0.2) is “a source that can notify”; jj support (v0.3) is one new impl; tests inject synthetic sources.- Elm architecture in the TUI: one
AppState, oneMsgenum, oneupdate(state, msg), pureview(state) -> Frame. Every interaction is testable asupdatecalls + an insta snapshot of the rendered frame. No scattered mutable widget state. - Lazy everything for big diffs: parse the changeset eagerly (cheap), highlight and intra-line-diff only what scrolls into view, cache per file. The UI thread never blocks; a background thread warms the cache.
- Write operations (v0.2) live in
margin-vcsbehind explicit, confirmation-gated Msgs. Discard is destructive: it requires a typed confirm and (config-on by default) drops a backup patch in.git/margin/trash/for undo.
6. Repository structure
Implemented in the repository root:
margin/
├── crates/ # (as above — compiles today)
├── tests/corpus/ # real-world .patch fixtures: renames, binary,
│ # CRLF, unicode, mode changes, 100k-line lockfile
├── benches/ # criterion giant-diff benchmarks (issue #6)
├── fuzz/ # cargo-fuzz parser target (issue #2)
├── examples/ # runnable demo diffs
├── docs/
│ ├── adr/ # Architecture Decision Records (0001–0012 + template)
│ ├── architecture.md # the crate map, kept current
│ ├── keybindings.md configuration.md themes.md
├── assets/ # demo.tape (vhs) → README GIF
├── .github/
│ ├── workflows/ci.yml # fmt, clippy, deny, 3-OS test matrix, MSRV
│ ├── ISSUE_TEMPLATE/{bug_report.yml,feature_request.yml,config.yml}
│ ├── PULL_REQUEST_TEMPLATE.md dependabot.yml
├── README.md CONTRIBUTING.md ROADMAP.md CHANGELOG.md
├── CODE_OF_CONDUCT.md SECURITY.md LICENSE-MIT LICENSE-APACHE
├── Cargo.toml # workspace + pinned workspace.dependencies
├── rust-toolchain.toml rustfmt.toml deny.toml cliff.toml
└── AGENTS.md # agent briefing: build/test commands, architecture map
7. README, docs, roadmap, contributing, issue templates
All written and in place in the repository root:
README.md— hero GIF slot, 30-second pitch, install, quick start, honest comparison table (delta, difftastic, Hunk, gitui — with credit to Hunk as inspiration), config, FAQ.CONTRIBUTING.md— dev setup in 3 commands, architecture + ADR pointers, test/snapshot workflow, commit conventions, “what makes a good PR”.ROADMAP.md— v0.1 → v1.0 published in-repo (roadmap-as-promise)..github/ISSUE_TEMPLATE/— structured YAML bug/feature forms + config.yml routing questions to Discussions.adr/— decision log; new significant decisions require an ADR (process indocs/adr/README.md).
Docs philosophy: the README is the product page; docs/ is the manual;
? in-app is the cheat sheet. Every feature PR updates all three or explains
why not (PR template enforces).
8. Testing, CI, release, packaging
Testing pyramid (ADR 0010)
- Unit (
margin-core): parser, model invariants, intra-line spans, collapse heuristics. Target: core is ~fully covered because it’s pure. - Snapshot (
margin-tui): render known changesets at fixed sizes (80×24, 200×50, 40×20) via ratatuiTestBackend, snapshot with insta. Keybinding flows = sequences ofupdate()calls then snapshot. - Integration (
margin-vcs+ bin): create real temp git repos (staged, untracked, renamed, binary, submodule cases), assert the Changeset; run the binary with--json/passthrough modes and assert stdout. - Corpus: every weird real-world patch that ever breaks us gets a fixture and a regression test. Fuzz target runs the parser on arbitrary bytes.
- Benchmarks: criterion on parse + first-frame for the giant fixture; CI fails on >20% regression (informational at first, blocking by v0.3).
CI (GitHub Actions — .github/workflows/ci.yml)
check: rustfmt, clippy-D warnings, cargo-deny (advisories + licenses)test: matrix {ubuntu, macos, windows} × {stable}, plus MSRV on ubuntufuzz-smoke: 60s parser fuzz on PRs touchingmargin-core(added with issue #2)bench(main only): post numbers as commit comment- Caching via
Swatinem/rust-cache; total PR wall-time target < 8 min
Release & packaging (cargo-dist, ADR 0011)
- Tag
v*→ release workflow builds:aarch64/x86_64-apple-darwin,x86_64/aarch64-unknown-linux-musl(static),x86_64-pc-windows-msvc - Artifacts: tarballs + checksums,
curl | sh+ PowerShell installers, Homebrew formula pushed to<org>/homebrew-tap cargo publish(all crates), cargo-binstall metadata for free- git-cliff generates CHANGELOG from Conventional Commits; release notes curated by hand on top
- Post-1.0 channels: AUR, nixpkgs, winget, scoop, mise — driven by community PRs
9. The first 10 GitHub issues
Create these on day one; they make the project look alive and give early
visitors something to grab. Each gets labels from:
area:core area:tui area:vcs area:ci type:feat type:infra
good first issue help wanted M1 (milestone v0.1).
-
#1 Scaffold workspace, CI, and quality gates
type:infraM1Cargo workspace with the four crates (stub lib.rs + doc comments stating each crate’s contract), rustfmt/clippy/deny configs, ci.yml green, README, dual license, CODE_OF_CONDUCT, PR/issue templates, ADRs. AC:cargo testgreen in CI on all three OSes from a fresh clone. ← largely done by this scaffold; close after first CI run. -
#2
margin-core: changeset model + unified-diff parserarea:coreM1DefineChangeset/FileDiff/Hunk/Line(+ rename/binary/mode metadata). Parse unified diffs incl. git extensions (rename from/to, mode, binary). Corpus fixtures + fuzz target. AC: parses every fixture byte-exactly round-trippable; fuzz 10⁶ iters clean. -
#3
margin-vcs:DiffSourcetrait + git worktree/staged/rev sourcesarea:vcsM1git2-backed sources for worktree (untracked included),--staged, revision ranges,show. Integration tests on generated temp repos. AC: matchesgit diffsemantics on the test matrix incl. renames. -
#4 Minimal TUI: file list + unified view + vim navigation
area:tuiM1Elm skeleton (AppState/Msg/update/view), sidebar, unified diff pane, j/k/J/K/]/[/gg/G/q, help overlay. Insta snapshots at 3 terminal sizes. AC: review a real repo’s diff end-to-end with keyboard only. -
#5 Side-by-side view + responsive auto layout
area:tuiM1Split view,vtoggle, auto mode switching on width threshold, wrap toggle. AC: snapshot tests for both layouts; no panic at 40×10. -
#6 Syntax highlighting + intra-line word diff, lazily
area:tuiarea:coreM1syntect with precompiled assets, highlight-on-visibility cache,similar-based word spans. Bench: first frame of giant fixture < 50 ms. AC: bench in CI; scrolling never blocks on highlighting. -
#7 stdin patch mode + two-file mode + pager passthrough
area:vcsM1margin patch -,margin diff a b,margin pagerwith non-TTY passthrough. The “safe as core.pager” guarantee. AC:git -c core.pager='margin pager' log -p | catis byte-identical to git’s output. -
#8 Config file + themes (4 built-in, custom via TOML, ANSI-16 fallback)
area:tuiM1Config discovery/merge, theme definitions,NO_COLOR, light/dark detection. AC: docs/configuration.md + docs/themes.md written; snapshot per theme. -
#9 Search (
/,n/N) and fuzzy file picker (f)area:tuiM1Regex search across changeset with match highlighting; fuzzy picker over file paths.good first issuecandidates inside (e.g., match-count badge). AC: snapshots; search on giant fixture < 100 ms. -
#10 Release pipeline + demo assets
type:infraM1cargo-dist config, release workflow, Homebrew tap repo, vhs tape rendering the README GIF, install docs. AC: taggingv0.1.0-rc1produces installable artifacts on all 5 targets.
(Then immediately file #11 “stage/unstage hunks” and #12 “watch mode” tagged
M2, so visitors see the trajectory.)
10. Roadmap: MVP → polished OSS launch
Phase 0 — Foundation (week 1)
Issue #1 (this scaffold) + #2–#3 started. Private or quiet-public repo. Decide name (crates.io ✅; check GitHub org + brew). Reserve the crate name with a 0.0.1 stub publish.
Phase 1 — Vertical slice (weeks 2–4)
Issues #4–#7. Gate: you stop using git diff yourself. Dogfood daily;
every paper cut becomes an issue.
Phase 2 — MVP polish (weeks 5–7)
Issues #8–#10. Windows/tmux/ssh verification pass. Fuzz + corpus hardening. Write docs/. Render demo GIF. Tag v0.1.0 — quietly. Post in 2–3 friendly spots (ratatui Discord showcase, a couple of git-tooling threads) for early feedback, not reach.
Phase 3 — The differentiator (weeks 8–10) → v0.2.0
Stage/unstage hunk (s/u), discard with typed confirm + trash-patch undo,
watch mode, persistent viewed-state. This release is the launch payload:
“review and act on your agent’s changes without leaving the terminal.”
Phase 4 — Launch prep (weeks 11–12)
- README final pass: GIF above the fold, comparison table, 60-second install→review
good first issue× ≥6, CONTRIBUTING tested by an outsider following it cold- Cut v0.2.0; verify brew/curl/cargo installs on clean machines
- Write the launch post: lead with the accept/reject-AI-changes workflow demo
Phase 5 — Launch (week 13)
Show HN (Tue–Thu morning ET), r/rust + r/commandline + r/git (staggered, tailored titles), lobste.rs, Terminal Trove, This Week in Rust PR. Be present in comments all day; convert every feature request into a labeled issue and reply with the link. Triage SLA for launch week: respond < 24 h.
Phase 6 — Post-launch (months 4–6) → v0.3, then 1.0 trajectory
- v0.3: review notes + Markdown export,
--json,margin pr <n>viagh, jj support - Release cadence: minor every 4–6 weeks, patch as needed; never break keybindings without a deprecation cycle
- Community: label discipline, monthly “what’s new” discussion post, recognize repeat contributors with triage rights
- 1.0 criteria: config/keybinding stability promise, zero known data-loss bugs in write paths, packaged in ≥4 ecosystems, 3 consecutive boring releases
Success metrics (12 months)
Honest ones: you and ≥5 people you don’t know use it daily (retention beats stars); median time-to-first-response on issues < 48 h; ≥10 non-trivial outside contributors; “margin” appears unprompted in “best terminal diff tool” threads.