Rendering a view's content again while a search is on leaves the "x of y"
describing the content that has just been replaced. The status is worked
out when the search is typed and again when a key steps through the
matches, and a render is neither. Change the diff context size while
searching the focused main view, and the count stays as it was, however
many matches the wider context brought in or took away.
Run the search again over the new content once the render has finished
putting it there.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A view's search positions were worked out again from every write, and
each of those walks the whole view. Content arrives a line at a time, so
rendering into a searched view costs a walk per line. Streaming 2000
lines takes 565ms, where the same render into an unsearched view takes
about 10ms.
Mark the positions stale on a write instead, and work them out where they
are read: when the view is drawn, when a key steps through the matches,
when the status is asked for. That is at most once a frame, and the same
2000 lines now take 8ms.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A view can only be drawn with the focused frame and title colors while it
is the current view, but a panel made of an outer view and an editable
field embedded in it has to look focused as a whole, whichever of the two
the keyboard is pointed at.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
It existed for the incremental re-render: a shorter render left the previous
one's view lines in the tail (deliberately, to avoid a blank frame), and this
cleared them once the new content was fully read. Async renders now build
off-screen and swap in whole, so refreshViewLinesIfNeeded truncates the view
lines to the buffer and no tail can form. All the call at end-of-input still
did was discard every wrapped line and force the whole buffer to be re-wrapped
on the next draw, which is pure work on a large diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A cmd/pty re-render used to overwrite the displayed buffer from the top
down as lines arrived, relying on keeping the previous render's view-line
tail to avoid a blank frame. That left the view showing a mixture of old
and new content while loading, and any reader (draw, clicks, the
view-line mapping) could observe a half-written buffer at the wrong
scroll.
Instead, build the new content in a second, off-screen viewBuffer: until
the task has read enough to paint, writes go there and the displayed
buffer — and so everything every reader sees — is left untouched. Once the
task reaches its first-paint point (InitialRefreshAfter, or EOF for short
content) it swaps the off-screen buffer in atomically, so the view jumps
straight from the previous render to the new one with no intermediate
frame. Subsequent lines append to the now-displayed buffer.
Swapping at the first-paint point means the displayed buffer is only a
viewport tall when it appears and then grows as the rest streams in toward
the count needed for an accurate scrollbar. The scrollbar is sized from the
displayed buffer's height, so left to itself the thumb would shrink and
snap back during that growth (most visibly: the files panel's periodic
refresh making the thumb jump while scrolled down). The total height the
scrollbar needs is a strictly later quantity than the viewport-fill paint,
so no single early swap can have both right. FreezeScrollbarHeight therefore
records the view's height when a load begins and the scrollbar is held there
— growing only if the new content turns out taller — until the load ends; a
synchronous render superseding the load releases it. This mirrors the layout
clamp, which already ignores the partial content height while a view loads.
With the swap doing a wholesale replace, refreshViewLinesIfNeeded can
truncate the view lines to the current buffer: there is no longer a
half-loaded shorter buffer whose tail we must keep showing, so a stale
tail never forms. clear()/Reset() abandon any in-progress off-screen
render so a synchronous SetContent after a stopped task writes to the
display.
The swap holds writeMutex for now; it could later move to the main thread.
Flicker behaviour still needs interactive verification (LAZYGIT_SLOW_RENDER
+ a real diff renderer).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
write, writeCells, makeWriteable, parseInput and
autoRenderHyperlinksInCurrentLine produced cells into v.buf; move them onto
viewBuffer so they can write into any buffer, not just the displayed one.
The display-side effects that don't belong to content production —
tainting, clearing hover, updating search positions — stay behind in the
View.write wrapper, which delegates the actual writing to v.buf.write(v).
Render config the writer needs (Editable, colors, width, tab width,
hyperlink auto-render) is read from the passed View. Behaviour-preserving:
the wrapper still always targets v.buf.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fields that make up a view's content and the act of writing to it —
the cell buffer (lines), the write cursor (wx/wy), the escape-sequence
decoder (ei) and the held-newline flag (pendingNewline) — were loose
fields on View. Bundle them into a viewBuffer struct that View holds by
pointer. This is a behaviour-preserving prep refactor: every access just
goes through v.buf now. It sets up rendering into a second, off-screen
viewBuffer that can be swapped in atomically, so an async re-render never
exposes a half-written buffer to readers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hyperlinkAt (the click path) and onMouseMove/findHyperlinkAt (hover) read
v.viewLines without holding writeMutex, unlike every other reader. They run
on the event-handling goroutine, so a re-render on the task goroutine can
shrink or rebuild viewLines between the bounds check and the indexing,
causing an out-of-range panic (observed: "index out of range [60] with
length 0" while hovering during a diff re-render).
Take writeMutex for the duration, like the other viewLines readers do, so
the check and the access see the same slice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading a view's internal buffer belongs on the view itself, next to
findHyperlinkAt, rather than in the event loop; and the view is where the
lock that guards that buffer can be taken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Several methods assigned v.ox and v.oy directly: SetOrigin, CopyContent,
the wrap/autoscroll branches in draw, FocusPoint, and
Scroll{Up,Down,Left,Right}. Funnelling them all through SetOriginX and
SetOriginY gives a single place to observe (or set a breakpoint on)
every change to a view's scroll position, which makes debugging scroll
behaviour much easier.
This means those call sites now also get the setters' `< 0` clamps, but
that is behaviour-preserving in every case: each assigned value is
already >= 0. calculateNewOrigin never returns a negative number;
CopyContent copies origins that are themselves always >= 0; and the draw
and scroll writes are all guarded (or fed only non-negative amounts) so
the result can't go below zero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When using lazygit in a multi-tab terminal it is useful to see if the
lazygit tab is currently active; ghostty does a very good job at dimming
down the inactive tabs, but VS Code's builtin terminal does not, so
indicate this on our side by removing the green highlight from panel
frames and tab titles, and showing the selection as inactive like we do
for a side panel when the main view is focused.
moveMainContextToTop copies the current top view's content into the view
it's promoting, to avoid a flicker. The source can be a main view with a
live streaming task (e.g. resolving a conflict promotes the merge-conflicts
view over a main view that's mid-diff), and CopyContent both read and
published that source's buffer unsafely:
- it read the source's lines/viewLines while locking only the
destination, racing the task's concurrent Write; and
- it aliased the source's row slices into the destination, so the
source's ongoing appends (growslice reading the shared array) and
refreshViewLinesIfNeeded's in-place wrapping-cache writes (&lines[i])
kept racing this view's rendering after the copy.
Lock the source for the read, and shallow-clone the row slices so the
destination gets its own arrays. The per-row cell data is immutable once
written, so it stays shared -- the clone cost is proportional to the
number of rows, not their contents.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A view's line buffer, its viewLines/tainted flags, and its hover state
are all written from the command-task goroutine (under writeMutex) as it
renders. But three accessors reached that same state from the UI thread
without the lock: SetView and the GUI-resize path cleared a view's lines
directly, viewsToRedrawContentOnly read the tainted flag, and Buffer read
the line buffer. Each raced a rendering task.
Guard them with writeMutex, matching the view's other buffer accessors.
These are reads/clears of state writeMutex already protects, not new
callers of it -- the view's geometry stays outside the mutex.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A command task streams its output into a view from its own goroutine. To
track soft-wraps (so cursor-positioning escapes from a pager land on the
right line) the write path read the view's live InnerWidth, and the pty
setup read its InnerSize -- both off the UI thread, racing the UI thread
mutating the view's dimensions during layout.
Capture the width on the UI thread instead and hand it to the task: the
escape interpreter keeps a screenColMax it reads from, seeded in NewView
and refreshed per render via View.SetContentWidth (called from
newCmdTask/newPtyTask before the task's goroutine starts), and the pty
size is computed in the after-layout callback rather than in the task's
start func. The view's dimensions stay UI-thread-only; the task uses the
snapshot rather than reading them live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refreshViewLinesIfNeeded re-wrapped every line of the buffer whenever
the view was tainted. That's cheap for short content, but scrolling a
long diff calls it constantly: adjustDownwardScrollAmount queries
ViewLinesHeight on every scroll event, and each newly-read line taints
the view, so every notch re-wrapped the entire buffer. Wrapping measures
each cell's width (uniseg) and allocates per line, so once you'd scrolled
far enough down the diff, scrolling turned sluggish - the cost grew with
how much had been read. (A CPU profile of scrolling deep in a long diff
put 77% of the time in lineWrap, reached almost entirely via
ViewLinesHeight rather than draw.)
Cache each line's wrapped result on the lineType, keyed by the width it
was wrapped at, and only re-wrap lines that have actually changed since
the last refresh. A firstDirtyLine index, updated in the same three
places that set `tainted` (write, clearViewLines' callers, SetHighlight),
marks the lowest line that might have changed; lines below it with a
matching cached width reuse their cached wrapping. The cache lives on the
line, so it's freed with the line when the view's content is replaced
(e.g. selecting a different commit) - it doesn't accumulate across a
session.
The wrapping cost per scroll now scales with the number of lines just
read, not with the total size of the buffer, so scrolling stays smooth
no matter how far down you are.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) instead of emitting them literally. ECH is still a
no-op for us — our buffer is built sequentially and has nothing to
erase — but CUF has to materialize as N visible space cells so the
gap actually appears, otherwise content the child wrote with leading
indentation slides left against the preceding cell.
The view's cursorForward branch reuses the same machinery as tab
expansion: substitute the trigger byte for a space and let the
repeatCount path emit the cells under the parser-tracked SGR. The
existing notifyCellsWritten plumbing then advances screenCol over
the gap, keeping subsequent CUP targets aligned.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConPTY presents its child's output as a screen buffer and uses CUP /
CUD / CNL / VPA to skip over blank rows rather than emitting LFs. The
previous behaviour swallowed all of those and the visible content
collapsed together. Now the escape parser tracks the screen-relative
cursor row, and any CSI that moves the cursor past the current row
emits a cursorDown instruction that the view turns into the matching
number of empty lines.
Column tracking is deliberately omitted: doing it correctly would mean
duplicating the view's grapheme-cluster width math in the parser, and
ConPTY in practice positions to column 1 after a CR-equivalent, which
the existing wx-reset path already handles. ConPTY-internal scrolling
needs no special handling either: it only emits cursor-positioning
escapes within the first, un-scrolled screenful — once its screen
scrolls it switches to plain linefeeds, which the view advances on
directly regardless of the tracked cursor.
Backward cursor moves are silently dropped — the view's buffer is
append-style and can't undo earlier writes. The exception is cursor-home
(CUP to row 1): ConPTY emits it at the start of every screen, so rather
than drop it we re-anchor the row tracking to the current write position.
Without that, a view not rewound in lockstep with ConPTY's screen (the
command log, which streams pty output without a rewind) accumulates
drift, and every later absolute CUP becomes a dropped backward move that
collapses the rows ConPTY positioned with.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tools like delta paint each diff line's background with '\x1b[K' so
the color reaches the right edge. Up to now the '\x1b[K' handler
appended (InnerWidth - cx) explicit padding cells with the fill bg
so rendering picked up the color. That worked for short lines but
silently degraded once content exceeded InnerWidth: the repeat
count went non-positive, no cells were added, and after wrapping
the partial tail segment was left without any cells carrying the
fill color, so draw() fell back to the view's default bg.
Record the fill colors on the source line as optional
trailingFillAttributes. In the '\x1b[K' handler set them (and drop
the padding-cell loop — the metadata covers both the wrap and the
non-wrap cases). In draw(), once per source line, pick the trailing
cell's fg/bg from the metadata if present and otherwise from the
view defaults; then the inner-loop fills past-content cells with
that.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sentinel was appended to every \n-terminated line solely so that
draw()'s prevFgColor tracking would reset to default for the trailing
area; without it, an AttrReverse-styled last cell would carry its
rendered bg past the end of the line.
The same prevFgColor mechanism propagated AttrReverse past content on
*unterminated* lines too — which doesn't match real terminal behavior
(try `print '\x1b[7m\x1b[31mfoo'` in a shell: the reverse stops at
the last character) and isn't relied on by anything in lazygit, since
all our writers terminate lines with \n.
Drop the sentinel cell, drop prevFgColor, and just have draw() paint
trailing cells with the view's default fg/bg. The
TestUnterminatedReverseLineExtendsToEdge regression test inverts to
document the new (terminal-matching) behavior, renamed accordingly.
TestWriteString expectations also drop the trailing "" that came
from the sentinel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cells of a source line will soon need to carry metadata about how
the line was terminated (newline vs filled to edge via \x1b[K). Move
to a struct so there's somewhere to put it; this commit only renames
[][]cell to []line{cells: ...} with no behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four ReplaceAll(str, "\x00", "") calls (and the equivalent
rune-by-rune skip in linesToString) are leftover from when cell.chr
was a rune and \x00 was used as an internal sentinel. With chr now
being a string and no code path writing \x00, the filtering never
strips anything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
I copied all files except dot files (.github and .gitignore), the _examples
folder, and go.mod/go.sum.
At some point we may want to copy the files back to the gocui repo when other
clients (e.g. lazydocker) want to use the newer versions of them.
2026-04-30 14:29:08 +02:00
Renamed from vendor/github.com/jesseduffield/gocui/view.go (Browse further)