1
1
mirror of https://github.com/walles/moar.git synced 2024-11-25 19:56:20 +03:00
moar/twin/styles.go
2023-12-20 14:59:55 +01:00

240 lines
5.5 KiB
Go

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
// 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
}
var StyleDefault Style
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")
}
if style.hyperlinkUrl != nil {
attrNames = append(attrNames, "\""+*style.hyperlinkUrl+"\"")
}
return fmt.Sprint(strings.Join(attrNames, " "), " ", style.fg, " on ", style.bg)
}
func (style Style) WithAttr(attr AttrMask) Style {
result := Style{
fg: style.fg,
bg: style.bg,
attrs: style.attrs | attr,
hyperlinkUrl: style.hyperlinkUrl,
}
// Bold and dim are mutually exclusive
if attr.has(AttrBold) {
return result.WithoutAttr(AttrDim)
}
if attr.has(AttrDim) {
return result.WithoutAttr(AttrBold)
}
return result
}
// Call with nil to remove the link
func (style Style) WithHyperlink(hyperlinkUrl *string) Style {
if hyperlinkUrl != nil && *hyperlinkUrl == "" {
// Use nil instead of empty string
hyperlinkUrl = nil
}
return Style{
fg: style.fg,
bg: style.bg,
attrs: style.attrs,
hyperlinkUrl: hyperlinkUrl,
}
}
func (style Style) WithoutAttr(attr AttrMask) Style {
return Style{
fg: style.fg,
bg: style.bg,
attrs: style.attrs & ^attr,
hyperlinkUrl: style.hyperlinkUrl,
}
}
func (attr AttrMask) has(attrs AttrMask) bool {
return attr&attrs != 0
}
func (style Style) WithBackground(color Color) Style {
return Style{
fg: style.fg,
bg: color,
attrs: style.attrs,
hyperlinkUrl: style.hyperlinkUrl,
}
}
func (style Style) WithForeground(color Color) Style {
return Style{
fg: color,
bg: style.bg,
attrs: style.attrs,
hyperlinkUrl: style.hyperlinkUrl,
}
}
// Emit an ANSI escape sequence switching from a previous style to the current
// one.
func (current Style) RenderUpdateFrom(previous Style, terminalColorCount ColorType) string {
if current == previous {
// Shortcut for the common case
return ""
}
hadHyperlink := previous.hyperlinkUrl != nil && *previous.hyperlinkUrl != ""
if current == StyleDefault && !hadHyperlink {
return "\x1b[m"
}
var builder strings.Builder
if current.fg != previous.fg {
builder.WriteString(current.fg.ForegroundAnsiString(terminalColorCount))
}
if current.bg != previous.bg {
builder.WriteString(current.bg.BackgroundAnsiString(terminalColorCount))
}
// 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")
}
}
if current.hyperlinkUrl != previous.hyperlinkUrl {
newUrl := ""
if current.hyperlinkUrl != nil {
newUrl = *current.hyperlinkUrl
}
previousUrl := ""
if previous.hyperlinkUrl != nil {
previousUrl = *previous.hyperlinkUrl
}
if newUrl != previousUrl {
builder.WriteString("\x1b]8;;")
builder.WriteString(newUrl)
builder.WriteString("\x1b\\")
}
}
return builder.String()
}