diff options
Diffstat (limited to 'ui/style.go')
| -rw-r--r-- | ui/style.go | 375 |
1 files changed, 195 insertions, 180 deletions
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 } |
