difftui

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: GPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package difftui is the terminal client of the shared diff session: the same changeset the console's Diff surface renders and an agent joins over MCP, read with a keyboard.

The CLI already shared the review COMPUTATION; what it did not share was the COORDINATION - where the human is looking, which hunks they have read, what an agent wants them to look at. That is the whole difference between three tools rendering one changeset and three clients of one review.

The model in this file owns navigation, the generated fold, the viewed set and the viewport window, and it touches no terminal at all: rows in, rows out. render.go turns it into a frame and run.go feeds it keys, so the state machine is testable without a pty.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Chrome

func Chrome(m *Model) int

Chrome is how many rows a frame spends on things that are not changeset rows, so a caller can size the viewport against the terminal before painting.

func Frame

func Frame(m *Model, color bool) string

Frame composes what the viewer looks like right now.

It returns a string rather than writing one so tty.InlineView can rewrite only the rows that differ - moving the cursor changes two lines and costs two lines of terminal traffic.

color is decided by the caller, from tty.WantsColor, and is a parameter rather than something read here so the model stays as testable as it is: false must produce the plain frame, byte for byte, with no escape sequence anywhere in it.

func Run

func Run(ctx context.Context, opts Options) error

Run draws the changeset and reads keys until the reader quits.

It never touches the alternate screen buffer: the transcript above stays where it is and survives the session, which is the rule every interactive surface in magus follows.

Types

type File

type File struct {
	Path string
	// Settled is a file a receipt covers at exactly its current content: read, and unmoved since.
	//
	// Folded by default for the same reason Generated is - it is not what the reader is here for -
	// but for a different reason, so it is a separate flag and a separate key. A generated file is
	// a machine's restatement of an edit made elsewhere; a settled file is one this reader already
	// weighed. Conflating them would fold a colleague's unreviewed generated file and a reader's
	// own finished work under one word.
	Settled   bool
	Generated bool
	Facts     []string
	Hunks     []Hunk
}

File is one changed file. Facts are the annotation lines ALREADY RENDERED by the caller, because `magus diff` owns that vocabulary and two renderings of "12 files reference its widest changed symbol" would drift.

type Hunk

type Hunk struct {
	// NewStart is the hunk's first line on the new side, and Declaration is the enclosing
	// declaration git named in its header. Together they are what the heading row says, in place
	// of the raw @@ coordinates: where the reader is, and what they are inside of.
	NewStart    int
	Declaration string
	// Index is the hunk's position in the PATCH, which is the coordinate a comment and a
	// suggestion are anchored by (see changeset.Hunk.Index). Carried rather than taken from the
	// position in Hunks, so a caller that ever hands over a subset cannot silently renumber
	// every anchor in the file.
	Index  int
	Header string
	Lines  []string
	Digest string
	// Emph is, per line of Lines, which part of it changed - as byte offsets into the RAW
	// line, marker included, because that is what the renderer slices. Empty or short is fine
	// and means no emphasis, which is what a caller that does not compute it gets.
	//
	// Passed in rather than derived here, like Digest above and for the same reason: the
	// parser works it out once and both surfaces read the one answer.
	Emph []changeset.Span
}

Hunk is one @@ section as the viewer shows it. Digest is the content address the viewed set is keyed by - the same one internal/diff computes, passed in rather than recomputed so the CLI and the console mark the same hunk.

type Input

type Input struct {
	Files    []File
	Unranked bool
	// Viewed carries the digests already marked read, from the session or the local store.
	Viewed      []string
	Comments    []types.DiffComment
	Suggestions []types.DiffSuggestion
	// Threads are the remarks already on the host's review, with Hunk resolved by
	// changeset.PlaceThreads. A thread whose line this changeset does not contain (Hunk < 0) renders
	// under the file heading rather than being dropped: a colleague said it, and a viewer that
	// silently withheld it would be telling the reader nobody had.
	Threads []types.ReviewThread
	// Unfolded starts with generated files expanded, which is what --generated asks for.
	Unfolded bool
	// Link decorates a path for display (an OSC 8 hyperlink). Nil renders it plain.
	Link func(string) string
}

Input is the changeset the viewer opens on. Files arrive in reading order and are never re-sorted here - types.Diff.SortForReading is the one definition of that order.

type Model

type Model struct {
	// contains filtered or unexported fields
}

Model is the navigation, fold and progress state machine.

func New

func New(in Input) *Model

New builds the model and composes its first row list.

func (*Model) CursorRow

func (m *Model) CursorRow() int

CursorRow is the index into Rows of the row the cursor marks, or -1 when there is none.

func (*Model) Height

func (m *Model) Height() int

Height is how many rows the viewport shows.

func (*Model) Overview

func (m *Model) Overview() bool

Overview reports whether the file-list overview is open.

func (*Model) Rows

func (m *Model) Rows() []Row

Rows returns every visible row, cursor included. The renderer windows it.

The slice ALIASES the model's own, which rebuild refills in place. A cursor move does not rebuild, but a fold or a read mark does, and a row list that grew REALLOCATES - so an earlier return value is left pointing at whichever backing array it was handed, stale rather than live. Read it and drop it rather than holding it. Copying instead would allocate the whole changeset on each of the frames a keypress draws, which is the cost the reuse exists to avoid.

func (*Model) Top

func (m *Model) Top() int

Top is the first row of the viewport.

func (*Model) Unfolded

func (m *Model) Unfolded() bool

Unfolded reports whether generated files are showing their hunks.

func (*Model) Unranked

func (m *Model) Unranked() bool

Unranked reports that there was no ranking key, so the order is path order.

func (*Model) Unsettled

func (m *Model) Unsettled() bool

Unsettled reports whether already-reviewed files are being shown.

func (*Model) Viewed

func (m *Model) Viewed(digest string) bool

Viewed reports whether a digest is marked read.

type Options

type Options struct {
	// In is read for keys and Out is drawn on. Both must be terminals; the caller has
	// already refused otherwise, and OpenInput refuses again rather than trusting that.
	In    *os.File
	Out   io.Writer
	Probe tty.Probe
	Input Input
	// Sync is nil when nothing is listening - no daemon, no console, no agent.
	Sync Sync
	// Summary is the one line left behind in the scrollback when the reader quits, so the
	// session records what was read rather than vanishing without a trace. It is called at that
	// moment and handed the fold the reader left in, because `.` changes what the line has to
	// describe. Nil leaves nothing behind.
	Summary func(unfolded bool) string
}

Options is one interactive session.

type OverviewRow

type OverviewRow struct {
	// Path is undecorated. The link, when there is one, is in Rendered.
	Path string
	// HunkCount rather than Hunks: File.Hunks in this same package is the hunks THEMSELVES, and
	// one name for two shapes reads as a copy of the slice at every use.
	HunkCount int
	Read      int
	Generated bool
	Rendered  string
}

OverviewRow is one file in the changeset overview. Rendered is the line the frame prints; the fields beside it are what that line SAYS, so a caller reads the answer rather than matching text out of it again.

type Row

type Row struct {
	Kind RowKind
	File int
	Hunk int
	// Thread is the host thread this row renders a line of, empty on every other row. It is
	// what lets the viewer report a remark as SHOWN once it has actually been drawn on screen,
	// rather than when the changeset was handed over.
	Thread string
	Text   string
	// Emph is which PART of Text changed, in BYTES of Text, on a RowLine that could be paired
	// with its counterpart. The zero span means there is nothing to draw harder than the rest -
	// the line has no partner, or the whole of it changed and the row color already says so.
	Emph changeset.Span
}

Row is one line of the changeset. Hunk is -1 outside a hunk.

type RowKind

type RowKind int

RowKind says what one visible row is, so the renderer can mark the cursor and the tests can assert structure rather than string-match a frame.

const (
	// RowFile is a file heading: the path and its hunk counts.
	RowFile RowKind = iota
	// RowFact is one annotation line under a heading.
	RowFact
	// RowFold stands in for the hunks of a folded generated file.
	RowFold
	// RowHunk is a hunk heading, carrying the viewed mark.
	RowHunk
	// RowLine is one line of a hunk body.
	RowLine
	// RowComment is a remark anchored to the hunk above it.
	RowComment
	// RowSuggestion is an agent asking for attention. It is displayed and never applied.
	RowSuggestion
	// RowBlank separates one file from the next. Nothing selects it.
	RowBlank
)

type Sync

type Sync interface {
	SetCursor(c types.DiffCursor)
	SetViewed(digest string, on bool)
	// SetThreadsSeen records that these of the host's review threads have been in front of the
	// reader, which is the watermark deciding what still counts as NEW. A workspace whose
	// watermark nobody ever advances is one the forge-watching job reads as never reviewed in,
	// and it then reports no colleague's remark at all - so a terminal reader has to write it
	// for the same reason a browser one does.
	SetThreadsSeen(ids []string)
}

Sync is the human's half of the session: where they are looking, and what they have read.

There is deliberately no read side. A cursor coming BACK from the session would be some other client moving this reader's viewport, and a viewport that moves on its own is the one thing the whole suggestion design exists to prevent - see types.DiffSuggestion. An agent suggests; only the person at the keyboard navigates.

No method reports an error, and that is the contract rather than an omission: a coordination write that failed must not interrupt somebody reading a diff.

type ViewedChange

type ViewedChange struct {
	Digest string
	On     bool
}

ViewedChange is one flip of a read mark: the hunk it was made on, and which way it went.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL