From 70b80d13750151abaa3e7235a311c9b4abeeffd6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 4 Jun 2026 10:41:04 +0200 Subject: [PATCH] Lock the view and guard the line index when reading a hyperlink HyperLinkInLine read v.lines/v.viewLines without holding writeMutex, so it could race a concurrent re-render rebuilding the buffer. It also indexed v.lines by viewLines[y].linesY after only checking y against len(viewLines); since refreshViewLinesIfNeeded overwrites viewLines in place without truncating, the tail can hold stale entries pointing past a shrunk v.lines, giving an out-of-range panic while a shorter diff is still loading. Take writeMutex (as the sibling view methods do) and bounds-check linesY against len(v.lines), returning "no link" rather than panicking. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/gocui/view.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index a84e0785e..10ed5672d 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -1654,11 +1654,26 @@ func (v *View) Word(x, y int) (string, bool) { } func (v *View) HyperLinkInLine(y int, urlScheme string) (string, bool) { + // Take the lock so we don't race a concurrent re-render that is rebuilding the + // buffer. + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + v.refreshViewLinesIfNeeded() + if y < 0 || y >= len(v.viewLines) { return "", false } - for _, c := range v.lines[v.viewLines[y].linesY].cells { + // refreshViewLinesIfNeeded overwrites viewLines in place without truncating, + // so while a shorter re-render is loading, the tail of viewLines can still + // hold stale entries pointing past the (shrunk) v.lines. Guard against that. + linesY := v.viewLines[y].linesY + if linesY >= len(v.lines) { + return "", false + } + + for _, c := range v.lines[linesY].cells { if strings.HasPrefix(c.hyperlink, urlScheme) { return c.hyperlink, true }