aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/parse/ansi.go402
-rw-r--r--lib/parse/ansi_test.go262
-rw-r--r--lib/ui/context.go19
-rw-r--r--lib/ui/table.go41
-rw-r--r--widgets/dirlist.go23
5 files changed, 714 insertions, 33 deletions
diff --git a/lib/parse/ansi.go b/lib/parse/ansi.go
index e1d603ea..a9a46fdd 100644
--- a/lib/parse/ansi.go
+++ b/lib/parse/ansi.go
@@ -3,15 +3,21 @@ package parse
import (
"bufio"
"bytes"
+ "errors"
"fmt"
"io"
"os"
"regexp"
+ "strconv"
+ "strings"
"git.sr.ht/~rjarry/aerc/log"
+ "github.com/gdamore/tcell/v2"
+ "github.com/gdamore/tcell/v2/terminfo"
+ "github.com/mattn/go-runewidth"
)
-var ansi = regexp.MustCompile("\x1B\\[[0-?]*[ -/]*[@-~]")
+var AnsiReg = regexp.MustCompile("\x1B\\[[0-?]*[ -/]*[@-~]")
// StripAnsi strips ansi escape codes from the reader
func StripAnsi(r io.Reader) io.Reader {
@@ -20,7 +26,7 @@ func StripAnsi(r io.Reader) io.Reader {
scanner.Buffer(nil, 1024*1024*1024)
for scanner.Scan() {
line := scanner.Bytes()
- line = ansi.ReplaceAll(line, []byte(""))
+ line = AnsiReg.ReplaceAll(line, []byte(""))
_, err := buf.Write(line)
if err != nil {
log.Warnf("failed write ", err)
@@ -35,3 +41,395 @@ func StripAnsi(r io.Reader) io.Reader {
}
return buf
}
+
+// StyledRune is a rune and it's associated style. The rune has already been
+// measured using go-runewidth
+type StyledRune struct {
+ Value rune
+ Width int
+ Style tcell.Style
+}
+
+// RuneBuffer is a buffer of runes styled with tcell.Style objects
+type RuneBuffer struct {
+ buf []*StyledRune
+}
+
+// Returns the internal slice of styled runes
+func (rb *RuneBuffer) Runes() []*StyledRune {
+ return rb.buf
+}
+
+// Write writes a rune and it's associated style to the RuneBuffer
+func (rb *RuneBuffer) Write(r rune, style tcell.Style) {
+ w := runewidth.RuneWidth(r)
+ rb.buf = append(rb.buf, &StyledRune{r, w, style})
+}
+
+// Prepend inserts the rune at the beginning of the rune buffer
+func (rb *RuneBuffer) Prepend(r rune, style tcell.Style) {
+ w := runewidth.RuneWidth(r)
+ rb.buf = append([]*StyledRune{{r, w, style}}, rb.buf...)
+}
+
+// String outputs a styled-string using TERM=xterm-256color
+func (rb *RuneBuffer) String() string {
+ return rb.string(0, false, 0)
+}
+
+// string returns a string no longer than n runes. If 'left' is true, the left
+// side of the text is truncated. Pass 0 to return the full string
+func (rb *RuneBuffer) string(n int, left bool, char rune) string {
+ // Use xterm-256color to generate the string. Ultimately all output will
+ // be re-parsed as 'xterm-256color' and tcell will handle the final
+ // output sequences based on the user's TERM
+ ti, err := terminfo.LookupTerminfo("xterm-256color")
+ if err != nil {
+ // Who knows what happened
+ return ""
+ }
+ var (
+ s = strings.Builder{}
+ style = tcell.StyleDefault
+ hasStyle = false
+ // w will track the length we have written, or would have
+ // written in the case of left truncate
+ w = 0
+ offset = 0
+ )
+
+ if left {
+ offset = rb.Len() - n
+ }
+
+ for _, r := range rb.buf {
+ if style != r.Style {
+ hasStyle = true
+ style = r.Style
+ s.WriteString(ti.AttrOff)
+ fg, bg, attrs := style.Decompose()
+
+ switch {
+ case fg.IsRGB() && bg.IsRGB() && ti.SetFgBgRGB != "":
+ fr, fg, fb := fg.RGB()
+ br, bg, bb := bg.RGB()
+ s.WriteString(ti.TParm(
+ ti.SetFgBgRGB,
+ int(fr),
+ int(fg),
+ int(fb),
+ int(br),
+ int(bg),
+ int(bb),
+ ))
+ case fg.IsRGB() && ti.SetFgRGB != "":
+ // RGB
+ r, g, b := fg.RGB()
+ s.WriteString(ti.TParm(ti.SetFgRGB, int(r), int(g), int(b)))
+ case bg.IsRGB() && ti.SetBgRGB != "":
+ // RGB
+ r, g, b := bg.RGB()
+ s.WriteString(ti.TParm(ti.SetBgRGB, int(r), int(g), int(b)))
+
+ // Indexed
+ case fg.Valid() && bg.Valid() && ti.SetFgBg != "":
+ s.WriteString(ti.TParm(ti.SetFgBg, int(fg&0xff), int(bg&0xff)))
+ case fg.Valid() && ti.SetFg != "":
+ s.WriteString(ti.TParm(ti.SetFg, int(fg&0xff)))
+ case bg.Valid() && ti.SetBg != "":
+ s.WriteString(ti.TParm(ti.SetBg, int(bg&0xff)))
+ }
+
+ if attrs&tcell.AttrBold != 0 {
+ s.WriteString(ti.Bold)
+ }
+ if attrs&tcell.AttrUnderline != 0 {
+ s.WriteString(ti.Underline)
+ }
+ if attrs&tcell.AttrReverse != 0 {
+ s.WriteString(ti.Reverse)
+ }
+ if attrs&tcell.AttrBlink != 0 {
+ s.WriteString(ti.Blink)
+ }
+ if attrs&tcell.AttrDim != 0 {
+ s.WriteString(ti.Dim)
+ }
+ if attrs&tcell.AttrItalic != 0 {
+ s.WriteString(ti.Italic)
+ }
+ if attrs&tcell.AttrStrikeThrough != 0 {
+ s.WriteString(ti.StrikeThrough)
+ }
+ }
+
+ w += r.Width
+ if left && w <= offset {
+ if w == offset && char != 0 {
+ s.WriteRune(char)
+ }
+ continue
+ }
+ s.WriteRune(r.Value)
+ if n != 0 && !left && w == n {
+ if char != 0 {
+ s.WriteRune(char)
+ }
+ break
+ }
+ }
+ if hasStyle {
+ s.WriteString(ti.AttrOff)
+ }
+ return s.String()
+}
+
+// Len is the length of the string, without ansi sequences
+func (rb *RuneBuffer) Len() int {
+ l := 0
+ for _, r := range rb.buf {
+ l += r.Width
+ }
+ return l
+}
+
+// Truncates to a width of n, optionally append a character to the string.
+// Appending via Truncate allows the character to retain the same style as the
+// string at the truncated location
+func (rb *RuneBuffer) Truncate(n int, char rune) string {
+ return rb.string(n, false, char)
+}
+
+// Truncates a width of n off the beginning of the string, optionally append a
+// character to the string. Appending via Truncate allows the character to
+// retain the same style as the string at the truncated location
+func (rb *RuneBuffer) TruncateHead(n int, char rune) string {
+ return rb.string(n, true, char)
+}
+
+// Applies a style to the buffer. Any currently applied styles will not be
+// overwritten
+func (rb *RuneBuffer) ApplyStyle(style tcell.Style) {
+ for _, sr := range rb.buf {
+ if sr.Style == tcell.StyleDefault {
+ sr.Style = style
+ }
+ }
+}
+
+// ApplyAttrs applies the style, and if another style is present ORs the
+// attributes
+func (rb *RuneBuffer) ApplyAttrs(style tcell.Style) {
+ for _, sr := range rb.buf {
+ if sr.Style == tcell.StyleDefault {
+ sr.Style = style
+ continue
+ }
+ _, _, srAttrs := sr.Style.Decompose()
+ _, _, attrs := style.Decompose()
+ sr.Style = sr.Style.Attributes(srAttrs | attrs)
+ }
+}
+
+// Applies a style to a string. Any currently applied styles will not be overwritten
+func ApplyStyle(style tcell.Style, str string) string {
+ rb := ParseANSI(str)
+ for _, sr := range rb.buf {
+ if sr.Style == tcell.StyleDefault {
+ sr.Style = style
+ }
+ }
+ return rb.String()
+}
+
+// Parses a styled string into a RuneBuffer
+func ParseANSI(s string) *RuneBuffer {
+ p := &parser{
+ buf: &RuneBuffer{},
+ curStyle: tcell.StyleDefault,
+ }
+ rdr := strings.NewReader(s)
+
+ for {
+ r, _, err := rdr.ReadRune()
+ if err == io.EOF {
+ break
+ }
+ switch r {
+ case 0x1b:
+ p.handleSeq(rdr)
+ default:
+ p.buf.Write(r, p.curStyle)
+ }
+ }
+ return p.buf
+}
+
+// A parser parses a string into a RuneBuffer
+type parser struct {
+ buf *RuneBuffer
+ curStyle tcell.Style
+}
+
+func (p *parser) handleSeq(rdr io.RuneReader) {
+ r, _, err := rdr.ReadRune()
+ if errors.Is(err, io.EOF) {
+ return
+ }
+ switch r {
+ case '[': // CSI
+ p.handleCSI(rdr)
+ case ']': // OSC
+ case '(': // Designate G0 charset
+ p.swallow(rdr, 1)
+ }
+}
+
+func (p *parser) handleCSI(rdr io.RuneReader) {
+ var (
+ params []int
+ param []rune
+ hasErr bool
+ er error
+ )
+outer:
+ for {
+ r, _, err := rdr.ReadRune()
+ if errors.Is(err, io.EOF) {
+ return
+ }
+ switch {
+ case r >= 0x30 && r <= 0x39:
+ param = append(param, r)
+ case r == ':' || r == ';':
+ var ps int
+ if len(param) > 0 {
+ ps, er = strconv.Atoi(string(param))
+ if er != nil {
+ hasErr = true
+ continue
+ }
+ }
+ params = append(params, ps)
+ param = []rune{}
+ case r == 'm':
+ var ps int
+ if len(param) > 0 {
+ ps, er = strconv.Atoi(string(param))
+ if er != nil {
+ hasErr = true
+ continue
+ }
+ }
+ params = append(params, ps)
+ break outer
+ }
+ }
+ if hasErr {
+ // leave the cursor unchanged
+ return
+ }
+ for i := 0; i < len(params); i++ {
+ param := params[i]
+ switch param {
+ case 0:
+ p.curStyle = tcell.StyleDefault
+ case 1:
+ p.curStyle = p.curStyle.Bold(true)
+ case 2:
+ p.curStyle = p.curStyle.Dim(true)
+ case 3:
+ p.curStyle = p.curStyle.Italic(true)
+ case 4:
+ p.curStyle = p.curStyle.Underline(true)
+ case 5:
+ p.curStyle = p.curStyle.Blink(true)
+ case 6:
+ // rapid blink, not supported by tcell. fallback to slow
+ // blink
+ p.curStyle = p.curStyle.Blink(true)
+ case 7:
+ p.curStyle = p.curStyle.Reverse(true)
+ case 8:
+ // Hidden. not supported by tcell
+ case 9:
+ p.curStyle = p.curStyle.StrikeThrough(true)
+ case 21:
+ p.curStyle = p.curStyle.Bold(false)
+ case 22:
+ p.curStyle = p.curStyle.Dim(false)
+ case 23:
+ p.curStyle = p.curStyle.Italic(false)
+ case 24:
+ p.curStyle = p.curStyle.Underline(false)
+ case 25:
+ p.curStyle = p.curStyle.Blink(false)
+ case 26:
+ // rapid blink, not supported by tcell. fallback to slow
+ // blink
+ p.curStyle = p.curStyle.Blink(false)
+ case 27:
+ p.curStyle = p.curStyle.Reverse(false)
+ case 28:
+ // Hidden. unsupported by tcell
+ case 29:
+ p.curStyle = p.curStyle.StrikeThrough(false)
+ case 30, 31, 32, 33, 34, 35, 36, 37:
+ p.curStyle = p.curStyle.Foreground(tcell.PaletteColor(param - 30))
+ case 38:
+ if i+2 < len(params) && params[i+1] == 5 {
+ p.curStyle = p.curStyle.Foreground(tcell.PaletteColor(params[i+2]))
+ i += 2
+ }
+ if i+4 < len(params) && params[i+1] == 2 {
+ switch len(params) {
+ case 6:
+ r := int32(params[i+3])
+ g := int32(params[i+4])
+ b := int32(params[i+5])
+ p.curStyle = p.curStyle.Foreground(tcell.NewRGBColor(r, g, b))
+ i += 5
+ default:
+ r := int32(params[i+2])
+ g := int32(params[i+3])
+ b := int32(params[i+4])
+ p.curStyle = p.curStyle.Foreground(tcell.NewRGBColor(r, g, b))
+ i += 4
+ }
+ }
+ case 40, 41, 42, 43, 44, 45, 46, 47:
+ p.curStyle = p.curStyle.Background(tcell.PaletteColor(param - 40))
+ case 48:
+ if i+2 < len(params) && params[i+1] == 5 {
+ p.curStyle = p.curStyle.Background(tcell.PaletteColor(params[i+2]))
+ i += 2
+ }
+ if i+4 < len(params) && params[i+1] == 2 {
+ switch len(params) {
+ case 6:
+ r := int32(params[i+3])
+ g := int32(params[i+4])
+ b := int32(params[i+5])
+ p.curStyle = p.curStyle.Background(tcell.NewRGBColor(r, g, b))
+ i += 5
+ default:
+ r := int32(params[i+2])
+ g := int32(params[i+3])
+ b := int32(params[i+4])
+ p.curStyle = p.curStyle.Background(tcell.NewRGBColor(r, g, b))
+ i += 4
+ }
+ }
+ case 90, 91, 92, 93, 94, 95, 96, 97:
+ p.curStyle = p.curStyle.Foreground(tcell.PaletteColor(param - 90 + 8))
+ case 100, 101, 102, 103, 104, 105, 106, 107:
+ p.curStyle = p.curStyle.Background(tcell.PaletteColor(param - 100 + 8))
+ }
+ }
+}
+
+func (p *parser) swallow(rdr io.RuneReader, n int) {
+ for i := 0; i < n; i++ {
+ rdr.ReadRune() //nolint:errcheck // we are throwing these reads away
+ }
+}
diff --git a/lib/parse/ansi_test.go b/lib/parse/ansi_test.go
new file mode 100644
index 00000000..f916412b
--- /dev/null
+++ b/lib/parse/ansi_test.go
@@ -0,0 +1,262 @@
+package parse_test
+
+import (
+ "os"
+ "testing"
+
+ "git.sr.ht/~rjarry/aerc/lib/parse"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParser(t *testing.T) {
+ //nolint:errcheck // we'll fail the test if this fails
+ _ = os.Setenv("COLORTERM", "truecolor")
+ tests := []struct {
+ name string
+ input string
+ expectedString string
+ expectedLen int
+ }{
+ {
+ name: "no style",
+ input: "hello, world",
+ expectedString: "hello, world",
+ expectedLen: 12,
+ },
+ {
+ name: "bold",
+ input: "\x1b[1mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[1mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "dim",
+ input: "\x1b[2mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[2mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "bold and dim",
+ input: "\x1b[1;2mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[1m\x1b[2mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "italic",
+ input: "\x1b[3mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[3mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "underline",
+ input: "\x1b[4mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[4mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "blink",
+ input: "\x1b[5mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[5mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "fast blink",
+ input: "\x1b[6mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[5mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "reverse",
+ input: "\x1b[7mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[7mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "hidden",
+ input: "\x1b[8mhello, world",
+ expectedString: "hello, world",
+ expectedLen: 12,
+ },
+ {
+ name: "strikethrough",
+ input: "\x1b[9mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[9mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "bold hello, normal world",
+ input: "\x1b[1mhello, \x1b[21mworld",
+ expectedString: "\x1b(B\x1b[m\x1b[1mhello, \x1b(B\x1b[mworld\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "bold hello, normal world v2",
+ input: "\x1b[1mhello, \x1b[mworld",
+ expectedString: "\x1b(B\x1b[m\x1b[1mhello, \x1b(B\x1b[mworld\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "8 bit color: foreground",
+ input: "\x1b[30mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[30mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "8 bit color: background",
+ input: "\x1b[41mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[41mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "8 bit color: foreground and background",
+ input: "\x1b[31;41mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[31;41mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "16 bit color: foreground",
+ input: "\x1b[90mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[90mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "16 bit color: background",
+ input: "\x1b[101mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[101mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "16 bit color: foreground and background",
+ input: "\x1b[91;101mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[91;101mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "256 color: foreground",
+ input: "\x1b[38;5;2mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[32mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "256 color: foreground",
+ input: "\x1b[38;5;132mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[38;5;132mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "256 color: background",
+ input: "\x1b[48;5;132mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[48;5;132mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "256 color: foreground and background",
+ input: "\x1b[38;5;20;48;5;20mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[38;5;20;48;5;20mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "256 color: background",
+ input: "\x1b[48;5;2mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[42mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "true color: foreground",
+ input: "\x1b[38;2;0;0;0mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[38;2;0;0;0mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "true color: foreground with color space",
+ input: "\x1b[38;2;;0;0;0mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[38;2;0;0;0mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "true color: foreground with color space and colons",
+ input: "\x1b[38:2::0:0:0mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[38;2;0;0;0mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "true color: background",
+ input: "\x1b[48;2;0;0;0mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[48;2;0;0;0mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "true color: background with color space",
+ input: "\x1b[48;2;;0;0;0mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[48;2;0;0;0mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ {
+ name: "true color: foreground and background",
+ input: "\x1b[38;2;200;200;200;48;2;0;0;0mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[38;2;200;200;200;48;2;0;0;0mhello, world\x1b(B\x1b[m",
+ expectedLen: 12,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ buf := parse.ParseANSI(test.input)
+ assert.Equal(t, test.expectedString, buf.String())
+ assert.Equal(t, test.expectedLen, buf.Len())
+ })
+ }
+}
+
+func TestTruncate(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expectedString string
+ }{
+ {
+ name: "no style, truncate at 5",
+ input: "hello, world",
+ expectedString: "hello",
+ },
+ {
+ name: "bold, truncate at 5",
+ input: "\x1b[1mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[1mhello\x1b(B\x1b[m",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ buf := parse.ParseANSI(test.input)
+ assert.Equal(t, test.expectedString, buf.Truncate(5, 0))
+ })
+ }
+}
+
+func TestTruncateHead(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expectedString string
+ expectedLen int
+ }{
+ {
+ name: "no style, truncate head at 5",
+ input: "hello, world",
+ expectedString: "world",
+ },
+ {
+ name: "bold, truncate head at 5",
+ input: "\x1b[1mhello, world",
+ expectedString: "\x1b(B\x1b[m\x1b[1mworld\x1b(B\x1b[m",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ buf := parse.ParseANSI(test.input)
+ assert.Equal(t, test.expectedString, buf.TruncateHead(5, 0))
+ })
+ }
+}
diff --git a/lib/ui/context.go b/lib/ui/context.go
index 12d65bbf..9ca7cc9d 100644
--- a/lib/ui/context.go
+++ b/lib/ui/context.go
@@ -3,9 +3,9 @@ package ui
import (
"fmt"
+ "git.sr.ht/~rjarry/aerc/lib/parse"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
- "github.com/mattn/go-runewidth"
)
// A context allows you to draw in a sub-region of the terminal
@@ -75,6 +75,9 @@ func (ctx *Context) Printf(x, y int, style tcell.Style,
str := fmt.Sprintf(format, a...)
+ buf := parse.ParseANSI(str)
+ buf.ApplyStyle(style)
+
old_x := x
newline := func() bool {
@@ -82,27 +85,27 @@ func (ctx *Context) Printf(x, y int, style tcell.Style,
y++
return y < height
}
- for _, ch := range str {
- switch ch {
+ for _, sr := range buf.Runes() {
+ switch sr.Value {
case '\n':
if !newline() {
- return runewidth.StringWidth(str)
+ return buf.Len()
}
case '\r':
x = old_x
default:
crunes := []rune{}
- ctx.viewport.SetContent(x, y, ch, crunes, style)
- x += runewidth.RuneWidth(ch)
+ ctx.viewport.SetContent(x, y, sr.Value, crunes, sr.Style)
+ x += sr.Width
if x == old_x+width {
if !newline() {
- return runewidth.StringWidth(str)
+ return buf.Len()
}
}
}
}
- return runewidth.StringWidth(str)
+ return buf.Len()
}
func (ctx *Context) Fill(x, y, width, height int, rune rune, style tcell.Style) {
diff --git a/lib/ui/table.go b/lib/ui/table.go
index 0cd6e706..704dd2be 100644
--- a/lib/ui/table.go
+++ b/lib/ui/table.go
@@ -3,10 +3,9 @@ package ui
import (
"math"
"regexp"
- "strings"
"git.sr.ht/~rjarry/aerc/config"
- "git.sr.ht/~rjarry/aerc/lib/format"
+ "git.sr.ht/~rjarry/aerc/lib/parse"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
@@ -94,9 +93,9 @@ func (t *Table) computeWidths(width int) {
if t.autoFitWidths {
for _, row := range t.Rows {
for c := range t.Columns {
- w := runewidth.StringWidth(row.Cells[c])
- if w > contentMaxWidths[c] {
- contentMaxWidths[c] = w
+ buf := parse.ParseANSI(row.Cells[c])
+ if buf.Len() > contentMaxWidths[c] {
+ contentMaxWidths[c] = buf.Len()
}
}
}
@@ -161,28 +160,38 @@ var metaCharsRegexp = regexp.MustCompile(`[\t\r\f\n\v]`)
func (col *Column) alignCell(cell string) string {
cell = metaCharsRegexp.ReplaceAllString(cell, " ")
- width := runewidth.StringWidth(cell)
+ buf := parse.ParseANSI(cell)
+ width := buf.Len()
switch {
case col.Def.Flags.Has(config.ALIGN_LEFT):
if width < col.Width {
- cell += strings.Repeat(" ", col.Width-width)
+ for i := 0; i < (col.Width - width); i += 1 {
+ buf.Write(' ', tcell.StyleDefault)
+ }
+ cell = buf.String()
} else if width > col.Width {
- cell = runewidth.Truncate(cell, col.Width, "…")
+ cell = buf.Truncate(col.Width, '…')
}
case col.Def.Flags.Has(config.ALIGN_CENTER):
if width < col.Width {
- padding := strings.Repeat(" ", col.Width-width)
- l := len(padding) / 2
- cell = padding[:l] + cell + padding[l:]
+ pad := (col.Width - width) / 2
+ for i := 0; i < pad; i += 1 {
+ buf.Prepend(' ', tcell.StyleDefault)
+ buf.Write(' ', tcell.StyleDefault)
+ }
+ cell = buf.String()
} else if width > col.Width {
- cell = runewidth.Truncate(cell, col.Width, "…")
+ cell = buf.Truncate(col.Width, '…')
}
case col.Def.Flags.Has(config.ALIGN_RIGHT):
if width < col.Width {
- cell = strings.Repeat(" ", col.Width-width) + cell
+ for i := 0; i < (col.Width - width); i += 1 {
+ buf.Prepend(' ', tcell.StyleDefault)
+ }
+ cell = buf.String()
} else if width > col.Width {
- cell = format.TruncateHead(cell, col.Width, "…")
+ cell = buf.TruncateHead(col.Width, '…')
}
}
@@ -205,6 +214,10 @@ func (t *Table) Draw(ctx *Context) {
}
cell := col.alignCell(row.Cells[c])
style := t.GetRowStyle(t, r)
+
+ buf := parse.ParseANSI(cell)
+ buf.ApplyAttrs(style)
+ cell = buf.String()
ctx.Printf(col.Offset, r, style, "%s%s", cell, col.Separator)
}
}
diff --git a/widgets/dirlist.go b/widgets/dirlist.go
index 1e1a8a61..986bd662 100644
--- a/widgets/dirlist.go
+++ b/widgets/dirlist.go
@@ -10,11 +10,10 @@ import (
"time"
"github.com/gdamore/tcell/v2"
- "github.com/mattn/go-runewidth"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
- "git.sr.ht/~rjarry/aerc/lib/format"
+ "git.sr.ht/~rjarry/aerc/lib/parse"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/lib/ui"
@@ -299,20 +298,26 @@ func (dirlist *DirectoryList) renderDir(
}
buf.Reset()
- lwidth := runewidth.StringWidth(left)
- rwidth := runewidth.StringWidth(right)
+ lbuf := parse.ParseANSI(left)
+ lbuf.ApplyAttrs(style)
+ lwidth := lbuf.Len()
+ rbuf := parse.ParseANSI(right)
+ rbuf.ApplyAttrs(style)
+ rwidth := rbuf.Len()
if lwidth+rwidth+1 > width {
if rwidth > 3*width/4 {
rwidth = 3 * width / 4
}
lwidth = width - rwidth - 1
- right = runewidth.FillLeft(right, rwidth)
- right = format.TruncateHead(right, rwidth, "…")
- left = runewidth.FillRight(left, lwidth)
- left = runewidth.Truncate(left, lwidth, "…")
+ right = rbuf.TruncateHead(rwidth, '…')
+ left = lbuf.Truncate(lwidth-1, '…')
} else {
- left = runewidth.FillRight(left, width-rwidth-1)
+ for i := 0; i < (width - lwidth - rwidth - 1); i += 1 {
+ lbuf.Write(' ', tcell.StyleDefault)
+ }
+ left = lbuf.String()
+ right = rbuf.String()
}
return left, right, style