summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/buffers.go84
-rw-r--r--ui/buffers_test.go20
-rw-r--r--ui/draw_utils.go39
-rw-r--r--ui/style.go375
-rw-r--r--ui/style_test.go103
-rw-r--r--ui/ui.go21
6 files changed, 367 insertions, 275 deletions
diff --git a/ui/buffers.go b/ui/buffers.go
index b6020a8..0ebaadf 100644
--- a/ui/buffers.go
+++ b/ui/buffers.go
@@ -19,8 +19,8 @@ type point struct {
type Line struct {
At time.Time
Head string
- Body string
- HeadColor int
+ Body StyledString
+ HeadColor tcell.Color
Highlight bool
Mergeable bool
@@ -34,29 +34,29 @@ func (l *Line) computeSplitPoints() {
l.splitPoints = []point{}
}
- var wb widthBuffer
+ width := 0
lastWasSplit := false
l.splitPoints = l.splitPoints[:0]
- for i, r := range l.Body {
+ for i, r := range l.Body.string {
curIsSplit := IsSplitRune(r)
if i == 0 || lastWasSplit != curIsSplit {
l.splitPoints = append(l.splitPoints, point{
- X: wb.Width(),
+ X: width,
I: i,
Split: curIsSplit,
})
}
lastWasSplit = curIsSplit
- wb.WriteRune(r)
+ width += runeWidth(r)
}
if !lastWasSplit {
l.splitPoints = append(l.splitPoints, point{
- X: wb.Width(),
- I: len(l.Body),
+ X: width,
+ I: len(l.Body.string),
Split: true,
})
}
@@ -118,16 +118,17 @@ func (l *Line) NewLines(width int) []int {
// terminal. In this case, no newline is placed before (like in the
// 2nd if-else branch). The for loop is used to place newlines in
// the word.
- var wb widthBuffer
+ // TODO handle multi-codepoint graphemes?? :(
+ wordWidth := 0
h := 1
- for j, r := range l.Body[sp1.I:sp2.I] {
- wb.WriteRune(r)
- if h*width < x+wb.Width() {
+ for j, r := range l.Body.string[sp1.I:sp2.I] {
+ wordWidth += runeWidth(r)
+ if h*width < x+wordWidth {
l.newLines = append(l.newLines, sp1.I+j)
h++
}
}
- x = (x + wb.Width()) % width
+ x = (x + wordWidth) % width
if x == 0 {
// The placement of the word is such that it ends right at the
// end of the row.
@@ -147,7 +148,7 @@ func (l *Line) NewLines(width int) []int {
}
}
- if 0 < len(l.newLines) && l.newLines[len(l.newLines)-1] == len(l.Body) {
+ if 0 < len(l.newLines) && l.newLines[len(l.newLines)-1] == len(l.Body.string) {
// DROP any newline that is placed at the end of the string because we
// don't care about those.
l.newLines = l.newLines[:len(l.newLines)-1]
@@ -255,14 +256,19 @@ func (bs *BufferList) AddLine(title string, highlight bool, line Line) {
b := &bs.list[idx]
n := len(b.lines)
- line.Body = strings.TrimRight(line.Body, "\t ")
line.At = line.At.UTC()
if line.Mergeable && n != 0 && b.lines[n-1].Mergeable {
l := &b.lines[n-1]
- l.Body += " " + line.Body
+ newBody := new(StyledStringBuilder)
+ newBody.Grow(len(l.Body.string) + 2 + len(line.Body.string))
+ newBody.WriteStyledString(l.Body)
+ newBody.WriteString(" ")
+ newBody.WriteStyledString(line.Body)
+ l.Body = newBody.StyledString()
l.computeSplitPoints()
l.width = 0
+ // TODO change b.scrollAmt if it's not 0 and bs.current is idx.
} else {
line.computeSplitPoints()
b.lines = append(b.lines, line)
@@ -307,7 +313,7 @@ func (bs *BufferList) AddLines(title string, lines []Line) {
limit = i
break
}
- if l.Body == firstLineBody {
+ if l.Body.string == firstLineBody.string {
// This line happened at the same millisecond
// as the first line of the buffer, and has the
// same contents. Heuristic: it's the same
@@ -396,7 +402,7 @@ func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width,
st = st.Reverse(true)
}
title := truncate(b.title, width, "\u2026")
- printString(screen, &x, y, st, title)
+ printString(screen, &x, y, Styled(title, st))
if 0 < b.highlights {
st = st.Foreground(tcell.ColorRed).Reverse(true)
screen.SetContent(x, y, ' ', nil, st)
@@ -417,10 +423,9 @@ func (bs *BufferList) DrawVerticalBufferList(screen tcell.Screen, x0, y0, width,
}
func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int) {
- st := tcell.StyleDefault
for x := x0; x < x0+bs.tlWidth; x++ {
for y := y0; y < y0+bs.tlHeight; y++ {
- screen.SetContent(x, y, ' ', nil, st)
+ screen.SetContent(x, y, ' ', nil, tcell.StyleDefault)
}
}
@@ -441,18 +446,25 @@ func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int
}
if i == 0 || b.lines[i-1].At.Truncate(time.Minute) != line.At.Truncate(time.Minute) {
- printTime(screen, x0, yi, st.Bold(true), line.At.Local())
+ st := tcell.StyleDefault.Bold(true)
+ printTime(screen, x0, yi, st, line.At.Local())
}
- identSt := st.Foreground(colorFromCode(line.HeadColor)).Reverse(line.Highlight)
- printIdent(screen, x0+7, yi, nickColWidth, identSt, line.Head)
+ identSt := tcell.StyleDefault.
+ Foreground(line.HeadColor).
+ Reverse(line.Highlight)
+ printIdent(screen, x0+7, yi, nickColWidth, Styled(line.Head, identSt))
x := x1
y := yi
+ style := tcell.StyleDefault
+ nextStyles := line.Body.styles
- var sb StyleBuffer
- sb.Reset()
- for i, r := range line.Body {
+ for i, r := range line.Body.string {
+ if 0 < len(nextStyles) && nextStyles[0].Start == i {
+ style = nextStyles[0].Style
+ nextStyles = nextStyles[1:]
+ }
if 0 < len(nls) && i == nls[0] {
x = x1
y++
@@ -466,26 +478,10 @@ func (bs *BufferList) DrawTimeline(screen tcell.Screen, x0, y0, nickColWidth int
continue
}
- if st, ok := sb.WriteRune(r); ok != 0 {
- if 1 < ok {
- screen.SetContent(x, y, ',', nil, st)
- x++
- }
- screen.SetContent(x, y, r, nil, st)
- x += runeWidth(r)
- }
+ screen.SetContent(x, y, r, nil, style)
+ x += runeWidth(r)
}
-
- sb.Reset()
}
b.isAtTop = y0 <= yi
}
-
-func IrcColorCode(code int) string {
- var c [3]rune
- c[0] = 0x03
- c[1] = rune(code/10) + '0'
- c[2] = rune(code%10) + '0'
- return string(c[:])
-}
diff --git a/ui/buffers_test.go b/ui/buffers_test.go
index b78f8e9..13a32de 100644
--- a/ui/buffers_test.go
+++ b/ui/buffers_test.go
@@ -6,7 +6,7 @@ import (
)
func assertSplitPoints(t *testing.T, body string, expected []point) {
- l := Line{Body: body}
+ l := Line{Body: PlainString(body)}
l.computeSplitPoints()
if len(l.splitPoints) != len(expected) {
@@ -71,7 +71,7 @@ func showSplit(s string, nls []int) string {
}
func assertNewLines(t *testing.T, body string, width int, expected int) {
- l := Line{Body: body}
+ l := Line{Body: PlainString(body)}
l.computeSplitPoints()
actual := l.NewLines(width)
@@ -141,6 +141,8 @@ func TestRenderedHeight(t *testing.T) {
assertNewLines(t, "have a good day!", 17, 1) // |have a good day! |
// LEN=15, WIDTH=11
+ // TODO find other zero- or two-width chars, since IRC color codes are
+ // now handled by app.go
assertNewLines(t, "\x0342barmand\x03: cc", 1, 10) // |b|a|r|m|a|n|d|:|c|c|
assertNewLines(t, "\x0342barmand\x03: cc", 2, 5) // |ba|rm|an|d:|cc|
assertNewLines(t, "\x0342barmand\x03: cc", 3, 4) // |bar|man|d: |cc |
@@ -160,17 +162,3 @@ func TestRenderedHeight(t *testing.T) {
assertNewLines(t, "cc en direct du word wrapping des familles le tests ça v a va va v a va", 46, 2)
}
-
-/*
-func assertTrimWidth(t *testing.T, s string, w int, expected string) {
- actual := trimWidth(s, w)
- if actual != expected {
- t.Errorf("%q (width=%d): expected to be trimmed as %q, got %q\n", s, w, expected, actual)
- }
-}
-
-func TestTrimWidth(t *testing.T) {
- assertTrimWidth(t, "ludovicchabant/fn", 16, "ludovicchabant/…")
- assertTrimWidth(t, "zzzzzzzzzzzzzz黒猫/sr", 16, "zzzzzzzzzzzzzz黒…")
-}
-// */
diff --git a/ui/draw_utils.go b/ui/draw_utils.go
index 893a77f..947e71b 100644
--- a/ui/draw_utils.go
+++ b/ui/draw_utils.go
@@ -7,24 +7,39 @@ import (
"github.com/gdamore/tcell/v2"
)
-func printIdent(screen tcell.Screen, x, y, width int, st tcell.Style, s string) {
- s = truncate(s, width, "\u2026")
- x += width - StringWidth(s)
- screen.SetContent(x-1, y, ' ', nil, st)
- printString(screen, &x, y, st, s)
- screen.SetContent(x, y, ' ', nil, st)
+func printString(screen tcell.Screen, x *int, y int, s StyledString) {
+ style := tcell.StyleDefault
+ nextStyles := s.styles
+ for i, r := range s.string {
+ if 0 < len(nextStyles) && nextStyles[0].Start == i {
+ style = nextStyles[0].Style
+ nextStyles = nextStyles[1:]
+ }
+ screen.SetContent(*x, y, r, nil, style)
+ *x += runeWidth(r)
+ }
}
-func printString(screen tcell.Screen, x *int, y int, st tcell.Style, s string) {
- for _, r := range s {
- screen.SetContent(*x, y, r, nil, st)
- *x += runeWidth(r)
+func printIdent(screen tcell.Screen, x, y, width int, s StyledString) {
+ s.string = truncate(s.string, width, "\u2026")
+ x += width - stringWidth(s.string)
+ st := tcell.StyleDefault
+ if len(s.styles) != 0 && s.styles[0].Start == 0 {
+ st = s.styles[0].Style
+ }
+ screen.SetContent(x-1, y, ' ', nil, st)
+ printString(screen, &x, y, s)
+ if len(s.styles) != 0 {
+ // TODO check if it's not a style that is from the truncated
+ // part of s.
+ st = s.styles[len(s.styles)-1].Style
}
+ screen.SetContent(x, y, ' ', nil, st)
}
func printNumber(screen tcell.Screen, x *int, y int, st tcell.Style, n int) {
- s := fmt.Sprintf("%d", n)
- printString(screen, x, y, st, s)
+ s := Styled(fmt.Sprintf("%d", n), st)
+ printString(screen, x, y, s)
}
func printTime(screen tcell.Screen, x int, y int, st tcell.Style, t time.Time) {
diff --git a/ui/style.go b/ui/style.go
index d86e9f4..f596ac0 100644
--- a/ui/style.go
+++ b/ui/style.go
@@ -1,7 +1,10 @@
package ui
import (
- "hash/fnv"
+ "fmt"
+ "strconv"
+ "strings"
+ "unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
@@ -13,234 +16,246 @@ func runeWidth(r rune) int {
return condition.RuneWidth(r)
}
+func stringWidth(s string) int {
+ return condition.StringWidth(s)
+}
+
func truncate(s string, w int, tail string) string {
return condition.Truncate(s, w, tail)
}
-type widthBuffer struct {
- width int
- color colorBuffer
+// Taken from <https://modern.ircdocs.horse/formatting.html>
+
+var baseCodes = []tcell.Color{
+ tcell.ColorWhite, tcell.ColorBlack, tcell.ColorBlue, tcell.ColorGreen,
+ tcell.ColorRed, tcell.ColorBrown, tcell.ColorPurple, tcell.ColorOrange,
+ tcell.ColorYellow, tcell.ColorLightGreen, tcell.ColorTeal, tcell.ColorLightCyan,
+ tcell.ColorLightBlue, tcell.ColorPink, tcell.ColorGrey, tcell.ColorLightGrey,
}
-func (wb *widthBuffer) Width() int {
- return wb.width
+// unused
+var ansiCodes = []uint64{
+ /* 16-27 */ 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
+ /* 28-39 */ 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
+ /* 40-51 */ 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
+ /* 52-63 */ 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198,
+ /* 64-75 */ 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
+ /* 76-87 */ 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
+ /* 88-98 */ 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231,
}
-func (wb *widthBuffer) WriteString(s string) {
- for _, r := range s {
- wb.WriteRune(r)
- }
+var hexCodes = []int32{
+ 0x470000, 0x472100, 0x474700, 0x324700, 0x004700, 0x00472c, 0x004747, 0x002747, 0x000047, 0x2e0047, 0x470047, 0x47002a,
+ 0x740000, 0x743a00, 0x747400, 0x517400, 0x007400, 0x007449, 0x007474, 0x004074, 0x000074, 0x4b0074, 0x740074, 0x740045,
+ 0xb50000, 0xb56300, 0xb5b500, 0x7db500, 0x00b500, 0x00b571, 0x00b5b5, 0x0063b5, 0x0000b5, 0x7500b5, 0xb500b5, 0xb5006b,
+ 0xff0000, 0xff8c00, 0xffff00, 0xb2ff00, 0x00ff00, 0x00ffa0, 0x00ffff, 0x008cff, 0x0000ff, 0xa500ff, 0xff00ff, 0xff0098,
+ 0xff5959, 0xffb459, 0xffff71, 0xcfff60, 0x6fff6f, 0x65ffc9, 0x6dffff, 0x59b4ff, 0x5959ff, 0xc459ff, 0xff66ff, 0xff59bc,
+ 0xff9c9c, 0xffd39c, 0xffff9c, 0xe2ff9c, 0x9cff9c, 0x9cffdb, 0x9cffff, 0x9cd3ff, 0x9c9cff, 0xdc9cff, 0xff9cff, 0xff94d3,
+ 0x000000, 0x131313, 0x282828, 0x363636, 0x4d4d4d, 0x656565, 0x818181, 0x9f9f9f, 0xbcbcbc, 0xe2e2e2, 0xffffff,
}
-func (wb *widthBuffer) WriteRune(r rune) {
- if ok := wb.color.WriteRune(r); ok != 0 {
- if 1 < ok {
- wb.width++
- }
- wb.width += runeWidth(r)
+func colorFromCode(code int) (color tcell.Color) {
+ if code < 0 || 99 <= code {
+ color = tcell.ColorDefault
+ } else if code < 16 {
+ color = baseCodes[code]
+ } else {
+ color = tcell.NewHexColor(hexCodes[code-16])
}
+ return
}
-func StringWidth(s string) int {
- var wb widthBuffer
- wb.WriteString(s)
- return wb.Width()
+type rangedStyle struct {
+ Start int // byte index at which Style is effective
+ Style tcell.Style
}
-type StyleBuffer struct {
- st tcell.Style
- color colorBuffer
- bold bool
- reverse bool
- italic bool
- strikethrough bool
- underline bool
+type StyledString struct {
+ string
+ styles []rangedStyle // sorted, elements cannot have the same Start value
}
-func (sb *StyleBuffer) Reset() {
- sb.color.Reset()
- sb.st = tcell.StyleDefault
- sb.bold = false
- sb.reverse = false
- sb.italic = false
- sb.strikethrough = false
- sb.underline = false
+func PlainString(s string) StyledString {
+ return StyledString{string: s}
}
-func (sb *StyleBuffer) WriteRune(r rune) (st tcell.Style, ok int) {
- if r == 0x00 || r == 0x0F {
- sb.Reset()
- return sb.st, 0
- }
- if r == 0x02 {
- sb.bold = !sb.bold
- sb.st = sb.st.Bold(sb.bold)
- return sb.st, 0
- }
- if r == 0x16 {
- sb.reverse = !sb.reverse
- sb.st = st.Reverse(sb.reverse)
- return sb.st, 0
- }
- if r == 0x1D {
- sb.italic = !sb.italic
- sb.st = st.Italic(sb.italic)
- return sb.st, 0
- }
- if r == 0x1E {
- sb.strikethrough = !sb.strikethrough
- sb.st = st.StrikeThrough(sb.strikethrough)
- return sb.st, 0
- }
- if r == 0x1F {
- sb.underline = !sb.underline
- sb.st = st.Underline(sb.underline)
- return sb.st, 0
+func PlainSprintf(format string, a ...interface{}) StyledString {
+ return PlainString(fmt.Sprintf(format, a...))
+}
+
+func Styled(s string, style tcell.Style) StyledString {
+ rStyle := rangedStyle{
+ Start: 0,
+ Style: style,
}
- if ok = sb.color.WriteRune(r); ok != 0 {
- sb.st = sb.color.Style(sb.st)
+ return StyledString{
+ string: s,
+ styles: []rangedStyle{rStyle},
}
-
- return sb.st, ok
}
-type colorBuffer struct {
- state int
- fg, bg int
+func (s StyledString) String() string {
+ return s.string
}
-func (cb *colorBuffer) Reset() {
- cb.state = 0
- cb.fg = -1
- cb.bg = -1
+func isDigit(c byte) bool {
+ return '0' <= c && c <= '9'
}
-func (cb *colorBuffer) Style(st tcell.Style) tcell.Style {
- if 0 <= cb.fg {
- st = st.Foreground(colorFromCode(cb.fg))
- } else {
- st = st.Foreground(tcell.ColorDefault)
+func parseColorNumber(raw string) (color tcell.Color, n int) {
+ if len(raw) == 0 || !isDigit(raw[0]) {
+ return
}
- if 0 <= cb.bg {
- st = st.Background(colorFromCode(cb.bg))
- } else {
- st = st.Background(tcell.ColorDefault)
+
+ // len(raw) >= 1 and its first character is a digit.
+
+ if len(raw) == 1 || !isDigit(raw[1]) {
+ code, _ := strconv.Atoi(raw[:1])
+ return colorFromCode(code), 1
}
- return st
+
+ // len(raw) >= 2 and the two first characters are digits.
+
+ code, _ := strconv.Atoi(raw[:2])
+ return colorFromCode(code), 2
}
-func (cb *colorBuffer) WriteRune(r rune) (ok int) {
- if cb.state == 1 {
- if '0' <= r && r <= '9' {
- cb.fg = int(r - '0')
- cb.state = 2
- return
- }
- } else if cb.state == 2 {
- if '0' <= r && r <= '9' {
- cb.fg = 10*cb.fg + int(r-'0')
- cb.state = 3
- return
- }
- if r == ',' {
- cb.state = 4
- return
- }
- } else if cb.state == 3 {
- if r == ',' {
- cb.state = 4
- return
- }
- } else if cb.state == 4 {
- if '0' <= r && r <= '9' {
- cb.bg = int(r - '0')
- cb.state = 5
- return
- }
- ok++
- } else if cb.state == 5 {
- cb.state = 0
- if '0' <= r && r <= '9' {
- cb.bg = 10*cb.bg + int(r-'0')
- return
- }
+func parseColor(raw string) (fg, bg tcell.Color, n int) {
+ fg, n = parseColorNumber(raw)
+ raw = raw[n:]
+
+ if len(raw) == 0 || raw[0] != ',' {
+ return fg, tcell.ColorDefault, n
}
- if r == 0x03 {
- cb.state = 1
- cb.fg = -1
- cb.bg = -1
- return
+ n++
+ bg, p := parseColorNumber(raw[1:])
+ n += p
+
+ if bg == tcell.ColorDefault {
+ // Lone comma, do not parse as part of a color code.
+ return fg, tcell.ColorDefault, n - 1
}
- cb.state = 0
- ok++
- return
+ return fg, bg, n
}
-const (
- ColorWhite = iota
- ColorBlack
- ColorBlue
- ColorGreen
- ColorRed
-)
+func IRCString(raw string) StyledString {
+ var formatted strings.Builder
+ var styles []rangedStyle
+ var last tcell.Style
-// Taken from <https://modern.ircdocs.horse/formatting.html>
+ for len(raw) != 0 {
+ r, runeSize := utf8.DecodeRuneInString(raw)
+ if r == utf8.RuneError {
+ break
+ }
+ _, _, lastAttrs := last.Decompose()
+ current := last
+ if r == 0x0F {
+ current = tcell.StyleDefault
+ } else if r == 0x02 {
+ lastWasBold := lastAttrs&tcell.AttrBold != 0
+ current = last.Bold(!lastWasBold)
+ } else if r == 0x03 {
+ fg, bg, n := parseColor(raw[1:])
+ raw = raw[n:]
+ if n == 0 {
+ // Both `fg` and `bg` are equal to
+ // tcell.ColorDefault.
+ current = last.Foreground(tcell.ColorDefault).
+ Background(tcell.ColorDefault)
+ } else if bg == tcell.ColorDefault {
+ current = last.Foreground(fg)
+ } else {
+ current = last.Foreground(fg).Background(bg)
+ }
+ } else if r == 0x16 {
+ lastWasReverse := lastAttrs&tcell.AttrReverse != 0
+ current = last.Reverse(!lastWasReverse)
+ } else if r == 0x1D {
+ lastWasItalic := lastAttrs&tcell.AttrItalic != 0
+ current = last.Italic(!lastWasItalic)
+ } else if r == 0x1E {
+ lastWasStrikeThrough := lastAttrs&tcell.AttrStrikeThrough != 0
+ current = last.StrikeThrough(!lastWasStrikeThrough)
+ } else if r == 0x1F {
+ lastWasUnderline := lastAttrs&tcell.AttrUnderline != 0
+ current = last.Underline(!lastWasUnderline)
+ } else {
+ formatted.WriteRune(r)
+ }
+ if last != current {
+ if len(styles) != 0 && styles[len(styles)-1].Start == formatted.Len() {
+ styles[len(styles)-1] = rangedStyle{
+ Start: formatted.Len(),
+ Style: current,
+ }
+ } else {
+ styles = append(styles, rangedStyle{
+ Start: formatted.Len(),
+ Style: current,
+ })
+ }
+ }
+ last = current
+ raw = raw[runeSize:]
+ }
-var baseCodes = []tcell.Color{
- tcell.ColorWhite, tcell.ColorBlack, tcell.ColorBlue, tcell.ColorGreen,
- tcell.ColorRed, tcell.ColorBrown, tcell.ColorPurple, tcell.ColorOrange,
- tcell.ColorYellow, tcell.ColorLightGreen, tcell.ColorTeal, tcell.ColorLightCyan,
- tcell.ColorLightBlue, tcell.ColorPink, tcell.ColorGrey, tcell.ColorLightGrey,
+ return StyledString{
+ string: formatted.String(),
+ styles: styles,
+ }
}
-var ansiCodes = []uint64{
- /* 16-27 */ 52, 94, 100, 58, 22, 29, 23, 24, 17, 54, 53, 89,
- /* 28-39 */ 88, 130, 142, 64, 28, 35, 30, 25, 18, 91, 90, 125,
- /* 40-51 */ 124, 166, 184, 106, 34, 49, 37, 33, 19, 129, 127, 161,
- /* 52-63 */ 196, 208, 226, 154, 46, 86, 51, 75, 21, 171, 201, 198,
- /* 64-75 */ 203, 215, 227, 191, 83, 122, 87, 111, 63, 177, 207, 205,
- /* 76-87 */ 217, 223, 229, 193, 157, 158, 159, 153, 147, 183, 219, 212,
- /* 88-98 */ 16, 233, 235, 237, 239, 241, 244, 247, 250, 254, 231,
+type StyledStringBuilder struct {
+ strings.Builder
+ styles []rangedStyle
}
-var hexCodes = []int32{
- 0x470000, 0x472100, 0x474700, 0x324700, 0x004700, 0x00472c, 0x004747, 0x002747, 0x000047, 0x2e0047, 0x470047, 0x47002a,
- 0x740000, 0x743a00, 0x747400, 0x517400, 0x007400, 0x007449, 0x007474, 0x004074, 0x000074, 0x4b0074, 0x740074, 0x740045,
- 0xb50000, 0xb56300, 0xb5b500, 0x7db500, 0x00b500, 0x00b571, 0x00b5b5, 0x0063b5, 0x0000b5, 0x7500b5, 0xb500b5, 0xb5006b,
- 0xff0000, 0xff8c00, 0xffff00, 0xb2ff00, 0x00ff00, 0x00ffa0, 0x00ffff, 0x008cff, 0x0000ff, 0xa500ff, 0xff00ff, 0xff0098,
- 0xff5959, 0xffb459, 0xffff71, 0xcfff60, 0x6fff6f, 0x65ffc9, 0x6dffff, 0x59b4ff, 0x5959ff, 0xc459ff, 0xff66ff, 0xff59bc,
- 0xff9c9c, 0xffd39c, 0xffff9c, 0xe2ff9c, 0x9cff9c, 0x9cffdb, 0x9cffff, 0x9cd3ff, 0x9c9cff, 0xdc9cff, 0xff9cff, 0xff94d3,
- 0x000000, 0x131313, 0x282828, 0x363636, 0x4d4d4d, 0x656565, 0x818181, 0x9f9f9f, 0xbcbcbc, 0xe2e2e2, 0xffffff,
+func (sb *StyledStringBuilder) WriteStyledString(s StyledString) {
+ start := len(sb.styles)
+ sb.styles = append(sb.styles, s.styles...)
+ for i := start; i < len(sb.styles); i++ {
+ sb.styles[i].Start += sb.Len()
+ }
+ sb.WriteString(s.string)
}
-func colorFromCode(code int) (color tcell.Color) {
- if code < 0 || 99 <= code {
- color = tcell.ColorDefault
- } else if code < 16 {
- color = baseCodes[code]
- } else {
- color = tcell.NewHexColor(hexCodes[code-16])
+func (sb *StyledStringBuilder) AddStyle(start int, style tcell.Style) {
+ for i := 0; i < len(sb.styles); i++ {
+ if sb.styles[i].Start == i {
+ sb.styles[i].Style = style
+ break
+ } else if sb.styles[i].Start < i {
+ sb.styles = append(sb.styles[:i+1], sb.styles[i:]...)
+ sb.styles[i+1] = rangedStyle{
+ Start: start,
+ Style: style,
+ }
+ break
+ }
}
- return
+ sb.styles = append(sb.styles, rangedStyle{
+ Start: start,
+ Style: style,
+ })
}
-// see <https://modern.ircdocs.horse/formatting.html>
-var identColorBlacklist = []int{
- 1, 8, 16, 17, 24, 27, 28, 36, 48, 60, 88, 89, 90, 91,
+func (sb *StyledStringBuilder) SetStyle(style tcell.Style) {
+ sb.styles = append(sb.styles, rangedStyle{
+ Start: sb.Len(),
+ Style: style,
+ })
}
-func IdentColor(s string) (code int) {
- h := fnv.New32()
- _, _ = h.Write([]byte(s))
-
- code = int(h.Sum32()) % (99 - len(identColorBlacklist))
- for _, c := range identColorBlacklist {
- if c <= code {
- code++
- }
+func (sb *StyledStringBuilder) StyledString() StyledString {
+ styles := sb.styles
+ if len(sb.styles) != 0 && sb.styles[len(sb.styles)-1].Start == sb.Len() {
+ styles = sb.styles[:len(sb.styles)-1]
+ }
+ return StyledString{
+ string: sb.String(),
+ styles: styles,
}
-
- return
}
diff --git a/ui/style_test.go b/ui/style_test.go
index ae478ab..4ba2e9d 100644
--- a/ui/style_test.go
+++ b/ui/style_test.go
@@ -1,25 +1,96 @@
package ui
-import "testing"
+import (
+ "testing"
-func assertStringWidth(t *testing.T, input string, expected int) {
- actual := StringWidth(input)
- if actual != expected {
- t.Errorf("%q: expected width of %d got %d", input, expected, actual)
+ "github.com/gdamore/tcell/v2"
+)
+
+func assertIRCString(t *testing.T, input string, expected StyledString) {
+ actual := IRCString(input)
+ if actual.string != expected.string {
+ t.Errorf("%q: expected string %q, got %q", input, expected.string, actual.string)
+ }
+ if len(actual.styles) != len(expected.styles) {
+ t.Errorf("%q: expected %d styles, got %d", input, len(expected.styles), len(actual.styles))
+ return
+ }
+ for i := range actual.styles {
+ if actual.styles[i] != expected.styles[i] {
+ t.Errorf("%q: style #%d expected to be %+v, got %+v", input, i, expected.styles[i], actual.styles[i])
+ }
}
}
-func TestStringWidth(t *testing.T) {
- assertStringWidth(t, "", 0)
+func TestIRCString(t *testing.T) {
+ assertIRCString(t, "", StyledString{
+ string: "",
+ styles: nil,
+ })
- assertStringWidth(t, "hello", 5)
- assertStringWidth(t, "\x02hello", 5)
- assertStringWidth(t, "\x035hello", 5)
- assertStringWidth(t, "\x0305hello", 5)
- assertStringWidth(t, "\x0305,0hello", 5)
- assertStringWidth(t, "\x0305,09hello", 5)
+ assertIRCString(t, "hello", StyledString{
+ string: "hello",
+ styles: nil,
+ })
+ assertIRCString(t, "\x02hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Bold(true)},
+ },
+ })
+ assertIRCString(t, "\x035hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x0305hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x0305,0hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
+ },
+ })
+ assertIRCString(t, "\x035,00hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
+ },
+ })
+ assertIRCString(t, "\x0305,00hello", StyledString{
+ string: "hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
+ },
+ })
- assertStringWidth(t, "\x0305,hello", 6)
- assertStringWidth(t, "\x03050hello", 6)
- assertStringWidth(t, "\x0305,090hello", 6)
+ assertIRCString(t, "\x035,hello", StyledString{
+ string: ",hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x0305,hello", StyledString{
+ string: ",hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x03050hello", StyledString{
+ string: "0hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown)},
+ },
+ })
+ assertIRCString(t, "\x0305,000hello", StyledString{
+ string: "0hello",
+ styles: []rangedStyle{
+ rangedStyle{Start: 0, Style: tcell.StyleDefault.Foreground(tcell.ColorBrown).Background(tcell.ColorWhite)},
+ },
+ })
}
diff --git a/ui/ui.go b/ui/ui.go
index 9f730a3..be43272 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -12,7 +12,7 @@ type Config struct {
NickColWidth int
ChanColWidth int
AutoComplete func(cursorIdx int, text []rune) []Completion
- Mouse bool
+ Mouse bool
}
type UI struct {
@@ -23,7 +23,7 @@ type UI struct {
bs BufferList
e Editor
- prompt string
+ prompt StyledString
status string
}
@@ -159,7 +159,7 @@ func (ui *UI) SetStatus(status string) {
ui.status = status
}
-func (ui *UI) SetPrompt(prompt string) {
+func (ui *UI) SetPrompt(prompt StyledString) {
ui.prompt = prompt
}
@@ -245,8 +245,7 @@ func (ui *UI) Draw() {
for x := ui.config.ChanColWidth; x < 9+ui.config.ChanColWidth+ui.config.NickColWidth; x++ {
ui.screen.SetContent(x, h-1, ' ', nil, tcell.StyleDefault)
}
- st := tcell.StyleDefault.Foreground(colorFromCode(IdentColor(ui.prompt)))
- printIdent(ui.screen, ui.config.ChanColWidth+7, h-1, ui.config.NickColWidth, st, ui.prompt)
+ printIdent(ui.screen, ui.config.ChanColWidth+7, h-1, ui.config.NickColWidth, ui.prompt)
ui.screen.Show()
}
@@ -262,8 +261,16 @@ func (ui *UI) drawStatusBar(x0, y, width int) {
return
}
+ s := new(StyledStringBuilder)
+ s.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
+ s.WriteString("--")
+
x := x0 + 5 + ui.config.NickColWidth
- printString(ui.screen, &x, y, st, "--")
+ printString(ui.screen, &x, y, s.StyledString())
x += 2
- printString(ui.screen, &x, y, st, ui.status)
+
+ s.Reset()
+ s.WriteString(ui.status)
+
+ printString(ui.screen, &x, y, s.StyledString())
}