Add underline style variants and underline color support
CodeQL / Analyze (go) (push) Has been cancelled
build / build (push) Has been cancelled
Test fzf on macOS / build (push) Has been cancelled

Support double, curly, dotted, and dashed underline styles via --color
(e.g. underline-curly) and ANSI passthrough (SGR 4:N, 58, 59) with --ansi.

Close #4633
Close #4678

Thanks to @shtse8 for the test cases.
This commit is contained in:
Junegunn Choi
2026-02-15 01:06:46 +09:00
parent 49ab253555
commit b56d614ba2
11 changed files with 371 additions and 64 deletions
+37 -7
View File
@@ -1323,7 +1323,18 @@ func attrCodes(attr Attr) []string {
codes = append(codes, "3")
}
if (attr & Underline) > 0 {
codes = append(codes, "4")
switch attr.UnderlineStyle() {
case UlStyleDouble:
codes = append(codes, "4:2")
case UlStyleCurly:
codes = append(codes, "4:3")
case UlStyleDotted:
codes = append(codes, "4:4")
case UlStyleDashed:
codes = append(codes, "4:5")
default:
codes = append(codes, "4")
}
}
if (attr & Blink) > 0 {
codes = append(codes, "5")
@@ -1361,8 +1372,27 @@ func colorCodes(fg Color, bg Color) []string {
return codes
}
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) (bool, string) {
func ulColorCode(c Color) string {
if c == colDefault {
return ""
}
if c.is24() {
r := (c >> 16) & 0xff
g := (c >> 8) & 0xff
b := (c) & 0xff
return fmt.Sprintf("58;2;%d;%d;%d", r, g, b)
}
if c >= 0 && c < 256 {
return fmt.Sprintf("58;5;%d", c)
}
return ""
}
func (w *LightWindow) csiColor(fg Color, bg Color, ul Color, attr Attr) (bool, string) {
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
if ulCode := ulColorCode(ul); ulCode != "" {
codes = append(codes, ulCode)
}
code := w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0, code
}
@@ -1376,13 +1406,13 @@ func cleanse(str string) string {
}
func (w *LightWindow) CPrint(pair ColorPair, text string) {
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Ul(), pair.Attr())
w.stderrInternal(cleanse(text), false, code)
w.csi("0m")
}
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
hasColors, code := w.csiColor(fg, bg, attr)
hasColors, code := w.csiColor(fg, bg, colDefault, attr)
if hasColors {
defer w.csi("0m")
}
@@ -1472,7 +1502,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
func (w *LightWindow) setBg() string {
if w.bg != colDefault {
_, code := w.csiColor(colDefault, w.bg, AttrRegular)
_, code := w.csiColor(colDefault, w.bg, colDefault, AttrRegular)
return code
}
// Should clear dim attribute after ␍ in the preview window
@@ -1494,7 +1524,7 @@ func (w *LightWindow) Fill(text string) FillReturn {
return w.fill(text, code)
}
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
func (w *LightWindow) CFill(fg Color, bg Color, ul Color, attr Attr, text string) FillReturn {
w.Move(w.posy, w.posx)
if fg == colDefault {
fg = w.fg
@@ -1502,7 +1532,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
if bg == colDefault {
bg = w.bg
}
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
if hasColors, resetCode := w.csiColor(fg, bg, ul, attr); hasColors {
defer w.csi("0m")
return w.fill(text, resetCode)
}
+33 -4
View File
@@ -825,6 +825,21 @@ func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
return style
}
func underlineStyleFromAttr(a Attr) tcell.UnderlineStyle {
switch a.UnderlineStyle() {
case UlStyleDouble:
return tcell.UnderlineStyleDouble
case UlStyleCurly:
return tcell.UnderlineStyleCurly
case UlStyleDotted:
return tcell.UnderlineStyleDotted
case UlStyleDashed:
return tcell.UnderlineStyleDashed
default:
return tcell.UnderlineStyleSolid
}
}
func (w *TcellWindow) printString(text string, pair ColorPair) {
lx := 0
a := pair.Attr()
@@ -833,11 +848,18 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
if a&AttrClear == 0 {
style = style.
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0).
Blink(a&Attr(tcell.AttrBlink) != 0).
Dim(a&Attr(tcell.AttrDim) != 0)
if a&Attr(tcell.AttrUnderline) != 0 {
style = style.Underline(underlineStyleFromAttr(a))
if pair.Ul() != colDefault {
style = style.Underline(asTcellColor(pair.Ul()))
}
} else {
style = style.Underline(false)
}
}
style = w.withUrl(style)
@@ -887,9 +909,16 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
Bold(a&Attr(tcell.AttrBold) != 0 || a&BoldForce != 0).
Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0).
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
Italic(a&Attr(tcell.AttrItalic) != 0)
if a&Attr(tcell.AttrUnderline) != 0 {
style = style.Underline(underlineStyleFromAttr(a))
if pair.Ul() != colDefault {
style = style.Underline(asTcellColor(pair.Ul()))
}
} else {
style = style.Underline(false)
}
style = w.withUrl(style)
gr := uniseg.NewGraphemes(text)
@@ -967,14 +996,14 @@ func (w *TcellWindow) LinkEnd() {
w.params = nil
}
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
func (w *TcellWindow) CFill(fg Color, bg Color, ul Color, a Attr, str string) FillReturn {
if fg == colDefault {
fg = w.normal.Fg()
}
if bg == colDefault {
bg = w.normal.Bg()
}
return w.fillString(str, NewColorPair(fg, bg, a))
return w.fillString(str, NewColorPair(fg, bg, a).WithUl(ul))
}
func (w *TcellWindow) DrawBorder() {
+40 -7
View File
@@ -17,15 +17,34 @@ const (
BoldForce = Attr(1 << 10)
FullBg = Attr(1 << 11)
Strip = Attr(1 << 12)
// Underline style stored in bits 13-15 (3 bits, values 0-4)
// Only meaningful when the Underline attribute bit is also set.
// 0 = solid (default)
UnderlineStyleShift = 13
UnderlineStyleMask = Attr(0b111 << UnderlineStyleShift)
UlStyleDouble = Attr(0b001 << UnderlineStyleShift)
UlStyleCurly = Attr(0b010 << UnderlineStyleShift)
UlStyleDotted = Attr(0b011 << UnderlineStyleShift)
UlStyleDashed = Attr(0b100 << UnderlineStyleShift)
)
func (a Attr) UnderlineStyle() Attr {
return a & UnderlineStyleMask
}
func (a Attr) Merge(b Attr) Attr {
if b&AttrRegular > 0 {
// Only keep bold attribute set by the system
return (b &^ AttrRegular) | (a & BoldForce)
}
return (a &^ AttrRegular) | b
merged := (a &^ AttrRegular) | b
// When b sets Underline, use b's underline style instead of OR'ing
if b&Underline > 0 {
merged = (merged &^ UnderlineStyleMask) | (b & UnderlineStyleMask)
}
return merged
}
// Types of user action
@@ -352,6 +371,7 @@ const (
type ColorPair struct {
fg Color
bg Color
ul Color
attr Attr
}
@@ -363,11 +383,11 @@ func HexToColor(rrggbb string) Color {
}
func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
return ColorPair{fg, bg, attr}
return ColorPair{fg, bg, colDefault, attr}
}
func NoColorPair() ColorPair {
return ColorPair{-1, -1, 0}
return ColorPair{-1, -1, -1, 0}
}
func (p ColorPair) Fg() Color {
@@ -378,6 +398,16 @@ func (p ColorPair) Bg() Color {
return p.bg
}
func (p ColorPair) Ul() Color {
return p.ul
}
func (p ColorPair) WithUl(ul Color) ColorPair {
dup := p
dup.ul = ul
return dup
}
func (p ColorPair) Attr() Attr {
return p.attr
}
@@ -404,6 +434,9 @@ func (p ColorPair) merge(other ColorPair, except Color) ColorPair {
if other.bg != except {
dup.bg = other.bg
}
if other.ul != except {
dup.ul = other.ul
}
return dup
}
@@ -415,13 +448,13 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
dup := p
fgPair := ColorPair{fg.Color, colUndefined, fg.Attr}
fgPair := ColorPair{fg.Color, colUndefined, colUndefined, fg.Attr}
return dup.Merge(fgPair)
}
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
dup := p
bgPair := ColorPair{colUndefined, bg.Color, bg.Attr}
bgPair := ColorPair{colUndefined, bg.Color, colUndefined, bg.Attr}
return dup.Merge(bgPair)
}
@@ -783,7 +816,7 @@ type Window interface {
Print(text string)
CPrint(color ColorPair, text string)
Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
CFill(fg Color, bg Color, ul Color, attr Attr, text string) FillReturn
LinkBegin(uri string, params string)
LinkEnd()
Erase()
@@ -1271,7 +1304,7 @@ func initPalette(theme *ColorTheme) {
if fg.Color == colDefault && (fg.Attr&Reverse) > 0 {
bg.Color = colDefault
}
return ColorPair{fg.Color, bg.Color, fg.Attr}
return ColorPair{fg.Color, bg.Color, colDefault, fg.Attr}
}
blank := theme.ListFg
blank.Attr = AttrRegular