Pad/doc/spec.md
Greg Pomerantz d3d11b5d20 Doc: reorganize — delete over-detailed docs, rewrite spec + architecture to match code
- Deleted (drift-prone point-in-time plans / dead specs):
  browser_implementation_plan.md, editor_implementation_plan.md,
  virtual_scroll_render_optimization.md, conflict_resolution.md,
  touch.md, element_model.md, layout_rendering.md, bugs.txt
- Rewrote architecture.md: single-owner/no-lock model, channel topology,
  Frame handoff contract, ownership rules, editor/browser/render internals,
  active vs dead task types, testing hooks.
- Rewrote spec.md: actual behavior (browser, editor, IME, autosave, 50 MB
  limit with measured numbers), actual code layout, invariants to preserve,
  explicit deferred-features table (undo, restoration, sync awareness).
- Added doc/README.md: doc index, documentation policy, build/install
  recipe, on-device observation loop.
- Updated development_plan.md: Phase 5 mostly done; §7 spec deltas written
  with two corrections (no undo at all; no external-change detection).
2026-08-16 13:22:21 -04:00

158 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Pad — Text Editor Specification
This is the specification of what Pad **actually is** (v1, current code).
Behavior that was once spec'd but never built is listed as deferred in §7, not
described as a requirement. If code and this document disagree, the code wins
— and this document should be fixed.
## 1. Overview
Pad is a minimal plain-text editor for Android, written in Go with the Gio UI
framework. It presents a single root directory of text files (a browser) and a
full-screen editor with the Android soft keyboard. The design goal is a
fast, honest editor for a directory that is synced across devices — instant
open, autosave on every edit, and large files that stay smooth.
**Design philosophy:**
- **Plain text only** — no formatting, no syntax highlighting.
- **Automatic everything** — autosave with a 1 s debounce; there is no save
button.
- **Minimal surface area** — a browser and an editor, nothing else.
- **Bounded memory** — a measured file-size limit with an explicit
"too large" state, instead of silently degrading or OOMing.
**Platform:** Android-first. Window `390×844` dp. Default root directory is
`/storage/emulated/0/Notes` on Android (overridable with `-root`); `.`
elsewhere.
## 2. Implemented behavior
### 2.1 File browser
- Lists files and directories under the current directory.
- **Sort:** four modes — name asc/desc, date asc/desc — cycled by the sort
button. Default: date descending (newest first).
- **Search:** incremental, case-insensitive filter over entry names via the
search bar (a Gio `widget.Editor`).
- **Pagination:** directory entries are loaded asynchronously in pages through
the worker pool; the first paint does not wait for a full directory read.
- **Navigation:** tap a directory to enter it, back to return. Single tap
opens a file — no long-press menus, no file management actions.
### 2.2 Editor
- **Input via the Android IME:** soft-keyboard typing, composition,
backspace, and autocorrect replacements all arrive through one IME
replace-range path and are converted from rune indices to byte offsets.
(Swipe-typing support follows from the same IME path; final sign-off on a
physical device is the one open validation item.)
- **Word wrap:** on by default; wrapped lines are virtual — the buffer stores
only real newlines.
- **Virtualized viewport:** only the visible byte range is shaped and drawn
each frame (typically ~4 KB of a large file), keeping frame cost and shaper
memory constant regardless of file size.
- **Cursor + scroll:** tap to place the cursor, drag/scroll to pan, Home/End
and Page Up/Down on hardware keyboards, arrow keys.
- **Autosave:** every edit restarts a 1 s debounce; on expiry the full content
is written to disk by a worker. Failed writes are retried. This is the only
persistence mechanism.
### 2.3 Large files (measured)
- Editable limit: **50 MB** (`MaxEditableFileSize`), measured on-device:
a 10 MB file (130,954 lines) opens in ~120 ms (stat ~76 ms, read ~27 ms,
line-index ~18 ms) and idles at **~150 MB PSS / ~230 MB RSS, flat under
scroll**. 50 MB extrapolates to a few hundred MB — acceptable on a modern
phone.
- Files above the limit open into a **"too large to edit"** state: a notice is
shown, edit operations are no-ops, and the browser still lists the file.
- The buffer is chunked (64 KB chunks, prefix-sum offsets, full load for
in-range files); edits splice only affected chunks. Details:
`architecture.md` §6.
## 3. Code organization (actual)
```
cmd/pad/ # Gio entry point, main loop, frameReceiver,
# Android defaults (impl_android.go)
internal/
browser/ # BrowserState, BrowserManager, sort, search,
# pagination, layout, handlers
editor/ # Logic goroutine, State, ChunkedBuffer, LineIndex,
# IME handling, autosave, Frame handoff (frame.go)
io/pool/ # Worker pool (priority lanes), task types,
# real/ — real filesystem (rooted at /)
# mock/ — in-memory FS for tests
# types/ — shared types (LineIndex, ...)
ui/ # Element model, Renderer, units (Dp/Px), theme
ui/icons/ # Vector icons
test/e2e/ # Logic-level e2e harness + tests
doc/ # spec.md (this file), architecture.md,
# development_plan.md
```
## 4. Architecture (summary)
The logic goroutine is the **sole owner** of mutable state; the main (Gio)
goroutine reads only a `Frame` snapshot and writes only via channels; a
frame-receiver goroutine stores frames and invalidates the window; a
priority-aware worker pool does all file I/O. No mutex guards application
state. The IME snippet, search box, and gesture state live on the
main-goroutine side because Gio mutates them during draw.
Full details, channel topology, and ownership rules: [`architecture.md`](./architecture.md).
## 5. Invariants to preserve (future development must not break these)
1. **Single owner, no locks on state** (architecture.md §1). Any new feature
adds a channel or an owner-executed callback — never a direct state touch
from another goroutine.
2. **Main never reads logic `State`** — only the `Frame` snapshot.
3. **Never shape the whole file.** The text shaper's internal line storage
retains the largest layout ever performed; one whole-file layout permanently
inflates memory (this caused a 1 GB leak, fixed in Phase 3).
4. **The IME snippet is the visible window**, and rune↔byte conversion happens
in one place (`HandleReplaceRange` / `RuneIndexToByte`).
5. **Autosave is the only persistence.** If state persistence (last file,
cursor, scroll) is added later, it must go through the same
owner-dispatches-a-task pattern.
6. **Logic work stays < 16 ms.** Anything that can block or scan more than the
viewport goes to the worker pool.
## 6. Performance expectations (validated on-device)
| Operation | Target | Measured (10 MB file, Android 35 emulator) |
|---|---|---|
| Open file | instant feel | ~120 ms (stat + read + line index) |
| Scroll | 60 fps | flat ~240 MB RSS, no growth, no OOM |
| Type | responsive | single IME path; rapid commits land cleanly |
| Memory | bounded | ~150 MB PSS / ~230 MB RSS at 10 MB file, flat |
## 7. Deferred / not implemented (explicit non-goals for v1)
These were in the original v1 spec but are **not in the code**. They are
recorded here so future rounds don't mistake doc text for behavior:
| Feature | Status | Notes |
|---|---|---|
| Undo (any) | **not implemented** | No undo stack exists; the old `SaveUndoTask` is dead code. |
| State restoration on relaunch | **not implemented** | Last file / cursor / scroll are not persisted. |
| External change detection | **not implemented** | No mtime compare on open/resume, no watcher, no Keep/Reload prompt. |
| Syncthing conflict handling | **not implemented** | No `.sync-conflict-*` file detection or merging. |
| File-system watcher | **not implemented** | Browser does not live-refresh; it re-scans on navigation. |
| Alphabetical index sidebar | **not implemented** | `AlphaIndex` element exists but is unused. |
| In-file search, tabs, split view | **not implemented** | — |
| Files > 50 MB | **not supported** | `TooLarge` state instead. |
| Desktop / other platforms | **not supported** | Android-first. |
## 8. Product requirements (standing)
| Requirement | Detail |
|---|---|
| Instant open | Files open without visible lag; content and line index load async. |
| Auto-save | Every edit persisted with a 1 s debounce; no save button. |
| External change awareness | Deferred (§7) — the sync-awareness story is explicitly out of v1. |
| Plain text only | No formatting, no highlighting, no file management UI. |
| Bounded memory | 50 MB editable limit with an explicit "too large" state. |