docs(vterm): approve Stage 2 TUI contract
Carry the reviewed Revision 7 framing onto the Stage 2 implementation branch as its first commit. The contract defines durable per-view state, authenticated input authority, TUI composition, strict Lua operations, and acceptance 15-27. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
643d1e1fcc
commit
9dafdd4c89
|
|
@ -1,19 +1,22 @@
|
|||
# Vterm — framing (Arc 5 stage 2, three-PR delivery)
|
||||
|
||||
**Revision 5 — 2026-07-21. Status: Stage 1 review round 2 fixes implemented
|
||||
and fully gated on `vterm-core`; awaiting merge authorization, not merged.
|
||||
Stages 2 and 3 are not implemented.**
|
||||
**Revision 7 — 2026-07-21. Status: Stage 1 landed on `main` as PR #126
|
||||
at merge `643d1e1`; Stages 2 and 3 are not implemented. Stage 2 framing review
|
||||
is complete and the contract is ready for implementation.**
|
||||
|
||||
Revision 5 establishes the renderer-facing cell invariant before Stage 2:
|
||||
terminal text discards C0/C1 controls rather than storing host-terminal control
|
||||
bytes in grapheme cells. SGR mouse release preserves the released button code;
|
||||
review cleanups remove dead screen paths and stale round-trip state; and the
|
||||
remaining VT-fidelity and allocation nits are explicit deferrals. Architecture
|
||||
is unchanged: `C-c` is the terminal editor escape (`C-c C-c` sends interrupt);
|
||||
main-screen resize reflows while alternate screen clips/pads; exited buffers
|
||||
remain with an Emacs-style process message; protocol v19 is additive with
|
||||
complete frames; shared `Style` stays unchanged; and one `BufferId` owns one
|
||||
shared process/screen whose most recently active frontend controls size.
|
||||
Revision 7 closes the final three precision findings: `at_bottom` is geometric
|
||||
and distinct from live-tail following; the fixed `C-c` transport escape
|
||||
deliberately makes ordinary `C-c`-leading bindings unreachable in terminal
|
||||
windows; and context-implicit Lua view operations require an authenticated
|
||||
interactive origin with exact boolean/error results. Revision 6's durable
|
||||
controller and per-view identities, logical-cell anchors, frontend-explicit
|
||||
grid rendering, per-frontend dispatch, authenticated v18 input, local
|
||||
clipboard/BEL drainage, default-name uniquification, mouse override, and
|
||||
resize ordering remain unchanged. Main-screen resize reflows while alternate
|
||||
screen clips/pads; exited buffers remain with an Emacs-style process message;
|
||||
protocol v19 is additive with complete frames; shared `Style` stays unchanged;
|
||||
and one `BufferId` owns one shared process/screen whose most recently accepted
|
||||
frontend/window context controls size.
|
||||
|
||||
This framing follows the compile-mode terminal substrate that landed in PR
|
||||
#113. `src/process.rs` already owns PTY creation, process groups, bounded
|
||||
|
|
@ -35,15 +38,14 @@ Arc 5 stage 2 ships as three separately reviewed PRs:
|
|||
There is no single mega-PR. Each stage is useful and testable by itself, and a
|
||||
later stage starts only after the preceding stage lands on `main`.
|
||||
|
||||
## 0. Revision 5 — Stage 1 implementation and review record
|
||||
## 0. Revision 7 — landed Stage 1 and reviewed Stage 2 contract
|
||||
|
||||
The first of the three vterm PRs is implemented, reviewed, and fully gated on
|
||||
`vterm-core`, awaiting merge authorization. Initial feature commit `bbc1f33`,
|
||||
first-review fixes through `bf972a7`, and second-review hardening `9797ada` are
|
||||
published as open, non-draft PR #126,
|
||||
<https://github.com/levineuwirth/pmacs/pull/126>, targeting `main`.
|
||||
It is deliberately headless: there is no `pmacs.terminal` Lua module,
|
||||
interactive terminal command, TUI paint branch, or GPU/protocol surface yet.
|
||||
The first of the three vterm PRs landed on `main` at merge `643d1e1`.
|
||||
Implementation commits `bbc1f33` and `962944b`, first-review fixes through
|
||||
`bf972a7`, and second-review hardening through `9797ada` shipped in PR #126,
|
||||
<https://github.com/levineuwirth/pmacs/pull/126>. The landed stage is
|
||||
deliberately headless: there is no `pmacs.terminal` Lua module, interactive
|
||||
terminal command, TUI paint branch, or GPU/protocol surface yet.
|
||||
|
||||
### 0.1 Public seam and ownership
|
||||
|
||||
|
|
@ -147,26 +149,54 @@ terminal_cells_reject_child_control_characters` returned `bite: OK` with a
|
|||
clean behavioral failure: the pre-hardening screen stored control bytes in a
|
||||
grapheme cluster rather than preserving the blank snapshot.
|
||||
|
||||
### 0.4 Downstream review findings (not implemented)
|
||||
### 0.4 Stage 2 re-scout resolutions
|
||||
|
||||
Stage 2 must derive PTY resize ownership from a durable accepted-input/focus
|
||||
owner before render fan-out, never transient `EditorCore::active_frontend`.
|
||||
Because `KeyDispatcher` pending state is global, a terminal `C-c` continuation
|
||||
must carry its owning `FrontendId`. Terminal copy should use the existing core
|
||||
kill-ring/clipboard setter, while the local run loop must drain/present
|
||||
clipboard signals; active-terminal BEL likewise uses the out-of-band frontend
|
||||
signal path.
|
||||
The post-Stage 1 review and a fresh read of landed `main` found six
|
||||
load-bearing Stage 2 seams. This revision resolves them before code:
|
||||
|
||||
- PTY resize ownership is stored durably per terminal session as an
|
||||
authenticated `(FrontendId, WindowId)` controller. Render-time
|
||||
`EditorCore::active_frontend` is not an ownership signal.
|
||||
- The single global `KeyDispatcher` cannot safely carry a terminal `C-c`
|
||||
continuation in a multi-frontend daemon. Pending dispatch and terminal
|
||||
escape state become per-`FrontendId`; dispatch-idle publication becomes
|
||||
frontend-specific too.
|
||||
- Terminal copy reuses the existing kill-ring/clipboard setter. The
|
||||
in-process run loop must drain that signal and feed it through
|
||||
`Frontend::present_messages`; the TUI must implement `InstanceSignal::Bell`
|
||||
rather than drop it.
|
||||
- `RenderState::render_frame` and `paint_frame` need the target
|
||||
`FrontendId` explicitly. A transient mutable active frontend may still
|
||||
attribute commands, but it may not select another frontend's layout,
|
||||
terminal view state, statusline context, or resize owner during fan-out.
|
||||
- Current v18 daemon key/mouse payload IDs are client supplied. Stage 2 routes
|
||||
key, mouse, paste, focus, and resize through the authenticated connection
|
||||
source before any terminal ownership or PTY effect.
|
||||
- The Stage 1 screen already preserves logical-line IDs and row cell offsets
|
||||
through main-screen reflow. Stage 2 selection and scroll anchors use those
|
||||
coordinates; numeric distance from the moving tail is derived metadata,
|
||||
never stored ownership state.
|
||||
|
||||
The final framing review pinned three precision contracts:
|
||||
|
||||
- `at_bottom` reports whether the live tail is currently visible; it is not
|
||||
the separate internal “follow future output” predicate.
|
||||
- `C-c` is a consumed transport escape, not an ordinary dispatcher prefix
|
||||
map; `C-c`-leading user bindings are deliberately unavailable in terminal
|
||||
windows for Stage 2.
|
||||
- Context-implicit Lua view operations require an authenticated interactive
|
||||
command origin and fail closed rather than borrowing a stale
|
||||
`active_frontend`.
|
||||
|
||||
Stage 3 additionally owns `pmacs-gpu/src/attach.rs` for gated terminal
|
||||
resize/pointer sending and coalescing. Daemon handlers must authenticate source
|
||||
frontend/buffer ownership before input, resize, or pointer routing. Wire-facing
|
||||
terminal state, selection, and limits must live in or be re-exported from
|
||||
`pmacs-protocol`. The current 16 MiB transport frame cap cannot hold the legal
|
||||
worst complete terminal frame (up to roughly 64 MiB of cluster bytes before
|
||||
encoding overhead): Stage 3 must either raise and test a measured cap at least
|
||||
as large as the legal worst case (review estimate at least 80 MiB), or add a
|
||||
shared aggregate payload bound. It must never silently chunk the locked
|
||||
complete-frame protocol.
|
||||
resize/pointer sending and coalescing. New daemon event variants must apply the
|
||||
same authenticated-source rule. Wire-facing terminal state, selection, and
|
||||
limits live in or are re-exported from `pmacs-protocol`. The current 16 MiB
|
||||
transport frame cap cannot hold the legal worst complete terminal frame (up to
|
||||
roughly 64 MiB of cluster bytes before encoding overhead): Stage 3 must either
|
||||
raise and test a measured cap at least as large as the legal worst case
|
||||
(review estimate at least 80 MiB), or add a shared aggregate payload bound. It
|
||||
must never silently chunk the locked complete-frame protocol.
|
||||
|
||||
### 0.5 Stage 1 review round 1
|
||||
|
||||
|
|
@ -542,8 +572,11 @@ cursor rewrites, erase operations, alternate-screen swaps, and resize.
|
|||
|
||||
### 4.3 Lua API
|
||||
|
||||
Stage 2 installs `pmacs.terminal` before user config, loads
|
||||
`builtin/runtime/terminal.lua`, and registers the interactive command:
|
||||
Stage 2 constructs the shared `TerminalManager` immediately after the process
|
||||
supervisor, installs strict Rust primitives, then loads
|
||||
`builtin/runtime/terminal.lua`; all happen before LSP/MCP builtins and before
|
||||
user config. The Lua chunk owns friendly wrappers, the interactive command,
|
||||
buffer-local bindings, and the built-in statusline provider.
|
||||
|
||||
```lua
|
||||
local buffer = pmacs.terminal.open {
|
||||
|
|
@ -558,138 +591,287 @@ local buffer = pmacs.terminal.open {
|
|||
}
|
||||
|
||||
pmacs.terminal.is_terminal(buffer) -- boolean
|
||||
pmacs.terminal.state(buffer) -- fresh plain metadata table
|
||||
pmacs.terminal.send(buffer, bytes) -- explicit raw bytes
|
||||
pmacs.terminal.resize(buffer, rows, cols)
|
||||
pmacs.terminal.state(buffer) -- fresh global metadata
|
||||
pmacs.terminal.view_state(ctx) -- fresh per-window metadata
|
||||
pmacs.terminal.send(buffer, bytes) -- explicit trusted raw bytes
|
||||
pmacs.terminal.terminate(buffer) -- SIGTERM; buffer remains
|
||||
pmacs.terminal.scroll(lines) -- active terminal window
|
||||
pmacs.terminal.scroll_to_bottom()
|
||||
pmacs.terminal.copy_selection() -- active terminal window
|
||||
pmacs.terminal.scroll(lines) -- boolean; interactive origin required
|
||||
pmacs.terminal.scroll_to_bottom() -- boolean; interactive origin required
|
||||
pmacs.terminal.copy_selection() -- boolean; interactive origin required
|
||||
```
|
||||
|
||||
`open` validates exact raw table fields before side effects. Unknown fields,
|
||||
metatable-provided fields, holes in `args`, non-string env keys/values,
|
||||
embedded NUL, non-integer dimensions, and out-of-range scrollback reject with
|
||||
the field named. The copied spec is immune to caller mutation. Returned and
|
||||
accepted identity is `BufferIdLua`, following the rest of the editor API.
|
||||
`open` accepts exactly the fields shown. It validates raw table fields before
|
||||
side effects: unknown fields, metatable-provided fields, holes in `args`,
|
||||
non-string environment keys/values, embedded NUL, non-integer dimensions, and
|
||||
out-of-range scrollback reject with the field named. The copied specification
|
||||
is immune to caller mutation. Returned and accepted identity is `BufferIdLua`,
|
||||
following the rest of the editor API.
|
||||
|
||||
The built-in chunk registers `terminal` as an interactive command. It opens
|
||||
`$SHELL` without a shell-command interpolation layer. There is no command
|
||||
string split and no implicit `sh -c`.
|
||||
It also installs terminal-buffer-local commands used after the escape prefix:
|
||||
`M-w` copies the terminal selection, `M-v`/`C-v` page scrollback up/down, and
|
||||
`M-<`/`M->` move to the oldest retained row/bottom. These shadow ordinary
|
||||
document commands only during the one-key editor escape; normal terminal input
|
||||
still sends those keys to the child.
|
||||
Generated names are reserved against the live buffer registry before insert:
|
||||
`*terminal:sh*`, `*terminal:sh*<2>`, `*terminal:sh*<3>`, and so on. The lowest
|
||||
available suffix wins; failed creation consumes no suffix. An explicit `name`
|
||||
is preserved exactly after ordinary `TerminalSpec` validation.
|
||||
|
||||
`state(buffer)` returns a fresh plain table:
|
||||
|
||||
```lua
|
||||
{
|
||||
buffer = buffer, pid = 123, rows = 24, cols = 80,
|
||||
title = nil, screen_generation = 7,
|
||||
process = { kind = "running" },
|
||||
-- or { kind="exited", code=0 },
|
||||
-- or { kind="signaled", signal="TERM" },
|
||||
-- or { kind="crashed", message="..." }
|
||||
}
|
||||
```
|
||||
|
||||
`view_state(ctx)` accepts the raw statusline context fields
|
||||
`{frontend, window, buffer}` and returns
|
||||
`{at_bottom=boolean, scroll_offset=integer, selection=boolean}`, or `nil` when
|
||||
that exact context is not a live terminal view. `state`/`view_state` expose no
|
||||
stored Lua tables, callbacks, or mutable manager state.
|
||||
|
||||
The three context-implicit view operations require a live authenticated
|
||||
interactive command origin. A plain programmatic `pmacs.command.invoke`
|
||||
outside such a dispatch does not synthesize one and raises a Lua error; so does
|
||||
an origin whose active window is not the addressed terminal context. Errors
|
||||
name the operation and leave view, selection, clipboard, and controller state
|
||||
unchanged. `scroll(lines)` requires an integer: positive moves toward older
|
||||
rows, negative toward the live tail, and zero returns `false`; otherwise it
|
||||
returns whether `top` changed. `scroll_to_bottom()` returns whether selection
|
||||
or `top` was cleared. `copy_selection()` returns `true` only when bytes were
|
||||
published and `false` for a valid terminal context with no selection.
|
||||
|
||||
There is deliberately no public Lua `resize`: the active controller's computed
|
||||
window content rectangle is the only geometry authority. `send` remains an
|
||||
explicit trusted escape hatch for packages and tests; ordinary user paste and
|
||||
keys use the mode-aware input path.
|
||||
|
||||
The built-in `terminal` command calls the public wrapper with `$SHELL` or
|
||||
`/bin/sh`, no shell-command interpolation, no string split, and no implicit
|
||||
`sh -c`. The wrapper installs terminal-buffer-local one-key commands:
|
||||
`M-w` copies, `M-v`/`C-v` page up/down, and `M-<`/`M->` move to the oldest
|
||||
retained row/bottom. They run only after the terminal escape prefix; normal
|
||||
terminal input sends those keys to the child.
|
||||
|
||||
### 4.4 Stage 2 view and controller contracts
|
||||
|
||||
Stage 2 adds `src/terminal/view.rs`. It is a projection over the one
|
||||
`TerminalScreen`, not another screen:
|
||||
|
||||
```rust
|
||||
TerminalViewKey { frontend_id, window_id, buffer_id }
|
||||
LogicalCellAnchor { logical_line_id, cell_offset }
|
||||
TerminalSelection { anchor, head } // inclusive display cells
|
||||
TerminalViewState { top, selection, drag } // keyed by TerminalViewKey
|
||||
TerminalController { frontend_id, window_id }
|
||||
```
|
||||
|
||||
`cell_offset` is the leading display-cell offset within one logical line.
|
||||
Clicks on a wide continuation canonicalize to its lead. Ordering is resolved
|
||||
against the current retained row sequence, not by comparing IDs. A top anchor
|
||||
resolves to the physical row containing that logical offset after reflow.
|
||||
|
||||
`TerminalManager` gains owned operations to register/retain/detach view keys,
|
||||
claim or release a session controller, create a
|
||||
`snapshot_for_view(key, viewport_size)`, scroll/select/copy one view, and query
|
||||
fresh global/view metadata. `detach_frontend` and live-layout retention remove
|
||||
stale view/controller state; buffer prune removes every context for that
|
||||
session. The context-free Stage 1 `snapshot(buffer)` remains available for
|
||||
headless/core callers.
|
||||
|
||||
`snapshot_for_view` returns exactly `viewport_size.area()` cells for a nonzero
|
||||
valid viewport. At bottom it tail-aligns rows, padding above and to the right
|
||||
with default cells; a smaller view clips top/rows and right/columns. A
|
||||
scrolled view starts at its resolved top anchor and pads only after retained
|
||||
content is exhausted. Cursor and selection spans are translated into this
|
||||
returned coordinate space. `scroll_offset` is a saturating derived count of
|
||||
display rows between the viewport and live tail.
|
||||
|
||||
`at_bottom` is purely geometric: it is `true` exactly when
|
||||
`scroll_offset == 0`, meaning the live tail is currently visible. It is
|
||||
independent of the follow predicate (`top == None && selection == None`).
|
||||
Therefore a Shift-selection frozen at the tail has `top != None`,
|
||||
`scroll_offset == 0`, and `at_bottom == true`: the active cursor remains
|
||||
visible, unshifted supported child mouse reporting remains eligible, and the
|
||||
statusline emits no `↑0`. The first later output retained behind that anchor
|
||||
makes `scroll_offset > 0` and `at_bottom == false`.
|
||||
|
||||
Main-screen reflow keeps logical anchors stable. Beginning a selection freezes
|
||||
the current first visible row as `top`, even when it was the live tail, so
|
||||
later output has an anchor to preserve. Oldest-row eviction clamps a missing
|
||||
anchor once to the first surviving leading cell; a selection whose two ends
|
||||
collapse is cleared. Alternate-screen entry/exit or a reset that removes the
|
||||
referenced logical IDs clears affected selection/top state and returns the view
|
||||
to bottom. Child output follows only views with `top=None` and no selection.
|
||||
|
||||
## 5. Stage 2 — TUI integration
|
||||
|
||||
### 5.1 Composition and cursor
|
||||
### 5.1 Composition, cursor, and status
|
||||
|
||||
For every window whose `buffer_id` belongs to `TerminalManager`, the window
|
||||
content rectangle is painted from a terminal snapshot instead of
|
||||
`TextView::render`. Modeline/statusline composition remains unchanged. Normal
|
||||
text overlays, line-number gutters, syntax, diagnostics, and wrapping are not
|
||||
run over terminal cells.
|
||||
`RenderState::render_frame` and `paint_frame` take an explicit target
|
||||
`FrontendId`. Layout lookup, statusline evaluation, active-window choice, and
|
||||
terminal view keys use that ID. A shared placement helper computes each
|
||||
window's outer rectangle, one-row modeline reservation, and content rectangle;
|
||||
both resize synchronization and painting consume the same result.
|
||||
|
||||
Each `(frontend_id, window_id, buffer_id)` owns a `TerminalViewState`. At
|
||||
bottom, the last screen row aligns with the content rectangle's last row.
|
||||
Scrolling records a stable logical-line top anchor rather than a numeric
|
||||
distance from a moving tail. New child output therefore does not move a
|
||||
scrolled-back viewport or selection. If retention evicts that anchor, it
|
||||
clamps once to the oldest retained row. A “not at bottom” marker is available
|
||||
to the built-in terminal statusline provider.
|
||||
For every terminal buffer, the content rectangle is painted from
|
||||
`snapshot_for_view` instead of `TextView::render`. Cell glyph/style values are
|
||||
copied directly; no ANSI is reinterpreted. Line-number gutters, document
|
||||
wrapping, syntax, diagnostics, inlays, ordinary selections, and local/peer
|
||||
document overlays are suppressed for that window. Modeline/statusline,
|
||||
sibling splits, and cells outside the rectangle retain the existing pipeline.
|
||||
|
||||
The active terminal cursor is translated from terminal-local coordinates into
|
||||
the window rectangle. It is hidden when the child hid it, the window is not
|
||||
active, the viewport is scrolled away from bottom, or the coordinate is
|
||||
clipped. Other terminal windows do not paint a cursor.
|
||||
Each `(frontend_id, window_id, buffer_id)` view follows §4.4. Merely rendering
|
||||
a passive view never resizes the PTY. Zero-area content is skipped without
|
||||
creating a view or changing the prior valid size.
|
||||
|
||||
A smaller window clips; a larger window pads with default cells. Merely
|
||||
rendering a passive view never resizes the PTY.
|
||||
The active terminal cursor is translated from snapshot-local coordinates into
|
||||
the content rectangle. It is hidden when the child hid it, the window is not
|
||||
the target frontend's active window, the viewport is scrolled from bottom, or
|
||||
the coordinate is clipped. Other terminal windows do not paint a cursor.
|
||||
|
||||
BEL is forwarded only from the active terminal through the existing frontend
|
||||
signal path. OSC title is sanitized and exposed in terminal metadata/frame and
|
||||
the terminal statusline; it does not rename the identity buffer or directly
|
||||
set the host window title.
|
||||
BEL is an out-of-band `InstanceSignal::Bell` only when a new bell occurs in
|
||||
the target frontend's active terminal. Historical bells are never replayed on
|
||||
later activation, and one bell count is delivered at most once per frontend.
|
||||
The in-process loop drains both clipboard and terminal signals and feeds them
|
||||
through `Frontend::present_messages`; the TUI Bell arm emits one host BEL.
|
||||
OSC title remains sanitized metadata (and a Stage 3 frame field). It never
|
||||
renames the identity buffer, injects host control bytes, or sets the host title.
|
||||
|
||||
### 5.2 Input precedence
|
||||
`terminal.lua` registers a right-side provider named `terminal`, priority 10,
|
||||
face `ui.modeline.terminal`. It emits `TERM` while running, `TERM:<code>` on
|
||||
normal exit, `TERM:<signal>` on signal, or `TERM:ERR` on crash, and appends
|
||||
` ↑<scroll_offset>` only when `scroll_offset > 0`. An unset child face
|
||||
inherits the existing modeline foreground through the Arc 4 contract.
|
||||
|
||||
Modal editor surfaces remain authoritative. Input precedence is:
|
||||
### 5.2 Input precedence and per-frontend dispatch
|
||||
|
||||
Stage 2 replaces the one pending `KeyDispatcher` with dispatch state keyed by
|
||||
`FrontendId`; keymaps and command registries remain shared. Terminal escape
|
||||
state is stored beside that dispatcher. `dispatch_idle_for(frontend_id)` uses
|
||||
the same frontend's pending prefix and active window; global modal surfaces
|
||||
still make every frontend non-idle while they own input.
|
||||
|
||||
`EditorState` carries an ephemeral authenticated command origin around
|
||||
key/menu/M-x interactive invocation and clears it with the invocation guard.
|
||||
Nested calls inherit that origin. Plain `pmacs.command.invoke` outside the
|
||||
guard neither stamps command history nor creates terminal view authority.
|
||||
|
||||
Input precedence is:
|
||||
|
||||
1. minibuffer, incremental search, completion/menu, query-replace, and other
|
||||
existing modal shadows;
|
||||
2. terminal escape-prefix state;
|
||||
3. terminal key/mouse/paste handling when the active buffer is terminal;
|
||||
4. ordinary buffer-local/global keymaps and self-insert.
|
||||
2. that frontend's terminal escape-prefix state;
|
||||
3. terminal key/mouse/paste handling when that frontend's active buffer is a
|
||||
terminal;
|
||||
4. that frontend's ordinary buffer-local/global dispatcher and self-insert.
|
||||
|
||||
All terminal buffers remain in `round_trip_buffers`, so GPU/TUI input reaches
|
||||
this daemon-owned decision before any optimistic edit.
|
||||
Escape-prefix state is per frontend, so one attached user's pending escape
|
||||
never captures another user's next key.
|
||||
All terminal buffers stay in `round_trip_buffers`, so attached frontends reach
|
||||
this daemon-owned decision before optimistic edit. One user's pending escape
|
||||
or longer ordinary prefix cannot consume, cancel, or display as another
|
||||
user's pending sequence.
|
||||
|
||||
When terminal input owns a normalized key, `terminal/input.rs` encodes:
|
||||
When terminal input owns a normalized key, `terminal/input.rs` encodes UTF-8
|
||||
printable characters; Ctrl mappings and Alt ESC-prefixing; Enter, Tab,
|
||||
Backspace, Escape; arrows/Home/End according to application-cursor mode;
|
||||
Insert/Delete/Page, F1–F12, Shift-Tab, and supported modifier parameters.
|
||||
Unknown/lock/media keys are ignored. Press is actionable; local repeat is
|
||||
treated as another press and release is not forwarded. The normalized
|
||||
protocol cannot distinguish number-row from numeric-keypad characters, so
|
||||
application-keypad mode remains tracked but cannot transform them.
|
||||
|
||||
- UTF-8 printable characters;
|
||||
- Ctrl-character mappings, Alt ESC-prefixing, Enter/Tab/Backspace/Escape;
|
||||
- arrows/Home/End according to application-cursor mode;
|
||||
- Insert/Delete/Page and F1–F12 xterm sequences;
|
||||
- Shift-Tab and supported modifier parameters.
|
||||
`C-c` is the fixed terminal escape prefix. It is consumed and makes the next
|
||||
key run through the same frontend's ordinary dispatcher; a resulting longer
|
||||
prefix stays in that frontend's dispatcher. `C-c C-c` instead sends literal
|
||||
Ctrl-C. Modal shadows consume their keys before either rule.
|
||||
|
||||
Unknown/lock/media keys are ignored, never converted into text. Press is the
|
||||
only actionable event in the current normalized protocol; repeat arrives as
|
||||
repeated press and release is not forwarded.
|
||||
The normalized protocol does not distinguish number-row digits from numeric
|
||||
keypad digits, so application-keypad mode is tracked but cannot transform
|
||||
those ambiguous `Key::Char` events.
|
||||
|
||||
`C-c` is the fixed stage-2 terminal escape prefix. It is consumed and makes the
|
||||
next key run through the ordinary editor dispatcher, allowing `C-c C-x ...`
|
||||
for editor commands. `C-c C-c` sends the literal Ctrl-C byte required to
|
||||
interrupt the child. This is an intentional fixed stage-2 policy.
|
||||
This is deliberately a consumed transport escape, not an Emacs-style
|
||||
`C-c` prefix map: the dispatcher receives the post-escape key as a fresh
|
||||
sequence, while `C-c C-c` is reserved for literal interrupt. Consequently a
|
||||
global or buffer-local binding whose first chord is `C-c` cannot fire in a
|
||||
terminal window. Packages may bind a post-escape one-key sequence, as the
|
||||
built-in terminal-local commands do. A configurable escape/prefix-map policy
|
||||
remains the named §11 deferral.
|
||||
|
||||
Paste sends exact bytes, wrapped in `ESC[200~` / `ESC[201~` only while the
|
||||
child enabled bracketed paste. It never passes through a command shell or Lua.
|
||||
When the child enabled focus reporting, authenticated frontend focus gain/loss
|
||||
sends `ESC[I` / `ESC[O` for the controlling terminal. With the mode off,
|
||||
focus changes send no PTY bytes.
|
||||
child enabled bracketed paste. It never passes through a shell or Lua.
|
||||
Authenticated focus gain claims the active terminal context and emits
|
||||
`ESC[I` when enabled. Focus loss emits `ESC[O` only when that source currently
|
||||
controls the session, then releases that controller; mode-off focus is silent.
|
||||
|
||||
### 5.3 Mouse, selection, copy, and scrollback
|
||||
|
||||
If the child enabled a supported mouse mode, pointer events inside the terminal
|
||||
content rectangle are encoded as SGR mouse reports, with coordinates translated
|
||||
to terminal-local 1-based cells. The active mode determines whether press,
|
||||
release, drag, move, and wheel are reported.
|
||||
Child mouse reporting owns a pointer only when the exact terminal view is at
|
||||
bottom, the child enabled a supported tracking mode plus SGR encoding, and
|
||||
Shift is not held. Events inside the content rectangle translate to
|
||||
terminal-local 1-based cells; the active mode filters press, release, drag,
|
||||
move, and wheel.
|
||||
|
||||
Otherwise the editor owns the gesture:
|
||||
|
||||
- wheel changes the per-window scrollback offset;
|
||||
- primary drag creates a terminal-cell selection across history and screen;
|
||||
- copy serializes selected rows as UTF-8, trims only trailing default blank
|
||||
cells, joins soft-wrapped rows without `\n`, and separates hard rows with
|
||||
`\n`;
|
||||
- wide-cell continuations are never emitted twice;
|
||||
- a new plain click clears the old selection;
|
||||
- child output does not move a scrolled-back viewport or selection anchor.
|
||||
- wheel scrolls that `TerminalViewKey`;
|
||||
- primary drag stores inclusive logical-cell endpoints across history/screen;
|
||||
- Shift is the explicit editor-selection override while child mouse reporting
|
||||
is active;
|
||||
- an editor-owned right press follows the existing context-menu path;
|
||||
- a new plain primary click clears the old selection before setting its anchor;
|
||||
- child output does not move a scrolled viewport or selection.
|
||||
|
||||
`pmacs.terminal.copy_selection()` publishes through the existing kill-ring /
|
||||
clipboard path. Ordinary document selection fields remain untouched.
|
||||
Copy resolves anchors against retained logical rows, emits each leading glyph
|
||||
once, trims only trailing default blank cells, joins soft wraps without `\n`,
|
||||
and separates hard rows with `\n`. Wide continuations are never duplicated;
|
||||
combining clusters remain one UTF-8 sequence. Reversed drags normalize by
|
||||
retained row order. `pmacs.terminal.copy_selection()` writes through the
|
||||
existing kill-ring/clipboard setter for the acting frontend; ordinary document
|
||||
selection state is untouched.
|
||||
|
||||
### 5.4 Resize ownership
|
||||
Scrolling uses physical retained display rows and clamps at oldest/bottom.
|
||||
Reaching bottom clears `top` only when no selection is active. The explicit
|
||||
`scroll_to_bottom`/`M->` action clears both selection and `top` so live-tail
|
||||
following resumes; ordinary copy leaves selection intact. Page commands use
|
||||
the current nonzero content-row count. Selection and scroll are available in
|
||||
the alternate screen only over its visible rows; no alternate output enters
|
||||
main history.
|
||||
|
||||
One PTY has one kernel window size even when displayed in several views. The
|
||||
controlling view is the active window of `core.active_frontend` — the frontend
|
||||
that most recently supplied accepted input/focus. Only that view may resize the
|
||||
PTY. Passive views clip/pad.
|
||||
### 5.4 Durable control and resize
|
||||
|
||||
For grid frontends, the daemon derives the terminal content `rows × cols` from
|
||||
the computed split rectangle and modeline reservation. Focus/split/frontend
|
||||
resize changes trigger one checked `resize_pty`; unchanged dimensions are
|
||||
suppressed. The screen model resizes before the child receives `SIGWINCH`, so
|
||||
its repaint lands into the new geometry.
|
||||
If the computed content rectangle has zero rows or columns, rendering skips it
|
||||
and the prior valid PTY size remains unchanged; zero is never sent to
|
||||
`resize_pty`.
|
||||
Each terminal session stores at most one `TerminalController`. Successful
|
||||
`open` claims the newly active `(frontend, window)`. Later authenticated
|
||||
terminal key, paste, editor-owned/child-owned pointer, or focus gain claims the
|
||||
source's currently active terminal window. A render pass never claims control.
|
||||
Switching that controller away, killing its window/buffer, focus loss, or
|
||||
frontend detach releases it; size stays unchanged until another accepted
|
||||
context claims it.
|
||||
|
||||
Before the next terminal process drain and before paint, the instance
|
||||
synchronizes live terminal layouts. Only a controller whose exact window still
|
||||
shows that session may resize. Unchanged dimensions are suppressed before any
|
||||
PTY syscall. The manager validates once, performs `resize_pty`, then applies
|
||||
the same prevalidated `TerminalScreen::resize` in the same main-thread call;
|
||||
no supervisor output drain can interleave between them. A PTY failure leaves
|
||||
the prior screen geometry intact. The child may emit after `SIGWINCH` only on a
|
||||
later drain, when the screen already has the new geometry.
|
||||
|
||||
Content rows/columns come from the shared placement helper after modeline
|
||||
reservation and with no document gutter. A zero row/column result performs no
|
||||
resize and preserves the prior valid geometry. Passive splits/frontends only
|
||||
clip or pad their own snapshots.
|
||||
|
||||
### 5.5 Local and v18-daemon adapters
|
||||
|
||||
The in-process event adapter handles key press/repeat, mouse, paste,
|
||||
focus-gained/lost, and resize through the same `EditorState` terminal methods.
|
||||
It synchronizes layout before `tick_processes`, then presents pending
|
||||
clipboard/BEL messages. Host restoration remains the existing `Frontend`
|
||||
drop/error contract.
|
||||
|
||||
The daemon passes the authenticated connection `source` explicitly to grid
|
||||
render and input. Client-supplied IDs in v18 Key, Mouse, Paste, Focus, Resize,
|
||||
and Detach payloads never choose a view or controller. Session detach removes
|
||||
that frontend's dispatcher, terminal views, controller claims, and bell
|
||||
baseline. These are Stage 2 changes to existing v18 grid behavior; no protocol
|
||||
bump or semantic/GPU terminal surface is added in this PR.
|
||||
|
||||
## 6. Stage 3 — protocol v19 and GPU integration
|
||||
|
||||
|
|
@ -805,21 +987,22 @@ coherent.
|
|||
|
||||
| Owner | Stable scope | Primary files |
|
||||
| --- | --- | --- |
|
||||
| Lead/integrator | contracts first; `TerminalManager`, buffer/Lua/builtin wiring, cross-surface acceptance, gates, docs, branches/PRs | `src/terminal/session.rs`, `src/lua_bindings/mod.rs`, `builtin/runtime/terminal.lua`, `tests/vterm_*_acceptance.rs`, docs |
|
||||
| VT core agent | streaming parser operations, screen state machine, input encoder, model units | `src/ansi.rs`, `src/terminal/screen.rs`, `src/terminal/input.rs` |
|
||||
| TUI agent | terminal window composition, cursor, per-view scroll/selection/copy, grid input and resize | `src/terminal/view.rs`, owned sections of `src/editor.rs`, focused TUI tests |
|
||||
| Protocol/GPU agent | v19 types/limits/gates, semantic terminal producer, authenticated daemon routing, GPU state/render/hit-test | `pmacs-protocol`, `src/protocol.rs`, `src/semantic_render.rs`, owned sections of `src/daemon.rs`, `pmacs-gpu/src/main.rs` |
|
||||
| Lead/integrator | contracts first; `TerminalManager`, Lua/builtin wiring, lifecycle/signal integration, shared acceptance, gates, docs, branches/PRs | `src/terminal/session.rs`, `src/lua_bindings/mod.rs`, `builtin/runtime/terminal.lua`, narrow Stage 2 authenticated-dispatch sections of `src/daemon.rs`, `tests/vterm_*_acceptance.rs`, docs |
|
||||
| VT core agent | encoder corrections and screen query seams needed by view projection; no renderer ownership | `src/ansi.rs`, `src/terminal/screen.rs`, `src/terminal/input.rs` |
|
||||
| TUI agent | view projection, frontend-explicit grid composition/cursor, per-frontend dispatch, local events, scroll/selection/copy/resize | `src/terminal/view.rs`, `src/instance_render.rs`, `src/frontend.rs`, owned sections of `src/editor.rs`, focused TUI tests |
|
||||
| Protocol/GPU agent | Stage 2 contract review; Stage 3 v19 types/limits/gates, semantic producer, authenticated new-event routing, GPU state/render/hit-test | `pmacs-protocol`, `src/protocol.rs`, `src/semantic_render.rs`, Stage 3 sections of `src/daemon.rs`, `pmacs-gpu/src/{main,attach}.rs` |
|
||||
|
||||
Coordination rules:
|
||||
|
||||
- Lead establishes types and method signatures before another lane edits a
|
||||
caller.
|
||||
- Lead establishes types, invariants, and method signatures before another
|
||||
lane edits callers.
|
||||
- Strict file ownership. `src/editor.rs` passes from lead to TUI only after
|
||||
stage-1 construction wiring is settled; `src/daemon.rs` belongs only to the
|
||||
protocol/GPU lane in stage 3.
|
||||
construction wiring; lead and TUI coordinate exact non-overlapping
|
||||
`src/daemon.rs`/signal hunks. Stage 3 daemon work begins only after Stage 2
|
||||
lands.
|
||||
- Workers do not update docs, ledgers, branches, or PRs and do not stash,
|
||||
checkout, rebase, or merge.
|
||||
- Workers add focused tests in their owned modules. Lead alone owns shared
|
||||
- Workers add focused tests in owned modules. Lead alone owns shared
|
||||
acceptance files.
|
||||
- Exact-path staging only; never `git add .`.
|
||||
- Four agents are the total vterm team, not four implementation workers plus a
|
||||
|
|
@ -827,26 +1010,26 @@ Coordination rules:
|
|||
|
||||
Per-stage utilization:
|
||||
|
||||
- Stage 1: lead + VT core implement; TUI and protocol/GPU owners review the
|
||||
snapshot/input contracts against their future consumers.
|
||||
- Stage 2: TUI implements; VT core owns encoder corrections; lead integrates
|
||||
lifecycle/acceptance; protocol/GPU owner checks that no TUI-only assumption
|
||||
enters the snapshot contract.
|
||||
- Stage 3: protocol/GPU implements; TUI and VT core owners add parity cases in
|
||||
their existing surfaces; lead integrates and gates.
|
||||
- Stage 1: completed by lead + VT core; TUI and protocol/GPU reviewed future
|
||||
consumer contracts.
|
||||
- Stage 2: lead establishes manager/Lua contracts and authenticated adapters;
|
||||
TUI implements projection/input/rendering; VT core adds only required
|
||||
encoder/query corrections; protocol/GPU checks snapshot neutrality.
|
||||
- Stage 3: protocol/GPU implements; TUI and VT core add parity cases in their
|
||||
existing surfaces; lead integrates and gates.
|
||||
|
||||
## 8. Branch and PR plan
|
||||
|
||||
Stage 1 is implemented and fully gated on `vterm-core`; review fixes through
|
||||
`bf972a7` are published in open, non-draft PR #126
|
||||
(<https://github.com/levineuwirth/pmacs/pull/126>), targeting `main` and not
|
||||
merged. Continue the approved plan only after the preceding PR lands:
|
||||
Stage 1 landed on `main` as PR #126 at merge `643d1e1`. Continue one clean PR
|
||||
at a time:
|
||||
|
||||
1. review PR #126; merge only when the user says;
|
||||
2. after stage 1 merges, create `pmacs-vterm-tui`, branch `vterm-tui`, from the
|
||||
new `main`; implement/gate/open a second PR;
|
||||
3. after stage 2 merges, create `pmacs-vterm-gpu`, branch `vterm-gpu`, from the
|
||||
new `main`; implement/gate/open the third PR.
|
||||
1. Revision 7 framing review complete; implementation contract approved;
|
||||
2. create worktree `pmacs-vterm-tui`, branch `vterm-tui`, from current
|
||||
`githubsucks/main`; carry this approved framing as the first commit,
|
||||
implement/gate, and open the second PR;
|
||||
3. merge Stage 2 only when the user says;
|
||||
4. create `pmacs-vterm-gpu`, branch `vterm-gpu`, from the then-current `main`;
|
||||
implement/gate/open the third PR.
|
||||
|
||||
The framing branch is `vterm-framing` in worktree `pmacs-vterm-framing`.
|
||||
Implementation branches are not stacked across an unmerged parent. This avoids
|
||||
|
|
@ -896,49 +1079,83 @@ base-branch deletion/auto-close risk and makes each PR's gate evidence honest.
|
|||
|
||||
### Stage 2 — TUI
|
||||
|
||||
15. Lua `open` performs the same strict raw-field validation, publishes no
|
||||
partial state on error, and switches the active window only after success.
|
||||
15. Lua `open` enforces the strict owned table contract, uniquifies generated
|
||||
names without consuming failed suffixes, publishes no partial state on
|
||||
error, and switches/claims the active window only after success.
|
||||
`state` and `view_state` return exact fresh plain tables; no public Lua
|
||||
resize bypass exists. Context-implicit view operations error without an
|
||||
authenticated interactive terminal origin and otherwise return the exact
|
||||
changed/copied booleans without partial mutation.
|
||||
16. A terminal window paints exact cells/styles inside its content rectangle;
|
||||
statusline, sibling splits, and outside cells are untouched.
|
||||
17. Active cursor translation, child-hidden cursor, passive window, clipping,
|
||||
and scrolled-back hiding are exact.
|
||||
18. Printable, Ctrl, Alt, arrows, Home/End, function keys, application cursor,
|
||||
focus reporting, and unknown keys produce the specified PTY bytes.
|
||||
19. `C-c` dispatches one editor key; `C-c C-c` sends Ctrl-C; modal minibuffer,
|
||||
search, menu, and query-replace remain authoritative.
|
||||
20. Paste is byte-exact with bracketed wrappers only when enabled.
|
||||
21. Mouse-reporting modes receive translated SGR reports. With reporting off,
|
||||
the same gestures scroll/select/copy and write no PTY bytes.
|
||||
22. Copy handles soft/hard wraps, trailing blanks, wide/combining glyphs,
|
||||
resize/reflow, eviction-clamped anchors, and selections crossing
|
||||
history/screen exactly once.
|
||||
23. The controlling active view alone resizes the PTY; passive split/frontend
|
||||
renders never cause resize thrash.
|
||||
24. A hermetic real TUI smoke opens `/bin/sh`, runs a cursor-addressed probe,
|
||||
resizes, scrolls/copies, exits, and restores the host terminal cleanly.
|
||||
top/right padding, clipping, modeline, sibling splits, outside cells, and
|
||||
suppression of document/peer overlays are exact and control-free.
|
||||
17. Per-context snapshots preserve logical top/selection anchors through
|
||||
main-screen reflow, derive the correct scroll offset, clamp oldest-row
|
||||
eviction once, and clear invalid alternate/reset anchors. A selection
|
||||
frozen at the live tail pins `top != None`, `scroll_offset == 0`, and
|
||||
`at_bottom == true`; cursor, child-mouse eligibility, status suffix, and
|
||||
the first subsequent output transition follow that definition exactly.
|
||||
18. Printable, Ctrl, Alt, navigation/function keys, application cursor, focus,
|
||||
unknown keys, and paste produce exact PTY bytes; bracketed wrappers appear
|
||||
only when enabled.
|
||||
19. `C-c` dispatches one fresh editor key and retains the owning frontend
|
||||
through a longer prefix; `C-c C-c` sends Ctrl-C. Ordinary bindings whose
|
||||
first chord is `C-c` are deliberately unreachable in terminal windows.
|
||||
Two frontends cannot consume one another's escape/pending state or
|
||||
dispatch-idle value, and existing modal shadows remain authoritative.
|
||||
20. Supported SGR mouse modes receive translated reports only at bottom without
|
||||
Shift. Mode-off, scrolled, unsupported-legacy, and Shift gestures remain
|
||||
editor-owned; wheel, drag, clear, and right-context behavior write no PTY
|
||||
bytes.
|
||||
21. Copy handles soft/hard wraps, trailing blanks, wide/combining glyphs,
|
||||
reversed drags, resize/reflow, eviction, alternate screen, and
|
||||
history/screen crossings exactly once, then publishes to the acting
|
||||
frontend's kill-ring/clipboard path without touching document selection.
|
||||
22. The durable authenticated controller alone resizes. Open/input/focus
|
||||
claims and focus/switch/kill/detach releases are exact; unchanged, zero,
|
||||
passive, and failed resize cases preserve prior geometry without thrash,
|
||||
and screen resize precedes the next child-output drain.
|
||||
23. Forged v18 grid payload frontend IDs for key, mouse, paste, focus, resize,
|
||||
or detach cannot select or affect another frontend's terminal context.
|
||||
Detach and layout retention remove only the matching views, dispatcher,
|
||||
controller, and bell baseline.
|
||||
24. Local and daemon-grid paths deliver clipboard and each new active-terminal
|
||||
BEL exactly once. Historical/passive bells and OSC titles do not become
|
||||
host control effects. The built-in terminal provider reports exact
|
||||
process/scroll state for each split.
|
||||
25. Two frontends and sibling splits over one terminal retain independent
|
||||
bottom/scroll/selection snapshots while sharing one process, screen, title,
|
||||
process outcome, and controller.
|
||||
26. Killing the identity buffer terminates/reaps the child and removes all
|
||||
views; switching away leaves it running; editor/drop and error paths
|
||||
restore the host terminal and leak no child, reader, or stale round-trip
|
||||
state.
|
||||
27. A hermetic real TUI smoke opens `/bin/sh`, runs a cursor-addressed probe,
|
||||
exercises key/paste, resize, scroll/select/copy, BEL, and clean exit, then
|
||||
proves host raw/alternate-screen state is restored.
|
||||
|
||||
### Stage 3 — GPU/protocol
|
||||
|
||||
25. Protocol v19 appends all new variants after v18 pins; v18 grid traffic
|
||||
28. Protocol v19 appends all new variants after v18 pins; v18 grid traffic
|
||||
round-trips unchanged and new outbound variants are version-gated.
|
||||
26. Terminal frame validation accepts exact shared boundaries and atomically
|
||||
29. Terminal frame validation accepts exact shared boundaries and atomically
|
||||
rejects over-area, bad area, out-of-bounds cursor, malformed cluster,
|
||||
orphan continuation, invalid selection spans, attachment, overlong title,
|
||||
and overlong process-state text while retaining the prior valid frame.
|
||||
27. Semantic terminal activation suppresses document-only messages; switching
|
||||
30. Semantic terminal activation suppresses document-only messages; switching
|
||||
back forces a complete document resync.
|
||||
28. Two frontends/splits on one terminal keep independent scroll/selection
|
||||
31. Two frontends/splits on one terminal keep independent scroll/selection
|
||||
snapshots; only the active controlling context resizes or writes input.
|
||||
29. Forged frontend/buffer IDs in terminal resize/pointer events cannot affect
|
||||
32. Forged frontend/buffer IDs in terminal resize/pointer events cannot affect
|
||||
another terminal or process.
|
||||
30. Headless GPU rendering pins background rectangles, indexed/truecolor,
|
||||
33. Headless GPU rendering pins background rectangles, indexed/truecolor,
|
||||
reverse, wide/combining cells, clipping, cursor visibility, status-band
|
||||
separation, and no frontend wrapping.
|
||||
31. Font/window resize emits cell dimensions, never pixels, and identical
|
||||
34. Font/window resize emits cell dimensions, never pixels, and identical
|
||||
resize requests are suppressed.
|
||||
32. Theme/font/terminal generation changes invalidate exactly the affected
|
||||
35. Theme/font/terminal generation changes invalidate exactly the affected
|
||||
caches; an unchanged terminal frame produces no redraw message.
|
||||
33. A real daemon + required-GPU smoke runs a full-screen alternate-screen
|
||||
36. A real daemon + required-GPU smoke runs a full-screen alternate-screen
|
||||
probe, handles input and resize, exits, and returns to the preserved main
|
||||
screen.
|
||||
|
||||
|
|
@ -996,7 +1213,8 @@ panic, unbounded allocation, or child leak.
|
|||
|
||||
## 12. Resolved decisions
|
||||
|
||||
The 2026-07-21 architecture discussion resolved every Revision 1 question:
|
||||
The 2026-07-21 architecture, re-scout, and final framing review resolved every
|
||||
current question:
|
||||
|
||||
1. Fixed terminal editor escape: `C-c`; `C-c C-c` sends literal Ctrl-C.
|
||||
2. Resize: reflow main-screen soft wraps; clip/pad alternate screen.
|
||||
|
|
@ -1005,5 +1223,22 @@ The 2026-07-21 architecture discussion resolved every Revision 1 question:
|
|||
no terminal surface.
|
||||
5. GPU wire: complete visible frames with complete-payload suppression.
|
||||
6. Style: preserve the shared encoding and defer unsupported attributes.
|
||||
7. Identity: one process/screen per terminal `BufferId`; the most recently
|
||||
active frontend's active view controls PTY size.
|
||||
7. Identity: one process/screen per terminal `BufferId`.
|
||||
8. View state: logical-line/cell anchors per
|
||||
`(FrontendId, WindowId, BufferId)`; `at_bottom` means
|
||||
`scroll_offset == 0`, while following additionally requires no top anchor
|
||||
and no selection; no second screen.
|
||||
9. Control: the most recently authenticated accepted terminal context owns
|
||||
resize until focus/switch/kill/detach releases it; render never claims.
|
||||
10. Dispatch: terminal escape and ordinary pending prefixes are per frontend;
|
||||
the consumed `C-c` escape deliberately hides ordinary `C-c`-leading
|
||||
bindings in terminal windows.
|
||||
11. Mouse: Shift or scrollback forces editor selection; supported SGR child
|
||||
reporting owns unshifted at-bottom gestures.
|
||||
12. Resize order: validate, suppress unchanged, resize PTY, resize the screen
|
||||
before any subsequent child-output drain; failure preserves old geometry.
|
||||
13. Lua: strict open/state/view/send/terminate/scroll/copy surface; no public
|
||||
geometry bypass; implicit view operations require authenticated interactive
|
||||
origin and otherwise error without mutation.
|
||||
14. Host effects: clipboard and BEL use explicit frontend signals; OSC title
|
||||
remains sanitized metadata.
|
||||
|
|
|
|||
Loading…
Reference in New Issue