mirror of
https://github.com/junegunn/fzf.git
synced 2026-07-19 14:40:36 +08:00
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:
+45
-21
@@ -39,6 +39,7 @@ func (r *LightRenderer) Bell() {
|
||||
|
||||
func (r *LightRenderer) PassThrough(str string) {
|
||||
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
||||
r.invalidateSGR()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) stderr(str string) {
|
||||
@@ -90,8 +91,28 @@ func (r *LightRenderer) csi(code string) string {
|
||||
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() {
|
||||
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
|
||||
// terminal applies it atomically. Terminals without support ignore
|
||||
// the unknown private mode and behave as before.
|
||||
@@ -130,6 +151,7 @@ type LightRenderer struct {
|
||||
fullscreen bool
|
||||
upOneLine bool
|
||||
queued strings.Builder
|
||||
sgr string
|
||||
y int
|
||||
x int
|
||||
maxHeightFunc func(int) int
|
||||
@@ -1002,6 +1024,9 @@ func (r *LightRenderer) disableModes() {
|
||||
|
||||
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
r.setupTerminal()
|
||||
// The programs that ran in the meantime may have left the terminal in an
|
||||
// arbitrary SGR state
|
||||
r.invalidateSGR()
|
||||
if clear {
|
||||
if r.fullscreen {
|
||||
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) {
|
||||
w.renderer.stderrInternal(str, allowNLCR, resetCode)
|
||||
}
|
||||
@@ -1415,13 +1436,18 @@ func ulColorCode(c Color) string {
|
||||
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) {
|
||||
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
||||
if ulCode := ulColorCode(ul); ulCode != "" {
|
||||
codes = append(codes, ulCode)
|
||||
}
|
||||
code := w.csi(";" + strings.Join(codes, ";") + "m")
|
||||
return len(codes) > 0, code
|
||||
if len(codes) == 0 {
|
||||
return false, "\x1b[0m"
|
||||
}
|
||||
return true, "\x1b[;" + strings.Join(codes, ";") + "m"
|
||||
}
|
||||
|
||||
func (w *LightWindow) Print(text string) {
|
||||
@@ -1434,15 +1460,13 @@ func cleanse(str string) string {
|
||||
|
||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr())
|
||||
w.renderer.setSGR(code)
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
w.csi("0m")
|
||||
}
|
||||
|
||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
hasColors, code := w.csiColor(fg, bg, colDefault, attr)
|
||||
if hasColors {
|
||||
defer w.csi("0m")
|
||||
}
|
||||
_, code := w.csiColor(fg, bg, colDefault, attr)
|
||||
w.renderer.setSGR(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.Move(w.posy+1, 0)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.renderer.setSGR(resetCode)
|
||||
if len(lines) > 1 {
|
||||
sign := w.wrapSign
|
||||
width := w.wrapSignWidth
|
||||
@@ -1473,7 +1497,8 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
width = truncatedWidth
|
||||
}
|
||||
w.stderrInternal(DIM+sign, false, resetCode)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.renderer.invalidateSGR()
|
||||
w.renderer.setSGR(resetCode)
|
||||
w.Move(w.posy, width)
|
||||
}
|
||||
}
|
||||
@@ -1484,20 +1509,19 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
return FillSuspend
|
||||
}
|
||||
w.Move(w.posy+1, 0)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.renderer.setSGR(resetCode)
|
||||
return FillNextLine
|
||||
}
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
func (w *LightWindow) setBg() string {
|
||||
if w.bg != colDefault {
|
||||
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
|
||||
return code
|
||||
}
|
||||
// Should clear dim attribute after ␍ in the preview window
|
||||
// The plain reset code for the default background is still required to
|
||||
// clear the dim attribute after ␍ in the preview window
|
||||
// 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) {
|
||||
@@ -1523,7 +1547,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, ul Color, attr Attr, text string
|
||||
bg = w.bg
|
||||
}
|
||||
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, w.setBg())
|
||||
|
||||
@@ -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[<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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user