From 00e62e4810babea2ba8fb4b6d5e83ffea74c6f39 Mon Sep 17 00:00:00 2001 From: Samuel Date: Sun, 8 Nov 2020 15:52:10 +0100 Subject: [PATCH] feat: support color names for basic 16 ANSI colors Closes #127 --- docs/docs/configuration.md | 25 ++++++++-- docs/docs/segment-battery.md | 8 +-- docs/docs/segment-exit.md | 4 +- docs/docs/segment-git.md | 14 +++--- docs/docs/segment-session.md | 6 +-- docs/src/pages/index.js | 2 +- renderer.go | 69 ++++++++++++++++++++++---- renderer_test.go | 96 ++++++++++++++++++++++++++++++++++++ 8 files changed, 192 insertions(+), 32 deletions(-) diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index cb7aa936..f2e0ba50 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -103,8 +103,8 @@ understand how to configure a segment. - invert_powerline: `boolean` - leading_diamond: `string` - trailing_diamond: `string` -- foreground: `string` [hex color code][colors] -- background: `string` [hex color code][colors] +- foreground: `string` [color][colors] +- background: `string` [color][colors] - properties: `array` of `Property`: `string` ### Type @@ -205,8 +205,21 @@ do so like this: "prefix": "<#CB4B16>┏[", ``` -Oh my Posh offers support for hex [colors][colors] as well as the `transparent` keyword to create either a transparent foreground -override or transparent background color using the segement's [foreground][fg] property. +Oh my Posh mainly supports three different color types being +* Typical [hex colors][hexcolors] (for example `#CB4B16`). + +* The `transparent` keyword which can be used to create either a transparent foreground override + or transparent background color using the segement's foreground property. + +* 16 [ANSI color names][ansicolors]. + + These include 8 basic ANSI colors and `default`: + + `black` `red` `green` `yellow` `blue` `magenta` `cyan` `white` `default` + + as well as 8 extended ANSI colors: + + `darkGray` `lightRed` `lightGreen` `lightYellow` `lightBlue` `lightMagenta` `lightCyan` `lightWhite` ## Full Sample @@ -294,5 +307,7 @@ override or transparent background color using the segement's [foreground][fg] p [releases]: https://github.com/JanDeDobbeleer/oh-my-posh3/releases/latest [nf]: https://www.nerdfonts.com/ [segments]: /docs/battery -[colors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ +[colors]: #colors +[hexcolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ +[ansicolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ [fg]: /docs/configure#foreground diff --git a/docs/docs/segment-battery.md b/docs/docs/segment-battery.md index 9ef98318..bab679c2 100644 --- a/docs/docs/segment-battery.md +++ b/docs/docs/segment-battery.md @@ -39,8 +39,8 @@ Battery displays the remaining power percentage for your battery. - discharging_icon: `string` - icon to display on the left when discharging - defaults to empty - charged_icon: `string` - icon to display on the left when fully charged - defaults to empty - color_background: `boolean` - color the background or foreground for properties below - defaults to `false` -- charged_color: `string` [hex color code][colors] - color to use when fully charged - defaults to segment color -- charging_color: `string` [hex color code][colors] - color to use when charging - defaults to segment color -- discharging_color: `string` [hex color code][colors] - color to use when discharging - defaults to segment color +- charged_color: `string` [color][colors] - color to use when fully charged - defaults to segment color +- charging_color: `string` [color][colors] - color to use when charging - defaults to segment color +- discharging_color: `string` [color][colors] - color to use when discharging - defaults to segment color -[colors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ +[colors]: /docs/configure#colors diff --git a/docs/docs/segment-exit.md b/docs/docs/segment-exit.md index bdef6440..d1c7f088 100644 --- a/docs/docs/segment-exit.md +++ b/docs/docs/segment-exit.md @@ -33,6 +33,6 @@ Displays the last exit code or that the last command failed based on the configu - display_exit_code: `boolean` - show or hide the exit code - defaults to `true` - always_enabled: `boolean` - always show the status - defaults to `false` - color_background: `boolean` - color the background or foreground when an error occurs - defaults to `false` -- error_color: `string` [hex color code][colors] - color to use when an error occured +- error_color: `string` [color][colors] - color to use when an error occured -[colors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ +[colors]: /docs/configure#colors diff --git a/docs/docs/segment-git.md b/docs/docs/segment-git.md index 6282df5e..1b2f2f19 100644 --- a/docs/docs/segment-git.md +++ b/docs/docs/segment-git.md @@ -67,17 +67,17 @@ Local changes can also shown by default using the following syntax for both the ### Colors -- working_color: `string` [hex color code][colors] - foreground color for the working area status - defaults to segment foreground -- staging_color: `string` [hex color code][colors] - foreground color for the staging area status - defaults to segment foreground +- working_color: `string` [color][colors] - foreground color for the working area status - defaults to segment foreground +- staging_color: `string` [color][colors] - foreground color for the staging area status - defaults to segment foreground - status_colors_enabled: `boolean` - color the segment based on the repository status - defaults to `false` - color_background: `boolean` - color background or foreground - defaults to `true` -- local_changes_color: `string` [hex color code][colors] - segment color when there are local changes - defaults to segment +- local_changes_color: `string` [color][colors] - segment color when there are local changes - defaults to segment foreground/background (see `color_background`) -- ahead_and_behind_color: `string` [hex color code][colors] - segment color when the branch is ahead and behind - +- ahead_and_behind_color: `string` [color][colors] - segment color when the branch is ahead and behind - defaults to segment foreground/background (see `color_background`) -- behind_color: `string` [hex color code][colors] - segment color when the branch is behind - defaults to segment +- behind_color: `string` [color][colors] - segment color when the branch is behind - defaults to segment foreground/background (see `color_background`) -- ahead_color: `string` [hex color code][colors] - segment color when the branch is ahead - defaults to segment +- ahead_color: `string` [color][colors] - segment color when the branch is ahead - defaults to segment foreground/background (see `color_background`) -[colors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ +[colors]: /docs/configure#colors diff --git a/docs/docs/segment-session.md b/docs/docs/segment-session.md index 0397eb64..2df872a0 100644 --- a/docs/docs/segment-session.md +++ b/docs/docs/segment-session.md @@ -26,9 +26,9 @@ Show the current user and host name. - user_info_separator: `string` - text/icon to put in between the user and host name - defaults to `@` - ssh_icon: `string` - text/icon to display first when in an active SSH session - defaults to `\uF817 ` -- user_color: `string` [hex color code][colors] - override the foreground color of the user name -- host_color: `string` [hex color code][colors] - override the foreground color of the host name +- user_color: `string` [color][colors] - override the foreground color of the user name +- host_color: `string` [color][colors] - override the foreground color of the host name - display_user: `boolean` - display the user name or not - defaults to `true` - display_host: `boolean` - display the host name or not - defaults to `true` -[colors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/ +[colors]: /docs/configure#colors diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 8c966b99..f6df5059 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -12,7 +12,7 @@ const features = [ description: ( <> Oh my Posh enables you to use the full color set of your terminal - by using hex colors to define and render the prompt. + by using colors to define and render the prompt. ), }, diff --git a/renderer.go b/renderer.go index 3911e558..ebb17f01 100755 --- a/renderer.go +++ b/renderer.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "errors" "fmt" "regexp" "strings" @@ -23,6 +24,41 @@ type formats struct { clearOEL string } +var ( + // Map for color names and their respective foreground [0] or background [1] color codes + ColorMap map[string][2]string = map[string][2]string { + "black": [2]string { "30", "40" }, + "red": [2]string { "31", "41" }, + "green": [2]string { "32", "42" }, + "yellow": [2]string { "33", "43" }, + "blue": [2]string { "34", "44" }, + "magenta": [2]string { "35", "45" }, + "cyan": [2]string { "36", "46" }, + "white": [2]string { "37", "47" }, + "default": [2]string { "39", "49" }, + "darkGray": [2]string { "90", "100" }, + "lightRed": [2]string { "91", "101" }, + "lightGreen": [2]string { "92", "102" }, + "lightYellow": [2]string { "93", "103" }, + "lightBlue": [2]string { "94", "104" }, + "lightMagenta": [2]string { "95", "105" }, + "lightCyan": [2]string { "96", "106" }, + "lightWhite": [2]string { "97", "107" }, + } +) + +// Returns the color code for a given color name +func getColorFromName(colorName string, isBackground bool) (string, error) { + colorMapOffset := 0 + if isBackground { + colorMapOffset = 1 + } + if colorCodes, found := ColorMap[colorName]; found { + return colorCodes[colorMapOffset], nil + } + return "", errors.New("This color name does not exist.") +} + //Renderer writes colorized strings type Renderer struct { Buffer *bytes.Buffer @@ -74,22 +110,29 @@ func (r *Renderer) init(shell string) { } } -func (r *Renderer) getAnsiFromHex(hexColor string, isBackground bool) string { - style := color.HEX(hexColor, isBackground) +// Gets the ANSI color code for a given color string. +// This can include a valid hex color in the format `#FFFFFF`, +// but also a name of one of the first 16 ANSI colors like `lightBlue`. +func (r *Renderer) getAnsiFromColorString(colorString string, isBackground bool) string { + colorFromName, err := getColorFromName(colorString, isBackground) + if err == nil { + return colorFromName + } + style := color.HEX(colorString, isBackground) return style.Code() } func (r *Renderer) writeColoredText(background string, foreground string, text string) { var coloredText string if foreground == Transparent && background != "" { - ansiColor := r.getAnsiFromHex(background, false) + ansiColor := r.getAnsiFromColorString(background, false) coloredText = fmt.Sprintf(r.formats.transparent, ansiColor, text) } else if background == "" || background == Transparent { - ansiColor := r.getAnsiFromHex(foreground, false) + ansiColor := r.getAnsiFromColorString(foreground, false) coloredText = fmt.Sprintf(r.formats.single, ansiColor, text) } else if foreground != "" && background != "" { - bgAnsiColor := r.getAnsiFromHex(background, true) - fgAnsiColor := r.getAnsiFromHex(foreground, false) + bgAnsiColor := r.getAnsiFromColorString(background, true) + fgAnsiColor := r.getAnsiFromColorString(foreground, false) coloredText = fmt.Sprintf(r.formats.full, bgAnsiColor, fgAnsiColor, text) } r.Buffer.WriteString(coloredText) @@ -101,13 +144,19 @@ func (r *Renderer) writeAndRemoveText(background string, foreground string, text } func (r *Renderer) write(background string, foreground string, text string) { - rex := regexp.MustCompile(`<((#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})|transparent))>(.*?)`) + // first we match for any potentially valid color enclosed in <> + rex := regexp.MustCompile(`<([#A-Za-z0-9]+)>(.*?)<\/>`) match := rex.FindAllStringSubmatch(text, -1) for i := range match { - // get the text before the color override and write that first - textBeforeColorOverride := strings.Split(text, match[i][0])[0] + extractedColor := match[i][1] + if col := r.getAnsiFromColorString(extractedColor, false); col == "" && extractedColor != Transparent { + continue // we skip invalid colors + } + escapedTextSegment := match[i][0] + innerText := match[i][2] + textBeforeColorOverride := strings.Split(text, escapedTextSegment)[0] text = r.writeAndRemoveText(background, foreground, textBeforeColorOverride, textBeforeColorOverride, text) - text = r.writeAndRemoveText(background, match[i][1], match[i][4], match[i][0], text) + text = r.writeAndRemoveText(background, extractedColor, innerText, escapedTextSegment, text) } // color the remaining part of text with background and foreground r.writeColoredText(background, foreground, text) diff --git a/renderer_test.go b/renderer_test.go index 73026c09..b67bb6cc 100755 --- a/renderer_test.go +++ b/renderer_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/gookit/color" ) func TestWriteAndRemoveText(t *testing.T) { @@ -49,6 +50,36 @@ func TestWriteColorTransparent(t *testing.T) { t.Log(renderer.string()) } +func TestWriteColorName(t *testing.T) { + // given + renderer := &Renderer{ + Buffer: new(bytes.Buffer), + } + renderer.init("pwsh") + text := "This is white, this is red, white again" + + // when + renderer.write("#193549", "red", text) + + // then + assert.NotContains(t, renderer.string(), "") +} + +func TestWriteColorInvalid(t *testing.T) { + // given + renderer := &Renderer{ + Buffer: new(bytes.Buffer), + } + renderer.init("pwsh") + text := "This is white, this is orange, white again" + + // when + renderer.write("#193549", "invalid", text) + + // then + assert.Contains(t, renderer.string(), "") +} + func TestLenWithoutANSI(t *testing.T) { text := "\x1b[44mhello\x1b[0m" renderer := &Renderer{ @@ -68,3 +99,68 @@ func TestLenWithoutANSIZsh(t *testing.T) { strippedLength := renderer.lenWithoutANSI(text) assert.Equal(t, 5, strippedLength) } + +func TestGetAnsiFromColorStringBg(t *testing.T) { + // given + renderer := &Renderer{ + Buffer: new(bytes.Buffer), + } + + // when + colorCode := renderer.getAnsiFromColorString("blue", true) + + // then + assert.Equal(t, color.BgBlue.Code(), colorCode) +} + +func TestGetAnsiFromColorStringFg(t *testing.T) { + // given + renderer := &Renderer{ + Buffer: new(bytes.Buffer), + } + + // when + colorCode := renderer.getAnsiFromColorString("red", false) + + // then + assert.Equal(t, color.FgRed.Code(), colorCode) +} + +func TestGetAnsiFromColorStringHex(t *testing.T) { + // given + renderer := &Renderer{ + Buffer: new(bytes.Buffer), + } + + // when + colorCode := renderer.getAnsiFromColorString("#AABBCC", false) + + // then + assert.Equal(t, color.HEX("#AABBCC").Code(), colorCode) +} + +func TestGetAnsiFromColorStringInvalidFg(t *testing.T) { + // given + renderer := &Renderer{ + Buffer: new(bytes.Buffer), + } + + // when + colorCode := renderer.getAnsiFromColorString("invalid", false) + + // then + assert.Equal(t, "", colorCode) +} + +func TestGetAnsiFromColorStringInvalidBg(t *testing.T) { + // given + renderer := &Renderer{ + Buffer: new(bytes.Buffer), + } + + // when + colorCode := renderer.getAnsiFromColorString("invalid", true) + + // then + assert.Equal(t, "", colorCode) +}