mirror of
https://github.com/VladimirMarkelov/clui.git
synced 2025-04-24 13:48:53 +08:00

Introduce SetColorMap()/GetColorMap() API's. With this the user may change the termbox's output (i.e with Output256) mode and set a broader color map. In the future - if needed - we could introduce an API to switch modes and automatically remap the colors as required. For now, this API's are good enough. Signed-off-by: Leandro Dorileo <leandro.maciel.dorileo@intel.com>
278 lines
6.6 KiB
Go
278 lines
6.6 KiB
Go
package clui
|
|
|
|
import (
|
|
xs "github.com/huandu/xstrings"
|
|
term "github.com/nsf/termbox-go"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
colorMap = map[string]term.Attribute{
|
|
"default": term.ColorDefault,
|
|
"black": term.ColorBlack,
|
|
"red": term.ColorRed,
|
|
"green": term.ColorGreen,
|
|
"yellow": term.ColorYellow,
|
|
"blue": term.ColorBlue,
|
|
"magenta": term.ColorMagenta,
|
|
"cyan": term.ColorCyan,
|
|
"white": term.ColorWhite,
|
|
"bold": term.AttrBold,
|
|
"bright": term.AttrBold, // windows make color brighter when it is bold
|
|
"underline": term.AttrUnderline,
|
|
"underlined": term.AttrUnderline,
|
|
"reverse": term.AttrReverse,
|
|
}
|
|
)
|
|
|
|
// Ellipsize truncates text to maxWidth by replacing a
|
|
// substring in the middle with ellipsis and keeping
|
|
// the beginning and ending of the string untouched.
|
|
// If maxWidth is less than 5 then no ellipsis is
|
|
// added, the text is just truncated from the right.
|
|
func Ellipsize(str string, maxWidth int) string {
|
|
ln := xs.Len(str)
|
|
if ln <= maxWidth {
|
|
return str
|
|
}
|
|
|
|
if maxWidth < 5 {
|
|
return xs.Slice(str, 0, maxWidth)
|
|
}
|
|
|
|
left := int((maxWidth - 3) / 2)
|
|
right := maxWidth - left - 3
|
|
return xs.Slice(str, 0, left) + "..." + xs.Slice(str, ln-right, -1)
|
|
}
|
|
|
|
// CutText makes a text no longer than maxWidth
|
|
func CutText(str string, maxWidth int) string {
|
|
ln := xs.Len(str)
|
|
if ln <= maxWidth {
|
|
return str
|
|
}
|
|
|
|
return xs.Slice(str, 0, maxWidth)
|
|
}
|
|
|
|
// AlignText calculates the initial position of the text
|
|
// output depending on str length and available width.
|
|
// The str is truncated in case of its lenght greater than
|
|
// width. Function returns shift that should be added to
|
|
// original label position before output instead of padding
|
|
// the string with spaces. The reason is to make possible
|
|
// to draw a label aligned but with transparent beginning
|
|
// and ending. If you do not need transparency you can
|
|
// add spaces manually using the returned shift value
|
|
func AlignText(str string, width int, align Align) (shift int, out string) {
|
|
length := xs.Len(str)
|
|
|
|
if length >= width {
|
|
return 0, CutText(str, width)
|
|
}
|
|
|
|
if align == AlignRight {
|
|
return width - length, str
|
|
} else if align == AlignCenter {
|
|
return (width - length) / 2, str
|
|
}
|
|
|
|
return 0, str
|
|
}
|
|
|
|
// AlignColorizedText does the same as AlignText does but
|
|
// it preserves the color of the letters by adding correct
|
|
// color tags to the line beginning.
|
|
// Note: function is ineffective and a bit slow - do not use
|
|
// it everywhere
|
|
func AlignColorizedText(str string, width int, align Align) (int, string) {
|
|
rawText := UnColorizeText(str)
|
|
length := xs.Len(rawText)
|
|
|
|
if length <= width {
|
|
shift, _ := AlignText(rawText, width, align)
|
|
return shift, str
|
|
}
|
|
|
|
skip := 0
|
|
if align == AlignRight {
|
|
skip = length - width
|
|
} else if align == AlignCenter {
|
|
skip = (length - width) / 2
|
|
}
|
|
|
|
fgChanged, bgChanged := false, false
|
|
curr := 0
|
|
parser := NewColorParser(str, term.ColorBlack, term.ColorBlack)
|
|
out := ""
|
|
for curr < skip+width {
|
|
elem := parser.NextElement()
|
|
|
|
if elem.Type == ElemEndOfText {
|
|
break
|
|
}
|
|
|
|
if elem.Type == ElemPrintable {
|
|
curr++
|
|
if curr == skip+1 {
|
|
if fgChanged {
|
|
out += "<t:" + ColorToString(elem.Fg) + ">"
|
|
}
|
|
if bgChanged {
|
|
out += "<b:" + ColorToString(elem.Bg) + ">"
|
|
}
|
|
out += string(elem.Ch)
|
|
} else if curr > skip+1 {
|
|
out += string(elem.Ch)
|
|
}
|
|
} else if elem.Type == ElemTextColor {
|
|
fgChanged = true
|
|
if curr > skip+1 {
|
|
out += "<t:" + ColorToString(elem.Fg) + ">"
|
|
}
|
|
} else if elem.Type == ElemBackColor {
|
|
bgChanged = true
|
|
if curr > skip+1 {
|
|
out += "<b:" + ColorToString(elem.Bg) + ">"
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0, out
|
|
}
|
|
|
|
// SliceColorized returns a slice of text with correct color
|
|
// tags. start and end are real printable rune indices
|
|
func SliceColorized(str string, start, end int) string {
|
|
if str == "" {
|
|
return str
|
|
}
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
|
|
fgChanged, bgChanged := false, false
|
|
curr := 0
|
|
parser := NewColorParser(str, term.ColorBlack, term.ColorBlack)
|
|
var out string
|
|
for {
|
|
if end != -1 && curr >= end {
|
|
break
|
|
}
|
|
elem := parser.NextElement()
|
|
if elem.Type == ElemEndOfText {
|
|
break
|
|
}
|
|
|
|
switch elem.Type {
|
|
case ElemTextColor:
|
|
fgChanged = true
|
|
if out != "" {
|
|
out += "<t:" + ColorToString(elem.Fg) + ">"
|
|
}
|
|
case ElemBackColor:
|
|
bgChanged = true
|
|
if out != "" {
|
|
out += "<b:" + ColorToString(elem.Bg) + ">"
|
|
}
|
|
case ElemPrintable:
|
|
if curr == start {
|
|
if fgChanged {
|
|
out += "<t:" + ColorToString(elem.Fg) + ">"
|
|
}
|
|
if bgChanged {
|
|
out += "<b:" + ColorToString(elem.Bg) + ">"
|
|
}
|
|
}
|
|
if curr >= start {
|
|
out += string(elem.Ch)
|
|
}
|
|
curr++
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// UnColorizeText removes all color-related tags from the
|
|
// string. Tags to remove: <(f|t|b|c):.*>
|
|
func UnColorizeText(str string) string {
|
|
rx := regexp.MustCompile("<(f|c|t|b):[^>]*>")
|
|
|
|
return rx.ReplaceAllString(str, "")
|
|
}
|
|
|
|
// StringToColor returns attribute by its string description.
|
|
// Description is the list of attributes separated with
|
|
// spaces, plus or pipe symbols. You can use 8 base colors:
|
|
// black, white, red, green, blue, magenta, yellow, cyan
|
|
// and a few modifiers:
|
|
// bold or bright, underline or underlined, reverse
|
|
// Note: some terminals do not support all modifiers, e.g,
|
|
// Windows one understands only bold/bright - it makes the
|
|
// color brighter with the modidierA
|
|
// Examples: "red bold", "green+underline+bold"
|
|
func StringToColor(str string) term.Attribute {
|
|
var parts []string
|
|
if strings.ContainsRune(str, '+') {
|
|
parts = strings.Split(str, "+")
|
|
} else if strings.ContainsRune(str, '|') {
|
|
parts = strings.Split(str, "|")
|
|
} else if strings.ContainsRune(str, ' ') {
|
|
parts = strings.Split(str, " ")
|
|
} else {
|
|
parts = append(parts, str)
|
|
}
|
|
|
|
var clr term.Attribute
|
|
for _, item := range parts {
|
|
item = strings.Trim(item, " ")
|
|
item = strings.ToLower(item)
|
|
|
|
c, ok := colorMap[item]
|
|
if ok {
|
|
clr |= c
|
|
}
|
|
}
|
|
|
|
return clr
|
|
}
|
|
|
|
// GetColorMap returns the color map (id is the color name, value its code)
|
|
func GetColorMap() map[string]term.Attribute {
|
|
return colorMap
|
|
}
|
|
|
|
// SetColorMap sets a custom color map (id is the color name, value its code)
|
|
func SetColorMap(cmap map[string]term.Attribute) {
|
|
colorMap = cmap
|
|
}
|
|
|
|
// ColorToString returns string representation of the attribute
|
|
func ColorToString(attr term.Attribute) string {
|
|
var out string
|
|
|
|
rawClr := attr & 15
|
|
if rawClr < 8 {
|
|
for k, v := range colorMap {
|
|
if v == rawClr {
|
|
out += k + " "
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if attr&term.AttrBold != 0 {
|
|
out += "bold "
|
|
}
|
|
if attr&term.AttrUnderline != 0 {
|
|
out += "underline "
|
|
}
|
|
if attr&term.AttrReverse != 0 {
|
|
out += "reverse "
|
|
}
|
|
|
|
return strings.TrimSpace(out)
|
|
}
|