diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/parse/ansi.go | 430 | ||||
-rw-r--r-- | lib/parse/ansi_test.go | 262 | ||||
-rw-r--r-- | lib/state/templates.go | 8 | ||||
-rw-r--r-- | lib/ui/context.go | 21 | ||||
-rw-r--r-- | lib/ui/string.go | 142 | ||||
-rw-r--r-- | lib/ui/table.go | 34 |
6 files changed, 171 insertions, 726 deletions
diff --git a/lib/parse/ansi.go b/lib/parse/ansi.go index 7fc37834..df50042c 100644 --- a/lib/parse/ansi.go +++ b/lib/parse/ansi.go @@ -3,38 +3,16 @@ package parse import ( "bufio" "bytes" - "errors" "fmt" "io" "os" "regexp" - "strconv" - "strings" "git.sr.ht/~rjarry/aerc/log" - "git.sr.ht/~rockorager/vaxis" - "github.com/mattn/go-runewidth" ) var AnsiReg = regexp.MustCompile("\x1B\\[[0-?]*[ -/]*[@-~]") -const ( - setfgbgrgb = "\x1b[38;2;%d;%d;%d;48;2;%d;%d;%dm" - setfgrgb = "\x1b[38;2;%d;%d;%dm" - setbgrgb = "\x1b[48;2;%d;%d;%dm" - setfgbg = "\x1b[38;5;%d;48;5;%dm" - setfg = "\x1b[38;5;%dm" - setbg = "\x1b[48;5;%dm" - attrOff = "\x1B[m" - bold = "\x1B[1m" - dim = "\x1B[2m" - italic = "\x1B[3m" - underline = "\x1B[4m" - blink = "\x1B[5m" - reverse = "\x1B[7m" - strikethrough = "\x1B[9m" -) - // StripAnsi strips ansi escape codes from the reader func StripAnsi(r io.Reader) io.Reader { buf := bytes.NewBuffer(nil) @@ -57,411 +35,3 @@ 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 vaxis.Style -} - -// RuneBuffer is a buffer of runes styled with vaxis.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 vaxis.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) PadLeft(width int, r rune, style vaxis.Style) { - w := rb.Len() - if w >= width { - return - } - w = width - w - for w > 0 { - ww := runewidth.RuneWidth(r) - w -= ww - rb.buf = append([]*StyledRune{{r, ww, style}}, rb.buf...) - } -} - -func (rb *RuneBuffer) PadRight(width int, r rune, style vaxis.Style) { - w := rb.Len() - if w >= width { - return - } - w = width - w - for w > 0 { - ww := runewidth.RuneWidth(r) - w -= ww - rb.buf = append(rb.buf, &StyledRune{r, ww, style}) - } -} - -// 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 { - var ( - s = bytes.NewBuffer(nil) - style = vaxis.Style{} - 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(attrOff) - // fg, bg, attrs := style.Decompose() - fg := style.Foreground.Params() - switch len(fg) { - case 0: - // default - case 1: - // indexed - fmt.Fprintf(s, setfg, fg[0]) - case 3: - // rgb - fmt.Fprintf(s, setfgrgb, fg[0], fg[1], fg[2]) - } - - bg := style.Background.Params() - switch len(bg) { - case 0: - // default - case 1: - // indexed - fmt.Fprintf(s, setbg, bg[0]) - case 3: - // rgb - fmt.Fprintf(s, setbgrgb, bg[0], bg[1], bg[2]) - } - - attrs := style.Attribute - - if attrs&vaxis.AttrBold != 0 { - s.WriteString(bold) - } - if attrs&vaxis.AttrReverse != 0 { - s.WriteString(reverse) - } - if attrs&vaxis.AttrBlink != 0 { - s.WriteString(blink) - } - if attrs&vaxis.AttrDim != 0 { - s.WriteString(dim) - } - if attrs&vaxis.AttrItalic != 0 { - s.WriteString(italic) - } - if attrs&vaxis.AttrStrikethrough != 0 { - s.WriteString(strikethrough) - } - - if style.UnderlineStyle != vaxis.UnderlineOff { - s.WriteString(underline) - } - } - - 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(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 vaxis.Style) { - d := vaxis.Style{} - for _, sr := range rb.buf { - if sr.Style == d { - sr.Style = style - } - } -} - -// ApplyAttrs applies the style, and if another style is present ORs the -// attributes -func (rb *RuneBuffer) ApplyAttrs(style vaxis.Style) { - for _, sr := range rb.buf { - if style.Foreground != 0 { - sr.Style.Foreground = style.Foreground - } - if style.Background != 0 { - sr.Style.Background = style.Background - } - sr.Style.Attribute |= style.Attribute - if style.UnderlineColor != 0 { - sr.Style.UnderlineColor = style.UnderlineColor - } - if style.UnderlineStyle != vaxis.UnderlineOff { - sr.Style.UnderlineStyle = style.UnderlineStyle - } - } -} - -// Applies a style to a string. Any currently applied styles will not be overwritten -func ApplyStyle(style vaxis.Style, str string) string { - rb := ParseANSI(str) - d := vaxis.Style{} - for _, sr := range rb.buf { - if sr.Style == d { - sr.Style = style - } - } - return rb.String() -} - -// Parses a styled string into a RuneBuffer -func ParseANSI(s string) *RuneBuffer { - p := &parser{ - buf: &RuneBuffer{}, - curStyle: vaxis.Style{}, - } - 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 vaxis.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 = vaxis.Style{} - case 1: - p.curStyle.Attribute |= vaxis.AttrBold - case 2: - p.curStyle.Attribute |= vaxis.AttrDim - case 3: - p.curStyle.Attribute |= vaxis.AttrItalic - case 4: - p.curStyle.UnderlineStyle = vaxis.UnderlineSingle - case 5: - p.curStyle.Attribute |= vaxis.AttrBlink - case 6: - // rapid blink, not supported by vaxis. fallback to slow - // blink - p.curStyle.Attribute |= vaxis.AttrBlink - case 7: - p.curStyle.Attribute |= vaxis.AttrReverse - case 8: - // Hidden. not supported by vaxis - case 9: - p.curStyle.Attribute |= vaxis.AttrStrikethrough - case 21: - p.curStyle.Attribute &^= vaxis.AttrBold - case 22: - p.curStyle.Attribute &^= vaxis.AttrDim - case 23: - p.curStyle.Attribute &^= vaxis.AttrItalic - case 24: - p.curStyle.UnderlineStyle = vaxis.UnderlineOff - case 25: - p.curStyle.Attribute &^= vaxis.AttrBlink - case 26: - // rapid blink, not supported by vaxis. fallback to slow - // blink - p.curStyle.Attribute &^= vaxis.AttrBlink - case 27: - p.curStyle.Attribute &^= vaxis.AttrReverse - case 28: - // Hidden. unsupported by vaxis - case 29: - p.curStyle.Attribute &^= vaxis.AttrStrikethrough - case 30, 31, 32, 33, 34, 35, 36, 37: - p.curStyle.Foreground = vaxis.IndexColor(uint8(param - 30)) - case 38: - if i+2 < len(params) && params[i+1] == 5 { - p.curStyle.Foreground = vaxis.IndexColor(uint8(params[i+2])) - i += 2 - } - if i+4 < len(params) && params[i+1] == 2 { - switch len(params) { - case 6: - r := uint8(params[i+3]) - g := uint8(params[i+4]) - b := uint8(params[i+5]) - p.curStyle.Foreground = vaxis.RGBColor(r, g, b) - i += 5 - default: - r := uint8(params[i+2]) - g := uint8(params[i+3]) - b := uint8(params[i+4]) - p.curStyle.Foreground = vaxis.RGBColor(r, g, b) - i += 4 - } - } - case 40, 41, 42, 43, 44, 45, 46, 47: - p.curStyle.Background = vaxis.IndexColor(uint8(param - 40)) - case 48: - if i+2 < len(params) && params[i+1] == 5 { - p.curStyle.Background = vaxis.IndexColor(uint8(params[i+2])) - i += 2 - } - if i+4 < len(params) && params[i+1] == 2 { - switch len(params) { - case 6: - r := uint8(params[i+3]) - g := uint8(params[i+4]) - b := uint8(params[i+5]) - p.curStyle.Background = vaxis.RGBColor(r, g, b) - i += 5 - default: - r := uint8(params[i+2]) - g := uint8(params[i+3]) - b := uint8(params[i+4]) - p.curStyle.Background = vaxis.RGBColor(r, g, b) - i += 4 - } - } - case 90, 91, 92, 93, 94, 95, 96, 97: - p.curStyle.Foreground = vaxis.IndexColor(uint8(param - 90 + 8)) - case 100, 101, 102, 103, 104, 105, 106, 107: - p.curStyle.Background = vaxis.IndexColor(uint8(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 deleted file mode 100644 index 5d8e8572..00000000 --- a/lib/parse/ansi_test.go +++ /dev/null @@ -1,262 +0,0 @@ -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[m\x1b[1mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "dim", - input: "\x1b[2mhello, world", - expectedString: "\x1b[m\x1b[2mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "bold and dim", - input: "\x1b[1;2mhello, world", - expectedString: "\x1b[m\x1b[1m\x1b[2mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "italic", - input: "\x1b[3mhello, world", - expectedString: "\x1b[m\x1b[3mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "underline", - input: "\x1b[4mhello, world", - expectedString: "\x1b[m\x1b[4mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "blink", - input: "\x1b[5mhello, world", - expectedString: "\x1b[m\x1b[5mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "fast blink", - input: "\x1b[6mhello, world", - expectedString: "\x1b[m\x1b[5mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "reverse", - input: "\x1b[7mhello, world", - expectedString: "\x1b[m\x1b[7mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "hidden", - input: "\x1b[8mhello, world", - expectedString: "hello, world", - expectedLen: 12, - }, - { - name: "strikethrough", - input: "\x1b[9mhello, world", - expectedString: "\x1b[m\x1b[9mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "bold hello, normal world", - input: "\x1b[1mhello, \x1b[21mworld", - expectedString: "\x1b[m\x1b[1mhello, \x1b[mworld\x1b[m", - expectedLen: 12, - }, - { - name: "bold hello, normal world v2", - input: "\x1b[1mhello, \x1b[mworld", - expectedString: "\x1b[m\x1b[1mhello, \x1b[mworld\x1b[m", - expectedLen: 12, - }, - { - name: "8 bit color: foreground", - input: "\x1b[30mhello, world", - expectedString: "\x1b[m\x1b[38;5;0mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "8 bit color: background", - input: "\x1b[41mhello, world", - expectedString: "\x1b[m\x1b[48;5;1mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "8 bit color: foreground and background", - input: "\x1b[31;41mhello, world", - expectedString: "\x1b[m\x1b[38;5;1m\x1b[48;5;1mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "16 bit color: foreground", - input: "\x1b[90mhello, world", - expectedString: "\x1b[m\x1b[38;5;8mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "16 bit color: background", - input: "\x1b[101mhello, world", - expectedString: "\x1b[m\x1b[48;5;9mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "16 bit color: foreground and background", - input: "\x1b[91;101mhello, world", - expectedString: "\x1b[m\x1b[38;5;9m\x1b[48;5;9mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "256 color: foreground", - input: "\x1b[38;5;2mhello, world", - expectedString: "\x1b[m\x1b[38;5;2mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "256 color: foreground", - input: "\x1b[38;5;132mhello, world", - expectedString: "\x1b[m\x1b[38;5;132mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "256 color: background", - input: "\x1b[48;5;132mhello, world", - expectedString: "\x1b[m\x1b[48;5;132mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "256 color: foreground and background", - input: "\x1b[38;5;20;48;5;20mhello, world", - expectedString: "\x1b[m\x1b[38;5;20m\x1b[48;5;20mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "256 color: background", - input: "\x1b[48;5;2mhello, world", - expectedString: "\x1b[m\x1b[48;5;2mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "true color: foreground", - input: "\x1b[38;2;0;0;0mhello, world", - expectedString: "\x1b[m\x1b[38;2;0;0;0mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "true color: foreground with color space", - input: "\x1b[38;2;;0;0;0mhello, world", - expectedString: "\x1b[m\x1b[38;2;0;0;0mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "true color: foreground with color space and colons", - input: "\x1b[38:2::0:0:0mhello, world", - expectedString: "\x1b[m\x1b[38;2;0;0;0mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "true color: background", - input: "\x1b[48;2;0;0;0mhello, world", - expectedString: "\x1b[m\x1b[48;2;0;0;0mhello, world\x1b[m", - expectedLen: 12, - }, - { - name: "true color: background with color space", - input: "\x1b[48;2;;0;0;0mhello, world", - expectedString: "\x1b[m\x1b[48;2;0;0;0mhello, world\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[m\x1b[38;2;200;200;200m\x1b[48;2;0;0;0mhello, world\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[m\x1b[1mhello\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[m\x1b[1mworld\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/state/templates.go b/lib/state/templates.go index d039f1b6..ec679211 100644 --- a/lib/state/templates.go +++ b/lib/state/templates.go @@ -10,7 +10,7 @@ import ( "time" "git.sr.ht/~rjarry/aerc/config" - "git.sr.ht/~rjarry/aerc/lib/parse" + "git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/lib/xdg" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" @@ -634,7 +634,7 @@ func (d *templateData) PendingKeys() string { func (d *templateData) Style(content, name string) string { cfg := config.Ui.ForAccount(d.Account()) style := cfg.GetUserStyle(name) - return parse.ApplyStyle(style, content) + return ui.ApplyStyle(style, content) } func (d *templateData) StyleSwitch(content string, cases ...models.Case) string { @@ -642,7 +642,7 @@ func (d *templateData) StyleSwitch(content string, cases ...models.Case) string if c.Matches(content) { cfg := config.Ui.ForAccount(d.Account()) style := cfg.GetUserStyle(c.Value()) - return parse.ApplyStyle(style, content) + return ui.ApplyStyle(style, content) } } return content @@ -659,7 +659,7 @@ top: } cfg := config.Ui.ForAccount(d.Account()) style := cfg.GetUserStyle(c.Value()) - e = parse.ApplyStyle(style, e) + e = ui.ApplyStyle(style, e) break } } diff --git a/lib/ui/context.go b/lib/ui/context.go index 2ca0d310..8544abdb 100644 --- a/lib/ui/context.go +++ b/lib/ui/context.go @@ -3,7 +3,6 @@ package ui import ( "fmt" - "git.sr.ht/~rjarry/aerc/lib/parse" "git.sr.ht/~rockorager/vaxis" ) @@ -68,8 +67,8 @@ func (ctx *Context) Printf(x, y int, style vaxis.Style, str := fmt.Sprintf(format, a...) - buf := parse.ParseANSI(str) - buf.ApplyStyle(style) + buf := StyledString(str) + ApplyAttrs(buf, style) old_x := x @@ -78,22 +77,16 @@ func (ctx *Context) Printf(x, y int, style vaxis.Style, y++ return y < height } - for _, sr := range buf.Runes() { - switch sr.Value { - case '\n': + for _, sr := range buf.Cells { + switch sr.Grapheme { + case "\n": if !newline() { return buf.Len() } - case '\r': + case "\r": x = old_x default: - ctx.window.SetCell(x, y, vaxis.Cell{ - Character: vaxis.Character{ - Grapheme: string(sr.Value), - Width: sr.Width, - }, - Style: sr.Style, - }) + ctx.window.SetCell(x, y, sr) x += sr.Width if x == old_x+width { if !newline() { diff --git a/lib/ui/string.go b/lib/ui/string.go new file mode 100644 index 00000000..407c9963 --- /dev/null +++ b/lib/ui/string.go @@ -0,0 +1,142 @@ +package ui + +import ( + "git.sr.ht/~rockorager/vaxis" +) + +func StyledString(s string) *vaxis.StyledString { + return state.vx.NewStyledString(s, vaxis.Style{}) +} + +// Applies a style to a string. Any currently applied styles will not be overwritten +func ApplyStyle(style vaxis.Style, str string) string { + ss := StyledString(str) + d := vaxis.Style{} + for i, sr := range ss.Cells { + if sr.Style == d { + sr.Style = style + ss.Cells[i] = sr + } + } + return ss.Encode() +} + +// PadLeft inserts blank spaces at the beginning of the StyledString to produce +// a string of the provided width +func PadLeft(ss *vaxis.StyledString, width int) { + w := ss.Len() + if w >= width { + return + } + cell := vaxis.Cell{ + Character: vaxis.Character{ + Grapheme: " ", + Width: 1, + }, + } + w = width - w + cells := make([]vaxis.Cell, 0, len(ss.Cells)+w) + for w > 0 { + cells = append(cells, cell) + w -= 1 + } + cells = append(cells, ss.Cells...) + ss.Cells = cells +} + +// PadLeft inserts blank spaces at the end of the StyledString to produce +// a string of the provided width +func PadRight(ss *vaxis.StyledString, width int) { + w := ss.Len() + if w >= width { + return + } + cell := vaxis.Cell{ + Character: vaxis.Character{ + Grapheme: " ", + Width: 1, + }, + } + w = width - w + for w > 0 { + w -= 1 + ss.Cells = append(ss.Cells, cell) + } +} + +// ApplyAttrs applies the style, and if another style is present ORs the +// attributes +func ApplyAttrs(ss *vaxis.StyledString, style vaxis.Style) { + for i, cell := range ss.Cells { + if style.Foreground != 0 { + cell.Style.Foreground = style.Foreground + } + if style.Background != 0 { + cell.Style.Background = style.Background + } + cell.Style.Attribute |= style.Attribute + if style.UnderlineColor != 0 { + cell.Style.UnderlineColor = style.UnderlineColor + } + if style.UnderlineStyle != vaxis.UnderlineOff { + cell.Style.UnderlineStyle = style.UnderlineStyle + } + ss.Cells[i] = cell + } +} + +// Truncates the styled string on the right and inserts a '…' as the last +// character +func Truncate(ss *vaxis.StyledString, width int) { + if ss.Len() <= width { + return + } + cells := make([]vaxis.Cell, 0, len(ss.Cells)) + total := 0 + for _, cell := range ss.Cells { + if total+cell.Width >= width { + // we can't fit this cell so put in our truncator + cells = append(cells, vaxis.Cell{ + Character: vaxis.Character{ + Grapheme: "…", + Width: 1, + }, + Style: cell.Style, + }) + break + } + total += cell.Width + cells = append(cells, cell) + } + ss.Cells = cells +} + +// TruncateHead truncates the left side of the string and inserts '…' as the +// first character +func TruncateHead(ss *vaxis.StyledString, width int) { + l := ss.Len() + if l <= width { + return + } + offset := l - width + cells := make([]vaxis.Cell, 0, len(ss.Cells)) + cells = append(cells, vaxis.Cell{ + Character: vaxis.Character{ + Grapheme: "…", + Width: 1, + }, + }) + total := 0 + for _, cell := range ss.Cells { + total += cell.Width + if total < offset { + // we always have at least one for our truncator. We + // copy this cells style to it so that it retains the + // style information from the first printed cell + cells[0].Style = cell.Style + continue + } + cells = append(cells, cell) + } + ss.Cells = cells +} diff --git a/lib/ui/table.go b/lib/ui/table.go index ceb01c78..3f5fc4b3 100644 --- a/lib/ui/table.go +++ b/lib/ui/table.go @@ -5,7 +5,6 @@ import ( "regexp" "git.sr.ht/~rjarry/aerc/config" - "git.sr.ht/~rjarry/aerc/lib/parse" "git.sr.ht/~rockorager/vaxis" "github.com/mattn/go-runewidth" ) @@ -93,7 +92,7 @@ func (t *Table) computeWidths(width int) { if t.autoFitWidths { for _, row := range t.Rows { for c := range t.Columns { - buf := parse.ParseANSI(row.Cells[c]) + buf := StyledString(row.Cells[c]) if buf.Len() > contentMaxWidths[c] { contentMaxWidths[c] = buf.Len() } @@ -160,32 +159,35 @@ var metaCharsRegexp = regexp.MustCompile(`[\t\r\f\n\v]`) func (col *Column) alignCell(cell string) string { cell = metaCharsRegexp.ReplaceAllString(cell, " ") - buf := parse.ParseANSI(cell) + buf := StyledString(cell) width := buf.Len() switch { case col.Def.Flags.Has(config.ALIGN_LEFT): if width < col.Width { - buf.PadRight(col.Width, ' ', vaxis.Style{}) - cell = buf.String() + PadRight(buf, col.Width) + cell = buf.Encode() } else if width > col.Width { - cell = buf.Truncate(col.Width, '…') + Truncate(buf, col.Width) + cell = buf.Encode() } case col.Def.Flags.Has(config.ALIGN_CENTER): if width < col.Width { pad := col.Width - width - buf.PadLeft(col.Width-(pad/2), ' ', vaxis.Style{}) - buf.PadRight(col.Width, ' ', vaxis.Style{}) - cell = buf.String() + PadLeft(buf, col.Width-(pad/2)) + PadRight(buf, col.Width) + cell = buf.Encode() } else if width > col.Width { - cell = buf.Truncate(col.Width, '…') + Truncate(buf, col.Width) + cell = buf.Encode() } case col.Def.Flags.Has(config.ALIGN_RIGHT): if width < col.Width { - buf.PadLeft(col.Width, ' ', vaxis.Style{}) - cell = buf.String() + PadLeft(buf, col.Width) + cell = buf.Encode() } else if width > col.Width { - cell = buf.TruncateHead(col.Width, '…') + TruncateHead(buf, col.Width) + cell = buf.Encode() } } @@ -209,9 +211,9 @@ 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() + buf := StyledString(cell) + ApplyAttrs(buf, style) + cell = buf.Encode() ctx.Printf(col.Offset, r, style, "%s%s", cell, col.Separator) } } |