diff options
-rw-r--r-- | lib/parse/ansi.go | 402 | ||||
-rw-r--r-- | lib/parse/ansi_test.go | 262 | ||||
-rw-r--r-- | lib/ui/context.go | 19 | ||||
-rw-r--r-- | lib/ui/table.go | 41 | ||||
-rw-r--r-- | widgets/dirlist.go | 23 |
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 |