aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--app/dirlist.go26
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--lib/parse/ansi.go430
-rw-r--r--lib/parse/ansi_test.go262
-rw-r--r--lib/state/templates.go8
-rw-r--r--lib/ui/context.go21
-rw-r--r--lib/ui/string.go142
-rw-r--r--lib/ui/table.go34
9 files changed, 190 insertions, 739 deletions
diff --git a/app/dirlist.go b/app/dirlist.go
index 71db2b6a..d4171a74 100644
--- a/app/dirlist.go
+++ b/app/dirlist.go
@@ -10,7 +10,6 @@ import (
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
- "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"
@@ -335,11 +334,11 @@ func (dirlist *DirectoryList) renderDir(
}
buf.Reset()
- lbuf := parse.ParseANSI(left)
- lbuf.ApplyAttrs(style)
+ lbuf := ui.StyledString(left)
+ ui.ApplyAttrs(lbuf, style)
lwidth := lbuf.Len()
- rbuf := parse.ParseANSI(right)
- rbuf.ApplyAttrs(style)
+ rbuf := ui.StyledString(right)
+ ui.ApplyAttrs(rbuf, style)
rwidth := rbuf.Len()
if lwidth+rwidth+1 > width {
@@ -347,14 +346,21 @@ func (dirlist *DirectoryList) renderDir(
rwidth = 3 * width / 4
}
lwidth = width - rwidth - 1
- right = rbuf.TruncateHead(rwidth, '…')
- left = lbuf.Truncate(lwidth-1, '…')
+ ui.TruncateHead(rbuf, rwidth)
+ right = rbuf.Encode()
+ ui.Truncate(lbuf, lwidth)
+ left = lbuf.Encode()
} else {
for i := 0; i < (width - lwidth - rwidth - 1); i += 1 {
- lbuf.Write(' ', vaxis.Style{})
+ lbuf.Cells = append(lbuf.Cells, vaxis.Cell{
+ Character: vaxis.Character{
+ Grapheme: " ",
+ Width: 1,
+ },
+ })
}
- left = lbuf.String()
- right = rbuf.String()
+ left = lbuf.Encode()
+ right = rbuf.Encode()
}
return left, right, style
diff --git a/go.mod b/go.mod
index 8e339dea..b7a9b09d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.18
require (
git.sr.ht/~rjarry/go-opt v1.3.0
git.sr.ht/~rockorager/go-jmap v0.3.0
- git.sr.ht/~rockorager/vaxis v0.4.7
+ git.sr.ht/~rockorager/vaxis v0.7.2
github.com/ProtonMail/go-crypto v0.0.0-20230417170513-8ee5748c52b5
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964
diff --git a/go.sum b/go.sum
index 10af757c..4bc68aa8 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,8 @@ git.sr.ht/~rjarry/go-opt v1.3.0 h1:9BLOcXi5OhDYVzH3Td48i2uM/byMGNqXY7YhBzvEZg8=
git.sr.ht/~rjarry/go-opt v1.3.0/go.mod h1:oEPZUTJKGn1FVye0znaLoeskE/QTuyoJw5q+fjusdM4=
git.sr.ht/~rockorager/go-jmap v0.3.0 h1:h2WuPcNyXRYFg9+W2HGf/mzIqC6ISy9EaS/BGa7Z5RY=
git.sr.ht/~rockorager/go-jmap v0.3.0/go.mod h1:aOTCtwpZSINpDDSOkLGpHU0Kbbm5lcSDMcobX3ZtOjY=
-git.sr.ht/~rockorager/vaxis v0.4.7 h1:9VlkBBF9dxy62AMHnKAU8GqEs2/mUTlke/ZJ9o/6luk=
-git.sr.ht/~rockorager/vaxis v0.4.7/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
+git.sr.ht/~rockorager/vaxis v0.7.2 h1:w+vBMkBWiGLSMMuHwZTDBrTXrjjAoyBc3k681WBT1kU=
+git.sr.ht/~rockorager/vaxis v0.7.2/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
github.com/ProtonMail/crypto v0.0.0-20200420072808-71bec3603bf3 h1:JW27/kGLQzeM1Fxg5YQhdkTEAU7HIAHMgSag35zVTnY=
github.com/ProtonMail/crypto v0.0.0-20200420072808-71bec3603bf3/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI=
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
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)
}
}