From 4572f5ebfb50e249bf7c29f8defbe2793976663c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 11 Jun 2026 07:37:48 +0200 Subject: [PATCH] Draft the diff-line-metadata OSC spec and settle the OSC number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the OSC number from the 456 placeholder to 1717. There is no central registry for OSC numbers, so the audit reduces to: pick a high, distinctive number that no real terminal *acts on* — an unknown OSC is skipped harmlessly, but a recognized one can fire a visible side-effect (OSC 555 flashes foot, OSC 777 raises a desktop notification), and the metadata flows through real terminals whenever the pager runs outside a host. Audited the live OSC allocations of xterm, VTE, kitty, foot, WezTerm, iTerm2, Windows Terminal, Ghostty, VS Code, ConEmu and urxvt; 1717 collides with none and sits in the empty 1400-5000 band (only iTerm2's 1337 is nearby). Write the spec as a standalone draft to circulate to pager developers for feedback — separate from the internal session notes, with motivation, the v1 wire format, the env handshake, semantics, emit rules (including the per-row wrapping correction and the side-by-side two-records-per-row case), and the known v2 candidates (both-numbers-always, the difftastic token-vs-line mismatch). The prototype code still emits the 456 placeholder; the 456->1717 rename across delta/difftastic/gocui/lazygit and the EMIT_OSC_METADATA env var is a tracked follow-up, not part of this commit. Co-Authored-By: Claude Opus 4.8 (1M context) --- diff-line-metadata-notes.md | 62 +++-- diff-line-metadata-osc-spec.md | 461 +++++++++++++++++++++++++++++++++ focused-main-view-notes.md | 14 +- 3 files changed, 512 insertions(+), 25 deletions(-) create mode 100644 diff-line-metadata-osc-spec.md diff --git a/diff-line-metadata-notes.md b/diff-line-metadata-notes.md index cdb369f12..ba63c8acc 100644 --- a/diff-line-metadata-notes.md +++ b/diff-line-metadata-notes.md @@ -218,17 +218,30 @@ through `Diff.AdjustLineNumber` before opening the editor, exactly as today OSC the same way). The metadata flowing through a real terminal must be harmless — this is a hard requirement, since pagers run outside lazygit too. -### 3.4 The OSC number +### 3.4 The OSC number — RESOLVED: `1717` -No central registry, but allocations are rare; pick a **high, distinctive** -number (the iTerm2 `1337` convention) and **verify against the sources of -xterm, VTE, kitty, foot, WezTerm, iTerm2, Windows Terminal** before committing. -`456` is a placeholder. Known-used slots to avoid (from memory, against a -knowledge cutoff — re-check): `0/1/2` (title/icon), `4`+`104` (palette), -`7` (cwd), `8` (hyperlinks), `9` (notifications; `9;4` progress), `10–12`+ -`110–119` (colors), `50` (font), `52` (clipboard), `99` (kitty notifications), -`133` (semantic prompt), `633` (VS Code shell integration), `777` (urxvt), -`1337` (iTerm2). +> **RESOLVED (audit done).** The final number is **`1717`**, replacing the `456` +> placeholder. There is no central registry; the convention is to pick a high, +> distinctive number and verify no real terminal *acts on* it (an unknown OSC is +> skipped by conformant terminals, but a *recognized* one can fire a visible +> side-effect — e.g. `OSC 555` flashes foot, `OSC 777` raises a desktop +> notification — which is exactly the wire-safety hazard the audit exists to +> avoid). Audited the live OSC allocations of **xterm, VTE, kitty, foot, WezTerm, +> iTerm2, Windows Terminal, Ghostty, VS Code, ConEmu, urxvt**; `1717` collides with +> none and sits in the large empty 1400–5000 band (only iTerm2's `1337` is nearby). +> The full danger list is in the spec appendix (`diff-line-metadata-osc-spec.md`). +> **The prototype code (delta/difftastic/gocui/lazygit + the `EMIT_OSC456_METADATA` +> env var) still uses `456`** — renaming `456`→`1717` across the three repos is a +> tracked follow-up, not yet done. + +Known-used slots to avoid (verified in the audit): `0–3` (title/icon/X11), +`4/5/6` (palette/special/tab color), `7` (cwd), `8` (hyperlinks), `9` +(notifications; `9;4` progress, `9;9` cwd), `10–19` (dynamic colors), `21/22` +(color query / pointer shape), `46/50/51/52` (logfile/font/Emacs/clipboard), `66` +(text sizing), `99` (kitty notifications), `104–106`+`110–119` (reset colors), +`133` (semantic prompt), `176` (foot app id), `555` (foot flash), `633` (VS Code), +`777` (rxvt notify), `1337` (iTerm2), `5522` (kitty clipboard), `30001/30101` +(kitty color stack). ### 3.5 Pagers to patch @@ -311,9 +324,11 @@ feedback) and to **inform a from-scratch production plan**. > NORMAL (unified, single-column) case — see §8 (#1) and §9 (#2). Side-by-side > delta is prototyped (focused-main-view-notes.md §17) and **difftastic is > prototyped in both modes (§10)** — so the emitter side now spans the full -> coverage table. What remains of step 5 is the *deliverables*: finalize and -> publish the spec (now informed by difftastic's §10.2 model-mismatch finding), -> and write the production plan. Host *consumption* of side-by-side / difftastic +> coverage table. What remains of step 5 is the *deliverables*: the **OSC spec +> draft is written** (`diff-line-metadata-osc-spec.md`, OSC number finalized to +> `1717` — §3.4 — and it speaks to difftastic's §10.2 model-mismatch finding), ready +> to circulate to pager developers; what's left is gathering that feedback and +> writing the production plan. Host *consumption* of side-by-side / difftastic > output is still a separate, later step. Sequence: @@ -339,8 +354,9 @@ Sequence: 4. **#2 emitter side** — the delta patch; this is what stress-tests the spec against reality, side-by-side being the hard case (§3.5). 5. **End-to-end → finalize the spec (publish for feedback) → write the - production plan.** End-to-end is **done** (§9.4); the spec and production - plan are the remaining deliverables. + production plan.** End-to-end is **done** (§9.4); the **spec draft is written** + (`diff-line-metadata-osc-spec.md`, OSC `1717`). Circulating it for feedback and + the production plan are the remaining deliverables. **Parallel de-risking (any time, doesn't block #1):** confirm by reading delta's source that it can produce the fields per region — for `-` lines and in @@ -482,10 +498,14 @@ layout changes. A **dedicated, purely-additive emitter** (only injects OSC bytes never touches styling/width/wrapping) is both safer and cleaner, and its counter logic is a near-copy of `linenumbers_and_styles`. -### 9.2 Pinned v1 wire format (placeholder OSC `456`) +### 9.2 Pinned v1 wire format (final OSC `1717`; prototype code still uses `456`) + +> The number was finalized to **`1717`** after the terminal audit (§3.4); the +> prototype code still emits `456` (rename is a tracked follow-up). The published +> spec (`diff-line-metadata-osc-spec.md`) uses `1717`. ``` -ESC ] 456 ; ; ; ; ; ST +ESC ] 1717 ; ; ; ; ; ST ``` - `ESC` = `0x1b`; `ST` = `ESC \` (`0x1b 0x5c`) — same framing as delta's OSC-8. @@ -507,9 +527,11 @@ emits V1 when the advertised list contains `V1`. ### 9.3 Deferred / known prototype limitations -- **Terminal-source audit of the OSC number is DEFERRED** (§3.4). `456` is a - placeholder; before publishing, verify it against xterm/VTE/kitty/foot/WezTerm/ - iTerm2/Windows Terminal and pick a high, distinctive final number. +- **Terminal-source audit of the OSC number is DONE** (§3.4). Final number is + **`1717`** (audited against xterm/VTE/kitty/foot/WezTerm/iTerm2/Windows Terminal/ + Ghostty/VS Code/ConEmu/urxvt; danger list in the spec appendix). The prototype + code still emits the `456` placeholder — the `456`→`1717` rename across + delta/difftastic/gocui/lazygit + the env var is a tracked follow-up. - **Wrapped continuation rows** (`Hunk*Wrapped`) get no attachment in the prototype — only the primary content row does. Fine for the normal case (gocui's own wrapping is handled host-side by the view-line→buffer-line mapping); delta-level diff --git a/diff-line-metadata-osc-spec.md b/diff-line-metadata-osc-spec.md new file mode 100644 index 000000000..0ee3cf4ca --- /dev/null +++ b/diff-line-metadata-osc-spec.md @@ -0,0 +1,461 @@ +# Diff Line Metadata over OSC 1717 — draft specification (v1) + +**Status: draft, for feedback.** This document describes a small terminal +escape-sequence protocol by which a diff pager (delta, difftastic, diff-so-fancy, +…) annotates each rendered line of a diff with the patch-space identity it +represents, so that a host program rendering the pager's output can map a screen +row (and column) back to *the exact line in the underlying diff*. + +It is published to gather feedback from pager authors before anything is +finalized. The wire format, the negotiation handshake, and the OSC number are all +open to revision — §9 lists the points where feedback is most wanted. + +The protocol grew out of [lazygit](https://github.com/jesseduffield/lazygit), but +nothing in it is lazygit-specific; "the host" below means any program that runs a +pager and consumes its output. + +--- + +## 1. Motivation — what this enables, and why parsing isn't enough + +A host that shows a diff rendered by a pager often wants to act on the line the +user is pointing at: + +- **dive into staging / patch-building** for that hunk line, +- **open an editor** at that file and line, +- **open that line in a code-review / PR web view** (needs the side — old vs new), +- **navigate by hunk or by file** within the rendered diff, +- **preserve the scroll position and selection** across a re-render (the diff is + re-rendered with a different context size, or a different pager, and the host + wants to keep the user anchored on the same patch line). + +Every one of these needs the same primitive: **given a rendered row, recover +`(file, side, line)`** — the precise line of the unified diff that row stands for. + +For *structure-preserving* renderings (no pager, `git diff --color`, or +`delta --color-only` without line numbers) the host can recover this by parsing +the on-screen text: walk up to the nearest `@@` and `diff --git`, count `+`/` ` +lines, read the leading `+`/`-`/space. That works and needs no cooperation from +the pager. + +But the moment a pager **restructures** the diff, the unified-diff structure the +parse relies on is gone: + +- `delta` in its default mode conveys the side (added/removed) purely by + background color and prefixes a line-number gutter, so there is no `+`/`-` + marker to read and no parseable `@@` body; +- `delta --color-only --line-numbers` (the configuration people actually use) + pushes the `+`/`-` marker off the start of the line behind the gutter, so a + naive parse is *confidently wrong*; +- `diff-so-fancy` rewrites the headers and strips the `+`/`-` markers entirely; +- `difftastic` is token-granular and side-by-side — there is no unified-diff line + structure left in *either* of its modes. + +In all of these the **pager is the only component that still knows** which file, +side, and line each rendered cell belongs to — it computed exactly that to render +the diff. This protocol asks the pager to *state* that knowledge inline, in a form +the host can read back and that is harmless everywhere else. + +--- + +## 2. Design at a glance + +1. The pager emits one OSC sequence carrying + `(version, type, new-line, old-line, file)` **immediately before** each + rendered region (a region is "one source line's worth of content in one + column" — see §6). +2. The host attaches each record to **the cell that follows it**, exactly the way + OSC-8 hyperlinks attach to the cells they precede. This makes the metadata + survive terminal wrapping and multi-column layouts **without the host ever + reasoning about layout** — it just reads the nearest preceding attachment. +3. The whole thing is gated behind an **environment-variable handshake**, so a + pager run outside a participating host (in a raw terminal, `less`, `tmux`, a + CI log) emits nothing and behaves byte-for-byte as before. + +The protocol is **layout-agnostic on the host side by construction**: all layout +knowledge (where a column starts, where a gutter ends, how a long line wraps) +stays in the pager, which is the only component that has it. + +--- + +## 3. Negotiation handshake + +``` +EMIT_OSC1717_METADATA = V1[,V2,…] +``` + +- The **host** sets this environment variable on the pager subprocess to the list + of protocol versions it understands, highest-preferred first is *not* required — + the list is a set. +- The **pager** emits the **highest version present in both** its own supported + set and the advertised set. If the variable is unset, empty, or shares no + version with the pager, the pager **emits nothing** and its output is unchanged. + +Why a handshake, and why it must exist in v1 even though v1's payload is tiny: + +- **It is the one piece that cannot be retrofitted.** Without negotiation you + cannot introduce a v2 later without a flag day. The payload itself is easy to + evolve *because* the handshake exists. +- **It guarantees zero cost when unwanted.** Outside a participating host the + variable is unset, so there is no output change to audit, no risk in a raw + terminal, no interference with `less`/`tmux`/pipelines. + +The variable name and the value grammar are themselves open to feedback (§9). + +--- + +## 4. Wire format (v1) + +### 4.1 The sequence + +``` +ESC ] 1717 ; ; ; ; ; ST +``` + +- `ESC` is `0x1B`; `ST` (String Terminator) is `ESC \` = `0x1B 0x5C`. A `BEL` + (`0x07`) terminator is also accepted, but `ST` is preferred — this matches the + framing of OSC-8 hyperlinks. +- The payload is **positional and `;`-delimited**. There are exactly five fields. +- `` is **last on purpose** so that it may itself contain `;`: the host + splits the payload into at most five fields and treats everything after the + fourth `;` as the path. + +Raw bytes, for a context line at new-file line 10 of `src/foo.go`: + +``` +\x1b]1717;1;c;10;;src/foo.go\x1b\ +``` + +### 4.2 Fields + +| field | presence | meaning | +|---|---|---| +| `version` | always | decimal protocol version; `1` for v1. Carried in every record so attachments are self-describing. | +| `type` | always | one character — see §5.1. v1 emits `c` (context), `a` (added), `d` (deleted). | +| `new-line` | content lines | new-file line number, in the **diff's new-file space** (see §5.2). | +| `old-line` | only `type=d` | old-file line number. **Empty** for `c` and `a`. | +| `file` | always | the file path the line belongs to; absolute or repo-root-relative (the host normalizes — emit whichever is convenient). Carried on **every** record so a single record is a complete answer. | + +### 4.3 Examples + +| rendered line | emitted record (`;`-form) | +|---|---| +| context, new line 10 | `1717;1;c;10;;src/foo.go` | +| addition, new line 11 | `1717;1;a;11;;src/foo.go` | +| deletion, old line 9, sits at new pos 11 | `1717;1;d;11;9;src/foo.go` | +| two consecutive deletions | `…;d;11;9;…` then `…;d;11;10;…` (same `new-line`, different `old-line` — see §5.3) | +| whole-file deletion | `1717;1;d;0;9;old/path` (`new-line` 0 — see §5.5) | + +--- + +## 5. Semantics + +### 5.1 Type + +`type` is one character. v1 defines and emits three (the content-line types): + +- `c` — context (unchanged) line +- `a` — added line +- `d` — deleted line + +Reserved for future use (v1 emitters do **not** emit these; header rows simply +carry no record and the host falls back): + +- `h` — hunk header (`@@ … @@`) +- `f` — file header (`diff --git`, `---`, `+++`) +- `o` — other + +**`type` is load-bearing and cannot be inferred from the other fields.** Under the +coordinate rules, `added` and `context` both carry `{new-line present, old-line +absent}`, so presence alone cannot distinguish them — yet the host must (scroll +preservation anchors specifically on *change* lines, so it has to tell `added` +from `context`). Hence an explicit type rather than an inferred one. + +### 5.2 Line-number spaces + +- `new-line` is the line number in the **diff's new-file space** — i.e. the + new-file line numbering the diff itself uses, *not* necessarily the working-tree + file (the diff may be against staged content). A host that opens an editor is + expected to re-map this through its own diff↔worktree adjustment; the pager + should emit the number as it appears in the diff it is rendering. +- `old-line` is the old-file line number, present **only** for deletions. + +### 5.3 The deleted-line convention (both numbers) + +A `d` record carries **both** numbers: + +- `old-line` is the deletion's own old-file line number. +- `new-line` is the new-file position the deletion *sits at*: `newStart` plus the + number of added/context lines above it within the hunk. This is exactly what + `git`'s patch arithmetic already computes for a removed line. + +Consequence, which all pagers must implement identically: **two consecutive +deletions share the same `new-line`** (nothing new-file-side advances between +them) and are told apart only by `old-line`. Example — two deletions at old lines +9 and 10, both sitting at new position 11: + +``` +1717;1;d;11;9;src/foo.go +1717;1;d;11;10;src/foo.go +``` + +A host uses `old-line` to find a deletion's patch line and its old-side +(review/PR `L`) anchor, and `new-line` for the editor target. + +### 5.4 Why the side must be carried at all + +It is tempting to think the side (added vs deleted) is redundant with the rendered +glyph. It is not: in `delta`'s default rendering there are **no `+`/`-` glyphs** — +the side is conveyed purely by background color, which a host reading decolorized +text cannot recover. The pager therefore has to state the side explicitly, which +is what the `a`/`d` type does. + +### 5.5 Whole-file add / delete + +A deleted file's lines carry `new-line` = `0` (mirroring git's `@@ -1,N +0,0 @@`); +an added file's lines carry the new-file numbers normally and `type=a`. + +--- + +## 6. Emit rules (placement) + +### 6.1 One record per region, at the region's start + +The pager emits each region's record at the **start of that region**. Everything +from there until the next region's record (or end of line) belongs to that +record — *including* any line-number gutter or other embellishment the pager +considers part of the region. Where a region "really" starts is the pager's +call, not the host's. The single firm requirement: + +> **The record must precede the region's first cell**, so that a host searching +> leftward from any cell lands in the correct region. + +### 6.2 Multiple regions per row (side-by-side) + +A side-by-side row shows two source lines at once, so it carries **two records**, +one before each column: + +- **left column → the old-side line** (`d`, carries `new-line` + `old-line`), +- **right column → the new-side line** (`a`, carries `new-line`), +- **a context line, shown in both columns → the same `c` record before each + half.** + +The blank counterpart of a pure add/delete (the empty half) carries **no** record. + +A side-by-side row carries **at most two** records — one per column. (An earlier +draft worried token-level tools might need *N* records per row; they do not. +Token-level novelty is *sub-cell coloring*, not separate identity: a column's cell +is one source line however many tokens within it are highlighted. Two columns → +at most two records.) + +**v1 needs no column/side discriminator field for this.** `type` already implies +the column — `a` is inherently the new/right side, `d` the old/left side — and a +context line's two halves are the *same* logical line, so the host tells the two +`c` columns apart by position (which it does anyway). A side field would only earn +its keep to disambiguate the symmetric `c` case, and that case needs no +disambiguation. + +### 6.3 Wrapping — emit on every output row + +> **The pager emits a line's record at the start of *every output row* it produces +> for that line, including its own wrapped continuations.** + +There are two distinct kinds of wrapping, and the rule differs: + +- **Terminal/host wrapping** — the pager emits *one* line (one `\n`) and the + terminal (or the host's view) wraps it onto several visual rows. Here only the + primary row needs a record; the host's own row→line mapping routes every visual + row of that line back to it. A pager that relies on terminal wrapping emits one + record and is fine. +- **Pager wrapping** — the pager itself emits *several* lines (several `\n`s) for + one logical line, as difftastic's side-by-side does and as delta does in + side-by-side with `wrap-max-lines`. Now each wrapped row is a **distinct line** + to the host, with nothing tying row *N+1* back to row *N* — so each must carry + its own record, or it has none. + +A continuation row re-emits the *same* record as the primary line it continues +(no line-number advance). A column that has run out of wrapped content carries no +record on its padding rows. + +Getting this wrong is not theoretical: without per-row records, acting on a +wrapped continuation row does nothing, and hunk/file navigation breaks because the +untagged rows fragment a wrapped line into one block per visual row. Both delta and +difftastic had this bug in their prototypes and were fixed to emit per-row. + +### 6.4 Header rows + +Hunk headers (`@@`), file headers (`diff --git`, `---`, `+++`), and any other +non-content rows carry **no** record in v1 (the `h`/`f`/`o` types are reserved but +unused). A host acting on a header row falls back to whatever non-metadata path it +has, or to no action. + +--- + +## 7. How the host consumes it (informative) + +This section is not normative — it describes how lazygit's prototype consumes the +protocol, to make the emit rules concrete. + +- The host attaches each record to the following cell, like an OSC-8 hyperlink. If + a record has no following cell (a genuinely empty rendered line), the host adds + a content-less cell to hold it. +- **Row-granular action** (e.g. a keyboard "act on this line"): use the **first** + record on the row. In side-by-side this is the left column — fine, since the two + sides of a change are one hunk for staging purposes. +- **Point-granular action** (a mouse click): use the **nearest record at or to the + left of the click column** → lands in the column actually clicked. +- The host normalizes `file` (resolving it relative to the repository working + tree) and otherwise treats the record as opaque identity. + +--- + +## 8. Known limitations and v2 candidates — feedback wanted + +These are surfaced honestly because the protocol is a draft. None blocks v1; each +is a place where pager-author input would shape v2. + +### 8.1 `c` and `a` records carry no old-file number + +For context and addition records, `old-line` is empty. This is invisible in a +single-column rendering, but **side-by-side makes it visible**: the old/left +column shows a real old-file line number on screen, yet the record for that cell +reports only `new-line`. With difftastic it bites hardest — difftastic *always* +shows old-file numbers, old ≠ new is the norm, and the offset is **not constant** +(it follows structural alignment, not a per-hunk delta), so the old number is not +even derivable from the new one. + +Nothing in the current host consumers needs the old number for `c`/`a`, so v1 +leaves it empty. The clean v2 fix, if needed, is **carry both numbers on every +record** (applies to both single-column and side-by-side) — *not* a side-only tag. + +### 8.2 The token-vs-line model mismatch (difftastic) + +The unified-diff pagers (git, delta, diff-so-fancy) derive from git's +**line-granular** patch, where a modified line is by construction a `-` line plus a +`+` line. The `c`/`a`/`d` type set fits that model exactly. + +difftastic is **token-granular**: it aligns an old line with a new line and marks +novelty *per token*. That produces aligned rows the line model has no clean slot +for. Real example — `let x=1; println!("{}", x);` changed to +`let x=2; let y=3; println!("{}", x + y);`: + +``` +…;c;4;;src/lib.rs 3 println!("{}", x); …;a;4;;src/lib.rs 4 println!("{}", x + y); +``` + +The old line `println!("{}", x);` has no novel tokens (they all survive into the +new line), while the new line added `+ y`. So difftastic's faithful per-cell +verdict is **`c` on the left, `a` on the right** — the same aligned row carrying a +context record and an addition record, with **no `d`**, because by difftastic's +model nothing was deleted. + +A host mapping cells to git's line-granular patch then resolves the right/added +cell correctly, but the left/old cell resolves as *context at the new line*, not +as git's `-` line — its old-side deletion identity is not recoverable from the +record. Practical impact is small (users click the changed side; an editor target +on the left still opens the right new-file line), but it is a genuine semantic gap. + +Options, none taken in v1, all wanted-as-feedback: + +- **Host-side:** treat an old-column cell aligned with a novel new line as the `-` + side of a modification (the host knows it is the left column). +- **Emitter-side, v1-compatible:** difftastic could emit `d` for the old side of + *any* aligned changed row — but that re-imposes the very line-granular model + difftastic exists to escape, discarding its more precise "this was not removed" + judgement. Probably wrong to force. +- **A `modified` / `m` type (v2):** names the case directly — "aligned, changed, + present on both sides" — but splits the clean `c`/`a`/`d` staging mapping and + needs every pager to agree when to use it. Recorded as a v2 candidate. + +### 8.3 Pure-deletion `new-line` in token tools is synthesized + +A line-granular pager always knows the new-file position a deletion sits at. A +token tool without a linear new-file counter (difftastic) computes it from the +previous aligned new line (`prev_rhs + 1`). This is exact for the common case but +can drift across hunk boundaries or with zero context lines. The deletion's +*old*-line (its real identity for staging) is always exact; only the editor-target +`new-line` is approximate. Pagers should emit the most precise `new-line` they can. + +### 8.4 Out of scope for v1 + +`git --word-diff` (inline `[-…-]{+…+}` markup, no per-line `+`/`-`) has no clean +per-line identity and is not addressed. + +--- + +## 9. Where feedback is most wanted + +1. **The OSC number, `1717`.** Chosen after auditing the OSC allocations of + xterm, VTE, kitty, foot, WezTerm, iTerm2, Windows Terminal, Ghostty, VS Code, + ConEmu and urxvt (see the appendix): `1717` is unused by all of them and sits + in the large empty 1400–5000 band (only iTerm2's `1337` is nearby). There is no + central registry, so this is "verified unused across the terminals that matter," + not "allocated." If you know of a terminal that interprets `1717`, please say so. +2. **The env-var name and grammar** (`EMIT_OSC1717_METADATA=V1,…`). +3. **The both-numbers question** (§8.1) — is per-column-exact old/new numbering + worth a v2, or is it never needed? +4. **The token-vs-line mismatch** (§8.2) — should there be an `m` type, or is + host-side inference the right home for it? +5. **Can your pager actually produce all four fields per region?** In particular + the side for deleted lines, and in side-by-side mode. (delta needed to track + its own old/new counters because its line-number counters are dormant unless + `--line-numbers` is on; difftastic had them natively. Your mileage may vary.) +6. **Optional trailing fields within a version** — a cheap, additive escape valve + short of a version bump: a consumer stops at the fields it knows. Is this worth + blessing in v1, or should all growth go through the version field? + +--- + +## 10. Reference implementations (prototype) + +All three are at prototype quality and emit the v1 format described here (modulo +the placeholder OSC number, which these used `456` for before `1717` was chosen): + +- **delta** — a dedicated additive emitter that injects only OSC bytes (no change + to styling, width, or wrapping); with the env var unset, output is byte-for-byte + identical to stock delta. Covers unified and side-by-side modes, including + wrapped rows. +- **difftastic** — the categorical case (#1 host-side parsing cannot serve it in + either mode). Emits the same v1 format under the same handshake; markedly less + code than delta because difftastic carries old/new line numbers natively. Covers + side-by-side and inline modes. +- **host carrier** — gocui (lazygit's TUI library) accumulates the OSC number, + collects the payload, and stamps it per-cell like a hyperlink, cleared at each + line boundary so it cannot bleed onto an untagged following line. + +A key validated property in both pagers: **with the handshake absent, output is +byte-identical to the unpatched pager** — the protocol is strictly additive. + +--- + +## Appendix — terminal OSC audit behind the `1717` choice + +There is no registry for OSC numbers; the de-facto convention is to pick a high, +distinctive number and verify no real terminal acts on it (an unknown OSC is +*skipped* by conformant terminals, but a *recognized* one could fire a visible +side-effect, e.g. `OSC 555` flashes foot and `OSC 777` raises a desktop +notification). The numbers below are interpreted by at least one surveyed terminal +and were therefore excluded: + +| OSC | meaning | actors | +|---|---|---| +| 0–3 | title / icon / X11 property | all | +| 4, 5, 6 | palette / special color / tab color | xterm, iTerm2 | +| 7, 8 | working directory, hyperlink | all major | +| 9 | notifications; ConEmu progress `9;4`, cwd `9;9` | iTerm2, kitty, WinTerm, foot | +| 10–19 | dynamic colors (fg/bg/cursor/mouse/Tek/highlight) | xterm, foot, VTE | +| 21, 22 | color query/set, pointer shape | kitty, foot | +| 46, 50, 51, 52 | logfile, font, Emacs, clipboard | xterm, iTerm2; 52 all major | +| 66 | text-sizing protocol | kitty, foot | +| 99 | desktop notifications | kitty | +| 104–106, 110–119 | reset colors | xterm, foot, VTE | +| 133 | semantic prompt / shell integration | iTerm2, kitty, foot, WezTerm, WinTerm | +| 176 | application id | foot | +| 555 | flash the terminal | foot | +| 633 | shell integration | VS Code | +| 777 | desktop notification (rxvt ext) | urxvt, VTE, foot, WezTerm | +| 1337 | proprietary namespace / file transfer | iTerm2, WezTerm | +| 5522 | extended clipboard | kitty | +| 30001 / 30101 | color-stack push / pop | kitty | + +`1717` collides with none of these and is far from every active cluster. diff --git a/focused-main-view-notes.md b/focused-main-view-notes.md index 445123f8c..90dcadfb0 100644 --- a/focused-main-view-notes.md +++ b/focused-main-view-notes.md @@ -1333,11 +1333,15 @@ restructures the diff. (doesn't touch lazygit). Feeds the spec (below). **[done — §17; v1 suffices, no format change]** -**OSC spec write-up:** a **draft**, not final — present it to pager devs *for -feedback*. The unified single-column wire format is validated (§9.2) and can be -presented confidently; **side-by-side is now validated too** (§17 — v1 needs no -addition), so the draft speaks to both modes. The one remaining open item to flag is -the **OSC number** (456 is a placeholder, pending a terminal-allocation audit). +**OSC spec write-up:** **DONE — draft written** (`diff-line-metadata-osc-spec.md` +at the worktree root). It's a draft for pager-dev *feedback*, not final. The +unified single-column wire format is validated (§9.2), side-by-side is validated +(§17 — v1 needs no addition), and the draft speaks to difftastic's token-vs-line +mismatch (diff-line-metadata-notes.md §10.2). The **OSC number is resolved to +`1717`** after a terminal-allocation audit (diff-line-metadata-notes.md §3.4 + +the spec appendix); `456` is retired as the placeholder. **Still pending:** the +`456`→`1717` rename across the prototype code (delta/difftastic/gocui/lazygit + the +`EMIT_OSC456_METADATA` env var), and circulating the draft for feedback. **Decisions locked (session 6):** concurrency stays **mutex-based**, including for productionization (the main-thread-mutation rework is a separate, later effort — do