From 581775dc17113a7e63f46684332ec959d3d5ef10 Mon Sep 17 00:00:00 2001 From: Mitchell Paulus Date: Fri, 14 Aug 2026 21:52:08 -0500 Subject: [PATCH 1/7] Add prompt wrapping design --- ai/interactive-wrapping-design.html | 1217 +++++++++++++++++++++++++++ 1 file changed, 1217 insertions(+) create mode 100644 ai/interactive-wrapping-design.html diff --git a/ai/interactive-wrapping-design.html b/ai/interactive-wrapping-design.html new file mode 100644 index 0000000..e4b05ea --- /dev/null +++ b/ai/interactive-wrapping-design.html @@ -0,0 +1,1217 @@ +The Known Cursor + + +
+
+
mshell · interactive prompt · design proposal
+

The Known Cursor

+

Wrapping and cursor tracking for the msh interactive prompt. + The thesis: the cursor's position should be computed from a model — never asked + of the terminal while you type.

+
+ draft for discussion + 2026-08-14 + ai/interactive-wrapping-design.md + no code changed +
+ +
Anything with a dotted underline explains itself on + hover (or keyboard focus, or tap). Every escape code and termios flag used on this page is also + collected in the glossary at the bottom.
+
+ +
+
01
+

What breaks today

+

The current renderer (Main.go Render()) makes two assumptions. + The command fits on one terminal row. And every rune is one column wide. + Each keystroke it: jumps to the prompt column with ESC[nG, + erases the rest of the row with ESC[K, re-prints the whole command, + and parks the cursor at column promptLength + 1 + index — where + index is a rune count.

+

Long commands break the first assumption: the terminal's + autowrap moves the text to the next row, but + ESC[nG can only move within a row. Wide characters break the + second: fills two columns but counts as one rune.

+ +

Below is a tiny terminal emulator. The left pane replays the exact bytes today's + renderer sends, one keystroke at a time. The right pane paints the same content from the + proposed model. Drag the slider to type.

+ +
+
+
+ +
+
+
+
msh today
+
24 cols · what the terminal showsvt
+
+
+
+
proposed model
+
24 cols · layout-computedvt
+
+
+
+
+
+ +

There is a second, quieter cost. A CPR is a question sent to + the terminal: where is the cursor? The answer travels back through the input stream. + Today msh asks on every prompt — and also inside ScrollDown, which runs when a + completion menu needs room. So a keystroke can stall waiting for the terminal to answer. + The goal: one buffered write(2) per keystroke, zero questions.

+
+ +
+
02
+

What is knowable, what is not

+

The whole design follows from a short list of facts. Worth stating plainly before anything else.

+ +
+
+
knowable — safe to build on
+
    +
  • The terminal's width and height. The kernel reports it; poll it or catch SIGWINCH.
  • +
  • Where the cursor is relative to where we started — if we account for every byte we write. This is dead reckoning. All three shells run on it.
  • +
  • ASCII is one column per character. Everywhere, always.
  • +
  • Once a complete grapheme cluster is printed, the cursor delta is fixed (given the right width model for this terminal).
  • +
  • CR always returns to column 1 and always clears the pending-wrap flag.
  • +
  • Printing past the last column always lands at column 1 of the next row — eventually.
  • +
+
+
+
unknowable — negotiate, measure, or tolerate
+
    +
  • Which absolute row the prompt is on. That is scrollback state. The one CPR per prompt buys exactly this.
  • +
  • How many columns this terminal advances for a ZWJ sequence, a VS16 emoji, or an ambiguous-width char. Two schools exist (§05). Negotiate or measure.
  • +
  • Widths of codepoints newer than the terminal's Unicode tables. Drift is permanent background noise.
  • +
  • Whether the terminal reflows scrollback on resize. Some do, some don't — this is why resize earns a fresh CPR.
  • +
  • What some other program printed while we weren't looking. fish checks file mtimes; readline just gives up and makes the app tell it.
  • +
  • Anything mid-cluster. See below — this one is strange.
  • +
+
+
+ +

Yes — the terminal can rewrite the past

+

Can incoming codepoints make a terminal go backwards? Yes. A cluster-aware terminal + paints a glyph when it sees a codepoint. Then a following joiner can change its mind about a + cell it already painted. Step through it, and flip the terminal type to compare:

+ +
+
+ + + + + +
+
8 colscursor: col 1
+
Press next codepoint. We will send: ❤ · VS16 · 🧑 · ZWJ · 🌾
+
+ +

The rule the design takes from this: compute widths over complete clusters, never per + codepoint — and never end a frame in the middle of a cluster. Mid-cluster, even the terminal + doesn't know how wide the text is yet.

+ +

Same termios, different terminal

+

Can two terminals have identical termios settings and still + behave differently in ways that matter? Also yes — and the reason is worth keeping in mind. + termios lives in the kernel, on the byte pipe. The emulator at the other end is a + separate program with its own opinions. With byte-identical termios, terminals still differ on:

+
    +
  • Pending-wrap semantics — which operations clear the flag.
  • +
  • Width tables and Unicode version — the entire §05 problem.
  • +
  • Whether EL destroys the soft-wrap state of a row.
  • +
  • Resize reflow of scrollback — yes, no, or partial.
  • +
  • Whether they answer CPR / DECRQM at all, or answer wrong.
  • +
  • Ambiguous-width configuration — a user setting inside the emulator.
  • +
+

termios controls how bytes reach us. It says nothing about how they are painted. And on + Windows there is no termios at all — see §08.

+
+ +
+
03
+

How fish, readline, and zsh solve it

+

All three converge on the same architecture — strong evidence it's the right one. The rule: + model everything, ask nothing. Readline and zsh contain zero + cursor position queries (verified by grep). Fish had none for two + decades. In 4.1 it added exactly one per prompt, with a fence so a silent terminal can't hang it.

+ +
+
+

fish 4.x

+
src/screen.rs · reader.rs
+
    +
  • Two full cell-grids: desired vs actual; rebuild desired each frame, diff per line, emit only changes, then actual = desired.
  • +
  • Coordinates relative to the prompt's first row. One CPR per prompt (since 4.1), fenced by DA1 with a timeout.
  • +
  • Own width tables (Unicode 17) + user knobs fish_emoji_width, fish_ambiguous_width. Still per-codepoint — no grapheme clusters.
  • +
  • Foreign output detected by fstat mtime on stdout/stderr — no query.
  • +
  • Whole frame = one buffered write(2); repaint skipped when nothing changed.
  • +
+
We take: the fenced-query protocol, donate/steal mode lifecycle, single-write frames, the ⏎ forced-wrap trick.
+
+
+

readline / bash

+
display.c · rltty.c (8.3)
+
    +
  • Visible/invisible flat buffers + lbreaks[] row-offset array; middle-diff per row; cheapest of overwrite / insert-char / delete-char.
  • +
  • Purely relative: down = print real newlines, up = cuu1 loop. No absolute addressing capability even requested.
  • +
  • Resolves the pending-wrap state by writing the next row's first character — keeps the terminal's soft-wrap flag intact for copy/paste.
  • +
  • Prompt width via \[ \] invisible-byte counting — its biggest historical bug source: byte-space and cell-space share one counter (~15 XXX fix-ups).
  • +
  • Without am+xn, gives up the last column entirely (width−1).
  • +
+
We take: the write-next-char wrap trick; the warning: never mix byte offsets with screen columns.
+
+
+

zsh ZLE

+
zle_refresh.c
+
    +
  • nbuf/obuf cell grids; per-row diff chooses updates by termcap cost accounting; line insert/delete for whole-row shifts.
  • +
  • Zero CPR. Deliberately leaves the cursor in pending-wrap and forces the wrap with a real character — "good for cut and paste".
  • +
  • Refuses clear-to-EOL on a soft-wrapped row: "clearing eol would be evil".
  • +
  • Buffers taller than the screen are windowed with >.... ellipsis markers, not scrolled.
  • +
  • Famous failure: after SIGWINCH, terminal-side reflow moves the prompt row and relative-only tracking re-homes to the wrong line — "unfixable without absolute position".
  • +
+
We take: the soft-wrap preservation rules; the resize lesson (that's exactly what our one CPR per prompt repairs).
+
+
+ +

The pending-wrap state, step by step

+

The most terminal-dependent corner of the whole problem. With + autowrap on, printing into the last column does not + move to the next line. The cursor parks at the edge with a hidden flag set. Which operations + clear that flag varies by terminal — wraptest + found real VT100s, DEC's own standard, and most emulators all disagree. Two escapes are safe + everywhere: CR, and printing the next character.

+ +
+
12 colspending wrap: false
+
+ + + + + +
+
A fresh 12-column row. Fill it to the edge to arm the flag.
+
+ +

Why soft wraps matter: copy & paste

+

When the terminal wraps a line itself, it remembers the break is soft. Selections + rejoin the line into one piece; resizes reflow it. If the editor prints its own \r\n + at the wrap point instead, the break is real and permanent. All three shells protect soft wraps. + My first draft of this design had it backwards.

+ +
+
+
+
soft wrap terminal wrapped it
+
18 colswrap: soft
+
+
 
+
+
+
hard wrap editor emitted \r\n
+
18 colswrap: hard
+
+
 
+
+
+
Identical pixels, different clipboard. The paste from the hard-wrapped pane has a newline in the middle of the command.
+
+
+ +
+
04
+

Four ways to repaint a line

+

Same edit, four strategies. A command wraps over two rows; we insert an X in the + middle, which shifts everything after it. The amber cells are what each strategy rewrites.

+ +
+ +

The differences shrink for this edit because insertion shifts the whole tail anyway. They + matter more for small changes: recolor one token, and fish rewrites just those cells, readline + uses one escape — while the repaint-the-region strategy still rewrites both rows. That is a few + hundred bytes at human typing speed: irrelevant. The bytes were never the bottleneck; the + round-trip questions were. So the proposal starts with the simple strategy and keeps fish-style + diffing as a later drop-in, since both consume the same layout.

+ +

Two ways to own the terminal

+
+ shell / REPL style
all three shells + this proposal: stay on the main screen, own only the edit region,
swap modes when a child takes over
+ full TUI style
vim, htop: switch to the alternate screen, own everything, restore on exit
+
+

No shell uses the alternate screen: a prompt has to live inside the scrollback, + interleaved with command output — that is the entire point of a shell. What shells swap instead + is modes (§07): a hand-picked edit mode while reading keys, the user's normal mode while a + command runs. Today's msh is an outlier in a third, accidental way — it flips between full + raw and cooked even to print + its own prompt. The proposal removes that flip entirely by keeping + ONLCR on.

+
+ +
+
05
+

Width is the terminal's opinion

+

To know where the cursor is without asking, msh must predict how many columns the terminal + moves for each glyph. Two schools exist. Legacy wcwidth: + sum a width per codepoint — a ZWJ family emoji counts every member. + Grapheme clusters: one width per user-perceived + character. A 2025 survey of 35 terminals found 19 different ZWJ behaviors. Your own + terminals split across the two schools:

+ +
+
+
+ + + + + +
stringcodepointslegacy wcwidthgrapheme cluster
+
+
+ +

Negotiation exists, but it is partial. Mode 2027 lets a + terminal declare "I advance by cluster" — ghostty, foot, WezTerm and Windows Terminal answer; + konsole clusters but stays silent; iTerm2 answers wrong. kitty declined the mode and built the + text-sizing protocol instead, + where the app dictates the width. Beneath both schools sit two more problems: + Unicode version drift (two wcwidth terminals disagree when their tables differ) and the + East Asian ambiguous class, which is a font/config choice no + algorithm can recover.

+ +

The width function, precisely

+

The terminal-dependent part is small. uniseg's Unicode property tables carry every + per-codepoint fact and ship with the binary. What actually varies between terminals collapses + into three parameters: does ZWJ join, does + VS16 widen, and are ambiguous chars + 1 or 2 cells. That is 2×2×2 = eight possible width functions — not a table of millions of + clusters.

+ +
type WidthParams:
+    zwjJoins  bool   // terminal merges ZWJ sequences into one 2-cell glyph?
+    vs16Wide  bool   // VS16 promotes a text-default char to 2 cells?
+    ambW      1 | 2  // East Asian ambiguous characters
+
+// pure; `cluster` is one extended grapheme cluster (uniseg segmentation)
+func clusterWidth(cluster, p) int:
+    if allPrintableASCII(cluster): return len(cluster)    // fast path, no tables
+    if isControl(cluster):         return ctrlWidth       // ^X=2 or ␉=1 (open Q4)
+    cps = codepoints(cluster)
+    if p.zwjJoins:                         // ── cluster school ──
+        if contains(cps, VS15): return 1
+        if contains(cps, VS16): return 2
+        if flagPair(cps):       return 2
+        w = eawWidth(firstNonZeroWidth(cps), p.ambW)   // uniseg table: 1 or 2
+        return min(w, 2)                               // clusters cap at 2 cells
+    else:                                  // ── legacy wcwidth school ──
+        return sum over cps: perCodepointWidth(cp, p.ambW)
+        // ZWJ, VS, combining = 0 · EAW Wide/Fullwidth = 2 · ambiguous = p.ambW · else 1
+ +

How msh picks the parameters at startup

+

Measurement is the primary source of truth — it asks the only authority that knows. + The steps around it are not parallel oracles; each survives for a specific reason. In priority + order (queries ride in the same batched, DA1-terminated, + timeout-guarded write as the per-prompt CPR — never a separate + round-trip):

+
+
User env vars: MSH_WIDTH_MODEL=cluster|legacy, MSH_EMOJI_WIDTH, MSH_AMBIGUOUS_WIDTH. Not redundant with measurement: it answers what the terminal does, these answer what the user wants — the escape hatch when the 3-parameter model is too coarse (partial clusterers) or a cached result went stale after the user changed a terminal setting. fish and fzf both landed here after years of this problem.
+
Cached measurement for this terminal identity (XTVERSION reply / TERM_PROGRAM / TERM). The cache exists because probing costs something per shell startup: visible probe characters on slow links, and a full timeout on terminals that never answer. Probe once per terminal ever; "doesn't answer" is cached too; XTVERSION in the key re-measures after upgrades. The stale case gets an explicit msh recalibrate, not heuristics.
+
Mode 2027 declared via DECRQMzwjJoins = vs16Wide = true. Kept because it is a declaration, not an inference: it covers future clusters better than three probes can, and the query prints nothing — terminals that answer are never visibly probed. Parse defensively (iTerm2 answers wrong, konsole not at all).
+
Measure (below), then cache on disk.
+
Fall back: legacy school, ambW=1 — what tmux assumes. For TERM=dumb and terminals that answer nothing.
+
+

An earlier draft also had a hardcoded known-behavior table (konsole → cluster, + alacritty → legacy, …). It's cut: measurement + cache + 2027 covers everything it did, and a + list of other people's terminals is a maintenance liability. At most it ships as seed entries + for the cache.

+ +
func calibrate(tty) WidthParams:
+    // one write; replies parsed by the normal input lexer; the DA1 reply
+    // (or a 250ms timeout) ends the read. Each probe starts with CR — column 1,
+    // pending-wrap cleared — and is erased with EL, so nothing stays visible.
+    write( CR + "👨‍👩‍👦" + CPR + CR + EL    // reply col a: legacy → 7, cluster → 3
+         + "❤️"  + CPR + CR + EL          // reply col b: legacy → 2, cluster → 3
+         + "°"   + CPR + CR + EL          // reply col c: narrow → 2, wide → 3
+         + DA1 )
+    zwjJoins = (a == 3);  vs16Wide = (b == 3);  ambW = c − 1
+    // a missing or nonsense reply → that parameter falls through to the legacy fallback
+    cacheOnDisk(terminalIdentity, params)
+ +

Why one probe per parameter is enough

+

The probes are class discriminators, not samples. Terminals implement these behaviors + as code paths over property classes — one shaping path joins all ZWJ sequences — so one + representative reveals the path. Each representative is the oldest member of its class: + 👨‍👩‍👦 components are Unicode 6.0 (2010), and ° are Unicode 1.1. + That way we measure the terminal's behavior, not its Unicode-version coverage — probing + a 2024 emoji would conflate "doesn't cluster" with "doesn't know this codepoint yet". And + 👨‍👩‍👦 answers 3 or 7, a four-column gap that is hard to misread.

+

Where the one-probe assumption is known to bend: partial clusterers. The ucs-detect + survey found 19 distinct ZWJ behaviors; konsole scores 96/100, with stragglers on newer + sequences. The defenses stack: the probe classifies the terminal's dominant path; + stragglers mis-measure only exotic clusters, which is cosmetic unless it moves a wrap point, and + every frame re-anchors so the error cannot accumulate; each parameter has an env override; and + the disk cache makes adding probes later (skin-tone modifier, flag pair, one recent codepoint to + estimate the terminal's effective Unicode version) a one-startup cost. Exhaustive measurement is + ucs-detect's job — and its own data shows the long tail is precisely where terminals stop being + internally consistent, so past a few probes there is no stable behavior left to measure.

+ +
The one fatal error class: a wrong width that changes a + wrap point changes the row count — and then every subsequent row and the repaint + anchor are off by a line. Horizontal drift is cosmetic; vertical drift is corruption. The width + machinery exists to get row counts right.
+
+ +
+
06
+

The proposal

+

Four pieces. Everything else in the full doc hangs off these.

+ +
+
A pure layout function. (prompt, command, cursor, width, widthModel) → rows + cursor(row,col). No I/O during the call; unit-testable, fuzzable. The width model is an explicit input — a frozen cluster → cells value built once per session by the (impure) startup selection in §05, the same way the terminal width is a measured value passed in. Calibration runs at the edge, before the first keystroke; layout itself never asks the terminal anything. If the model changes mid-session, that's a new input → full repaint, just like a resize.
+
Region-relative rendering. Each keystroke, repaint the whole editing region into one buffer: anchor with CR + cursor-up, stream the content, let autowrap break it exactly where the model predicted (soft wraps), then place the cursor with relative moves. One write(2), zero queries.
+
Grapheme-cluster width, dual model. Segment with rivo/uniseg (cursor movement needs clusters anyway); compute width with either the cluster table (ghostty, WT, konsole) or the legacy per-codepoint table (alacritty, xterm, tmux), selected once per session.
+
One fenced query per prompt. A single batched write — CPR, plus DECRQM 2027 and XTVERSION when needed — fenced with DA1 as the guaranteed last answer, with a timeout that degrades gracefully. It runs right after a command finishes, where the latency is invisible. Never while typing.
+
+ +

The layout function, live

+

This is the whole idea in one widget: rows and cursor position fall out of pure arithmetic. + Drag the width to watch wrap points move; drag the cursor; flip the width model and notice the + row count change — that's the disagreement that matters.

+ +
+
+ + + + + +
+
layout()
+
+
+ +

Where CPR goes away

+
+ + + + + + + + +
sitetodayproposed
printPromptCPR for prompt row + columncolumn computed from our own prompt string; one fenced CPR keeps the absolute row (for completion-menu space math)
ensurePromptNewline (the mark)CPRderived from the same per-prompt reply — or fish's zero-query forced-wrap trick (open question 2)
ScrollDown / ClearScreenCPR, on the render pathpure bookkeeping from the tracked prompt row
resizeone fenced CPR on height change only (reflow can move our origin)
editing / renderingnone, but brokennone, by construction
+
+ +
+
07
+

Terminal modes: a deliberate strategy

+

Today msh uses term.MakeRaw — everything off. It then has to switch back to + cooked mode just to print its own prompt, because full + raw mode kills OPOST/ONLCR, + and a bare \n stops returning to column 1 (the "staircase effect"). None of the + three shells do this. All keep OPOST on and hand-pick the rest:

+ +
+ + + + + + + + + + + +
flagmsh today (MakeRaw)proposedfishreadlinezsh
ICANONoffoffoffoffoff
ECHOoffoffoffoffoff
OPOST + ONLCRoffonforced onforced onon
ISIGoffoff (keep in-band ^C)ononon
IXONoffoffoffoffoff*
ICRNLoffoffoffoffswapped
IEXTENoffoffoffpartialcc disabled
VMIN / VTIME1 / 01 / 01 / 01 / 01 / 0
+

The one highlighted cell is the payoff: with ONLCR kept on, printPrompt's + cooked/raw dance is deleted outright, and fmt.Fprintln diagnostics just work. + Keeping ISIG off diverges from the big three, but msh's in-band ^C handling already works and + avoids a signal racing a repaint.

+ +

Mode lifecycle (fish's donate / steal)

+
+ edit mode
prompt + typing
+ → donate → + external mode
child runs, any command
+ → steal → + edit mode
re-applied unconditionally
+
+
    +
  • Adopt a child's stty changes only if it exited successfully — broken modes are "99% a crashed program" (fish).
  • +
  • Commands run from key bindings (msh's Ctrl-O file manager) keep edit mode — donating there races new input against the mode switch.
  • +
  • SIGWINCH sets a flag; next loop resizes + repaints. SIGCONT re-applies edit mode + repaints.
  • +
  • Bracketed paste (and any future kitty-keyboard support) toggles at the same two points.
  • +
+ +
+ +
+
08
+

Linux terminals vs Windows consoles

+

Everything above assumed the Unix shape: a kernel tty device — where + termios lives — plus a terminal emulator that interprets VT + escape bytes. Windows grew up differently. msh ships a Windows build, so the differences are + design constraints, not trivia.

+

Classically there were no escape codes at all. A console program talked to + conhost through an API: WriteConsoleOutput pokes + character cells directly, ReadConsoleInput returns key-event records. "Modes" are + console modes set with SetConsoleMode — there + is no termios. Modern Windows bolted a VT layer on top: with the right flags, output escapes are + interpreted and keys arrive as escape bytes. And ConPTY (2018) + lets a real terminal such as Windows Terminal host any console program through a translation + layer.

+ +
+ + + + + + + + + + +
conceptLinux / ttyWindows console
line buffering, echoICANON, ECHOENABLE_LINE_INPUT, ENABLE_ECHO_INPUT
^C becomes an interruptISIG → SIGINTENABLE_PROCESSED_INPUTCTRL_C_EVENT (a handler callback, not a signal)
output escapes honoredalways — the emulator's jobonly with ENABLE_VIRTUAL_TERMINAL_PROCESSING
keys arrive as escape bytesalwaysonly with ENABLE_VIRTUAL_TERMINAL_INPUT
resize notificationSIGWINCHnone — poll, or read WINDOW_BUFFER_SIZE_EVENT records
who interprets our VT bytesthe terminal emulatorConPTY and the terminal — two interpreters in the path
job control, tcsetpgrpyesno equivalent
+ +

What this means for the design:

+
    +
  • One renderer, two mode backends. The VT output path is shared — Windows Terminal ≥1.22 is actually best-in-class here (full grapheme clustering, answers mode 2027). Only the mode layer forks: a termios implementation and a console-mode implementation behind one interface. x/term.MakeRaw already does this translation today; the proposed custom edit mode needs the same two-backend treatment.
  • +
  • In-band ^C ports cleanly. Turning off ENABLE_PROCESSED_INPUT is the Windows analogue of ISIG off: ^C arrives as a byte. One more point for keeping the status quo in open question 3.
  • +
  • Polling for size is the portable choice. Windows has no SIGWINCH. msh already polls (UpdateSize); the design keeps polling as the base, with the signal as a Linux-side accelerant.
  • +
  • ConPTY is the terminal's plumbing, not ours. msh never creates or opts into it: CreatePseudoConsole is for programs that host console apps (terminals, sshd, editors) — msh is the hosted side, and its children just inherit its console. Nor can msh turn it off: a classic conhost window has no ConPTY, while Windows Terminal and VS Code always interpose it, with no bypass API for the hosted app. Its cost (a re-interpreting translation layer, an extra hop for query replies) is imposed by the user's choice of terminal — msh's only levers are clean VT output and the timeout-guarded query protocol. Console modes, meanwhile, are set directly with SetConsoleMode on msh's own handles in both worlds (as Pathbin_windows.go already does).
  • +
  • Legacy conhost degrades. An old-style console window has weaker VT support (DECAWM handling only arrived around 2020) and its own width quirks. Detect it, use the legacy width model, render conservatively.
  • +
  • The WT_SESSION special case already in printPrompt (OSC 9;9 working-directory reporting) is a small existing acknowledgment of this split.
  • +
+
+ +
+
09
+

Open questions

+
1 · Calibration probe in v1? Measurement is the design's primary truth source (§05), but the editor works before it exists: env vars + the mode 2027 query + the legacy fallback cover the interim (ghostty and Windows Terminal declare 2027; alacritty correctly gets legacy by default).
lean: ship those first; add measurement + cache + msh recalibrate once the editor is correct
+
2 · The partial-line mark: fish's zero-query forced-wrap trick vs deriving it from the CPR we already make each prompt.
lean: derive from the existing CPR — zero extra cost, less trickery
+
3 · ISIG off (in-band ^C, status quo) or on (fish/readline/zsh convention)?
lean: keep off — working code, cleaner editor semantics
+
4 · Control chars in the buffer: ^X (readline/zsh, 2 cells) or Control Pictures (fish, 1 cell)? Baked into layout tests, so pick once.
cosmetic — your call
+
5 · Multiline editing (embedded \n): the layout model supports it by design. Wire a key in v1 (Alt-Enter?) or defer the UX?
model supports either
+
6 · tmux: multiplexers answer queries from their own model. Proposal treats tmux as its own terminal identity (legacy widths). Care, or out of scope?
lean: treat as its own identity
+
+ +
+
reference
+

Glossary

+

Every dotted term on this page, in one place.

+
+
+ +
+
reference
+

Sources

+ +
+
+ + + From 807d4a37fe05ccc661910b030b1d550afdecf2e1 Mon Sep 17 00:00:00 2001 From: Mitchell Paulus Date: Sat, 15 Aug 2026 09:22:00 -0500 Subject: [PATCH 2/7] Finalized design --- ai/interactive-wrapping-design.html | 60 +++++++++++++++++++++++------ ai/interactive-wrapping-plan.md | 35 +++++++++++++++++ 2 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 ai/interactive-wrapping-plan.md diff --git a/ai/interactive-wrapping-design.html b/ai/interactive-wrapping-design.html index e4b05ea..7bdb042 100644 --- a/ai/interactive-wrapping-design.html +++ b/ai/interactive-wrapping-design.html @@ -205,7 +205,7 @@

The Known Cursor

draft for discussion 2026-08-14 - ai/interactive-wrapping-design.md + ai/interactive-wrapping-design.html no code changed