2021-04-15 16:14:40 +03:00
|
|
|
package twin
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
type AttrMask uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
AttrBold AttrMask = 1 << iota
|
|
|
|
AttrBlink
|
|
|
|
AttrReverse
|
|
|
|
AttrUnderline
|
|
|
|
AttrDim
|
|
|
|
AttrItalic
|
|
|
|
AttrStrikeThrough
|
|
|
|
AttrNone AttrMask = 0 // Normal text
|
|
|
|
)
|
|
|
|
|
|
|
|
type Style struct {
|
|
|
|
fg Color
|
|
|
|
bg Color
|
|
|
|
attrs AttrMask
|
2023-05-01 16:56:08 +03:00
|
|
|
|
|
|
|
// This hyperlinkUrl is a URL for in-terminal hyperlinks.
|
|
|
|
//
|
|
|
|
// Since we don't want to do error handling of broken URLs, we just store
|
|
|
|
// these URLs as strings.
|
|
|
|
//
|
|
|
|
// Ref:
|
|
|
|
// * https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
|
|
|
// * https://github.com/walles/moar/issues/131
|
|
|
|
hyperlinkUrl *string
|
2021-04-15 16:14:40 +03:00
|
|
|
}
|
|
|
|
|
2021-05-11 16:14:15 +03:00
|
|
|
var StyleDefault Style
|
2021-04-15 16:14:40 +03:00
|
|
|
|
|
|
|
func (style Style) String() string {
|
|
|
|
if style.attrs == AttrNone {
|
|
|
|
return fmt.Sprint(style.fg, " on ", style.bg)
|
|
|
|
}
|
|
|
|
|
|
|
|
attrNames := make([]string, 0)
|
|
|
|
if style.attrs.has(AttrBold) {
|
|
|
|
attrNames = append(attrNames, "bold")
|
|
|
|
}
|
|
|
|
if style.attrs.has(AttrBlink) {
|
|
|
|
attrNames = append(attrNames, "blinking")
|
|
|
|
}
|
|
|
|
if style.attrs.has(AttrReverse) {
|
|
|
|
attrNames = append(attrNames, "reverse")
|
|
|
|
}
|
|
|
|
if style.attrs.has(AttrUnderline) {
|
|
|
|
attrNames = append(attrNames, "underlined")
|
|
|
|
}
|
|
|
|
if style.attrs.has(AttrDim) {
|
|
|
|
attrNames = append(attrNames, "dim")
|
|
|
|
}
|
|
|
|
if style.attrs.has(AttrItalic) {
|
|
|
|
attrNames = append(attrNames, "italic")
|
|
|
|
}
|
|
|
|
if style.attrs.has(AttrStrikeThrough) {
|
|
|
|
attrNames = append(attrNames, "strikethrough")
|
|
|
|
}
|
2023-05-03 20:45:04 +03:00
|
|
|
if style.hyperlinkUrl != nil {
|
|
|
|
attrNames = append(attrNames, "\""+*style.hyperlinkUrl+"\"")
|
|
|
|
}
|
2021-04-15 16:14:40 +03:00
|
|
|
|
|
|
|
return fmt.Sprint(strings.Join(attrNames, " "), " ", style.fg, " on ", style.bg)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (style Style) WithAttr(attr AttrMask) Style {
|
|
|
|
result := Style{
|
2023-05-01 16:56:08 +03:00
|
|
|
fg: style.fg,
|
|
|
|
bg: style.bg,
|
|
|
|
attrs: style.attrs | attr,
|
|
|
|
hyperlinkUrl: style.hyperlinkUrl,
|
2021-04-15 16:14:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Bold and dim are mutually exclusive
|
|
|
|
if attr.has(AttrBold) {
|
|
|
|
return result.WithoutAttr(AttrDim)
|
|
|
|
}
|
|
|
|
if attr.has(AttrDim) {
|
|
|
|
return result.WithoutAttr(AttrBold)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2023-05-01 16:56:08 +03:00
|
|
|
// Call with nil to remove the link
|
|
|
|
func (style Style) WithHyperlink(hyperlinkUrl *string) Style {
|
2023-05-03 20:07:32 +03:00
|
|
|
if hyperlinkUrl != nil && *hyperlinkUrl == "" {
|
2023-10-10 07:16:09 +03:00
|
|
|
// Use nil instead of empty string
|
2023-05-03 20:07:32 +03:00
|
|
|
hyperlinkUrl = nil
|
|
|
|
}
|
|
|
|
|
2023-05-01 16:56:08 +03:00
|
|
|
return Style{
|
|
|
|
fg: style.fg,
|
|
|
|
bg: style.bg,
|
|
|
|
attrs: style.attrs,
|
|
|
|
hyperlinkUrl: hyperlinkUrl,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-15 16:14:40 +03:00
|
|
|
func (style Style) WithoutAttr(attr AttrMask) Style {
|
|
|
|
return Style{
|
2023-05-01 16:56:08 +03:00
|
|
|
fg: style.fg,
|
|
|
|
bg: style.bg,
|
|
|
|
attrs: style.attrs & ^attr,
|
|
|
|
hyperlinkUrl: style.hyperlinkUrl,
|
2021-04-15 16:14:40 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (attr AttrMask) has(attrs AttrMask) bool {
|
|
|
|
return attr&attrs != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (style Style) Background(color Color) Style {
|
|
|
|
return Style{
|
2023-05-01 16:56:08 +03:00
|
|
|
fg: style.fg,
|
|
|
|
bg: color,
|
|
|
|
attrs: style.attrs,
|
|
|
|
hyperlinkUrl: style.hyperlinkUrl,
|
2021-04-15 16:14:40 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (style Style) Foreground(color Color) Style {
|
|
|
|
return Style{
|
2023-05-01 16:56:08 +03:00
|
|
|
fg: color,
|
|
|
|
bg: style.bg,
|
|
|
|
attrs: style.attrs,
|
|
|
|
hyperlinkUrl: style.hyperlinkUrl,
|
2021-04-15 16:14:40 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Emit an ANSI escape sequence switching from a previous style to the current
|
|
|
|
// one.
|
|
|
|
func (current Style) RenderUpdateFrom(previous Style) string {
|
2023-10-09 20:45:43 +03:00
|
|
|
if current == previous {
|
|
|
|
// Shortcut for the common case
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
if current == StyleDefault {
|
|
|
|
return "\x1b[m"
|
|
|
|
}
|
|
|
|
|
2021-04-15 16:14:40 +03:00
|
|
|
var builder strings.Builder
|
|
|
|
if current.fg != previous.fg {
|
|
|
|
builder.WriteString(current.fg.ForegroundAnsiString())
|
|
|
|
}
|
|
|
|
|
|
|
|
if current.bg != previous.bg {
|
|
|
|
builder.WriteString(current.bg.BackgroundAnsiString())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle AttrDim / AttrBold changes
|
|
|
|
previousBoldDim := previous.attrs & (AttrBold | AttrDim)
|
|
|
|
currentBoldDim := current.attrs & (AttrBold | AttrDim)
|
|
|
|
if currentBoldDim != previousBoldDim {
|
|
|
|
if previousBoldDim != 0 {
|
|
|
|
builder.WriteString("\x1b[22m") // Reset to neither bold nor dim
|
|
|
|
}
|
|
|
|
if current.attrs.has(AttrBold) {
|
|
|
|
builder.WriteString("\x1b[1m")
|
|
|
|
}
|
|
|
|
if current.attrs.has(AttrDim) {
|
|
|
|
builder.WriteString("\x1b[2m")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle AttrBlink changes
|
|
|
|
if current.attrs.has(AttrBlink) != previous.attrs.has(AttrBlink) {
|
|
|
|
if current.attrs.has(AttrBlink) {
|
|
|
|
builder.WriteString("\x1b[5m")
|
|
|
|
} else {
|
|
|
|
builder.WriteString("\x1b[25m")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle AttrReverse changes
|
|
|
|
if current.attrs.has(AttrReverse) != previous.attrs.has(AttrReverse) {
|
|
|
|
if current.attrs.has(AttrReverse) {
|
|
|
|
builder.WriteString("\x1b[7m")
|
|
|
|
} else {
|
|
|
|
builder.WriteString("\x1b[27m")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle AttrUnderline changes
|
|
|
|
if current.attrs.has(AttrUnderline) != previous.attrs.has(AttrUnderline) {
|
|
|
|
if current.attrs.has(AttrUnderline) {
|
|
|
|
builder.WriteString("\x1b[4m")
|
|
|
|
} else {
|
|
|
|
builder.WriteString("\x1b[24m")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle AttrItalic changes
|
|
|
|
if current.attrs.has(AttrItalic) != previous.attrs.has(AttrItalic) {
|
|
|
|
if current.attrs.has(AttrItalic) {
|
|
|
|
builder.WriteString("\x1b[3m")
|
|
|
|
} else {
|
|
|
|
builder.WriteString("\x1b[23m")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle AttrStrikeThrough changes
|
|
|
|
if current.attrs.has(AttrStrikeThrough) != previous.attrs.has(AttrStrikeThrough) {
|
|
|
|
if current.attrs.has(AttrStrikeThrough) {
|
|
|
|
builder.WriteString("\x1b[9m")
|
|
|
|
} else {
|
|
|
|
builder.WriteString("\x1b[29m")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-01 16:56:08 +03:00
|
|
|
if current.hyperlinkUrl != previous.hyperlinkUrl {
|
|
|
|
newUrl := ""
|
|
|
|
if current.hyperlinkUrl != nil {
|
|
|
|
newUrl = *current.hyperlinkUrl
|
|
|
|
}
|
2023-05-02 07:30:09 +03:00
|
|
|
|
|
|
|
previousUrl := ""
|
|
|
|
if previous.hyperlinkUrl != nil {
|
|
|
|
previousUrl = *previous.hyperlinkUrl
|
|
|
|
}
|
|
|
|
|
|
|
|
if newUrl != previousUrl {
|
|
|
|
builder.WriteString("\x1b]8;;")
|
|
|
|
builder.WriteString(newUrl)
|
|
|
|
builder.WriteString("\x1b\\")
|
|
|
|
}
|
2023-05-01 16:56:08 +03:00
|
|
|
}
|
|
|
|
|
2021-04-15 16:14:40 +03:00
|
|
|
return builder.String()
|
|
|
|
}
|