From 826c27196474e6dfd4dc19a5d4883af8757ccfaa Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Mon, 4 Mar 2024 20:22:09 -0800 Subject: [PATCH] Colored underlines. This supports UNIX and Windows. --- _demos/style.go | 12 +++++++ console_win.go | 18 ++++++++-- style.go | 78 ++++++++++++++++---------------------------- style_test.go | 6 ++-- terminfo/terminfo.go | 3 ++ tscreen.go | 63 ++++++++++++++++++++++++++++++----- 6 files changed, 117 insertions(+), 63 deletions(-) diff --git a/_demos/style.go b/_demos/style.go index 31d42a1..d8a801e 100644 --- a/_demos/style.go +++ b/_demos/style.go @@ -171,6 +171,18 @@ func main() { puts(s, style, 2, row, "Dashed Underline") row++ + style = plain.Underline(true).UnderlineColor(tcell.ColorBlue) + puts(s, style, 2, row, "Blue Underline") + row++ + + style = plain.Underline(true).UnderlineColor(tcell.ColorHoneydew) + puts(s, style, 2, row, "Honeydew Underline") + row++ + + style = plain.CurlyUnderline(true).UnderlineColor(tcell.NewRGBColor(0xc5, 0x8a, 0xf9)) + puts(s, style, 2, row, "Pink Curly Underline") + row++ + style = plain.Url("http://github.com/gdamore/tcell") puts(s, style, 2, row, "HyperLink") row++ diff --git a/console_win.go b/console_win.go index 8def0bb..eab107c 100644 --- a/console_win.go +++ b/console_win.go @@ -168,6 +168,9 @@ const ( vtCurlyUnderline = "\x1b[4:3m" vtDottedUnderline = "\x1b[4:4m" vtDashedUnderline = "\x1b[4:5m" + vtUnderColor = "\x1b[58:5:%dm" + vtUnderColorRGB = "\x1b[58:2::%d:%d:%dm" + vtUnderColorReset = "\x1b[59m" ) var vtCursorStyles = map[CursorStyle]string{ @@ -879,7 +882,7 @@ func mapColor2RGB(c Color) uint16 { // Map a tcell style to Windows attributes func (s *cScreen) mapStyle(style Style) uint16 { - f, b, a := style.Decompose() + f, b, a := style.fg, style.bg, style.attrs fa := s.oscreen.attrs & 0xf ba := (s.oscreen.attrs) >> 4 & 0xf if f != ColorDefault && f != ColorReset { @@ -916,7 +919,7 @@ func (s *cScreen) mapStyle(style Style) uint16 { func (s *cScreen) sendVtStyle(style Style) { esc := &strings.Builder{} - fg, bg, attrs := style.Decompose() + fg, bg, uc, attrs := style.fg, style.bg, style.under, style.attrs esc.WriteString(vtSgr0) @@ -927,6 +930,17 @@ func (s *cScreen) sendVtStyle(style Style) { esc.WriteString(vtBlink) } if attrs&(AttrUnderline|AttrDoubleUnderline|AttrCurlyUnderline|AttrDottedUnderline|AttrDashedUnderline) != 0 { + if uc.Valid() { + if uc == ColorReset { + esc.WriteString(vtUnderColorReset) + } else if uc.IsRGB() { + r, g, b := uc.RGB() + _, _ = fmt.Fprintf(esc, vtUnderColorRGB, int(r), int(g), int(b)) + } else { + _, _ = fmt.Fprintf(esc, vtUnderColor, uc&0xff) + } + } + esc.WriteString(vtUnderline) // legacy ConHost does not understand these but Terminal does if (attrs & AttrDoubleUnderline) != 0 { diff --git a/style.go b/style.go index 134f0fd..04dfe13 100644 --- a/style.go +++ b/style.go @@ -25,6 +25,7 @@ package tcell type Style struct { fg Color bg Color + under Color attrs AttrMask url string urlId string @@ -40,50 +41,35 @@ var styleInvalid = Style{attrs: AttrInvalid} // Foreground returns a new style based on s, with the foreground color set // as requested. ColorDefault can be used to select the global default. func (s Style) Foreground(c Color) Style { - return Style{ - fg: c, - bg: s.bg, - attrs: s.attrs, - url: s.url, - urlId: s.urlId, - } + s2 := s + s2.fg = c + return s2 } // Background returns a new style based on s, with the background color set // as requested. ColorDefault can be used to select the global default. func (s Style) Background(c Color) Style { - return Style{ - fg: s.fg, - bg: c, - attrs: s.attrs, - url: s.url, - urlId: s.urlId, - } + s2 := s + s2.bg = c + return s2 } // Decompose breaks a style up, returning the foreground, background, // and other attributes. The URL if set is not included. +// Deprecated: Applications should not attempt to decompose style, +// as this content is not sufficient to describe the actual style. func (s Style) Decompose() (fg Color, bg Color, attr AttrMask) { return s.fg, s.bg, s.attrs } func (s Style) setAttrs(attrs AttrMask, on bool) Style { + s2 := s if on { - return Style{ - fg: s.fg, - bg: s.bg, - attrs: s.attrs | attrs, - url: s.url, - urlId: s.urlId, - } - } - return Style{ - fg: s.fg, - bg: s.bg, - attrs: s.attrs &^ attrs, - url: s.url, - urlId: s.urlId, + s2.attrs |= attrs + } else { + s2.attrs &^= attrs } + return s2 } // Normal returns the style with all attributes disabled. @@ -152,29 +138,27 @@ func (s Style) DashedUnderline(on bool) Style { return s.setAttrs(AttrDashedUnderline, on) } +func (s Style) UnderlineColor(c Color) Style { + s2 := s + s2.under = c + return s2 +} + // Attributes returns a new style based on s, with its attributes set as // specified. func (s Style) Attributes(attrs AttrMask) Style { - return Style{ - fg: s.fg, - bg: s.bg, - attrs: attrs, - url: s.url, - urlId: s.urlId, - } + s2 := s + s2.attrs = attrs + return s2 } // Url returns a style with the Url set. If the provided Url is not empty, // and the terminal supports it, text will typically be marked up as a clickable // link to that Url. If the Url is empty, then this mode is turned off. func (s Style) Url(url string) Style { - return Style{ - fg: s.fg, - bg: s.bg, - attrs: s.attrs, - url: url, - urlId: s.urlId, - } + s2 := s + s2.url = url + return s2 } // UrlId returns a style with the UrlId set. If the provided UrlId is not empty, @@ -182,11 +166,7 @@ func (s Style) Url(url string) Style { // terminal supports it, any text with the same UrlId will be grouped as if it // were one Url, even if it spans multiple lines. func (s Style) UrlId(id string) Style { - return Style{ - fg: s.fg, - bg: s.bg, - attrs: s.attrs, - url: s.url, - urlId: "id=" + id, - } + s2 := s + s2.urlId = "id=" + id + return s2 } diff --git a/style_test.go b/style_test.go index 861c65f..b771c11 100644 --- a/style_test.go +++ b/style_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 The TCell Authors +// Copyright 2024 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use file except in compliance with the License. @@ -23,7 +23,7 @@ func TestStyle(t *testing.T) { defer s.Fini() style := StyleDefault - fg, bg, attr := style.Decompose() + fg, bg, attr := style.fg, style.bg, style.attrs if fg != ColorDefault || bg != ColorDefault || attr != AttrNone { t.Errorf("Bad default style (%v, %v, %v)", fg, bg, attr) @@ -34,7 +34,7 @@ func TestStyle(t *testing.T) { Foreground(ColorBlue). Blink(true) - fg, bg, attr = s2.Decompose() + fg, bg, attr = s2.fg, s2.bg, s2.attrs if fg != ColorBlue || bg != ColorRed || attr != AttrBlink { t.Errorf("Bad custom style (%v, %v, %v)", fg, bg, attr) } diff --git a/terminfo/terminfo.go b/terminfo/terminfo.go index b74f8c8..6125809 100644 --- a/terminfo/terminfo.go +++ b/terminfo/terminfo.go @@ -238,6 +238,9 @@ type Terminfo struct { CurlyUnderline string // Smulx with param 3 DottedUnderline string // Smulx with param 4 DashedUnderline string // Smulx with param 5 + UnderlineColor string // Setuc1 + UnderlineColorRGB string // Setulc + UnderlineColorReset string // ol XTermLike bool // (XT) has XTerm extensions } diff --git a/tscreen.go b/tscreen.go index a854486..7be2dab 100644 --- a/tscreen.go +++ b/tscreen.go @@ -32,7 +32,6 @@ import ( "golang.org/x/text/transform" "github.com/gdamore/tcell/v2/terminfo" - ) // NewTerminfoScreen returns a Screen that uses the stock TTY interface @@ -156,6 +155,9 @@ type tScreen struct { curlyUnder string dottedUnder string dashedUnder string + underColor string + underRGB string + underFg string cursorStyles map[CursorStyle]string cursorStyle CursorStyle saved *term.State @@ -356,20 +358,44 @@ func (t *tScreen) prepareUnderlines() { } if t.ti.CurlyUnderline != "" { t.curlyUnder = t.ti.CurlyUnderline - } else { + } else if t.ti.XTermLike { t.curlyUnder = "\x1b[4:3m" } if t.ti.DottedUnderline != "" { t.dottedUnder = t.ti.DottedUnderline - } else { + } else if t.ti.XTermLike { t.dottedUnder = "\x1b[4:4m" } if t.ti.DashedUnderline != "" { t.dashedUnder = t.ti.DashedUnderline - } else { + } else if t.ti.XTermLike { t.dashedUnder = "\x1b[4:5m" } - // Still TODO: Underline Color + + // Underline colors. We're not going to rely upon terminfo for this + // Essentially all terminals that support the curly underlines are + // expected to also support coloring them too - which reflects actual + // practice since these were introduced at about the same time. + if t.ti.UnderlineColor != "" { + t.underColor = t.ti.UnderlineColor + } else if t.ti.CurlyUnderline != "" { + t.underColor = "\x1b[58:5:%p1%dm" + } + if t.ti.UnderlineColorRGB != "" { + // An interesting wart here is that in order to facilitate + // using just a single parameter, the Setulc parameter takes + // the 24-bit color as an integer rather than separate bytes. + // This matches the "new" style direct color approach that + // ncurses took, even though everyone else when another way. + t.underRGB = t.ti.UnderlineColorRGB + } else if t.ti.CurlyUnderline != "" { + t.underRGB = "\x1b[58:2::%p1%d:%p2%d:%p3%dm" + } + if t.ti.UnderlineColorReset != "" { + t.underFg = t.ti.UnderlineColorReset + } else if t.ti.CurlyUnderline != "" { + t.underFg = "\x1b[59m" + } } func (t *tScreen) prepareExtendedOSC() { @@ -435,7 +461,6 @@ func (t *tScreen) prepareCursorStyles() { } } - // Still TODO: Cursor Color } func (t *tScreen) prepareKey(key Key, val string) { @@ -771,7 +796,7 @@ func (t *tScreen) drawCell(x, y int) int { style = t.style } if style != t.curstyle { - fg, bg, attrs := style.Decompose() + fg, bg, attrs, uc := style.fg, style.bg, style.attrs, style.under t.TPuts(ti.AttrOff) @@ -780,6 +805,27 @@ func (t *tScreen) drawCell(x, y int) int { t.TPuts(ti.Bold) } if attrs&(AttrUnderline|AttrDoubleUnderline|AttrCurlyUnderline|AttrDottedUnderline|AttrDashedUnderline) != 0 { + if uc.Valid() && (t.underColor != "" || t.underRGB != "") { + if uc == ColorReset { + t.TPuts(t.underFg) + } else if uc.IsRGB() { + if t.underRGB != "" { + r, g, b := uc.RGB() + t.TPuts(ti.TParm(t.underRGB, int(r), int(g), int(b))) + } else { + if v, ok := t.colors[uc]; ok { + uc = v + } else { + v = FindColor(uc, t.palette) + t.colors[uc] = v + uc = v + } + t.TPuts(ti.TParm(t.underColor, int(uc&0xff))) + } + } else { + t.TPuts(ti.TParm(t.underColor, int(uc&0xff))) + } + } t.TPuts(ti.Underline) // to ensure everyone gets at least a basic underline if (attrs & AttrDoubleUnderline) != 0 { t.TPuts(t.doubleUnder) @@ -928,8 +974,7 @@ func (t *tScreen) Show() { func (t *tScreen) clearScreen() { t.TPuts(t.ti.AttrOff) t.TPuts(t.exitUrl) - fg, bg, _ := t.style.Decompose() - _ = t.sendFgBg(fg, bg, AttrNone) + _ = t.sendFgBg(t.style.fg, t.style.bg, AttrNone) t.TPuts(t.ti.Clear) t.clear = false }