# Right-click context menus — framing + as-built pmacs's first interactive floating surface. Unlike search (a band + washes), a context menu is a thing you *point at and click*, which broke new ground in both frontends: pointer hit-testing against a popup, a highlighted active row, and dismissal rules. This doc records the design as shipped; where the build diverged from the framing stance, the "As-built" notes and the divergences section say what actually landed and why. User-decided up front (AskUserQuestion): - **Context-sensitive** — items adapt to what's under the pointer (a selection → Cut/Copy/Paste; a symbol → LSP go-to-def / rename; a diagnostic → quick fix). - **Both frontends, GPU-primary** — a shared core menu mode rendered in the TUI *and* pmacs-gpu; the GPU surface was the bulk of the work. - **Lua registry** — items defined in Lua like commands and keymaps, user-configurable, actions invoking named commands. - **OS clipboard** — copy/cut publish to the system clipboard (the menu needed real Cut/Copy/Paste, which didn't exist). See Q#CM6. ## Architecture ### Q#CM1 — Menu state lives in the core, mirroring the search store The open menu is a frontend-agnostic **`SharedMenu = Arc>>`** on `EditorCore` — the menu twin of `search_store`. `MenuState { rows, active, anchor, width }`; a `MenuRow` is `Separator | Item { label, command }`; `active` indexes `rows` and always points at an `Item` (navigation skips separators). The core drives it through `menu_open` / `menu_close` / `menu_step` / `menu_set_active_row` / `menu_active_command` / `menu_hit`. `EditorState` (which has both the core and the Lua host) is where the menu is *built* and *invoked*: `open_context_menu` (TUI, cell anchor) / `open_menu_at_byte` (GPU, byte anchor) resolve the rows and call `menu_open`; `dispatch_menu_key` navigates; `menu_invoke_active` runs the chosen command by name. This is the *same* dispatch path the daemon runs for round-tripped GPU input, so the menu behaves identically in both frontends — only the *surface* differs. The SearchSession bet held; the new axis was mouse interaction. ### Q#CM2 — Items defined in a Lua registry, mirroring commands/keymaps A `MenuRegistry` (`Rc>`, like `CommandRegistry` / `KeymapStack`), installed as `pmacs.menu`, loaded from `builtin/menus/default.lua` after keymaps in `lua.rs::attach_editor` (items reference commands, so they load last). The Lua surface: ```lua pmacs.menu.item { id = "edit.cut", -- optional; enables override / removal label = "Cut", command = "edit.cut", -- invoked by name via the command registry context = "selection", -- sugar (Q#CM3); or predicate = fn(cx) group = "edit", order = 10, } ``` `MenuItem { id, label, command, context, predicate, group, order }`. `item` registers (validating non-empty label/command and that `context` is one of `always|selection|symbol|diagnostic` — the same R50 typo-paranoia as the command spec); a matching `id` replaces in place, so re-running config and user overrides are idempotent. `list` / `remove` / `clear` round out the surface; `_raw` (internal) exposes items *with* their predicate functions for the Lua builder. The default menu lives in Lua, so users re-order, hide, or add items without recompiling — the pmacs way. ### Q#CM3 — Visibility resolves in Lua; `context` is sugar over a predicate The dormant `Command.predicate` field finally got a consumer. The whole resolve happens in **`pmacs.menu.build()`** (Lua), called once from Rust (`EditorState::build_menu_rows`) at open: it reads `_raw()`, evaluates each item's visibility, groups and sorts, and returns the rows. Building in Lua keeps the LSP/diagnostic queries where their APIs live and the core frontend-agnostic. Visibility = an explicit `predicate(cx)` (a `pcall`, so a throwing predicate hides its item rather than aborting the menu), else the `context` tag via `pmacs.menu._context_eval(tag, cx)`, else visible. The context table is built from core facts: ```lua cx = { has_selection, -- ed.region() ~= nil word, -- ed.word_at_cursor() (core helper), or nil line, col, -- ed.cursor_line() / cursor_col() (0-based) attachment } -- pmacs.lsp.active_attachment(), or nil ``` Tag semantics: `always` → true; `selection` → `cx.has_selection`; `symbol` → `cx.word ~= nil and cx.attachment ~= nil`; `diagnostic` → a published diagnostic in `pmacs.diag.list(cx.attachment.uri)` spans `(cx.line, cx.col)`. After filtering, items sort by `(group first-appearance order, item order, insertion)`, and a separator falls between distinct groups. **Bet (held):** synchronous local context picks the right groups without an LSP round-trip at open. `ed.word_at_cursor()` is a core helper (identifier run of ASCII alphanumerics / `_` around the cursor). `pmacs.lsp.active_attachment()` is a new *pure* accessor added for this — the existing `attached_for_active` triggers an attach as a side effect, which a visibility check must not do. ### Q#CM4 — Right-click anchoring If a selection exists, right-click keeps it (so Copy/Cut act on it) and leaves the cursor; otherwise it moves the cursor to the click and clears any selection. The byte anchor means LSP items act where you clicked. (The framing's finer "keep iff click is *inside* the selection" was simplified to "keep iff a selection exists" — uniform across both frontends, and the TUI path never needs the click byte.) ## Commands & the clipboard ### Q#CM5 — The default menu, as shipped `builtin/menus/default.lua`, grouped (separators between groups): - **edit** — Cut, Copy (`selection`), Paste, Select All (`always`). - **symbol** (`symbol`: word + attached server) — Go to Definition, Find References, Rename, Hover. Each invokes the existing async `lsp.*` command, which acts at the cursor the right-click anchored. - **diagnostic** (`diagnostic`: a diagnostic spans the cursor) — Quick Fix → `lsp.code-actions`. Streaming the *individual* fix titles into the menu is deferred (Q#CM10); one "Quick Fix" entry that fires the async command was the Phase-1 stance. - **history** — Undo, Redo (`always`). ### Q#CM6 — Clipboard with OS interop A right-click menu without working Cut/Copy/Paste isn't credible, and none existed — pmacs had no clipboard at all, and had **never honored a paste** (`FrontendEvent::Paste` was silently dropped). This arc added `edit.copy/cut/paste/select-all` with copy/cut publishing to the real system clipboard. The wrinkle: **commands run in the core (daemon), but the OS clipboard belongs to the frontend's environment** (the winit display, or the terminal — possibly across SSH). So the model is an *internal slot* (core-owned, the synchronous paste source) plus outbound publish and inbound capture across the wire: - **`edit.copy` / `edit.cut`**: extract the region text → write the slot → queue an outbound publish the dispatcher drains as **`InstanceSignal::Clipboard(bytes)`** (a v6-floor variant, already in the protocol but never produced — **no new message, no version bump**) → the frontend writes the OS clipboard (pmacs-gpu via `arboard`, TUI via **OSC 52**, write-reliable). Cut then deletes the region. - **`edit.paste`**: insert the slot at the cursor, replacing any region. - **Inbound OS → pmacs** rides each frontend's native paste affordance, landing as `FrontendEvent::Paste`: the TUI's bracketed paste (already enabled), and a new pmacs-gpu `Ctrl-V` → `arboard.get()` → `Paste`. The core inserts *and* refreshes the slot, so a later in-app paste repeats the external text. Default keys are the **Emacs kill/yank set** — `M-w` copy, `C-w` cut, `C-y` yank, `C-x h` select-all — because the CUA trio collides (`C-a` is line-start, `C-v` is page-down). In pmacs-gpu these chords are forwarded like the search-entry chords (otherwise withheld); `Ctrl-V` is handled locally for OS paste. `arboard` (with `wayland-data-control`) is a new pmacs-gpu dep. ## Input & routing Right-click **opens**; while open, the menu **captures** pointer and keyboard until an item fires or it's dismissed. - **Keyboard (both):** `Down`/`C-n` next, `Up`/`C-p` prev, `RET` invoke, `Esc`/`C-g` dismiss; any other key dismisses. Decoded by `MenuKey::from_chord`, the SearchKey pattern — the same path both frontends reach via the `FrontendEvent::Key` round-trip. `dispatch_key` / the GPU's `dispatch_idle` gate both treat an open menu like an active search. - **TUI:** a self-suppressing **`MenuView` overlay** (the `SearchView` pattern — deduped by kind, renders nothing while closed) pushed on the active window at open, drawn to cells over the buffer text. Right-click arrives already as `FrontendEvent::Mouse(Down(Right))` (was dropped); `dispatch_mouse` gains a Right arm to open and a while-open branch routing `Move`/`Drag`→highlight, `Down(Left)`→invoke (hit-testing the cell against the popup rect), click-outside→dismiss. - **GPU:** the core ships the resolved rows as a **`MenuPrompt`** semantic message; the GPU renders the popup at the remembered right-click pixel and owns hit-testing (it drew the rect), translating hover/click into **`FrontendEvent::MenuPointer { index, invoke }`** — never shipping pixels the core can't read. Right-click opens via `PointerKind::Context`; Escape dismisses instead of quitting. ## Wire (protocol v11) Additive over v10; the ladder resumes (`SUPPORTED = [6,7,8,9,10,11]`): - **`PointerKind::Context`** — right-button-down as a semantic pointer (carries the hit byte). Frontend-gated like `Pointer`/`TripleDown` (pmacs-gpu drops it against a `< 11` daemon). - **`FrontendEvent::MenuPointer { frontend_id, index: Option, invoke: bool }`** — GPU→daemon navigation. `index: None` = pointer off the menu; `invoke` = click (invoke the row, or dismiss when `None`). - **`InstanceMessage::MenuPrompt { buffer_id, rows: Vec, active: Option }`**, `MenuPromptRow { label, separator }`. Empty `rows` closes the menu. Emitted by the semantic producer with cached-compare suppression (like `SearchPrompt`); daemon-gated `>= 11`, so a v10 peer never opens a GPU menu rather than mis-decoding it. Clipboard added **no new wire** (reused `InstanceSignal::Clipboard` + `FrontendEvent::Paste`). The TUI needs **no new wire at all** (menu = overlay cells + existing `Mouse`; clipboard = existing Signal/Paste); the v11 bump is the GPU menu's alone. ## Phasing (delivered; each commit binary-build-green) 1. **Registry** — `MenuRegistry` + `pmacs.menu` + load wiring. Introspection only. 2. **Clipboard + select-all** — commands + internal slot + reused `InstanceSignal::Clipboard` + per-frontend OS write (arboard / OSC 52) + GPU `Ctrl-V` inbound + wired the previously-dropped `FrontendEvent::Paste`. Emacs kill/yank keys. Validated standalone. 3. **Core mode + TUI surface** — `MenuState`/`SharedMenu`, `MenuView`, `dispatch_menu_key`/`_mouse`, right-click open, the Lua `build` resolver. End-to-end in the terminal (user-validated). 4. **Default menu content** — the symbol/diagnostic context tags (`word_at_cursor`, `active_attachment`, diagnostic containment) and the LSP items. 5. **Protocol v11 + GPU surface** — `Context` / `MenuPointer` / `MenuPrompt`, the daemon routing + gate, the producer, and the GPU popup (a second `TextRenderer` over bg quads, pixel hit-testing → `MenuPointer`). End-to-end in pmacs-gpu (user-validated). 6. **Docs** — this consolidation. Phases 1–4 validated the whole feature in the TUI before the GPU surface (the expensive half) began. The v11 bump means the daemon and pmacs-gpu must both be rebuilt to negotiate the menu. ## As-built divergences from the framing 1. **No `SetClipboard` message.** The framing proposed a new daemon→frontend clipboard message; the protocol already carried `InstanceSignal::Clipboard` (outbound) and `FrontendEvent::Paste` (inbound) since the v6 floor — both unused. Reusing them meant clipboard cost *zero* new wire and no bump. 2. **Resolve runs entirely in Lua, not Rust.** The framing had the core evaluate predicates against a Rust-built context table. As-built, `pmacs.menu.build()` does the whole resolve (predicate eval, context tags, grouping, sorting, separators) in Lua; Rust calls it once and parses rows. The `symbol`/`diagnostic` tags need `pmacs.lsp` / `pmacs.diag`, which live in Lua — so the policy belongs there. 3. **`MenuPrompt` carries rows, not "MenuItemWire".** The framing sketched `MenuItemWire { label, enabled, separator_before }`; the shipped `MenuPromptRow { label, separator }` is leaner — the GPU needs only labels + separator flags + the active index, never the command names (the core invokes by index). 4. **Menu state is a `SharedMenu`, no `origin`.** The framing's `MenuSession { …, origin }` implied a cursor-restore on dismiss; right-click already anchors the cursor intentionally, so there's nothing to restore. The state is the search store's `Arc` shape, read by the TUI overlay. 5. **Anchoring simplified** to keep-iff-a-selection-exists (Q#CM4). 6. **Emacs clipboard keys** (`M-w`/`C-w`/`C-y`/`C-x h`), because the CUA trio's keys were already bound (Q#CM6). 7. **GPU select-all has no keyboard binding** — `C-x` is a prefix the GPU doesn't forward, so `C-x h` is TUI-only; in pmacs-gpu select-all is menu-only. ## Categorical bets (all held) - **Core-mode generalized from search to a pointed-at popup.** `SharedMenu` + `MenuPointer` was the right seam; the core stayed frontend-agnostic. - **Predicates were enough for context** — sync local facts, no LSP round-trip at open. - **The GPU popup was just quads + glyph rows** — no new pipeline; the status-band `MinimapRect` quad path plus a second glyphon `TextRenderer` (so the popup layers over the buffer text) composed into it. The only real risk was draw order, handled by drawing the menu last. - **The internal clipboard was a clean prerequisite, not scope creep.** ## Deferred (named, not silently dropped) - **Q#CM10** Async quick-fix titles streamed into the menu (vs the one "Quick Fix" item firing `lsp.code-actions`). - menu-Paste reading the OS clipboard *directly* in pmacs-gpu (vs the slot), and clipboard history / kill-ring. - Paste routed into an open minibuffer prompt (today inbound paste always targets the buffer). - A keyboard menu key (Shift-F10 / Menu) to open at the caret, and GPU select-all-by-keyboard (needs `C-x` prefix forwarding). - Submenus / nested groups; first-letter mnemonic jump within a menu. - CUA clipboard chords (`Ctrl-C`/`Ctrl-X`) as an alternative binding set.