diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a36fddc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +--- +language: go +go: +- 1.12.x + +install: +- curl --silent --location https://goo.gl/g1CpPX | bash -s v1.0.7 + +script: +- export GO111MODULE=on +- go mod download +- go mod verify +- ginkgo -r -nodes 4 -randomizeAllSpecs -randomizeSuites -race -trace +- staticcheck ./... +- golint ./... diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..533ba6a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 gonvenience + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3510529 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# bunt + +[![License](https://img.shields.io/github/license/gonvenience/bunt.svg)](https://github.com/gonvenience/bunt/blob/master/LICENSE) +[![Go Report Card](https://goreportcard.com/badge/github.com/gonvenience/bunt)](https://goreportcard.com/report/github.com/gonvenience/bunt) +[![Build Status](https://travis-ci.org/gonvenience/bunt.svg?branch=master)](https://travis-ci.org/gonvenience/bunt) +[![GoDoc](https://godoc.org/github.com/gonvenience/bunt/pkg?status.svg)](https://godoc.org/github.com/gonvenience/bunt/pkg) +[![Release](https://img.shields.io/github/release/gonvenience/bunt.svg)](https://github.com/gonvenience/bunt/releases/latest) + +Golang package for creating true color output in terminals diff --git a/bunt.go b/bunt.go new file mode 100644 index 0000000..0837f5b --- /dev/null +++ b/bunt.go @@ -0,0 +1,56 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt + +import ( + "github.com/gonvenience/term" +) + +// ColorSetting defines the coloring setting to be used +var ColorSetting = AUTO + +// TrueColorSetting defines the true color usage setting to be used +var TrueColorSetting = AUTO + +// SwitchState is the type to cover different preferences/settings like: +// on, off, or auto +type SwitchState int + +// Supported setting states +const ( + OFF = SwitchState(-1) + AUTO = SwitchState(0) + ON = SwitchState(+1) +) + +// UseColors return whether colors are used or not based on the configured color +// setting or terminal capabilities +func UseColors() bool { + return (ColorSetting == ON) || + (ColorSetting == AUTO && term.IsTerminal() && !term.IsDumbTerminal()) +} + +// UseTrueColor returns whether true color colors should be used or not based on +// the configured true color usage setting or terminal capabilities +func UseTrueColor() bool { + return (TrueColorSetting == ON) || + (TrueColorSetting == AUTO && term.IsTrueColor()) +} diff --git a/bunt_suite_test.go b/bunt_suite_test.go new file mode 100644 index 0000000..9d08fa2 --- /dev/null +++ b/bunt_suite_test.go @@ -0,0 +1,33 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestBunt(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "bunt suite") +} diff --git a/colors.go b/colors.go new file mode 100644 index 0000000..22040c2 --- /dev/null +++ b/colors.go @@ -0,0 +1,335 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt + +import ( + colorful "github.com/lucasb-eyer/go-colorful" +) + +// The named colors are based upon https://en.wikipedia.org/wiki/Web_colors +var ( + Pink = hexColor("#FFC0CB") + LightPink = hexColor("#FFB6C1") + HotPink = hexColor("#FF69B4") + DeepPink = hexColor("#FF1493") + PaleVioletRed = hexColor("#DB7093") + MediumVioletRed = hexColor("#C71585") + + LightSalmon = hexColor("#FFA07A") + Salmon = hexColor("#FA8072") + DarkSalmon = hexColor("#E9967A") + LightCoral = hexColor("#F08080") + IndianRed = hexColor("#CD5C5C") + Crimson = hexColor("#DC143C") + FireBrick = hexColor("#B22222") + DarkRed = hexColor("#8B0000") + Red = hexColor("#FF0000") + + OrangeRed = hexColor("#FF4500") + Tomato = hexColor("#FF6347") + Coral = hexColor("#FF7F50") + DarkOrange = hexColor("#FF8C00") + Orange = hexColor("#FFA500") + + Yellow = hexColor("#FFFF00") + LightYellow = hexColor("#FFFFE0") + LemonChiffon = hexColor("#FFFACD") + LightGoldenrodYellow = hexColor("#FAFAD2") + PapayaWhip = hexColor("#FFEFD5") + Moccasin = hexColor("#FFE4B5") + PeachPuff = hexColor("#FFDAB9") + PaleGoldenrod = hexColor("#EEE8AA") + Khaki = hexColor("#F0E68C") + DarkKhaki = hexColor("#BDB76B") + Gold = hexColor("#FFD700") + + Cornsilk = hexColor("#FFF8DC") + BlanchedAlmond = hexColor("#FFEBCD") + Bisque = hexColor("#FFE4C4") + NavajoWhite = hexColor("#FFDEAD") + Wheat = hexColor("#F5DEB3") + BurlyWood = hexColor("#DEB887") + Tan = hexColor("#D2B48C") + RosyBrown = hexColor("#BC8F8F") + SandyBrown = hexColor("#F4A460") + Goldenrod = hexColor("#DAA520") + DarkGoldenrod = hexColor("#B8860B") + Peru = hexColor("#CD853F") + Chocolate = hexColor("#D2691E") + SaddleBrown = hexColor("#8B4513") + Sienna = hexColor("#A0522D") + Brown = hexColor("#A52A2A") + Maroon = hexColor("#800000") + + DarkOliveGreen = hexColor("#556B2F") + Olive = hexColor("#808000") + OliveDrab = hexColor("#6B8E23") + YellowGreen = hexColor("#9ACD32") + LimeGreen = hexColor("#32CD32") + Lime = hexColor("#00FF00") + LawnGreen = hexColor("#7CFC00") + Chartreuse = hexColor("#7FFF00") + GreenYellow = hexColor("#ADFF2F") + SpringGreen = hexColor("#00FF7F") + MediumSpringGreen = hexColor("#00FA9A") + LightGreen = hexColor("#90EE90") + PaleGreen = hexColor("#98FB98") + DarkSeaGreen = hexColor("#8FBC8F") + MediumAquamarine = hexColor("#66CDAA") + MediumSeaGreen = hexColor("#3CB371") + SeaGreen = hexColor("#2E8B57") + ForestGreen = hexColor("#228B22") + Green = hexColor("#008000") + DarkGreen = hexColor("#006400") + + Aqua = hexColor("#00FFFF") + Cyan = hexColor("#00FFFF") + LightCyan = hexColor("#E0FFFF") + PaleTurquoise = hexColor("#AFEEEE") + Aquamarine = hexColor("#7FFFD4") + Turquoise = hexColor("#40E0D0") + MediumTurquoise = hexColor("#48D1CC") + DarkTurquoise = hexColor("#00CED1") + LightSeaGreen = hexColor("#20B2AA") + CadetBlue = hexColor("#5F9EA0") + DarkCyan = hexColor("#008B8B") + Teal = hexColor("#008080") + + LightSteelBlue = hexColor("#B0C4DE") + PowderBlue = hexColor("#B0E0E6") + LightBlue = hexColor("#ADD8E6") + SkyBlue = hexColor("#87CEEB") + LightSkyBlue = hexColor("#87CEFA") + DeepSkyBlue = hexColor("#00BFFF") + DodgerBlue = hexColor("#1E90FF") + CornflowerBlue = hexColor("#6495ED") + SteelBlue = hexColor("#4682B4") + RoyalBlue = hexColor("#4169E1") + Blue = hexColor("#0000FF") + MediumBlue = hexColor("#0000CD") + DarkBlue = hexColor("#00008B") + Navy = hexColor("#000080") + MidnightBlue = hexColor("#191970") + + Lavender = hexColor("#E6E6FA") + Thistle = hexColor("#D8BFD8") + Plum = hexColor("#DDA0DD") + Violet = hexColor("#EE82EE") + Orchid = hexColor("#DA70D6") + Fuchsia = hexColor("#FF00FF") + Magenta = hexColor("#FF00FF") + MediumOrchid = hexColor("#BA55D3") + MediumPurple = hexColor("#9370DB") + BlueViolet = hexColor("#8A2BE2") + DarkViolet = hexColor("#9400D3") + DarkOrchid = hexColor("#9932CC") + DarkMagenta = hexColor("#8B008B") + Purple = hexColor("#800080") + Indigo = hexColor("#4B0082") + DarkSlateBlue = hexColor("#483D8B") + SlateBlue = hexColor("#6A5ACD") + MediumSlateBlue = hexColor("#7B68EE") + + White = hexColor("#FFFFFF") + Snow = hexColor("#FFFAFA") + Honeydew = hexColor("#F0FFF0") + MintCream = hexColor("#F5FFFA") + Azure = hexColor("#F0FFFF") + AliceBlue = hexColor("#F0F8FF") + GhostWhite = hexColor("#F8F8FF") + WhiteSmoke = hexColor("#F5F5F5") + Seashell = hexColor("#FFF5EE") + Beige = hexColor("#F5F5DC") + OldLace = hexColor("#FDF5E6") + FloralWhite = hexColor("#FFFAF0") + Ivory = hexColor("#FFFFF0") + AntiqueWhite = hexColor("#FAEBD7") + Linen = hexColor("#FAF0E6") + LavenderBlush = hexColor("#FFF0F5") + MistyRose = hexColor("#FFE4E1") + + Gainsboro = hexColor("#DCDCDC") + LightGray = hexColor("#D3D3D3") + Silver = hexColor("#C0C0C0") + DarkGray = hexColor("#A9A9A9") + Gray = hexColor("#808080") + DimGray = hexColor("#696969") + LightSlateGray = hexColor("#778899") + SlateGray = hexColor("#708090") + DarkSlateGray = hexColor("#2F4F4F") + Black = hexColor("#000000") +) + +var colorByNameMap = map[string]colorful.Color{ + "Pink": Pink, + "LightPink": LightPink, + "HotPink": HotPink, + "DeepPink": DeepPink, + "PaleVioletRed": PaleVioletRed, + "MediumVioletRed": MediumVioletRed, + "LightSalmon": LightSalmon, + "Salmon": Salmon, + "DarkSalmon": DarkSalmon, + "LightCoral": LightCoral, + "IndianRed": IndianRed, + "Crimson": Crimson, + "FireBrick": FireBrick, + "DarkRed": DarkRed, + "Red": Red, + "OrangeRed": OrangeRed, + "Tomato": Tomato, + "Coral": Coral, + "DarkOrange": DarkOrange, + "Orange": Orange, + "Yellow": Yellow, + "LightYellow": LightYellow, + "LemonChiffon": LemonChiffon, + "LightGoldenrodYellow": LightGoldenrodYellow, + "PapayaWhip": PapayaWhip, + "Moccasin": Moccasin, + "PeachPuff": PeachPuff, + "PaleGoldenrod": PaleGoldenrod, + "Khaki": Khaki, + "DarkKhaki": DarkKhaki, + "Gold": Gold, + "Cornsilk": Cornsilk, + "BlanchedAlmond": BlanchedAlmond, + "Bisque": Bisque, + "NavajoWhite": NavajoWhite, + "Wheat": Wheat, + "BurlyWood": BurlyWood, + "Tan": Tan, + "RosyBrown": RosyBrown, + "SandyBrown": SandyBrown, + "Goldenrod": Goldenrod, + "DarkGoldenrod": DarkGoldenrod, + "Peru": Peru, + "Chocolate": Chocolate, + "SaddleBrown": SaddleBrown, + "Sienna": Sienna, + "Brown": Brown, + "Maroon": Maroon, + "DarkOliveGreen": DarkOliveGreen, + "Olive": Olive, + "OliveDrab": OliveDrab, + "YellowGreen": YellowGreen, + "LimeGreen": LimeGreen, + "Lime": Lime, + "LawnGreen": LawnGreen, + "Chartreuse": Chartreuse, + "GreenYellow": GreenYellow, + "SpringGreen": SpringGreen, + "MediumSpringGreen": MediumSpringGreen, + "LightGreen": LightGreen, + "PaleGreen": PaleGreen, + "DarkSeaGreen": DarkSeaGreen, + "MediumAquamarine": MediumAquamarine, + "MediumSeaGreen": MediumSeaGreen, + "SeaGreen": SeaGreen, + "ForestGreen": ForestGreen, + "Green": Green, + "DarkGreen": DarkGreen, + "Aqua": Aqua, + "Cyan": Cyan, + "LightCyan": LightCyan, + "PaleTurquoise": PaleTurquoise, + "Aquamarine": Aquamarine, + "Turquoise": Turquoise, + "MediumTurquoise": MediumTurquoise, + "DarkTurquoise": DarkTurquoise, + "LightSeaGreen": LightSeaGreen, + "CadetBlue": CadetBlue, + "DarkCyan": DarkCyan, + "Teal": Teal, + "LightSteelBlue": LightSteelBlue, + "PowderBlue": PowderBlue, + "LightBlue": LightBlue, + "SkyBlue": SkyBlue, + "LightSkyBlue": LightSkyBlue, + "DeepSkyBlue": DeepSkyBlue, + "DodgerBlue": DodgerBlue, + "CornflowerBlue": CornflowerBlue, + "SteelBlue": SteelBlue, + "RoyalBlue": RoyalBlue, + "Blue": Blue, + "MediumBlue": MediumBlue, + "DarkBlue": DarkBlue, + "Navy": Navy, + "MidnightBlue": MidnightBlue, + "Lavender": Lavender, + "Thistle": Thistle, + "Plum": Plum, + "Violet": Violet, + "Orchid": Orchid, + "Fuchsia": Fuchsia, + "Magenta": Magenta, + "MediumOrchid": MediumOrchid, + "MediumPurple": MediumPurple, + "BlueViolet": BlueViolet, + "DarkViolet": DarkViolet, + "DarkOrchid": DarkOrchid, + "DarkMagenta": DarkMagenta, + "Purple": Purple, + "Indigo": Indigo, + "DarkSlateBlue": DarkSlateBlue, + "SlateBlue": SlateBlue, + "MediumSlateBlue": MediumSlateBlue, + "White": White, + "Snow": Snow, + "Honeydew": Honeydew, + "MintCream": MintCream, + "Azure": Azure, + "AliceBlue": AliceBlue, + "GhostWhite": GhostWhite, + "WhiteSmoke": WhiteSmoke, + "Seashell": Seashell, + "Beige": Beige, + "OldLace": OldLace, + "FloralWhite": FloralWhite, + "Ivory": Ivory, + "AntiqueWhite": AntiqueWhite, + "Linen": Linen, + "LavenderBlush": LavenderBlush, + "MistyRose": MistyRose, + "Gainsboro": Gainsboro, + "LightGray": LightGray, + "Silver": Silver, + "DarkGray": DarkGray, + "Gray": Gray, + "DimGray": DimGray, + "LightSlateGray": LightSlateGray, + "SlateGray": SlateGray, + "DarkSlateGray": DarkSlateGray, + "Black": Black, +} + +func hexColor(scol string) colorful.Color { + c, _ := colorful.Hex(scol) + return c +} + +func lookupColorByName(colorName string) *colorful.Color { + if color, ok := colorByNameMap[colorName]; ok { + return &color + } + + return nil +} diff --git a/convenience.go b/convenience.go new file mode 100644 index 0000000..2322c27 --- /dev/null +++ b/convenience.go @@ -0,0 +1,97 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt + +import colorful "github.com/lucasb-eyer/go-colorful" + +// StyleOption defines style option for strings +type StyleOption func(*String) + +// PlainTextLength returns the length of the input text without any escape +// sequences. The function will panic in the unlikely case of a parse issue. +func PlainTextLength(text string) int { + result, err := ParseString(text) + if err != nil { + panic(err) + } + + return len(*result) +} + +// Substring returns a substring of a text that may contains escape sequences. +// The function will panic in the unlikely case of a parse issue. +func Substring(text string, start int, end int) string { + result, err := ParseString(text) + if err != nil { + panic(err) + } + + result.Substring(start, end) + + return result.String() +} + +// Bold applies the bold text parameter +func Bold() StyleOption { + return func(s *String) { + for i := range *s { + (*s)[i].Settings |= 1 << 2 + } + } +} + +// Italic applies the italic text parameter +func Italic() StyleOption { + return func(s *String) { + for i := range *s { + (*s)[i].Settings |= 1 << 3 + } + } +} + +// Foreground sets the given color as the foreground color of the text +func Foreground(color colorful.Color) StyleOption { + r, g, b := color.RGB255() + return func(s *String) { + for i := range *s { + (*s)[i].Settings |= 1 + (*s)[i].Settings |= uint64(r) << 8 + (*s)[i].Settings |= uint64(g) << 16 + (*s)[i].Settings |= uint64(b) << 24 + } + } +} + +// Style is a multi-purpose function to programmatically apply styles and other +// changes to an input text. The function will panic in the unlikely case of a +// parse issue. +func Style(text string, styleOptions ...StyleOption) string { + result, err := ParseString(text) + if err != nil { + panic(err) + } + + for _, styleOption := range styleOptions { + styleOption(result) + } + + return result.String() +} diff --git a/convenience_test.go b/convenience_test.go new file mode 100644 index 0000000..eee1258 --- /dev/null +++ b/convenience_test.go @@ -0,0 +1,80 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/gonvenience/bunt" +) + +var _ = Describe("convenience functions", func() { + BeforeEach(func() { + ColorSetting = ON + TrueColorSetting = ON + }) + + AfterEach(func() { + ColorSetting = AUTO + TrueColorSetting = AUTO + }) + + Context("substring function", func() { + It("should work to correctly cut a string with ANSI sequences", func() { + input := Substring("Text: \x1b[1mThis\x1b[0m text is too _long_", 6, 22) + expected := "\x1b[1mThis\x1b[0m text is too" + Expect(input).To(BeEquivalentTo(expected)) + }) + }) + + Context("text length function", func() { + It("should return the correct text length of a string with ANSI sequences", func() { + Expect(PlainTextLength("\x1b[0;32mINFO \x1b[mNo dependencies found")).To(BeEquivalentTo(len("INFO No dependencies found"))) + }) + + It("should return the right size when used on strings created by the bunt package", func() { + Expect(PlainTextLength(Sprintf("*This* text is too long"))).To(BeEquivalentTo(len(Sprintf("This text is too long")))) + }) + }) + + Context("style function", func() { + It("should apply bold parameter to a input string", func() { + Expect(Style("text", Bold())).To( + BeEquivalentTo("\x1b[1mtext\x1b[0m")) + }) + + It("should apply italic parameter to a input string", func() { + Expect(Style("text", Italic())).To( + BeEquivalentTo("\x1b[3mtext\x1b[0m")) + }) + + It("should apply a custom foreground color to a input string", func() { + Expect(Style("text", Foreground(CornflowerBlue))).To( + BeEquivalentTo("\x1b[38;2;100;149;237mtext\x1b[0m")) + }) + + It("should apply the bold parameter and a custom foreground color to a input string", func() { + Expect(Style("text", Bold(), Foreground(CornflowerBlue))).To( + BeEquivalentTo("\x1b[1;38;2;100;149;237mtext\x1b[0m")) + }) + }) +}) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..29abc6e --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/gonvenience/bunt + +go 1.12 + +require ( + github.com/gonvenience/term v1.0.0 + github.com/lucasb-eyer/go-colorful v1.0.2 + github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 + github.com/onsi/ginkgo v1.8.0 + github.com/onsi/gomega v1.5.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9ac7969 --- /dev/null +++ b/go.sum @@ -0,0 +1,53 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gonvenience/term v1.0.0 h1:joCB/j0Ngmdakd3muuLgAGPMf7DNKdoe708c1I6RiBs= +github.com/gonvenience/term v1.0.0/go.mod h1:wohD4Iqso9Eol7qc2VnNhSFFhZxok5PvO7pZhdrAn4E= +github.com/homeport/gonvenience v1.8.0/go.mod h1:G2NH1mGKb2RtQ/xy7Axv5Tnnwzq4yE6NoYyINd1Lvuk= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/lucasb-eyer/go-colorful v0.0.0-20180526135729-345fbb3dbcdb/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= +github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= +github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk= +github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936 h1:kw1v0NlnN+GZcU8Ma8CLF2Zzgjfx95gs3/GN3vYAPpo= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/model.go b/model.go new file mode 100644 index 0000000..d8ed686 --- /dev/null +++ b/model.go @@ -0,0 +1,47 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt + +// String is a string with color information +type String []ColoredRune + +// ColoredRune is a rune with additional color information. +// +// Bit details: +// - 1st bit, foreground color on/off +// - 2nd bit, background color on/off +// - 3rd bit, bold on/off +// - 4th bit, italic on/off +// - 5th bit, underline on/off +// - 6th-8th bit, unused/reserved +// - 9th-32nd bit, 24 bit RGB foreground color +// - 33rd-56th bit, 24 bit RGB background color +// - 57th-64th bit, unused/reserved +type ColoredRune struct { + Symbol rune + Settings uint64 +} + +// Substring cuts the String to a sub-string using the provided absolute start +// and end indicies. +func (s *String) Substring(from, to int) { + *s = (*s)[from:to] +} diff --git a/model_test.go b/model_test.go new file mode 100644 index 0000000..5c5c143 --- /dev/null +++ b/model_test.go @@ -0,0 +1,53 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/gonvenience/bunt" +) + +var _ = Describe("working with colored strings", func() { + Context("cut a substring from a colored string", func() { + BeforeEach(func() { + ColorSetting = ON + }) + + AfterEach(func() { + ColorSetting = AUTO + }) + + It("should be possible to cut out a substring from a colored string", func() { + input := "Example: \x1b[1mbold\x1b[0m, \x1b[3mitalic\x1b[0m, \x1b[4munderline\x1b[0m, \x1b[38;2;133;247;7mforeground\x1b[0m, and \x1b[48;2;133;247;7mbackground\x1b[0m." + result, err := ParseString(input) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + result.Substring(11, 28) + + expected := "\x1b[1mld\x1b[0m, \x1b[3mitalic\x1b[0m, \x1b[4munder\x1b[0m" + actual := result.String() + Expect(actual).To(BeEquivalentTo(expected)) + }) + }) +}) diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..27d009d --- /dev/null +++ b/parse.go @@ -0,0 +1,206 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt + +import ( + "bytes" + "fmt" + "regexp" + "sort" + "strconv" + "strings" +) + +// ProcessMarkdownStyleTextAnnotations specifies whether Mardown style text +// annotations like *bold*, or _italic_ should be processed during parsing. +var ProcessMarkdownStyleTextAnnotations = true + +var ( + escapeSeqRegExp = regexp.MustCompile(`\x1b\[(\d+(;\d+)*)m`) + boldMarker = regexp.MustCompile(`\*([^*]+?)\*`) + italicMarker = regexp.MustCompile(`_([^_]+?)_`) + underlineMarker = regexp.MustCompile(`~([^~]+?)~`) + colorMarker = regexp.MustCompile(`(\w+)\{([^}]+?)\}`) +) + +// ParseString parses a string that can contain both ANSI escape code Select +// Graphic Rendition (SGR) codes and Markdown style text annotations, for +// example *bold* or _italic_. +// SGR details : https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +func ParseString(input string) (*String, error) { + var ( + pointer = 0 + current = uint64(0) + result = String{} + err = error(nil) + ) + + // Special case: the escape sequence without any parameter is equivalent to + // the reset escape sequence. + input = strings.ReplaceAll(input, "\x1b[m", "\x1b[0m") + + for _, submatch := range escapeSeqRegExp.FindAllStringSubmatchIndex(input, -1) { + fullMatchStart, fullMatchEnd := submatch[0], submatch[1] + settingsStart, settingsEnd := submatch[2], submatch[3] + + for i := pointer; i < fullMatchStart; i++ { + result = append(result, ColoredRune{rune(input[i]), current}) + } + + current, err = parseSelectGraphicRenditionEscapeSequence(input[settingsStart:settingsEnd]) + if err != nil { + return nil, err + } + + pointer = fullMatchEnd + } + + // Flush the remaining input string part into the result + for i := pointer; i < len(input); i++ { + result = append(result, ColoredRune{rune(input[i]), current}) + } + + if ProcessMarkdownStyleTextAnnotations { + if err := processMarkdownStyleTextAnnotations(&result); err != nil { + return nil, err + } + } + + return &result, nil +} + +func parseSelectGraphicRenditionEscapeSequence(escapeSeq string) (uint64, error) { + values := []uint8{} + for _, x := range strings.Split(escapeSeq, ";") { + // Note: This only works, because of the regular expression only matching + // digits. Therefore, it should be okay to omit the error. + value, _ := strconv.Atoi(x) + values = append(values, uint8(value)) + } + + result := uint64(0) + + for i := 0; i < len(values); i++ { + switch values[i] { + case 1: // bold + result |= 1 << 2 + + case 3: // italic + result |= 1 << 3 + + case 4: // underline + result |= 1 << 4 + + case 38: // foreground color + if i+4 >= len(values) { + return 0, fmt.Errorf("insufficient data to parse RGB foreground color") + } + + result |= 1 << 0 + result |= uint64(values[i+2]) << 8 + result |= uint64(values[i+3]) << 16 + result |= uint64(values[i+4]) << 24 + i += 4 + + case 48: // background color + if i+4 >= len(values) { + return 0, fmt.Errorf("insufficient data to parse RGB background color") + } + + result |= 1 << 1 + result |= uint64(values[i+2]) << 32 + result |= uint64(values[i+3]) << 40 + result |= uint64(values[i+4]) << 48 + i += 4 + } + } + + return result, nil +} + +func processMarkdownStyleTextAnnotations(text *String) error { + var buffer bytes.Buffer + for _, coloredRune := range *text { + buffer.WriteRune(coloredRune.Symbol) + } + + raw := buffer.String() + toBeDeleted := []int{} + + // Process text annotation markers for bold, italic and underline + helperMap := map[uint64]*regexp.Regexp{ + uint64(1 << 2): boldMarker, + uint64(1 << 3): italicMarker, + uint64(1 << 4): underlineMarker, + } + + for mask, regex := range helperMap { + for _, match := range regex.FindAllStringSubmatchIndex(raw, -1) { + fullMatchStart, fullMatchEnd := match[0], match[1] + textStart, textEnd := match[2], match[3] + + for i := textStart; i < textEnd; i++ { + (*text)[i].Settings |= mask + } + + toBeDeleted = append(toBeDeleted, fullMatchStart, fullMatchEnd-1) + } + } + + // Process text annotation markers that specify a foreground color for a + // specific part of the text + for _, match := range colorMarker.FindAllStringSubmatchIndex(raw, -1) { + fullMatchStart, fullMatchEnd := match[0], match[1] + colorName := raw[match[2]:match[3]] + textStart, textEnd := match[4], match[5] + + color := lookupColorByName(colorName) + if color == nil { + return fmt.Errorf("unable to find color by name: %s", colorName) + } + + r, g, b := color.RGB255() + for i := textStart; i < textEnd; i++ { + (*text)[i].Settings |= 1 + (*text)[i].Settings |= uint64(r) << 8 + (*text)[i].Settings |= uint64(g) << 16 + (*text)[i].Settings |= uint64(b) << 24 + } + + for i := fullMatchStart; i < fullMatchEnd; i++ { + if i < textStart || i > textEnd-1 { + toBeDeleted = append(toBeDeleted, i) + } + } + } + + // Finally, sort the runes to be deleted in descending order and delete them + // one by one to get rid of the text annotation markers + sort.Slice(toBeDeleted, func(i, j int) bool { + return toBeDeleted[i] > toBeDeleted[j] + }) + + for _, idx := range toBeDeleted { + (*text) = append((*text)[:idx], (*text)[idx+1:]...) + } + + return nil +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..d3e4e03 --- /dev/null +++ b/parse_test.go @@ -0,0 +1,199 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/gonvenience/bunt" +) + +var _ = Describe("parse input string", func() { + Context("parse Select Graphic Rendition (SGR) based input", func() { + BeforeEach(func() { + ColorSetting = ON + }) + + AfterEach(func() { + ColorSetting = AUTO + }) + + It("should parse an input string with SGR parameters", func() { + input := "Example: \x1b[1mbold\x1b[0m, \x1b[3mitalic\x1b[0m, \x1b[4munderline\x1b[0m, \x1b[38;2;133;247;7mforeground\x1b[0m, and \x1b[48;2;133;247;7mbackground\x1b[0m." + result, err := ParseString(input) + Expect(err).ToNot(HaveOccurred()) + Expect(*result).To( + BeEquivalentTo(String([]ColoredRune{ + {'E', 0x0000000000000000}, + {'x', 0x0000000000000000}, + {'a', 0x0000000000000000}, + {'m', 0x0000000000000000}, + {'p', 0x0000000000000000}, + {'l', 0x0000000000000000}, + {'e', 0x0000000000000000}, + {':', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'b', 0x0000000000000004}, + {'o', 0x0000000000000004}, + {'l', 0x0000000000000004}, + {'d', 0x0000000000000004}, + {',', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'i', 0x0000000000000008}, + {'t', 0x0000000000000008}, + {'a', 0x0000000000000008}, + {'l', 0x0000000000000008}, + {'i', 0x0000000000000008}, + {'c', 0x0000000000000008}, + {',', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'u', 0x0000000000000010}, + {'n', 0x0000000000000010}, + {'d', 0x0000000000000010}, + {'e', 0x0000000000000010}, + {'r', 0x0000000000000010}, + {'l', 0x0000000000000010}, + {'i', 0x0000000000000010}, + {'n', 0x0000000000000010}, + {'e', 0x0000000000000010}, + {',', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'f', 0x0000000007F78501}, + {'o', 0x0000000007F78501}, + {'r', 0x0000000007F78501}, + {'e', 0x0000000007F78501}, + {'g', 0x0000000007F78501}, + {'r', 0x0000000007F78501}, + {'o', 0x0000000007F78501}, + {'u', 0x0000000007F78501}, + {'n', 0x0000000007F78501}, + {'d', 0x0000000007F78501}, + {',', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'a', 0x0000000000000000}, + {'n', 0x0000000000000000}, + {'d', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'b', 0x0007F78500000002}, + {'a', 0x0007F78500000002}, + {'c', 0x0007F78500000002}, + {'k', 0x0007F78500000002}, + {'g', 0x0007F78500000002}, + {'r', 0x0007F78500000002}, + {'o', 0x0007F78500000002}, + {'u', 0x0007F78500000002}, + {'n', 0x0007F78500000002}, + {'d', 0x0007F78500000002}, + {'.', 0x0000000000000000}, + }))) + }) + + It("should bail out nicely in case of invalid 24 bit foreground color parameters", func() { + input := "Invalid: \x1b[38;2;1;2mfoobar\x1b[0m." + result, err := ParseString(input) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + }) + + It("should bail out nicely in case of invalid 24 bit background color parameters", func() { + input := "Invalid: \x1b[48;2;1;2mfoobar\x1b[0m." + result, err := ParseString(input) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + }) + }) + + Context("parse markdown style text annotations", func() { + BeforeEach(func() { + ColorSetting = ON + }) + + AfterEach(func() { + ColorSetting = AUTO + }) + + It("should parse an input string with markdown style text annotations", func() { + input := "Example: *bold*, _italic_, ~underline~, and CornflowerBlue{foreground}." + result, err := ParseString(input) + Expect(err).ToNot(HaveOccurred()) + Expect(*result).To( + BeEquivalentTo(String([]ColoredRune{ + {'E', 0x0000000000000000}, + {'x', 0x0000000000000000}, + {'a', 0x0000000000000000}, + {'m', 0x0000000000000000}, + {'p', 0x0000000000000000}, + {'l', 0x0000000000000000}, + {'e', 0x0000000000000000}, + {':', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'b', 0x0000000000000004}, + {'o', 0x0000000000000004}, + {'l', 0x0000000000000004}, + {'d', 0x0000000000000004}, + {',', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'i', 0x0000000000000008}, + {'t', 0x0000000000000008}, + {'a', 0x0000000000000008}, + {'l', 0x0000000000000008}, + {'i', 0x0000000000000008}, + {'c', 0x0000000000000008}, + {',', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'u', 0x0000000000000010}, + {'n', 0x0000000000000010}, + {'d', 0x0000000000000010}, + {'e', 0x0000000000000010}, + {'r', 0x0000000000000010}, + {'l', 0x0000000000000010}, + {'i', 0x0000000000000010}, + {'n', 0x0000000000000010}, + {'e', 0x0000000000000010}, + {',', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'a', 0x0000000000000000}, + {'n', 0x0000000000000000}, + {'d', 0x0000000000000000}, + {' ', 0x0000000000000000}, + {'f', 0x00000000ED956401}, + {'o', 0x00000000ED956401}, + {'r', 0x00000000ED956401}, + {'e', 0x00000000ED956401}, + {'g', 0x00000000ED956401}, + {'r', 0x00000000ED956401}, + {'o', 0x00000000ED956401}, + {'u', 0x00000000ED956401}, + {'n', 0x00000000ED956401}, + {'d', 0x00000000ED956401}, + {'.', 0x0000000000000000}, + }))) + }) + + It("should bail out nicely in case of invalid color name", func() { + input := "Invalid: InvalidColor{foobar}." + result, err := ParseString(input) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + }) + }) +}) diff --git a/print.go b/print.go new file mode 100644 index 0000000..9465d69 --- /dev/null +++ b/print.go @@ -0,0 +1,91 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt + +import ( + "fmt" + "io" +) + +func evaluateInputs(in ...interface{}) []interface{} { + result := make([]interface{}, len(in)) + for i, x := range in { + switch str := x.(type) { + case string: + result[i] = evaluateString(str) + } + } + + return result +} + +func evaluateString(in string) string { + if result, err := ParseString(in); err == nil { + return result.String() + } + + return in +} + +// Print wraps fmt.Print(a ...interface{}) and evaluates any text markers into its respective format +func Print(a ...interface{}) (n int, err error) { + return fmt.Print(evaluateInputs(a...)...) +} + +// Printf wraps fmt.Printf(format string, a ...interface{}) and evaluates any text markers into its respective format +func Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(evaluateString(format), a...) +} + +// Println wraps fmt.Println(a ...interface{}) and evaluates any text markers into its respective format +func Println(a ...interface{}) (n int, err error) { + return fmt.Println(evaluateInputs(a...)...) +} + +// Fprint wraps fmt.Fprint(w io.Writer, a ...interface{}) and evaluates any text markers into its respective format +func Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, evaluateInputs(a...)...) +} + +// Fprintf wraps fmt.Fprintf(w io.Writer, format string, a ...interface{}) and evaluates any text markers into its respective format +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, evaluateString(format), a...) +} + +// Fprintln wraps fmt.Fprintln(w io.Writer, a ...interface{}) and evaluates any text markers into its respective format +func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, evaluateInputs(a...)...) +} + +// Sprint wraps fmt.Sprint(a ...interface{}) and evaluates any text markers into its respective format +func Sprint(a ...interface{}) string { + return fmt.Sprint(evaluateInputs(a...)...) +} + +// Sprintf wraps fmt.Sprintf(format string, a ...interface{}) and evaluates any text markers into its respective format +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(evaluateString(format), a...) +} + +// Sprintln wraps fmt.Sprintln(a ...interface{}) and evaluates any text markers into its respective format +func Sprintln(a ...interface{}) string { + return fmt.Sprintln(evaluateInputs(a...)...) +} diff --git a/print_test.go b/print_test.go new file mode 100644 index 0000000..fec0085 --- /dev/null +++ b/print_test.go @@ -0,0 +1,154 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt_test + +import ( + "bufio" + "bytes" + "io" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/gonvenience/bunt" +) + +var _ = Describe("print functions", func() { + Context("process markdown style in Print functions", func() { + var captureStdout = func(f func()) string { + r, w, err := os.Pipe() + Expect(err).ToNot(HaveOccurred()) + + tmp := os.Stdout + defer func() { + os.Stdout = tmp + }() + + os.Stdout = w + f() + w.Close() + + var buf bytes.Buffer + io.Copy(&buf, r) + + return buf.String() + } + + BeforeEach(func() { + ColorSetting = ON + }) + + AfterEach(func() { + ColorSetting = AUTO + }) + + It("should parse and process markdown style in Print", func() { + Expect(captureStdout(func() { + Print("This should be *bold*.") + })).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.")) + }) + + It("should parse and process markdown style in Printf", func() { + Expect(captureStdout(func() { + Printf("This should be *%s*.", "bold") + })).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.")) + }) + + It("should parse and process markdown style in Println", func() { + Expect(captureStdout(func() { + Println("This should be *bold*.") + })).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.\n")) + }) + }) + + Context("process markdown style in Fprint functions", func() { + var ( + buf bytes.Buffer + out *bufio.Writer + ) + + BeforeEach(func() { + ColorSetting = ON + buf = bytes.Buffer{} + out = bufio.NewWriter(&buf) + }) + + AfterEach(func() { + ColorSetting = AUTO + }) + + It("should parse and process markdown style in Fprint", func() { + Fprint(out, "This should be *bold*.") + out.Flush() + Expect(buf.String()).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.")) + }) + + It("should parse and process markdown style in Fprintf", func() { + Fprintf(out, "This should be *%s*.", "bold") + out.Flush() + Expect(buf.String()).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.")) + }) + + It("should parse and process markdown style in Fprintln", func() { + Fprintln(out, "This should be *bold*.") + out.Flush() + Expect(buf.String()).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.\n")) + }) + }) + + Context("process markdown style in Sprint functions", func() { + BeforeEach(func() { + ColorSetting = ON + }) + + AfterEach(func() { + ColorSetting = AUTO + }) + + It("should parse and process markdown style in Sprint", func() { + Expect(Sprint("This should be *bold*.")).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.")) + }) + + It("should parse and process markdown style in Sprintf", func() { + Expect(Sprintf("This should be *%s*.", "bold")).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.")) + }) + + It("should parse and process markdown style in Sprintln", func() { + Expect(Sprintln("This should be *bold*.")).To(BeEquivalentTo("This should be \x1b[1mbold\x1b[0m.\n")) + }) + }) + + Context("weird use cases", func() { + BeforeEach(func() { + ColorSetting = ON + }) + + AfterEach(func() { + ColorSetting = AUTO + }) + + It("should ignore escape sequences that cannot be processed", func() { + Expect(Sprint("ok", "\x1b[38;2;1;2mnot ok\x1b[0m")).To( + BeEquivalentTo("ok\x1b[38;2;1;2mnot ok\x1b[0m")) + }) + }) +}) diff --git a/render.go b/render.go new file mode 100644 index 0000000..a639aaf --- /dev/null +++ b/render.go @@ -0,0 +1,145 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt + +import ( + "bytes" + "fmt" + "image/color" + "math" + "strconv" + "strings" + + ciede2000 "github.com/mattn/go-ciede2000" +) + +func (s String) String() string { + var ( + buffer = &bytes.Buffer{} + current = uint64(0) + useColors = UseColors() + ) + + for _, coloredRune := range s { + if useColors && (coloredRune.Settings != current) { + buffer.WriteString(renderSelectGraphicRenditionEscapeSequence(coloredRune.Settings)) + current = coloredRune.Settings + } + + buffer.WriteRune(coloredRune.Symbol) + } + + // Make sure to finish with a reset escape sequence + if current != 0 { + buffer.WriteString(renderSelectGraphicRenditionEscapeSequence(0)) + } + + return buffer.String() +} + +func renderSelectGraphicRenditionEscapeSequence(setting uint64) string { + if setting == 0 { + return renderEscapeSequence(0) + } + + parameters := []uint8{} + + if (setting & 0x04) != 0 { + parameters = append(parameters, 1) + } + + if (setting & 0x08) != 0 { + parameters = append(parameters, 3) + } + + if (setting & 0x10) != 0 { + parameters = append(parameters, 4) + } + + if (setting & 0x1) != 0 { + r, g, b := uint8((setting>>8)&0xFF), uint8((setting>>16)&0xFF), uint8((setting>>24)&0xFF) + if UseTrueColor() { + parameters = append(parameters, 38, 2, r, g, b) + + } else { + parameters = append(parameters, closest4bitColorParameter(r, g, b)) + } + } + + if (setting & 0x2) != 0 { + r, g, b := uint8((setting>>32)&0xFF), uint8((setting>>40)&0xFF), uint8((setting>>48)&0xFF) + if UseTrueColor() { + parameters = append(parameters, 48, 2, r, g, b) + + } else { + parameters = append(parameters, 10+closest4bitColorParameter(r, g, b)) + } + } + + return renderEscapeSequence(parameters...) +} + +func renderEscapeSequence(a ...uint8) string { + values := make([]string, len(a)) + for i := range a { + values[i] = strconv.Itoa(int(a[i])) + } + + return fmt.Sprintf("\x1b[%sm", strings.Join(values, ";")) +} + +// closest4bitColorParameter returns the color attribute which matches the best +// with the provided RGB color +func closest4bitColorParameter(r, g, b uint8) uint8 { + var ( + result = uint8(0) + target = &color.RGBA{r, g, b, 0xFF} + min = math.MaxFloat64 + helperMap = map[uint8]*color.RGBA{ + 30: &color.RGBA{0x00, 0x00, 0x00, 0xFF}, + 31: &color.RGBA{0xAA, 0x00, 0x00, 0xFF}, + 32: &color.RGBA{0x00, 0xAA, 0x00, 0xFF}, + 33: &color.RGBA{0xFF, 0xFF, 0x00, 0xFF}, + 34: &color.RGBA{0x00, 0x00, 0xAA, 0xFF}, + 35: &color.RGBA{0xAA, 0x00, 0xAA, 0xFF}, + 36: &color.RGBA{0x00, 0xAA, 0xAA, 0xFF}, + 37: &color.RGBA{0xAA, 0xAA, 0xAA, 0xFF}, + 90: &color.RGBA{0x55, 0x55, 0x55, 0xFF}, + 91: &color.RGBA{0xFF, 0x55, 0x55, 0xFF}, + 92: &color.RGBA{0x55, 0xFF, 0x55, 0xFF}, + 93: &color.RGBA{0xFF, 0xFF, 0x55, 0xFF}, + 94: &color.RGBA{0x55, 0x55, 0xFF, 0xFF}, + 95: &color.RGBA{0xFF, 0x55, 0xFF, 0xFF}, + 96: &color.RGBA{0x55, 0xFF, 0xFF, 0xFF}, + 97: &color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}, + } + ) + + // Calculate the distance between the target color and the available 4-bit + // colors using the `deltaE` algorithm to find the best match. + for attribute, candidate := range helperMap { + if distance := ciede2000.Diff(target, candidate); distance < min { + min, result = distance, attribute + } + } + + return result +} diff --git a/render_test.go b/render_test.go new file mode 100644 index 0000000..b67f330 --- /dev/null +++ b/render_test.go @@ -0,0 +1,240 @@ +// Copyright © 2019 The Homeport Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package bunt_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/gonvenience/bunt" +) + +var _ = Describe("render colored strings", func() { + Context("verify that rendering of colored strings returns correct results", func() { + BeforeEach(func(){ + ColorSetting = ON + TrueColorSetting = ON + }) + + AfterEach(func() { + ColorSetting = AUTO + TrueColorSetting = AUTO + }) + + It("should render colored output when colors are enabled", func() { + input := "Example: \x1b[1mbold\x1b[0m, \x1b[3mitalic\x1b[0m, \x1b[4munderline\x1b[0m, \x1b[38;2;133;247;7mforeground\x1b[0m, and \x1b[48;2;133;247;7mbackground\x1b[0m." + result, err := ParseString(input) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + Expect(result.String()).To(BeEquivalentTo(input)) + }) + + It("should render plain output when colors are not enabled", func() { + ColorSetting = OFF + + input := "Example: \x1b[1mbold\x1b[0m, \x1b[3mitalic\x1b[0m, \x1b[4munderline\x1b[0m, \x1b[38;2;133;247;7mforeground\x1b[0m, and \x1b[48;2;133;247;7mbackground\x1b[0m." + result, err := ParseString(input) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + Expect(result.String()).To(BeEquivalentTo("Example: bold, italic, underline, foreground, and background.")) + }) + }) + + Context("fallback to 4 bit colors for non true color terminals", func() { + BeforeEach(func() { + ColorSetting = ON + TrueColorSetting = OFF + }) + + AfterEach(func() { + ColorSetting = AUTO + TrueColorSetting = AUTO + }) + + var ( + f1 = func(color string) string { + result, err := ParseString(fmt.Sprintf("%s{%s}", color, "text")) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + return result.String() + } + + f2 = func(color uint8) string { + return fmt.Sprintf("\x1b[%dm%s\x1b[0m", color, "text") + } + ) + + It("should find a suitable 4 bit equivalent color for both foreground and background", func() { + input := "Example: \x1b[38;2;133;247;7mforeground\x1b[0m, and \x1b[48;2;133;247;7mbackground\x1b[0m." + result, err := ParseString(input) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + Expect(result.String()).To(BeEquivalentTo("Example: \x1b[92mforeground\x1b[0m, and \x1b[102mbackground\x1b[0m.")) + }) + + It("should match known combinations of a true color to a 4 bit color", func() { + Expect(f1("Black")).To(BeEquivalentTo(f2(30))) // Black matches Black (#30) + Expect(f1("Brown")).To(BeEquivalentTo(f2(31))) // Brown matches Red (#31) + Expect(f1("DarkRed")).To(BeEquivalentTo(f2(31))) // DarkRed matches Red (#31) + Expect(f1("FireBrick")).To(BeEquivalentTo(f2(31))) // FireBrick matches Red (#31) + Expect(f1("Maroon")).To(BeEquivalentTo(f2(31))) // Maroon matches Red (#31) + Expect(f1("SaddleBrown")).To(BeEquivalentTo(f2(31))) // SaddleBrown matches Red (#31) + Expect(f1("Sienna")).To(BeEquivalentTo(f2(31))) // Sienna matches Red (#31) + Expect(f1("DarkGreen")).To(BeEquivalentTo(f2(32))) // DarkGreen matches Green (#32) + Expect(f1("DarkSeaGreen")).To(BeEquivalentTo(f2(32))) // DarkSeaGreen matches Green (#32) + Expect(f1("ForestGreen")).To(BeEquivalentTo(f2(32))) // ForestGreen matches Green (#32) + Expect(f1("Green")).To(BeEquivalentTo(f2(32))) // Green matches Green (#32) + Expect(f1("LimeGreen")).To(BeEquivalentTo(f2(32))) // LimeGreen matches Green (#32) + Expect(f1("MediumSeaGreen")).To(BeEquivalentTo(f2(32))) // MediumSeaGreen matches Green (#32) + Expect(f1("Olive")).To(BeEquivalentTo(f2(32))) // Olive matches Green (#32) + Expect(f1("OliveDrab")).To(BeEquivalentTo(f2(32))) // OliveDrab matches Green (#32) + Expect(f1("SeaGreen")).To(BeEquivalentTo(f2(32))) // SeaGreen matches Green (#32) + Expect(f1("Gold")).To(BeEquivalentTo(f2(33))) // Gold matches Yellow (#33) + Expect(f1("Yellow")).To(BeEquivalentTo(f2(33))) // Yellow matches Yellow (#33) + Expect(f1("Blue")).To(BeEquivalentTo(f2(34))) // Blue matches Blue (#34) + Expect(f1("DarkBlue")).To(BeEquivalentTo(f2(34))) // DarkBlue matches Blue (#34) + Expect(f1("DarkSlateBlue")).To(BeEquivalentTo(f2(34))) // DarkSlateBlue matches Blue (#34) + Expect(f1("Indigo")).To(BeEquivalentTo(f2(34))) // Indigo matches Blue (#34) + Expect(f1("MediumBlue")).To(BeEquivalentTo(f2(34))) // MediumBlue matches Blue (#34) + Expect(f1("MidnightBlue")).To(BeEquivalentTo(f2(34))) // MidnightBlue matches Blue (#34) + Expect(f1("Navy")).To(BeEquivalentTo(f2(34))) // Navy matches Blue (#34) + Expect(f1("BlueViolet")).To(BeEquivalentTo(f2(35))) // BlueViolet matches Magenta (#35) + Expect(f1("DarkMagenta")).To(BeEquivalentTo(f2(35))) // DarkMagenta matches Magenta (#35) + Expect(f1("DarkOrchid")).To(BeEquivalentTo(f2(35))) // DarkOrchid matches Magenta (#35) + Expect(f1("DarkViolet")).To(BeEquivalentTo(f2(35))) // DarkViolet matches Magenta (#35) + Expect(f1("MediumVioletRed")).To(BeEquivalentTo(f2(35))) // MediumVioletRed matches Magenta (#35) + Expect(f1("Purple")).To(BeEquivalentTo(f2(35))) // Purple matches Magenta (#35) + Expect(f1("CadetBlue")).To(BeEquivalentTo(f2(36))) // CadetBlue matches Cyan (#36) + Expect(f1("DarkCyan")).To(BeEquivalentTo(f2(36))) // DarkCyan matches Cyan (#36) + Expect(f1("DarkTurquoise")).To(BeEquivalentTo(f2(36))) // DarkTurquoise matches Cyan (#36) + Expect(f1("DeepSkyBlue")).To(BeEquivalentTo(f2(36))) // DeepSkyBlue matches Cyan (#36) + Expect(f1("LightSeaGreen")).To(BeEquivalentTo(f2(36))) // LightSeaGreen matches Cyan (#36) + Expect(f1("MediumAquamarine")).To(BeEquivalentTo(f2(36))) // MediumAquamarine matches Cyan (#36) + Expect(f1("Teal")).To(BeEquivalentTo(f2(36))) // Teal matches Cyan (#36) + Expect(f1("BurlyWood")).To(BeEquivalentTo(f2(37))) // BurlyWood matches White (#37) + Expect(f1("DarkGoldenrod")).To(BeEquivalentTo(f2(37))) // DarkGoldenrod matches White (#37) + Expect(f1("DarkGray")).To(BeEquivalentTo(f2(37))) // DarkGray matches White (#37) + Expect(f1("Gray")).To(BeEquivalentTo(f2(37))) // Gray matches White (#37) + Expect(f1("LightPink")).To(BeEquivalentTo(f2(37))) // LightPink matches White (#37) + Expect(f1("LightSkyBlue")).To(BeEquivalentTo(f2(37))) // LightSkyBlue matches White (#37) + Expect(f1("LightSlateGray")).To(BeEquivalentTo(f2(37))) // LightSlateGray matches White (#37) + Expect(f1("LightSteelBlue")).To(BeEquivalentTo(f2(37))) // LightSteelBlue matches White (#37) + Expect(f1("Pink")).To(BeEquivalentTo(f2(37))) // Pink matches White (#37) + Expect(f1("RosyBrown")).To(BeEquivalentTo(f2(37))) // RosyBrown matches White (#37) + Expect(f1("SandyBrown")).To(BeEquivalentTo(f2(37))) // SandyBrown matches White (#37) + Expect(f1("Silver")).To(BeEquivalentTo(f2(37))) // Silver matches White (#37) + Expect(f1("Tan")).To(BeEquivalentTo(f2(37))) // Tan matches White (#37) + Expect(f1("Thistle")).To(BeEquivalentTo(f2(37))) // Thistle matches White (#37) + Expect(f1("DarkOliveGreen")).To(BeEquivalentTo(f2(90))) // DarkOliveGreen matches BrightBlack (#90) + Expect(f1("DarkSlateGray")).To(BeEquivalentTo(f2(90))) // DarkSlateGray matches BrightBlack (#90) + Expect(f1("DimGray")).To(BeEquivalentTo(f2(90))) // DimGray matches BrightBlack (#90) + Expect(f1("Chocolate")).To(BeEquivalentTo(f2(91))) // Chocolate matches BrightRed (#91) + Expect(f1("Coral")).To(BeEquivalentTo(f2(91))) // Coral matches BrightRed (#91) + Expect(f1("Crimson")).To(BeEquivalentTo(f2(91))) // Crimson matches BrightRed (#91) + Expect(f1("DarkOrange")).To(BeEquivalentTo(f2(91))) // DarkOrange matches BrightRed (#91) + Expect(f1("DarkSalmon")).To(BeEquivalentTo(f2(91))) // DarkSalmon matches BrightRed (#91) + Expect(f1("IndianRed")).To(BeEquivalentTo(f2(91))) // IndianRed matches BrightRed (#91) + Expect(f1("LightCoral")).To(BeEquivalentTo(f2(91))) // LightCoral matches BrightRed (#91) + Expect(f1("LightSalmon")).To(BeEquivalentTo(f2(91))) // LightSalmon matches BrightRed (#91) + Expect(f1("OrangeRed")).To(BeEquivalentTo(f2(91))) // OrangeRed matches BrightRed (#91) + Expect(f1("PaleVioletRed")).To(BeEquivalentTo(f2(91))) // PaleVioletRed matches BrightRed (#91) + Expect(f1("Peru")).To(BeEquivalentTo(f2(91))) // Peru matches BrightRed (#91) + Expect(f1("Red")).To(BeEquivalentTo(f2(91))) // Red matches BrightRed (#91) + Expect(f1("Salmon")).To(BeEquivalentTo(f2(91))) // Salmon matches BrightRed (#91) + Expect(f1("Tomato")).To(BeEquivalentTo(f2(91))) // Tomato matches BrightRed (#91) + Expect(f1("Chartreuse")).To(BeEquivalentTo(f2(92))) // Chartreuse matches BrightGreen (#92) + Expect(f1("GreenYellow")).To(BeEquivalentTo(f2(92))) // GreenYellow matches BrightGreen (#92) + Expect(f1("LawnGreen")).To(BeEquivalentTo(f2(92))) // LawnGreen matches BrightGreen (#92) + Expect(f1("LightGreen")).To(BeEquivalentTo(f2(92))) // LightGreen matches BrightGreen (#92) + Expect(f1("Lime")).To(BeEquivalentTo(f2(92))) // Lime matches BrightGreen (#92) + Expect(f1("MediumSpringGreen")).To(BeEquivalentTo(f2(92))) // MediumSpringGreen matches BrightGreen (#92) + Expect(f1("PaleGreen")).To(BeEquivalentTo(f2(92))) // PaleGreen matches BrightGreen (#92) + Expect(f1("SpringGreen")).To(BeEquivalentTo(f2(92))) // SpringGreen matches BrightGreen (#92) + Expect(f1("YellowGreen")).To(BeEquivalentTo(f2(92))) // YellowGreen matches BrightGreen (#92) + Expect(f1("DarkKhaki")).To(BeEquivalentTo(f2(93))) // DarkKhaki matches BrightYellow (#93) + Expect(f1("Goldenrod")).To(BeEquivalentTo(f2(93))) // Goldenrod matches BrightYellow (#93) + Expect(f1("Khaki")).To(BeEquivalentTo(f2(93))) // Khaki matches BrightYellow (#93) + Expect(f1("Orange")).To(BeEquivalentTo(f2(93))) // Orange matches BrightYellow (#93) + Expect(f1("PaleGoldenrod")).To(BeEquivalentTo(f2(93))) // PaleGoldenrod matches BrightYellow (#93) + Expect(f1("CornflowerBlue")).To(BeEquivalentTo(f2(94))) // CornflowerBlue matches BrightBlue (#94) + Expect(f1("DodgerBlue")).To(BeEquivalentTo(f2(94))) // DodgerBlue matches BrightBlue (#94) + Expect(f1("MediumPurple")).To(BeEquivalentTo(f2(94))) // MediumPurple matches BrightBlue (#94) + Expect(f1("MediumSlateBlue")).To(BeEquivalentTo(f2(94))) // MediumSlateBlue matches BrightBlue (#94) + Expect(f1("RoyalBlue")).To(BeEquivalentTo(f2(94))) // RoyalBlue matches BrightBlue (#94) + Expect(f1("SlateBlue")).To(BeEquivalentTo(f2(94))) // SlateBlue matches BrightBlue (#94) + Expect(f1("SlateGray")).To(BeEquivalentTo(f2(94))) // SlateGray matches BrightBlue (#94) + Expect(f1("SteelBlue")).To(BeEquivalentTo(f2(94))) // SteelBlue matches BrightBlue (#94) + Expect(f1("DeepPink")).To(BeEquivalentTo(f2(95))) // DeepPink matches BrightMagenta (#95) + Expect(f1("Fuchsia")).To(BeEquivalentTo(f2(95))) // Fuchsia matches BrightMagenta (#95) + Expect(f1("HotPink")).To(BeEquivalentTo(f2(95))) // HotPink matches BrightMagenta (#95) + Expect(f1("Magenta")).To(BeEquivalentTo(f2(95))) // Magenta matches BrightMagenta (#95) + Expect(f1("MediumOrchid")).To(BeEquivalentTo(f2(95))) // MediumOrchid matches BrightMagenta (#95) + Expect(f1("Orchid")).To(BeEquivalentTo(f2(95))) // Orchid matches BrightMagenta (#95) + Expect(f1("Plum")).To(BeEquivalentTo(f2(95))) // Plum matches BrightMagenta (#95) + Expect(f1("Violet")).To(BeEquivalentTo(f2(95))) // Violet matches BrightMagenta (#95) + Expect(f1("Aqua")).To(BeEquivalentTo(f2(96))) // Aqua matches BrightCyan (#96) + Expect(f1("Aquamarine")).To(BeEquivalentTo(f2(96))) // Aquamarine matches BrightCyan (#96) + Expect(f1("Cyan")).To(BeEquivalentTo(f2(96))) // Cyan matches BrightCyan (#96) + Expect(f1("LightBlue")).To(BeEquivalentTo(f2(96))) // LightBlue matches BrightCyan (#96) + Expect(f1("MediumTurquoise")).To(BeEquivalentTo(f2(96))) // MediumTurquoise matches BrightCyan (#96) + Expect(f1("PaleTurquoise")).To(BeEquivalentTo(f2(96))) // PaleTurquoise matches BrightCyan (#96) + Expect(f1("PowderBlue")).To(BeEquivalentTo(f2(96))) // PowderBlue matches BrightCyan (#96) + Expect(f1("SkyBlue")).To(BeEquivalentTo(f2(96))) // SkyBlue matches BrightCyan (#96) + Expect(f1("Turquoise")).To(BeEquivalentTo(f2(96))) // Turquoise matches BrightCyan (#96) + Expect(f1("AliceBlue")).To(BeEquivalentTo(f2(97))) // AliceBlue matches BrightWhite (#97) + Expect(f1("AntiqueWhite")).To(BeEquivalentTo(f2(97))) // AntiqueWhite matches BrightWhite (#97) + Expect(f1("Azure")).To(BeEquivalentTo(f2(97))) // Azure matches BrightWhite (#97) + Expect(f1("Beige")).To(BeEquivalentTo(f2(97))) // Beige matches BrightWhite (#97) + Expect(f1("Bisque")).To(BeEquivalentTo(f2(97))) // Bisque matches BrightWhite (#97) + Expect(f1("BlanchedAlmond")).To(BeEquivalentTo(f2(97))) // BlanchedAlmond matches BrightWhite (#97) + Expect(f1("Cornsilk")).To(BeEquivalentTo(f2(97))) // Cornsilk matches BrightWhite (#97) + Expect(f1("FloralWhite")).To(BeEquivalentTo(f2(97))) // FloralWhite matches BrightWhite (#97) + Expect(f1("Gainsboro")).To(BeEquivalentTo(f2(97))) // Gainsboro matches BrightWhite (#97) + Expect(f1("GhostWhite")).To(BeEquivalentTo(f2(97))) // GhostWhite matches BrightWhite (#97) + Expect(f1("Honeydew")).To(BeEquivalentTo(f2(97))) // Honeydew matches BrightWhite (#97) + Expect(f1("Ivory")).To(BeEquivalentTo(f2(97))) // Ivory matches BrightWhite (#97) + Expect(f1("Lavender")).To(BeEquivalentTo(f2(97))) // Lavender matches BrightWhite (#97) + Expect(f1("LavenderBlush")).To(BeEquivalentTo(f2(97))) // LavenderBlush matches BrightWhite (#97) + Expect(f1("LemonChiffon")).To(BeEquivalentTo(f2(97))) // LemonChiffon matches BrightWhite (#97) + Expect(f1("LightCyan")).To(BeEquivalentTo(f2(97))) // LightCyan matches BrightWhite (#97) + Expect(f1("LightGoldenrodYellow")).To(BeEquivalentTo(f2(97))) // LightGoldenrodYellow matches BrightWhite (#97) + Expect(f1("LightGray")).To(BeEquivalentTo(f2(97))) // LightGray matches BrightWhite (#97) + Expect(f1("LightYellow")).To(BeEquivalentTo(f2(97))) // LightYellow matches BrightWhite (#97) + Expect(f1("Linen")).To(BeEquivalentTo(f2(97))) // Linen matches BrightWhite (#97) + Expect(f1("MintCream")).To(BeEquivalentTo(f2(97))) // MintCream matches BrightWhite (#97) + Expect(f1("MistyRose")).To(BeEquivalentTo(f2(97))) // MistyRose matches BrightWhite (#97) + Expect(f1("Moccasin")).To(BeEquivalentTo(f2(97))) // Moccasin matches BrightWhite (#97) + Expect(f1("NavajoWhite")).To(BeEquivalentTo(f2(97))) // NavajoWhite matches BrightWhite (#97) + Expect(f1("OldLace")).To(BeEquivalentTo(f2(97))) // OldLace matches BrightWhite (#97) + Expect(f1("PapayaWhip")).To(BeEquivalentTo(f2(97))) // PapayaWhip matches BrightWhite (#97) + Expect(f1("PeachPuff")).To(BeEquivalentTo(f2(97))) // PeachPuff matches BrightWhite (#97) + Expect(f1("Seashell")).To(BeEquivalentTo(f2(97))) // Seashell matches BrightWhite (#97) + Expect(f1("Snow")).To(BeEquivalentTo(f2(97))) // Snow matches BrightWhite (#97) + Expect(f1("Wheat")).To(BeEquivalentTo(f2(97))) // Wheat matches BrightWhite (#97) + Expect(f1("White")).To(BeEquivalentTo(f2(97))) // White matches BrightWhite (#97) + Expect(f1("WhiteSmoke")).To(BeEquivalentTo(f2(97))) // WhiteSmoke matches BrightWhite (#97) + }) + }) +})