Fix panic caused by incorrect update ordering

Fix #4442

Make sure to prepare windows before rendering elements.

Thanks to @nugged for the report.
This commit is contained in:
Junegunn Choi
2025-07-02 21:28:11 +09:00
parent ce95adc66c
commit d34675d3c9
2 changed files with 48 additions and 33 deletions

View File

@@ -450,31 +450,35 @@ func (a byTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at) return a[i].at.Before(a[j].at)
} }
// EventTypes are listed in the order of their priority.
const ( const (
reqPrompt util.EventType = iota reqResize util.EventType = iota
reqReinit
reqFullRedraw
reqRedraw
reqJump
reqPrompt
reqInfo reqInfo
reqHeader reqHeader
reqFooter reqFooter
reqList reqList
reqJump
reqActivate
reqReinit
reqFullRedraw
reqResize
reqRedraw
reqRedrawInputLabel reqRedrawInputLabel
reqRedrawHeaderLabel reqRedrawHeaderLabel
reqRedrawFooterLabel reqRedrawFooterLabel
reqRedrawListLabel reqRedrawListLabel
reqRedrawBorderLabel reqRedrawBorderLabel
reqRedrawPreviewLabel reqRedrawPreviewLabel
reqClose
reqPrintQuery
reqPreviewReady reqPreviewReady
reqPreviewEnqueue reqPreviewEnqueue
reqPreviewDisplay reqPreviewDisplay
reqPreviewRefresh reqPreviewRefresh
reqPreviewDelayed reqPreviewDelayed
reqActivate
reqClose
reqPrintQuery
reqBecome reqBecome
reqQuit reqQuit
reqFatal reqFatal
@@ -1624,14 +1628,12 @@ func (t *Terminal) changeHeader(header string) bool {
return needFullRedraw return needFullRedraw
} }
func (t *Terminal) changeFooter(footer string) bool { func (t *Terminal) changeFooter(footer string) {
var lines []string var lines []string
if len(footer) > 0 { if len(footer) > 0 {
lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n") lines = strings.Split(strings.TrimSuffix(footer, "\n"), "\n")
} }
needFullRedraw := len(t.footer) != len(lines)
t.footer = lines t.footer = lines
return needFullRedraw
} }
// UpdateHeader updates the header // UpdateHeader updates the header
@@ -2889,6 +2891,13 @@ func (t *Terminal) resizeIfNeeded() bool {
return true return true
} }
// Check footer window
if len(t.footer) > 0 && (t.footerWindow == nil || t.footerWindow.Height() != len(t.footer)) ||
len(t.footer) == 0 && t.footerWindow != nil {
t.printAll()
return true
}
// Check if the header borders are used and header has changed // Check if the header borders are used and header has changed
allHeaderLines := t.visibleHeaderLines() allHeaderLines := t.visibleHeaderLines()
primaryHeaderLines := allHeaderLines primaryHeaderLines := allHeaderLines
@@ -5116,6 +5125,8 @@ func (t *Terminal) Loop() error {
t.uiMutex.Lock() t.uiMutex.Lock()
t.mutex.Lock() t.mutex.Lock()
info := false info := false
header := false
footer := false
for _, key := range keys { for _, key := range keys {
req := util.EventType(key) req := util.EventType(key)
value := (*events)[req] value := (*events)[req]
@@ -5153,13 +5164,9 @@ func (t *Terminal) Loop() error {
} }
t.printList() t.printList()
case reqHeader: case reqHeader:
if !t.resizeIfNeeded() { header = true
t.printHeader()
}
case reqFooter: case reqFooter:
if !t.resizeIfNeeded() { footer = true
t.printFooter()
}
case reqActivate: case reqActivate:
t.suppress = false t.suppress = false
if t.hasPreviewer() { if t.hasPreviewer() {
@@ -5243,8 +5250,16 @@ func (t *Terminal) Loop() error {
return return
} }
} }
if info && !t.resizeIfNeeded() { if (info || header || footer) && !t.resizeIfNeeded() {
t.printInfo() if info {
t.printInfo()
}
if header {
t.printHeader()
}
if footer {
t.printFooter()
}
} }
t.flush() t.flush()
t.mutex.Unlock() t.mutex.Unlock()
@@ -5670,24 +5685,17 @@ func (t *Terminal) Loop() error {
t.cx = len(t.input) t.cx = len(t.input)
case actChangeHeader, actTransformHeader, actBgTransformHeader: case actChangeHeader, actTransformHeader, actBgTransformHeader:
capture(false, func(header string) { capture(false, func(header string) {
// When a dedicated header window is not used, we may need to
// update other elements as well.
if t.changeHeader(header) { if t.changeHeader(header) {
if t.headerWindow != nil { req(reqList, reqPrompt, reqInfo)
// Need to resize header window
req(reqRedraw)
} else {
req(reqHeader, reqList, reqPrompt, reqInfo)
}
} else {
req(reqHeader)
} }
req(reqHeader)
}) })
case actChangeFooter, actTransformFooter, actBgTransformFooter: case actChangeFooter, actTransformFooter, actBgTransformFooter:
capture(false, func(footer string) { capture(false, func(footer string) {
if t.changeFooter(footer) { t.changeFooter(footer)
req(reqRedraw) req(reqFooter)
} else {
req(reqFooter)
}
}) })
case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel: case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel:
capture(true, func(label string) { capture(true, func(label string) {

View File

@@ -1981,4 +1981,11 @@ class TestCore < TestInteractive
refute lines.any_include?('[1]') refute lines.any_include?('[1]')
end end
end end
def test_render_order
tmux.send_keys %(seq 100 | #{FZF} --bind='focus:preview(echo boom)+change-footer(bam)'), :Enter
tmux.until { assert_equal 100, it.match_count }
tmux.until { assert it.any_include?('boom') }
tmux.until { assert it.any_include?('bam') }
end
end end