diff options
Diffstat (limited to 'ui')
-rw-r--r-- | ui/buffers.go | 84 | ||||
-rw-r--r-- | ui/buffers_test.go | 20 | ||||
-rw-r--r-- | ui/draw_utils.go | 39 | ||||
-rw-r--r-- | ui/style.go | 375 | ||||
-rw-r--r-- | ui/style_test.go | 103 | ||||
-rw-r--r-- | ui/ui.go | 21 |
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)}, + }, + }) } @@ -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()) } |