From d7daf5f72411f29eca3ab398c7a8a951ca230cad Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 25 Mar 2023 10:23:05 +0900 Subject: [PATCH] =?UTF-8?q?Render=20CR=20and=20LF=20as=20=E2=90=8D=20and?= =?UTF-8?q?=20=E2=90=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close #2529 --- CHANGELOG.md | 12 +++++++- src/terminal.go | 6 ++-- src/tui/light.go | 75 +++++++++++++++++++++++++++++------------------- src/tui/tcell.go | 41 ++++++++++++++++---------- src/util/util.go | 10 +++++-- test/test_go.rb | 6 ++-- 6 files changed, 95 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba10cd1e..fe1bf3e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,17 @@ CHANGELOG # Say hello curl "localhost:$(cat /tmp/fzf-port)" -d 'preview:echo Hello, fzf is listening on $FZF_PORT.' ``` -- Bug fixes +- A carriage return and a line feed character will be rendered as dim ␍ and + ␊ respectively. + ```sh + printf "foo\rbar\nbaz" | fzf --read0 --preview 'echo {}' + ``` +- fzf will stop rendering a non-displayable characters as a space. This will + likely cause less glitches in the preview window. + ```sh + fzf --preview 'head -1000 /dev/random' + ``` +- Bug fixes and improvements 0.38.0 ------ diff --git a/src/terminal.go b/src/terminal.go index dfc21a3b..5196d5cc 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -752,7 +752,7 @@ func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool) // Simpler printer for strings without ANSI colors or tab characters if colors == nil && strings.IndexRune(str, '\t') < 0 { - length := runewidth.StringWidth(str) + length := util.StringWidth(str) if length == 0 { return nil, 0 } @@ -1415,7 +1415,7 @@ func (t *Terminal) printInfo() { pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 str := t.infoSep maxWidth := t.window.Width() - pos - width := runewidth.StringWidth(str) + width := util.StringWidth(str) if width > maxWidth { trimmed, _ := t.trimRight([]rune(str), maxWidth) str = string(trimmed) @@ -1950,7 +1950,7 @@ func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { w = t.tabstop - l%t.tabstop strbuf.WriteString(strings.Repeat(" ", w)) } else { - w = runewidth.StringWidth(str) + w = util.StringWidth(str) strbuf.WriteString(str) } l += w diff --git a/src/tui/light.go b/src/tui/light.go index 322584dd..411238d4 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -32,20 +32,26 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9] var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") func (r *LightRenderer) stderr(str string) { - r.stderrInternal(str, true) + r.stderrInternal(str, true, "") } -// FIXME: Need better handling of non-displayable characters -func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { +const CR string = "\x1b[2m␍" +const LF string = "\x1b[2m␊" + +func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) { bytes := []byte(str) runes := []rune{} for len(bytes) > 0 { r, sz := utf8.DecodeRune(bytes) nlcr := r == '\n' || r == '\r' if r >= 32 || r == '\x1b' || nlcr { - if r == utf8.RuneError || nlcr && !allowNLCR { - runes = append(runes, ' ') - } else { + if nlcr && !allowNLCR { + if r == '\r' { + runes = append(runes, []rune(CR+resetCode)...) + } else { + runes = append(runes, []rune(LF+resetCode)...) + } + } else if r != utf8.RuneError { runes = append(runes, r) } } @@ -54,8 +60,10 @@ func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { r.queued.WriteString(string(runes)) } -func (r *LightRenderer) csi(code string) { - r.stderr("\x1b[" + code) +func (r *LightRenderer) csi(code string) string { + fullcode := "\x1b[" + code + r.stderr(fullcode) + return fullcode } func (r *LightRenderer) flush() { @@ -825,12 +833,12 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) { w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, (w.width-bcw)/hw)+repeat(' ', rem)+string(w.border.bottomRight)) } -func (w *LightWindow) csi(code string) { - w.renderer.csi(code) +func (w *LightWindow) csi(code string) string { + return w.renderer.csi(code) } -func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { - w.renderer.stderrInternal(str, allowNLCR) +func (w *LightWindow) stderrInternal(str string, allowNLCR bool, resetCode string) { + w.renderer.stderrInternal(str, allowNLCR, resetCode) } func (w *LightWindow) Top() int { @@ -936,10 +944,10 @@ func colorCodes(fg Color, bg Color) []string { return codes } -func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool { +func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) { codes := append(attrCodes(attr), colorCodes(fg, bg)...) - w.csi(";" + strings.Join(codes, ";") + "m") - return len(codes) > 0 + code := w.csi(";" + strings.Join(codes, ";") + "m") + return len(codes) > 0, code } func (w *LightWindow) Print(text string) { @@ -951,16 +959,17 @@ func cleanse(str string) string { } func (w *LightWindow) CPrint(pair ColorPair, text string) { - w.csiColor(pair.Fg(), pair.Bg(), pair.Attr()) - w.stderrInternal(cleanse(text), false) + _, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr()) + w.stderrInternal(cleanse(text), false, code) w.csi("m") } func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { - if w.csiColor(fg, bg, attr) { + hasColors, code := w.csiColor(fg, bg, attr) + if hasColors { defer w.csi("m") } - w.stderrInternal(cleanse(text), false) + w.stderrInternal(cleanse(text), false, code) } type wrappedLine struct { @@ -980,6 +989,8 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin if len(rs) == 1 && rs[0] == '\t' { w = tabstop - (prefixLength+width)%tabstop str = repeat(' ', w) + } else if rs[0] == '\r' { + w++ } else { w = runewidth.StringWidth(str) } @@ -998,12 +1009,12 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin return lines } -func (w *LightWindow) fill(str string, onMove func()) FillReturn { +func (w *LightWindow) fill(str string, resetCode string) FillReturn { allLines := strings.Split(str, "\n") for i, line := range allLines { lines := wrapLine(line, w.posx, w.width, w.tabstop) for j, wl := range lines { - w.stderrInternal(wl.text, false) + w.stderrInternal(wl.text, false, resetCode) w.posx += wl.displayWidth // Wrap line @@ -1013,7 +1024,7 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn { } w.MoveAndClear(w.posy, w.posx) w.Move(w.posy+1, 0) - onMove() + w.renderer.stderr(resetCode) } } } @@ -1022,22 +1033,26 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn { return FillSuspend } w.Move(w.posy+1, 0) - onMove() + w.renderer.stderr(resetCode) return FillNextLine } return FillContinue } -func (w *LightWindow) setBg() { +func (w *LightWindow) setBg() string { if w.bg != colDefault { - w.csiColor(colDefault, w.bg, AttrRegular) + _, code := w.csiColor(colDefault, w.bg, AttrRegular) + return code } + // Should clear dim attribute after ␍ in the preview window + // e.g. printf "foo\rbar" | fzf --ansi --preview 'printf "foo\rbar"' + return "\x1b[m" } func (w *LightWindow) Fill(text string) FillReturn { w.Move(w.posy, w.posx) - w.setBg() - return w.fill(text, w.setBg) + code := w.setBg() + return w.fill(text, code) } func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { @@ -1048,11 +1063,11 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu if bg == colDefault { bg = w.bg } - if w.csiColor(fg, bg, attr) { + if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors { defer w.csi("m") - return w.fill(text, func() { w.csiColor(fg, bg, attr) }) + return w.fill(text, resetCode) } - return w.fill(text, w.setBg) + return w.fill(text, w.setBg()) } func (w *LightWindow) FinishFill() { diff --git a/src/tui/tcell.go b/src/tui/tcell.go index ad0182cf..b2cab0fb 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -8,6 +8,7 @@ import ( "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/encoding" + "github.com/junegunn/fzf/src/util" "github.com/mattn/go-runewidth" "github.com/rivo/uniseg" @@ -572,26 +573,27 @@ func (w *TcellWindow) printString(text string, pair ColorPair) { gr := uniseg.NewGraphemes(text) for gr.Next() { + st := style rs := gr.Runes() if len(rs) == 1 { r := rs[0] - if r < rune(' ') { // ignore control characters - continue + if r == '\r' { + st = style.Dim(true) + rs[0] = '␍' } else if r == '\n' { - w.lastY++ - lx = 0 - continue - } else if r == '\u000D' { // skip carriage return + st = style.Dim(true) + rs[0] = '␊' + } else if r < rune(' ') { // ignore control characters continue } } var xPos = w.left + w.lastX + lx var yPos = w.top + w.lastY if xPos < (w.left+w.width) && yPos < (w.top+w.height) { - _screen.SetContent(xPos, yPos, rs[0], rs[1:], style) + _screen.SetContent(xPos, yPos, rs[0], rs[1:], st) } - lx += runewidth.StringWidth(string(rs)) + lx += util.StringWidth(string(rs)) } w.lastX += lx } @@ -620,13 +622,22 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn { Italic(a&Attr(tcell.AttrItalic) != 0) gr := uniseg.NewGraphemes(text) +Loop: for gr.Next() { + st := style rs := gr.Runes() - if len(rs) == 1 && rs[0] == '\n' { - w.lastY++ - w.lastX = 0 - lx = 0 - continue + if len(rs) == 1 { + r := rs[0] + switch r { + case '\r': + st = style.Dim(true) + rs[0] = '␍' + case '\n': + w.lastY++ + w.lastX = 0 + lx = 0 + continue Loop + } } // word wrap: @@ -643,8 +654,8 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn { return FillSuspend } - _screen.SetContent(xPos, yPos, rs[0], rs[1:], style) - lx += runewidth.StringWidth(string(rs)) + _screen.SetContent(xPos, yPos, rs[0], rs[1:], st) + lx += util.StringWidth(string(rs)) } w.lastX += lx if w.lastX == w.width { diff --git a/src/util/util.go b/src/util/util.go index cb211cbb..73f53daf 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -11,6 +11,11 @@ import ( "github.com/rivo/uniseg" ) +// StringWidth returns string width where each CR/LF character takes 1 column +func StringWidth(s string) int { + return runewidth.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r") +} + // RunesWidth returns runes width func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) { width := 0 @@ -22,8 +27,7 @@ func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int if len(rs) == 1 && rs[0] == '\t' { w = tabstop - (prefixWidth+width)%tabstop } else { - s := string(rs) - w = runewidth.StringWidth(s) + strings.Count(s, "\n") + w = StringWidth(string(rs)) } width += w if width > limit { @@ -41,7 +45,7 @@ func Truncate(input string, limit int) ([]rune, int) { gr := uniseg.NewGraphemes(input) for gr.Next() { rs := gr.Runes() - w := runewidth.StringWidth(string(rs)) + w := StringWidth(string(rs)) if width+w > limit { return runes, width } diff --git a/test/test_go.rb b/test/test_go.rb index d5996113..8da6cb5e 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1930,7 +1930,7 @@ class TestGoFZF < TestBase def test_keep_right tmux.send_keys "seq 10000 | #{FZF} --read0 --keep-right", :Enter - tmux.until { |lines| assert lines.any_include?('9999 10000') } + tmux.until { |lines| assert lines.any_include?('9999␊10000') } end def test_backward_eof @@ -2807,9 +2807,9 @@ module TestShell tmux.send_keys 'C-r' tmux.until { |lines| assert_equal '>', lines[-1] } tmux.send_keys 'foo bar' - tmux.until { |lines| assert lines[-3]&.end_with?('bar"') } + tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) } tmux.send_keys :Enter - tmux.until { |lines| assert lines[-1]&.end_with?('bar"') } + tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) } tmux.send_keys :Enter tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] } end