Add nyxt-parity roadmap + wishlist (emacs default, foundation-first)
ober
a17ba77d946af35d8e7f2c38f0f6f82693941291
new file mode 100644 --- /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. new file mode 100644 --- /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.