Add nyxt-parity roadmap + wishlist (emacs default, foundation-first)

ober

a17ba77d946af35d8e7f2c38f0f6f82693941291

diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..679de44
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,168 @@
+# Jerboa Browser — nyxt-parity roadmap
+
+A keyboard-driven, **nyxt**-style browser on the existing Qt WebEngine FFI +
+self-contained native binary. Buffers (not tabs), a fuzzy minibuffer, an
+emacs-default keymap, an in-process password manager. Offline Qt snapshot +
+functional tests modeled on `~/mine/jerboa-emacs`.
+
+Commit + push after every stage. Every stage ships tests (state assertions +
+PNG snapshots) and a functional scenario.
+
+**Decisions** (2026-05-25): default keyscheme = **emacs** (structure for vi as a
+second scheme later); build **foundation-first** (Stage 0→6 as ordered below —
+password manager at Stage 5, once the keymap/minibuffer exist to drive it).
+
+---
+
+## Reference findings (don't re-derive these)
+
+**nyxt UX** (`~/mine/nyxt`)
+- Buffer model — `source/buffer.lisp`: `make-buffer[-focus]`, `switch-buffer`
+  (fuzzy), `switch-buffer-next/previous/last`, `delete-buffer`,
+  `set-url[-new-buffer]`, `reload-current-buffer`. Buffers, NOT tabs: one window
+  shows one buffer; you switch via the prompt-buffer.
+- Keymap — `source/mode/base.lisp` `define-keyscheme-map` (emacs scheme is our
+  default). Core: `C-l` set-url, `M-l` set-url-new-buffer, `C-r` reload,
+  `C-x b` switch-buffer, `M-n`/`M-p` next/prev, `C-x left/right` next/prev,
+  `C-t` make-buffer-focus, `C-x C-b` list-buffers, `C-x k` delete-buffer,
+  `C-x C-k` delete-current, `M-x` execute-command, `C-x C-c` quit,
+  `C-M-l` copy-url, `C-M-t` copy-title.
+- Document — `source/mode/document.lisp`: scroll (`C-up`/`C-down` top/bottom,
+  page up/down), zoom `C-+`/`C-=`/`C-hyphen`/`C-0`, copy `M-w` paste `C-y`.
+- Hints — `source/mode/hint.lisp`: `f`/`M-g`/`C-j` follow-hint,
+  `; f`/`M-G` follow-hint-new-buffer.
+- Search — `source/mode/search-buffer.lisp`: `C-s s`/`C-f` search-buffer,
+  `C-s k`/`M-f` remove-search-marks.
+- Prompt-buffer — `source/mode/prompt-buffer.lisp`: `C-n`/`C-p` next/prev
+  suggestion, `M-a` mark-all, `C-space` toggle-mark, `M-return` set-action,
+  RET = default action, `C-g`/escape = abort. **This fuzzy prompt is the
+  essence of nyxt** — every command/URL/buffer goes through it.
+
+**jerboa-emacs test harness** (`~/mine/jerboa-emacs`)
+- `tests/test-qt.ss`: `QT_QPA_PLATFORM=offscreen make test-qt`, runs
+  **synchronously** (never calls `qt-app-exec!`). `test-group` /
+  `run-test-case` with pass/fail counters; a reused headless singleton widget.
+- Drive via `execute-command!` and `send-keys!`; assert on state
+  (`cur-text`, `cur-buf-name`, `win-count`, …) introspected over a REPL
+  (`jeval!`).
+- Snapshots: `qt_widget_grab(w) -> pixmap` (`QWidget::grab()`) then
+  `qt_pixmap_save(pixmap, path, "PNG")` — `vendor/qt_shim.cpp:2577,2582`.
+- Key injection: `qt_send_key_event(widget, type, key, modifiers, text)` —
+  `vendor/qt_shim.cpp:2522`.
+
+**jerboa-browser today**
+- `(browser)` API (`scheme/browser.ss`): `browser-init`, `-version`,
+  `-capabilities`, `-open-context`, `-open-view`, `-load`, `-eval`, `-title`,
+  `-url`, `-back`, `-forward`, `-reload`, `-stop`, `-show`, `-hide`, `-focus`,
+  `-set-title`, `-pump`, `-exec`, `-quit`, `-close-view`, `-close-context`,
+  predicates `-ok?`/`-error?`/`-value`, `-context?`/`-view?`/handles.
+- FFI (`qt-webengine/src/*.cpp`, `JWB_API jwb_*`): runtime-init, context
+  new/open/free, view new/open/free/show/hide/focus/set-title, load[_url[_sync]],
+  eval_js, back, forward, reload, stop, current_url, title, pump_events, exec,
+  quit, string_free.
+- One bare `QWebEngineView` per window — no chrome, no minibuffer.
+- **Missing**: multi-buffer, minibuffer, keymap dispatch, key capture +
+  injection, find-in-page, hint-mode, scroll/zoom FFI, clipboard, grab/PNG.
+
+**Password manager ref**: jsh `(jsh pass)` + `(jsh embed)` — ChaCha20-Poly1305,
+passphrase-derived key, constant-time compare, bytevector zeroing, no-echo
+prompt `ffi_embed_read_passphrase`. We want **in-process & memory-only** (no env,
+no config/file): secrets entered via a no-echo prompt, held + zeroed in RAM,
+autofilled into login forms via JS. (jsh's encrypted store is the model for the
+crypto hygiene, not for on-disk persistence.)
+
+---
+
+## Stages
+
+### Stage 0 — Foundation: window chrome + offline test/snapshot harness
+A `QMainWindow` (`BrowserWindow`) that hosts a `QStackedWidget` of web views, a
+status bar, and a minibuffer line — replacing the bare single view. Even before
+buffers/keymap exist, this gives us something to grab + drive in tests.
+- **C++ FFI add**: `jwb_window_new/show/grab_png(path)`,
+  `jwb_send_key(view,key,mods,text)` (QTest-style / `sendEvent`),
+  `jwb_window_set_minibuffer(text)`, `jwb_window_set_status(text)`.
+- **Scheme add**: `(browser test)` helpers — `snapshot!`, `send-key!`,
+  `with-headless-browser`, a `test-group`/`run-test-case` framework copied from
+  jerboa-emacs.
+- **REPL-on-launch**: `jerboa-browser repl` already preloads `(browser)`; add a
+  `--repl` flag to the GUI path so the window comes up WITH a live REPL for
+  interactive driving (the jerboa-emacs debugging pattern).
+- **Tests**: launch offscreen, load example.com, `snapshot!` → assert PNG
+  non-empty + expected size; assert `browser-url`/`browser-title`.
+- CI: add a `test-qt` make target run under `xvfb`/offscreen in `.build.yml`.
+
+### Stage 1 — Buffer model
+Multiple views in one window; nyxt buffer semantics.
+- **C++**: window holds N views in the stack; `jwb_window_add_buffer`,
+  `_switch_buffer(i)`, `_delete_buffer(i)`, `_buffer_count`, `_current_buffer`,
+  per-buffer title/url already available via view API.
+- **Scheme**: `(browser buffers)` — a buffer list with id/title/url/view;
+  `make-buffer`, `switch-buffer`, `switch-buffer-next/previous/last`,
+  `delete-buffer`, `current-buffer`, `buffer-list`.
+- **Tests (the canonical scenario)**: open A → `make-buffer` B → open B →
+  `switch-buffer-previous` → assert current url == A, buffer-count == 2;
+  snapshot each step.
+
+### Stage 2 — Keymap engine + emacs bindings + minibuffer (prompt-buffer)
+The heart of the nyxt feel.
+- **C++**: install a `QObject` event filter on the window that forwards
+  key events to a Scheme callback (keysym + modifier mask); a minibuffer
+  `QLineEdit` + a completion `QListView` overlay.
+- **Scheme**: `(browser keymap)` — key parsing (`C-`, `M-`, `S-`, chords like
+  `C-x b`), a keymap tree, command dispatch; `(browser minibuffer)` — a fuzzy
+  prompt with sources + candidates + `C-n/C-p` + RET/`C-g`; `(browser commands)`
+  — the command table; bind the emacs scheme from base.lisp.
+- **Tests**: inject `C-x b`, type a buffer name, RET → assert switched; inject
+  `M-x`, type `reload`, RET → assert reload fired; minibuffer fuzzy filter
+  unit tests; snapshot the open minibuffer.
+
+### Stage 3 — Navigation parity
+- **C++**: `jwb_set_zoom(view,factor)`/`jwb_zoom`, `jwb_find_text(view,str,flags)`
+  + find-result callback (`QWebEnginePage::findText`), `jwb_scroll(view,dx,dy)`
+  (or JS), `jwb_clipboard_set(text)` (`QClipboard`).
+- **Scheme + bindings**: scroll (`C-up/C-down`, page), zoom
+  (`C-+/C-=/C-hyphen/C-0`), find (`C-s`/`C-f`, next/prev, remove-marks),
+  history (`back`/`forward` already), `copy-url` (`C-M-l`), `copy-title`
+  (`C-M-t`).
+- **Tests**: find on example.com → assert match count; zoom → assert factor;
+  copy-url → assert clipboard.
+
+### Stage 4 — Hint-mode
+nyxt-style link hinting.
+- **Scheme/JS**: inject JS that overlays hint labels on links/inputs, then a
+  transient keymap collects the hint chars and follows (`follow-hint`),
+  `; f`/`M-G` open in a new buffer.
+- **Tests**: load a page with known links, enter hint-mode, snapshot the
+  overlay, send the hint keys, assert navigation to the link target.
+
+### Stage 5 — Password manager (in-process, memory-only)
+- **Scheme**: `(browser passwords)` — an in-RAM table keyed by origin →
+  {username, secret-bytevector}; entries added via a no-echo prompt
+  (reuse/port `ffi_embed_read_passphrase`); bytevectors zeroed after use
+  (port jsh's `zero-bv!` + constant-time compare). NEVER written to disk, env,
+  or any config. Optional: import on demand from jsh's `,pass` store.
+- **Autofill**: on a login page, a command (`fill-login`, bound e.g. `C-c C-p`)
+  injects JS to set the username/password fields for the current origin.
+- **Tests**: store a secret for a local test form, trigger autofill, eval the
+  field values back, assert they match; assert nothing hits disk/env.
+
+### Stage 6 — Polish
+Status bar (url + buffer index + modes), echo-area messages/errors, complete the
+command palette (`execute-command` lists all commands with docs), bookmarks
+(`set-url-from-bookmark`), help (`describe-bindings`), README/keybinding docs.
+
+---
+
+## Testing doctrine (mirrors jerboa-emacs)
+1. **Headless + synchronous**: `QT_QPA_PLATFORM=offscreen`, never enter the Qt
+   exec loop in tests; pump events explicitly.
+2. **State assertions** for logic (urls, buffer list, find counts, keymap
+   dispatch) — fast, deterministic.
+3. **PNG snapshots** (`grab`→save) for visual stages (minibuffer, hints,
+   layout) — committed under `tests/snapshots/` for human review + size/non-empty
+   regression checks (not strict pixel-diff initially; WebEngine renders vary).
+4. **Functional scenarios** drive real key sequences through the keymap, e.g.:
+   *open site → new buffer → open site → switch back → verify url*; *find works*;
+   *hint-follow navigates*; *autofill fills*.
+5. CI runs `make test-qt` offscreen on every push.
diff --git a/WISHLIST.md b/WISHLIST.md
new file mode 100644
index 0000000..c4d189f
--- /dev/null
+++ b/WISHLIST.md
@@ -0,0 +1,19 @@
+# Jerboa Browser — wish list
+
+Running list of things to build. Detailed staging in [ROADMAP.md](ROADMAP.md).
+
+Goal: a **keyboard-driven, nyxt-style browser** — buffers (not tabs), a fuzzy
+minibuffer/prompt, an emacs-default keymap, an in-process password manager —
+built on the existing Qt WebEngine FFI + self-contained native binary, with
+offline Qt snapshot + functional tests modeled on `~/mine/jerboa-emacs`.
+
+- [ ] **Offline Qt test + snapshot harness** (offscreen, grab→PNG, key injection, REPL-on-launch) — *Stage 0*
+- [ ] **Buffer model** — multiple web views in one window; switch / list / delete — *Stage 1*
+- [ ] **Keymap engine + emacs bindings + minibuffer (prompt-buffer)** with fuzzy completion — *Stage 2*
+- [ ] **Navigation parity** — scroll, zoom, find-in-page, history, reload, copy url/title — *Stage 3*
+- [ ] **Hint-mode** — link hinting / follow (`f`, `M-g`, `; f`) via JS overlay — *Stage 4*
+- [ ] **Password manager** — in-process, memory-only (no env, no config/file), autofill — *Stage 5*
+- [ ] **Polish** — status bar, echo-area messages, command-palette completeness, bookmarks, docs — *Stage 6*
+
+Later / maybe: vi keyscheme parity, AppImage packaging, history persistence
+(opt-in, encrypted), search-engine prefixes, download manager, ad/JS toggles.