Skip to content

Commit

Permalink
Colored underlines.
Browse files Browse the repository at this point in the history
This supports UNIX and Windows.
  • Loading branch information
gdamore committed Mar 5, 2024
1 parent ed75277 commit 826c271
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 63 deletions.
12 changes: 12 additions & 0 deletions _demos/style.go
Expand Up @@ -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++
Expand Down
18 changes: 16 additions & 2 deletions console_win.go
Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand All @@ -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 {
Expand Down
78 changes: 29 additions & 49 deletions style.go
Expand Up @@ -25,6 +25,7 @@ package tcell
type Style struct {
fg Color
bg Color
under Color
attrs AttrMask
url string
urlId string
Expand All @@ -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.
Expand Down Expand Up @@ -152,41 +138,35 @@ 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,
// any marked up Url with this style will be given the UrlId also. If the
// 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
}
6 changes: 3 additions & 3 deletions 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.
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions terminfo/terminfo.go
Expand Up @@ -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
}

Expand Down
63 changes: 54 additions & 9 deletions tscreen.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

This comment has been minimized.

Copy link
@Consolatis

Consolatis Mar 5, 2024

Nitpick: 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() {
Expand Down Expand Up @@ -435,7 +461,6 @@ func (t *tScreen) prepareCursorStyles() {
}
}

// Still TODO: Cursor Color
}

func (t *tScreen) prepareKey(key Key, val string) {
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit 826c271

Please sign in to comment.