# 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. |