Pad/doc/SPEC.md

14 KiB
Raw Blame History

Pad — Text Editor Specification

1. Overview

Pad is a minimal, high-performance plain text editor for Android. It is configured with a single directory of text files and provides instant file access, automatic persistence, and seamless recovery from process death. The editor is designed for a directory synced across devices, with reactive awareness of external file modifications.

Design philosophy:

  • No practical limits — files and directories of arbitrary size must work without degradation
  • Minimal surface area — plain text only, no formatting, no file management UI
  • Automatic everything — save, state persistence, sync awareness happen without user intervention
  • Survive anything — process kill, battery death, device restart: the editor restores exactly where the user left off

2. Core Requirements

Requirement Detail
Instant open Files open immediately regardless of size; content loads on demand
Auto-save Every edit is persisted to disk with a short debounce; no save button exists
Unlimited scale No caps on file size, line count, or directory entry count
External change detection The editor monitors the configured directory for modifications from sync tools
Cross-session undo Full undo history survives process death, file close, and device restart
State restoration On relaunch, the editor restores the exact file, cursor position, and scroll state
Per-file cursor memory Reopening any previously visited file restores the last cursor position
Plain text only No formatting, no syntax highlighting, no markdown rendering

3. Architecture

cmd/
  pad/                  # Mobile entry point

internal/
  browser/              # Directory listing, pagination, search, alphabetical index
  editor/               # Virtual scroll renderer, chunked edit buffer, input handling
  filesystem/           # Chunked file I/O, debounced writes, atomic operations
  watcher/              # File system monitoring, change detection, notification
  conflict/             # External change reconciliation, merge strategies
  state/                # App state persistence, per-file cursor map, restore logic
  undo/                 # Undo operation log, context anchoring, rebasing

4. File Browser

The browser presents the configured directory's contents. It must handle directories with hundreds of thousands of entries.

Behavior

  • Lazy loading — directory entries are loaded in pages, not all at once
  • Alphabetical index — a tap-on-letter sidebar jumps to the corresponding section
  • Search — incremental text filter over entry names
  • Single tap to open — no long-press menus, no file management actions

Performance

  • First paint: directory root visible before all entries are read
  • Scroll: smooth at 60 fps with virtualized list (only visible rows rendered)
  • Search: results filter in real time, no debounce on keystrokes

5. Text Editor

The editor renders plain text with virtual scrolling and chunked loading. The entire file is never loaded into memory.

Virtual Scrolling

Only lines visible on screen (plus a small prefetch buffer) are rendered. Scrolling triggers on-demand loading of adjacent chunks.

Chunked Buffer

  • Files are divided into fixed-size chunks (configurable, e.g., 64 KB)
  • Only chunks near the cursor or visible region reside in memory
  • Chunks are evicted under memory pressure, reloaded from disk on demand
  • Edits at chunk boundaries are handled by splitting/merging chunks

Line Index

On first open, the editor builds a line-offset index (mapping line number → byte offset). This index is:

  • Built in a single pass over the file (streaming, O(n) time, O(1) memory per line)
  • Cached on disk for subsequent opens
  • Invalidated when the file is edited (rebuilt on next open, or incrementally updated)

The index enables O(1) or O(log n) jumps to any line number.

Input

  • Standard text input: insert characters, backspace, delete
  • Selection: tap-and-drag or tap-hold to select a range
  • Paste: clipboard content inserted at cursor or replacing selection
  • No keyboard shortcuts beyond what the OS input method provides

6. Undo System

The undo system records reversible edit operations with context anchoring, persisted to disk for cross-session survival.

6.1 Operation Model

Edits are recorded as a stack of chained operations. A chain is a contiguous sequence of operations sharing a common anchor (position and context). A new chain begins whenever the cursor moves or a new selection is made.

6.2 Entry Formats

Insert chain (typing at a fixed cursor position):

Head:   {pos: 1024, before: "<N bytes before>", after: "<M bytes after>", inserted: "h"}
Tail:   {inserted: "e"}
Tail:   {inserted: "l"}
Tail:   {inserted: "l"}
Tail:   {inserted: "o"}

Entries without pos, before, or after inherit from the head. Position advances by the length of the previous entry's inserted text.

Delete chain (backspacing without hitting original text):

Head:   {pos: 1023, after: "<M bytes after>", deleted: "o"}
Tail:   {deleted: "l"}
Tail:   {deleted: "l"}

Entries without pos or after inherit from the head. Position for entry i is head.pos + i. The before field is omitted because it shifts with each deletion; after is the stable anchor.

Selection replacement (select text, type to replace):

Head:   {pos: 5000, before: "<N>", after: "<M>", deleted: "<selected text>", inserted: "x"}
Tail:   {inserted: "y"}
Tail:   {inserted: "z"}

The head records both the deleted selection and the first inserted character. Continuation typing appends to the chain.

Field semantics:

Field Present on Meaning
pos Head only Byte offset of the first operation in the chain
before Head of insert/replace chains N bytes immediately before pos, used for re-anchoring
after Head of all chains M bytes immediately after the affected range, used for re-anchoring
deleted Head of delete chains, selection replace Text that was removed
inserted Every entry Text that was added (empty string for pure deletion entries)

Fields absent from an entry are inherited from the chain head.

6.3 Backspace Semantics

Backspace interacts with the undo stack:

  1. If the last stack entry is an inserted entry and the character immediately before the cursor matches that entry's inserted text: pop that entry from the stack. No new operation is recorded. The character is removed from the file.

  2. Otherwise (stack empty, or last entry is a delete): record a new chain head with deleted set to the backspaced character, pos set to the byte offset of that character, and after set to the context after the cursor.

  3. Subsequent backspaces (after entering delete mode): each appends a {deleted: "<char>"} entry to the current chain, inheriting pos and after from the head.

This means backspacing over text the user just typed is "free" — it removes operations from the stack rather than adding new ones. Undo after backspace naturally restores the popped character.

6.4 Undo Replay

To undo, pop the top chain from the stack and replay in reverse:

  1. Re-anchor: search the file for the head's context (before + after for insert chains, after for delete chains). If found, update pos to the new location. If not found, skip the entire chain.

  2. Apply in reverse: for each entry from last to first:

    • Insert entry: delete inserted text at the current position
    • Delete entry: insert deleted text at the current position
    • Advance/retreat position by the length of the text affected
  3. For delete chains, replay right-to-left (last entry first) so that each insert shifts subsequent text right without affecting earlier positions.

6.5 Stack Limits

Parameter Value Rationale
Max operations 1000 Sufficient for ~1000 typing sessions or individual keystrokes
Context window 50 bytes each side Unique enough for prose/code, small enough for persistence
deleted field No cap Large deletions are the most critical undo case; the philosophy of the editor is no practical limits
Redo Not supported Can be added later; keeps initial implementation minimal

6.6 Persistence

The undo stack is serialized into the state file (see §8) and persisted on every edit, debounced separately from auto-save. On restore, each operation's context is verified; operations that cannot be anchored are silently dropped from the stack.

The stack survives:

  • Process kill by the OS
  • Device restart
  • File close and reopen
  • App restart

The stack does not survive:

  • Explicit user action to clear it (if such a feature is added)
  • External file changes that make anchoring impossible (text context no longer exists)

7. Auto-save

Every edit to a file is written to disk automatically.

Behavior

  • Debounce: 5001000 ms after the last keystroke (configurable)
  • Atomic write: write to a temporary file, then rename over the target (prevents corruption if killed mid-write)
  • No UI: there is no save button, no unsaved indicator, no "do you want to save?" dialog

Interaction with Undo

Auto-save writes the current file state. The undo stack is persisted separately. Undo works transparently across saves — saving does not "commit" or "clear" undo history.

8. State Management

The editor continuously persists its state to a small file (.pad/state.json) inside the configured directory.

Persisted State

{
  "active_file": "path/to/open/file.txt",
  "cursor_pos": 1024,
  "scroll_offset": 500,
  "file_cursors": {
    "path/to/file1.txt": 2048,
    "path/to/file2.txt": 512
  },
  "browser_scroll": 1200,
  "undo_stack": [ ... ]
}
Field Description
active_file Path of the currently open file (relative to configured directory)
cursor_pos Byte offset of the cursor in the active file
scroll_offset Vertical scroll position (line or pixel offset)
file_cursors Map of file path → last cursor position, for all visited files
browser_scroll Scroll position in the directory browser
undo_stack Serialized undo operations (see §6)

Write Strategy

  • Continuous: state is written on every navigation, edit, or cursor movement
  • Debounced: writes are batched with a short debounce (separate from auto-save debounce)
  • Atomic: write to temp file, then rename (same as auto-save)
  • Bounded: file_cursors evicts oldest entries to stay under a size limit (e.g., 1000 entries). undo_stack is capped at 1000 operations.

Restore Flow

  1. App launches → read state file
  2. If active_file exists on disk → open it, load chunk around cursor_pos, place cursor
  3. If active_file was deleted → show browser at browser_scroll position
  4. If no state file exists → show browser at root
  5. Undo stack is loaded and verified; unanchorable operations are dropped

9. File System Monitor

The editor watches the configured directory for external modifications (e.g., from a sync tool on another device).

Detection

  • Uses the platform's file system watch API (inotify on Android/Linux equivalent)
  • Detects: file content changes, file creation, file deletion, file rename
  • Watches the entire directory tree recursively

Response

Event Action
File modified externally (editor not open) Update browser listing (timestamp, size)
File modified externally (editor is open) Trigger conflict resolution (§10)
File created Appear in browser on next scroll/search
File deleted Remove from browser; if open, close and show browser
File renamed Update browser listing

10. Conflict Resolution

When the file system monitor detects an external modification to the currently open file:

Detection

Compare the file's last-modified timestamp and size against the editor's known state. If either differs, the file has changed externally.

Strategies

Strategy Behavior
Discard local Replace buffer with disk content, preserve cursor position if possible
Keep local Ignore external change, continue editing (next sync may overwrite)
Manual Show a brief prompt asking the user to choose

The default strategy is configurable. The prompt (if shown) is the only modal UI in the application.

Interaction with Undo

When an external change is applied to the buffer:

  • The undo stack is preserved (not flushed)
  • Operations are re-anchored at undo time using context search (§6.4)
  • Operations that cannot be anchored are silently skipped
  • New operations recorded after the external change use positions in the new coordinate space

11. Performance Guarantees

Operation Target Mechanism
Open any file < 100 ms Chunked load: only the chunk around the cursor is read
Scroll 60 fps Virtual rendering: only visible lines + prefetch buffer
Type < 16 ms per keystroke In-memory chunk edit, async chunk flush
Search in directory < 100 ms for 100k files Lazy index built on first browse, cached on disk
Undo < 100 ms Context search is O(n) in file size but bounded by context window
State restore < 500 ms Read state file, open file, load chunk, place cursor
Auto-save < 100 ms Debounced atomic write

Memory

Component Budget
Chunk cache Configurable, e.g., 1664 MB
Undo stack ~200 KB for 1000 ops with 50B context (excluding large deleted fields)
State file ~200 KB typical, bounded by largest deletion in undo window
Line index ~4 bytes per line (stored on disk, paged into memory)

12. Out of Scope

  • Syntax highlighting, themes, or formatting
  • Multiple tabs or split views
  • Find and replace (may be added later)
  • File management (create, delete, rename, move) — handled by external tools
  • Cloud sync — handled by external tools (Syncthing, Dropbox, etc.)
  • Collaborative editing
  • Redo (may be added later)
  • Keyboard shortcut configuration
  • Plugins or extensions