Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Support OSC-1717 metadata#1014

Draft
stefanhaller wants to merge 4 commits into
Wilfred:masterWilfred/difftastic:masterfrom
stefanhaller:osc-1717-metadatastefanhaller/difftastic:osc-1717-metadataCopy head branch name to clipboard
Draft

Support OSC-1717 metadata#1014
stefanhaller wants to merge 4 commits into
Wilfred:masterWilfred/difftastic:masterfrom
stefanhaller:osc-1717-metadatastefanhaller/difftastic:osc-1717-metadataCopy head branch name to clipboard

Conversation

@stefanhaller

Copy link
Copy Markdown

This adds support for the OSC-1717 spec described in jesseduffield/lazygit#5731; see also jesseduffield/lazygit#5732 for more context.

Closes #1004.

stefanhaller and others added 4 commits July 19, 2026 09:31
A host that renders difftastic's output (e.g. lazygit) needs to map a
rendered diff cell back to its patch-space identity -- file, line type,
new/old line numbers -- to act on the line the user points at.
difftastic produces a structural, non-unified rendering (side-by-side
columns, no @@/+/- markers, syntax-aware alignment), so that identity
cannot be recovered from the painted text at all; the pager, which has
it at render time, has to state it.

Gate the emission on the OSC1717 environment variable so output is
byte-for-byte unchanged outside such a host -- harmless in a raw
terminal, less, or tmux. The host advertises the protocol versions it
understands and difftastic emits the highest mutually-understood one
(currently always v1 because that's the only version that exists, but
the mechanism is extensible).

The metadata is attached per cell, since side-by-side is difftastic's
default and signature mode: the old-file (left) column carries the
deleted/old line's identity, the new-file (right) column the added/new
line's. A modification -- one aligned row, old on the left and new on
the right -- thus emits two records (d left, a right), exactly as a
unified diff splits a change into a - and a + line; context lines, shown
on both sides, emit the same c record before each column. When a hunk's
changes are all one-sided, difftastic collapses it to a single column; a
modified row is then rendered once but still represents both patch
lines, so it carries its d and its a back-to-back at the row's start
(the d's region is zero-width -- spec 6.2), letting a host stage both
halves from the one row it can see. difftastic wraps a long line into
several output rows itself, and a host sees each as a distinct line, so
every one carries the same record (so the host can act on a continuation
row and treat the wrapped line as one block when navigating); only a
side's padding rows, once its wrapped content is exhausted, and the
blank counterpart half of a pure add/delete carry none.

The line type is the row's patch-space type, from comparing the aligned
lines' contents -- not the display's per-token novelty. A line changed
only by added tokens has no novel old-side tokens, and a blank line has
no tokens at all; classifying by novelty would tag such old lines c, and
a host staging by the records would drop the deletion half of the
modification (spec 5.1, 8). Genuinely identical lines that merely fall
inside a hunk still compare equal and stay c.

Unlike a unified-diff pager, difftastic already has the new/old line
numbers natively (it always renders them as gutters) and receives the
file path as a parameter, so no hunk-header counter tracking is needed:
each record falls out of the row's aligned (Option<LineNumber>,
Option<LineNumber>) and the content comparison. The one synthesized
field is a pure deletion's new-file line, which difftastic (having no
linear new-file counter) derives from the previous aligned new line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inline mode renders the diff grouped: all old-side content
(before-context, then deletions) is printed before all new-side content
(additions, then after-context), rather than interleaved like a unified
diff. Attach the same per-line OSC records here -- the old-side passes
emit the left-column record, the new-side passes the right-column one.

Because the records carry the file and new/old line numbers explicitly,
a host can reconstruct each line's identity despite the grouping: a
modification's d (in the deletions group) and a (in the additions group)
are emitted far apart in the output yet both reference the same new-file
line, with the d also carrying the old-file line. A positional or
structural re-parse of inline output could not pair them; the metadata
makes each line self-describing.

The line type is the row's patch-space type, from comparing the aligned
lines' contents (as in side-by-side) rather than the all-or-nothing
novelty the display colors hunk rows with -- so a context line that
happens to fall inside a hunk is tagged c, not a/d, while a line changed
only by added tokens (nothing highlighted on its old side) is still the
d half of its modification. In the context passes, an old line with no
new-side counterpart is a deletion even though this display files it
under context (a deleted blank line has no tokens to be novel), and the
mirrored new-side line is an addition; an aligned context row stays c
even if its sides differ in whitespace, since only one side of it is
ever rendered here and a d would hand the host a deletion whose addition
it can never show. A pure deletion's new-file line is derived from the
previous new-file line carried through the old-side passes, as in
side-by-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A host can already learn difftastic's per-line diff metadata, but only
from the records emitted before content lines — so a diff with no
content (a binary file, or the empty diff a host would use to probe)
emits nothing, and "speaks the protocol" is indistinguishable from
"unsupported pager".

Emit a version-only OSC 1717 record (no further fields) once, as
difftastic's first output, whenever a version is negotiated. It's
content-independent, so a host can probe difftastic on an empty diff and
get a conclusive answer. Flushed explicitly because difftastic exits via
process::exit, which skips the implicit stdout flush — without it the
lone handshake of an empty probe could be lost.

See the diff-line-metadata OSC spec, §4.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
difftastic renders no `@@`/`diff --git` rows; its only structural row
is one banner per hunk (`path --- N/total --- Format`) announcing both
the file and the hunk. Annotate it with the header records so a host
can anchor file and hunk navigation on the banners themselves and can
see files with no content lines: every banner carries its hunk's `h`
record with the hunk's first new-file line, and the first hunk's
banner -- the only row announcing the file -- additionally carries the
file's `f` record before the `h`, per the spec's combined-header rule
(§5.5). `f` never carries a line number; the spec fixes each header
type's payload rather than leaving it to the renderer (a streaming
renderer like delta cannot know the first-hunk line at its file
header).

Unlike delta, difftastic builds the whole diff before rendering, so
the hunk's first new-file line is in hand when the banner is printed:
the first aligned row with a new-file line provides it, falling back
to the position the next new line would occupy for a hunk with none
(a pure deletion), mirroring the deletion convention. In inline mode
the banner print moves below the context calculation for the same
reason. A whole-file add/delete renders through the single-column
path as one hunk, so its banner carries `f` plus an `h` at line 1
(addition) or 0 (deletion, matching its `d` records). A rename's
two-row banner repeats the records on each row (spec §6.4).

With OSC1717 unset, output remains byte-for-byte identical to stock
difftastic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OSC-1717 support for better integration in lazygit

1 participant

Morty Proxy This is a proxified and sanitized view of the page, visit original site.