Skip redundant SGR sequences in light renderer

Track the last emitted SGR sequence and skip no-op transitions.
Reduces rendering output by 10-23% depending on the scenario.
This commit is contained in:
Junegunn Choi
2026-07-17 10:34:42 +09:00
parent 374b01242d
commit 3c0de84dab
2 changed files with 103 additions and 21 deletions
+45 -21
View File
@@ -39,6 +39,7 @@ func (r *LightRenderer) Bell() {
func (r *LightRenderer) PassThrough(str string) { func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8") r.queued.WriteString("\x1b7" + str + "\x1b8")
r.invalidateSGR()
} }
func (r *LightRenderer) stderr(str string) { func (r *LightRenderer) stderr(str string) {
@@ -90,8 +91,28 @@ func (r *LightRenderer) csi(code string) string {
return fullcode return fullcode
} }
// setSGR emits the given SGR sequence only when the terminal is not already
// in that state. Sequences built by csiColor start with a reset parameter,
// so each of them fully determines the state on its own. The zero value of
// r.sgr never matches a sequence, so the first update after Init, Resume, or
// invalidateSGR is always emitted.
func (r *LightRenderer) setSGR(code string) {
if code != r.sgr {
r.stderr(code)
r.sgr = code
}
}
// invalidateSGR marks the terminal SGR state as unknown, e.g. after raw
// output that may have changed it behind the renderer's back.
func (r *LightRenderer) invalidateSGR() {
r.sgr = ""
}
func (r *LightRenderer) flush() { func (r *LightRenderer) flush() {
if r.queued.Len() > 0 { if r.queued.Len() > 0 {
// Leave the terminal in the default SGR state between frames
r.setSGR("\x1b[0m")
// Wrap the frame in synchronized update mode (2026) so that the // Wrap the frame in synchronized update mode (2026) so that the
// terminal applies it atomically. Terminals without support ignore // terminal applies it atomically. Terminals without support ignore
// the unknown private mode and behave as before. // the unknown private mode and behave as before.
@@ -130,6 +151,7 @@ type LightRenderer struct {
fullscreen bool fullscreen bool
upOneLine bool upOneLine bool
queued strings.Builder queued strings.Builder
sgr string
y int y int
x int x int
maxHeightFunc func(int) int maxHeightFunc func(int) int
@@ -1002,6 +1024,9 @@ func (r *LightRenderer) disableModes() {
func (r *LightRenderer) Resume(clear bool, sigcont bool) { func (r *LightRenderer) Resume(clear bool, sigcont bool) {
r.setupTerminal() r.setupTerminal()
// The programs that ran in the meantime may have left the terminal in an
// arbitrary SGR state
r.invalidateSGR()
if clear { if clear {
if r.fullscreen { if r.fullscreen {
r.smcup() r.smcup()
@@ -1273,10 +1298,6 @@ func (w *LightWindow) drawBorder(onlyHorizontal bool) {
} }
} }
func (w *LightWindow) csi(code string) string {
return w.renderer.csi(code)
}
func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) { func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) {
w.renderer.stderrInternal(str, allowNLCR, resetCode) w.renderer.stderrInternal(str, allowNLCR, resetCode)
} }
@@ -1415,13 +1436,18 @@ func ulColorCode(c Color) string {
return "" return ""
} }
// csiColor builds the SGR sequence for the given colors and attributes
// without emitting it. The sequence starts with a reset parameter, so it
// fully determines the SGR state on its own.
func (w *LightWindow) csiColor(fg Color, bg Color, ul Color, attr Attr) (bool, string) { func (w *LightWindow) csiColor(fg Color, bg Color, ul Color, attr Attr) (bool, string) {
codes := append(attrCodes(attr), colorCodes(fg, bg)...) codes := append(attrCodes(attr), colorCodes(fg, bg)...)
if ulCode := ulColorCode(ul); ulCode != "" { if ulCode := ulColorCode(ul); ulCode != "" {
codes = append(codes, ulCode) codes = append(codes, ulCode)
} }
code := w.csi(";" + strings.Join(codes, ";") + "m") if len(codes) == 0 {
return len(codes) > 0, code return false, "\x1b[0m"
}
return true, "\x1b[;" + strings.Join(codes, ";") + "m"
} }
func (w *LightWindow) Print(text string) { func (w *LightWindow) Print(text string) {
@@ -1434,15 +1460,13 @@ func cleanse(str string) string {
func (w *LightWindow) CPrint(pair ColorPair, text string) { func (w *LightWindow) CPrint(pair ColorPair, text string) {
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr()) _, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr())
w.renderer.setSGR(code)
w.stderrInternal(cleanse(text), false, code) w.stderrInternal(cleanse(text), false, code)
w.csi("0m")
} }
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
hasColors, code := w.csiColor(fg, bg, colDefault, attr) _, code := w.csiColor(fg, bg, colDefault, attr)
if hasColors { w.renderer.setSGR(code)
defer w.csi("0m")
}
w.stderrInternal(cleanse(text), false, code) w.stderrInternal(cleanse(text), false, code)
} }
@@ -1463,7 +1487,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
} }
w.MoveAndClear(w.posy, w.posx) w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
w.renderer.stderr(resetCode) w.renderer.setSGR(resetCode)
if len(lines) > 1 { if len(lines) > 1 {
sign := w.wrapSign sign := w.wrapSign
width := w.wrapSignWidth width := w.wrapSignWidth
@@ -1473,7 +1497,8 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
width = truncatedWidth width = truncatedWidth
} }
w.stderrInternal(DIM+sign, false, resetCode) w.stderrInternal(DIM+sign, false, resetCode)
w.renderer.stderr(resetCode) w.renderer.invalidateSGR()
w.renderer.setSGR(resetCode)
w.Move(w.posy, width) w.Move(w.posy, width)
} }
} }
@@ -1484,20 +1509,19 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
return FillSuspend return FillSuspend
} }
w.Move(w.posy+1, 0) w.Move(w.posy+1, 0)
w.renderer.stderr(resetCode) w.renderer.setSGR(resetCode)
return FillNextLine return FillNextLine
} }
return FillContinue return FillContinue
} }
func (w *LightWindow) setBg() string { func (w *LightWindow) setBg() string {
if w.bg != colDefault { // The plain reset code for the default background is still required to
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular) // clear the dim attribute after ␍ in the preview window
return code
}
// Should clear dim attribute after ␍ in the preview window
// e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"' // e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"'
return "\x1b[m" _, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
w.renderer.setSGR(code)
return code
} }
func (w *LightWindow) LinkBegin(uri string, params string) { func (w *LightWindow) LinkBegin(uri string, params string) {
@@ -1523,7 +1547,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, ul Color, attr Attr, text string
bg = w.bg bg = w.bg
} }
if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors { if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors {
defer w.csi("0m") w.renderer.setSGR(resetCode)
return w.fill(text, resetCode) return w.fill(text, resetCode)
} }
return w.fill(text, w.setBg()) return w.fill(text, w.setBg())
+58
View File
@@ -419,3 +419,61 @@ func TestLightRendererScrollWheel(t *testing.T) {
assertScroll("\x1b[<94;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + left (ignored) assertScroll("\x1b[<94;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + left (ignored)
assertScroll("\x1b[<95;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + right (ignored) assertScroll("\x1b[<95;1;1M", 0, "ctrl-alt-shift") // ctrl+alt+shift + right (ignored)
} }
// Assert the exact byte stream emitted for a scripted rendering sequence,
// locking down the SGR deduplication behavior of the light renderer.
func TestLightRendererSGRDeduplication(t *testing.T) {
renderer, _ := NewLightRenderer(
"", nil, &ColorTheme{}, true, false, 8, false, true,
func(h int) int { return h })
r := renderer.(*LightRenderer)
r.width = 80
r.height = 24
out, err := os.CreateTemp(t.TempDir(), "fzf-ttyout")
if err != nil {
t.Fatal(err)
}
r.ttyout = out
w := r.NewWindow(0, 0, 40, 4, WindowList, MakeBorderStyle(BorderNone, true), false).(*LightWindow)
w.fg = colDefault
w.bg = colDefault
r.queued.Reset()
red := NewColorPair(196, colDefault, AttrRegular)
blue := NewColorPair(21, colDefault, AttrRegular)
w.Move(0, 0)
w.CPrint(red, "ab")
w.CPrint(red, "cd") // same pair; no SGR expected
w.CPrint(blue, "ef")
w.Print("gh") // default colors; one reset expected
w.Print("ij") // still default; no SGR expected
w.Move(1, 2)
w.CPrint(red, "kl")
r.invalidateSGR()
w.CPrint(red, "mn") // re-emitted after invalidation
r.flush() // adds a trailing reset to leave the default state
expected := "\x1b[?2026h\x1b[?7l\x1b[?25l" +
"\r" +
"\x1b[;38;5;196mab" +
"cd" +
"\x1b[;38;5;21mef" +
"\x1b[0mgh" +
"ij" +
"\x1b[1B\x1b[3G" +
"\x1b[;38;5;196mkl" +
"\x1b[;38;5;196mmn" +
"\x1b[0m" +
"\x1b[?25h\x1b[?7h\x1b[?2026l"
bytes, err := os.ReadFile(out.Name())
if err != nil {
t.Fatal(err)
}
if string(bytes) != expected {
t.Errorf("expected: %q\n got: %q", expected, string(bytes))
}
}