From 2b9e25fa39e0f11dc2668ab68b5ec720d8dbb8dc Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 24 Jan 2019 14:38:35 +0100 Subject: [PATCH] base code | coloring format v1 (foreground and/or background) | only _real_ issue for now, text cannot contain the character '$' due to regexp mechanism --- colors.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ printer.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 colors.go create mode 100644 printer.go diff --git a/colors.go b/colors.go new file mode 100644 index 0000000..8595c2b --- /dev/null +++ b/colors.go @@ -0,0 +1,82 @@ +package clifmt + +import ( + "fmt" + "io" + "strconv" +) + +type terminalColor uint32 + +var colorMap = make(map[string]terminalColor) + +func init() { + colorMap["red"] = 0xff0000 + colorMap["green"] = 0x00ff00 + colorMap["blue"] = 0x0000ff +} + +// fromName returns the integer value of a color name +// from the built-in color map ; it is case insensitive +func fromName(s string) (terminalColor, error) { + value, ok := colorMap[s] + if !ok { + return 0, fmt.Errorf("unknown color name '%'", s) + } + return value, nil +} + +// fromHex returns the integer value associated with +// an hexadecimal string (full-sized or short version) +// the format is 'abc' or 'abcdef' +func fromHex(s string) (terminalColor, error) { + if len(s) != 3 && len(s) != 6 { + return 0, fmt.Errorf("expect a size of 3 or 6 (remove the '#' prefix)") + } + + // short version + input := s + if len(s) == 3 { + input = fmt.Sprintf("%c%c%c%c%c%c", s[0], s[0], s[1], s[1], s[2], s[2]) + } + + n, err := strconv.ParseUint(input, 16, 32) + if err != nil { + return 0, err + } + + return terminalColor(n), nil +} + +// parseColor tries to parse a color string (can be a name or an hexa value) +func parseColor(s string) (terminalColor, error) { + + // (0) ... + if len(s) < 1 { + return 0, io.ErrUnexpectedEOF + } + + // (1) hexa + if s[0] == '#' { + return fromHex(s[1:]) + } + + // (2) name + return fromName(s) + +} + +// Red component of the color +func (c terminalColor) Red() uint8 { + return uint8((c >> 16) & 0xff) +} + +// Green component of the color +func (c terminalColor) Green() uint8 { + return uint8((c >> 8) & 0xff) +} + +// Blue component of the color +func (c terminalColor) Blue() uint8 { + return uint8(c & 0xff) +} diff --git a/printer.go b/printer.go new file mode 100644 index 0000000..137bdb5 --- /dev/null +++ b/printer.go @@ -0,0 +1,85 @@ +package clifmt + +import ( + "fmt" + "regexp" +) + +// extractor helps extract features from the coloring format defined as follows : +// +// - [Color] -> [a-z] # named color +// - [Color] -> #[0-9a-f]{3} # hexa color (shortcode) +// - [Color] -> #[0-9a-f]{6} # hexa color (full-sized) +// - [Text] -> ANY +// - [Format] -> ${Text}(Color:Color) # foreground, background colors +// - [Format] -> ${Text}(Color) # foreground color only +// - [Format] -> ${Text}(:Color) # background color only +var extractor = regexp.MustCompile(`(?m)\${([^$]+)}\(((?:[a-z]+|#(?:[0-9a-f]{3}|[0-9a-f]{6})))?(?:\:((?:[a-z]+|#(?:[0-9a-f]{3}|[0-9a-f]{6}))))?\)`) + +func Printf(format string, a ...interface{}) error { + // 1. Pre-process format with 'fmt' + input := fmt.Sprintf(format, a...) + output := "" + cursor := int(0) + + // 2. extract color format matches + for _, match := range extractor.FindAllStringSubmatchIndex(input, -1) { + // (1) add gap between input start OR previous match + output += input[cursor:match[0]] + cursor = match[1] + + // (2) extract features + var ( + err = error(nil) + text = "" + sForeground = "" + sBackground = "" + foreground = terminalColor(0) + background = terminalColor(0) + ) + + if match[3]-match[2] > 0 { + text = input[match[2]:match[3]] + } + if match[5]-match[4] > 0 { + sForeground = input[match[4]:match[5]] + foreground, err = parseColor(sForeground) + if err != nil { + return err + } + } + if match[7]-match[6] > 0 { + sBackground = input[match[6]:match[7]] + background, err = parseColor(sBackground) + if err != nil { + return err + } + } + + // (3) replace text with colorized text + if len(sForeground) > 0 { + text = colorize(text, true, foreground) + } + if len(sBackground) > 0 { + text = colorize(text, false, background) + } + output += text + } + + // 3. Add end of input + if cursor < len(input)-1 { + output += input[cursor:] + } + + // 3. print final output + fmt.Print(output) + return nil +} + +func colorize(text string, foregound bool, color terminalColor) string { + if foregound { + return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", color.Red(), color.Green(), color.Blue(), text) + } + + return fmt.Sprintf("\033[48;2;%d;%d;%dm%s\033[0m", color.Red(), color.Green(), color.Blue(), text) +}