# Annotator — Implementation Plan & Log
A small Rust TUI application for annotating code files. You open a file, "paint"
(select) a range of lines, write a review-style note, and the app saves the
annotation to a file named `<UUID>.md` containing the note plus metadata.
## Requirements
- Open a code file and view it in a terminal UI.
- Select ("paint") a range of lines, like a review comment.
- Write a free-text note about the selected lines.
- Save the annotation to `<UUID>.md` (UUID generated by the app).
- The saved file stores metadata in addition to the comment:
- git commit hash (if available)
- hash of the file contents
- the file name
- the lines in question (start–end range)
## Design decisions
- **UI**: Terminal UI (TUI) built with `ratatui` + `crossterm`. "Painting" lines
maps naturally to a vim-like visual selection in the terminal.
- **Interaction model** (modal, vim-inspired), one TUI session for everything.
The second header line always shows the main keys for the current mode
(kept within 80 columns); `?` opens a help screen listing every key.
- File view: `j`/`k` or arrows move, `g`/`G` top/bottom, `v` paint (select)
lines, `c`/`Enter` comment the line(s), `f` comment the whole file,
`b` open the file browser (in the open file's directory), `q` quit
- Painting: move to extend, `c`/`Enter` comment the selection, `v`/`Esc` stop
- File browser: `j`/`k` `g`/`G` move, `Enter` open file or directory,
`l`/`→` enter directory, `h`/`←`/`Backspace` parent, `c` comment the
selected file or directory, `C` comment the current directory, `Esc` back
to the open file, `q` quit
- Note editor: title says what is being commented on; `Esc` save, `Ctrl+Q`
discard (back to where you were), `Enter` newline, `←`/`→` move; text
word-wraps and the box grows up to half the screen, then scrolls
- Name prompt: the first save of a session asks the user's name, used for
every annotation until quitting (no config file, so it is not remembered
between runs). `Enter` saves, `Esc` goes back to the comment. A blank name
asks "save without a name?": `y` saves, `n` goes back to typing one
- Mouse: the wheel scrolls the file or browser (the cursor is pulled along
to stay on screen, so while painting it extends the selection); in the
file view a click moves to a line and dragging with the left button paints
lines (dragging past the top/bottom edge scrolls); in the browser a click
selects an entry and clicking the selected entry opens it. Ignored while
writing a comment. Mouse capture means terminal text selection needs the
terminal's modifier (e.g. Shift, or Option in iTerm2)
- `annotator` starts in the browser at the working directory,
`annotator <file>` starts in the file view
- **Scopes**: an annotation is about a line range (TUI painting or
`<file> --lines A-B`), a whole file (TUI `f` / browser `c`, or `--path FILE`)
or a whole directory (browser `c`/`C`, or `--path DIR`). A directory gets one
annotation, not one per file. Paths inside the working directory are stored
relative to it.
- **Storage**: one Markdown file per annotation, named `<UUID>.md`, written to
an `annotations/` directory in the current working directory (configurable via `--output-dir`).
- **Metadata**: collected at save time:
- `git rev-parse HEAD` of the repo containing the file (optional, if in a repo)
- SHA-256 of the *whole* file, also for line ranges, so a working tree that
has drifted from `git_commit` can be detected
- for directories: SHA-256 of a manifest with one `<sha256> <relative path>`
line per file, sorted by path — the same as
`git ls-files -co --exclude-standard | sort | xargs sha256sum | sha256sum`.
Inside a repo the file list is tracked + untracked-but-not-ignored files
(so `target/` is skipped, deleted files are listed as `deleted`); outside a
repo it is a recursive walk that skips hidden entries
- scope: the path plus, for line annotations, the line range, as one
code-link style reference — `src/` (directory, trailing slash),
`src/foo.rs` (file), `src/foo.rs#L3-7` (lines, 1-based inclusive; a single
line is `#L3`)
- timestamp (UTC)
- user name: asked in the TUI, `--user NAME` in batch mode; `unavailable`
when left blank
- **Output format** (`<UUID>.md`):
````markdown
# Annotation
- scope: `path/to/file.rs#L12-18` (or: `path/to/file.rs`, `path/to/dir/`)
- git_commit: <hash or "unavailable">
- content_sha256: <hash>
- created_at: <RFC3339 UTC>
- user: <name or "unavailable">
## Context (only for line ranges)
```rs (language tag = file extension)
<the annotated lines>
```
## Comment
<note text>
````
## Dependencies
- `ratatui` + `crossterm` — TUI
- `uuid` (v4) — annotation file names
- `sha2` — content hashing
- `chrono` — timestamps
## Milestones
1. [x] Plan written
2. [x] Project scaffolded (`cargo new`, deps compile)
3. [x] TUI: load + display file, cursor movement
4. [x] TUI: visual line selection ("painting")
5. [x] TUI: note input mode
6. [x] Save annotation `<UUID>.md` with all metadata
7. [x] End-to-end test: annotate a sample file, verify output file
8. [x] Plan updated as implementation log
9. [x] Fix issues from user-testing annotations (see below)
10. [x] Test suite: unit, TUI (in-memory terminal) and CLI integration tests
## Testing
`cargo test` runs:
- `src/annotation.rs` — hashing, Markdown output (incl. the `Context` code block), git detection, file and
directory annotations, `.gitignore`-aware directory hash.
- `src/app.rs` — syntax highlighting (Rust, Markdown and code fences, non-ASCII text), comment wrapping (`wrap_note`), key
handling for every action (painting, mouse wheel/click/drag, line/file/directory comments, discard,
empty comments, browser navigation and switching files, help screen) and
rendering into ratatui's `TestBackend` (comment box wrap/grow/scroll and
cursor placement, key hints per mode and their 80-column limit, help screen
lists every key).
- `tests/cli.rs` — runs the built binary in temp dirs and git repos: all
metadata fields, dirty-tree detection, line-range clamping, `--path` for
files and directories, and every argument/runtime error path.
## Implementation log
- 16:55 — Started: explored environment (Rust 1.98.1, git 2.50), wrote initial PLAN.md
- 16:58 — Scaffolded `cargo init annotator`, added deps: ratatui 0.30, crossterm 0.29, uuid{v4}, sha2, chrono
- 17:02 — Wrote `src/annotation.rs`: `Annotation` struct, git commit detection, SHA-256 content hash, UUID filename, Markdown serialization, 7 unit tests
- 17:07 — Wrote `src/app.rs`: TUI state (`Normal` / `Visual` / `Editing`), cursor + line selection (painting), note editor widget, rendering with ratatui
- 17:14 — Wrote `src/main.rs`: CLI parsing (`--lines A-B`, `--note TEXT`, `--output-dir DIR`), batch mode, TUI entry via `app::run`
- 17:22 — Fixed `sha256_hex`: sha2 0.11 returns `GenericArray` without `LowerHex`, switched to manual byte formatting; all 7 unit tests pass
- 17:24 — End-to-end batch mode test passed: `annotator src/main.rs --lines 1-5 --note "test"` produced valid `<UUID>.md` with file, lines, git_commit (unavailable), content_sha256, created_at, and note
- 17:30 — Updated PLAN.md as implementation log
- 20:14 — Added `Ctrl+Q` to cancel comment editing without saving
- 20:14 — Added Rust syntax highlighting for keywords, strings, and comments in TUI
- 20:48 — Implemented `--path` flag for recursive directory annotation
- 20:48 — Implemented `--hash-scope` flag for configurable hash scope (file/range)
- 20:48 — Added file browser UI with navigation (j/k move, Enter select, h/Left parent)
- 2026-10-20 — Fixed open issues from annotations: removed `--hash-scope` (whole-file hash always),
`--path` now writes one annotation (scope file/directory, manifest hash for directories) and
requires a non-empty `--note`; lowercase `v` paints; comment box word-wraps, grows and scrolls
- 2026-10-20 — Added TUI and CLI integration tests. They found two bugs, now fixed: saving from
the TUI cleared the painted selection before reading it (multi-line selections were saved as
the cursor line only), and running with no arguments printed usage instead of opening the
file browser
- 2026-10-20 — Made every action reachable from the TUI (user feedback: "I could not see how to
navigate the directories and select another file"): single TUI session with `b` to browse
from a file and `Esc` back, file/directory comments from the TUI (`f`, browser `c`/`C`),
per-mode key hints on their own header line, `?` help screen. Fixed: quitting after opening
a file from the browser reopened that file in a second session
- 2026-10-21 — Line annotations save the annotated lines in a `## Context` section. Mouse
support: wheel scrolling, click to move/select, drag to paint lines, click again to open a
browser entry; listed under "Mouse" in the `?` help. Checked end to end by driving the binary
in a pty with SGR mouse sequences
- 2026-10-21 — Unified the `scope`, `file`/`path` and `lines` fields into one `scope` reference
(`src/`, `src/foo.rs`, `src/foo.rs#L3-7`)
- 2026-10-21 — Markdown highlighting for `.md`/`.markdown` files (other files keep the Rust
highlighter): headings, rules, quote and list markers, inline code, links, bold, italic, and
fenced code blocks, with Rust highlighting inside `rs`/`rust` blocks. Fixed: the Rust
highlighter sliced lines by char index as if it were a byte index, so lines with non-ASCII
text panicked (e.g. 32 lines of this file) or would show the wrong text
- 2026-10-21 — Prepared publishing on crates.io as `annotations`, matching the repository name
(`annotator` is taken there); the installed command stays `annotator`. Added package metadata,
`rust-version = "1.89"` and an `exclude` list for `annotations/` and `.DS_Store`
- 2026-10-22 — Annotations record a `user`. The TUI asks the name at the first save of a
session (a blank name needs a `y` to confirm); batch mode takes `--user NAME`. Names with
control characters are refused, like paths, so they cannot forge metadata lines
## Feedback from User Testing
Issues and feature requests collected from annotation files in `annotations/`
(source annotation ID prefix in brackets).
### Open issues
- **Default output directory** [`e089cf26`]: the default (`annotations/` relative to where
the tool is run) is not really visible. Requested: `<repository root>/annotations/` when inside
a git repository; otherwise the working directory the tool was started from. To clarify:
whether outside a repo that means the working directory itself or `<cwd>/annotations/`.
(Related: the header shows `saves to: annotations`, a relative path, not the resolved
location.)
### UX Improvements (Addressed)
- **Visual mode key** [`2792dba9`]: lowercase `v` starts/stops painting (`V` still works)
- **Line wrapping in the comment box** [`437fa259`]: the editor word-wraps, grows up to half
the screen and then scrolls to keep the cursor visible
- **Output location visibility** [`9e367b20`]: Added `saves to: <output_dir>` in the header
- **Help text** [`9e367b20`]: Clarified "v paint multiple lines" in the header
- **Cancel comment** [`5b79496a`]: Added `Ctrl+Q` in editing mode to discard a comment without saving
(requested as a special `/exit` command; `Ctrl+Q` chosen as more idiomatic for a TUI)
- **Syntax highlighting** [`9e367b20`]: Added basic Rust syntax coloring for keywords (fn, let, pub, etc.), strings, and comments (line and block)
### Feature Requests (Addressed)
- **One unified scope** [`64bca40a`]: `scope`, `file`/`path` and `lines` were redundant; now a
single `- scope:` field holds `src/`, `src/foo.rs` or `src/foo.rs#L3-7` (GitLab-style range,
as suggested; a single line is `#L3`). Older annotation files keep the previous fields
- **Copy the painted lines into the annotation** [`998108af`]: line annotations (TUI and
`--lines`) get a `## Context` section with the lines in a code block, before `## Comment`;
the fence is lengthened when the code itself contains backtick runs
- **Mouse support** [`38f00d92`]: the wheel scrolls, click moves to a line, left-drag paints
lines; in the browser click selects and a second click opens
- **Support commenting entire directories or paths** [`6e12dd0d`]: `--path FILE|DIR --note TEXT`
writes a single annotation for the whole file or directory; an empty note is rejected
(the earlier version wrote one empty annotation per file: `03fb2d0f`, `4ef8b1af`, `a879f431`)
- **Hash scope** [`41ee6bdc`, `6e12dd0d`]: `--hash-scope` removed; file and line annotations
always hash the whole file, directories hash a manifest of their files (see Design decisions)
- **Built-in file selection/navigation** [`b3ef44ab`]: Implemented file browser mode in TUI with
`j/k` navigation, `Enter` to select, `h/Left` to go to parent
- **Comment whole files from the TUI** [`8b628f99`]: `f` in the file view (the annotation
suggested `w`), or `c` on a file in the browser
- **All actions available in the UI** (user feedback, not an annotation): the browser was only
reachable by starting without a file and its keys were not shown. Now `b` opens it from any
file, `Esc` returns, whole-file and directory comments work from the TUI, each mode shows its
keys and `?` lists them all
### Test artifacts (no action)
- [`53c8ebe2`, `bf5e6bb4`]: "test batch mode" — batch-mode smoke tests
- [`03fb2d0f`, `4ef8b1af`, `a879f431`]: empty comments from a `--path` test run
- [`8965fffe`]: "Testing commenting the file" — confirms `f` saves a `scope: file` annotation