feat(palette): a map of named color values

introducing a map of named standard color
values that can be referenced in theme segments
This commit is contained in:
Yehor Borkov 2021-11-22 16:25:56 +02:00 committed by GitHub
parent 862d37bb7b
commit 9ecd7c09a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1395 additions and 130 deletions

View file

@ -2,7 +2,7 @@
"go.lintTool": "golangci-lint",
"go.useLanguageServer": true,
"go.languageServerExperimentalFeatures": {
"diagnostics": false,
"diagnostics": false
},
"go.lintFlags": ["--fast"],
"go.testOnSave": true,
@ -13,7 +13,5 @@
}
},
"go.formatTool": "gofmt",
"go.formatFlags": [
"-s"
]
"go.formatFlags": ["-s"]
}

35
.vscode/tasks.json vendored
View file

@ -30,14 +30,45 @@
},
"statusbar": {
"hide": false,
"color" : "#22C1D6",
"label" : "$(beaker) devcontainer: build omp",
"color": "#22C1D6",
"label": "$(beaker) devcontainer: build omp",
"tooltip": "Compiles *oh-my-posh* from this repo while **overwriting** your preinstalled stable release."
}
},
"group": "build",
"problemMatcher": "$go",
"args": ["build", "-v", "-o", "`readlink", "/usr/bin/oh-my-posh`"]
},
{
"label": "golangci-lint - docker",
"type": "shell",
"command": "docker",
"group": "build",
"presentation": {
"reveal": "always",
"focus": false,
"echo": true,
"showReuseMessage": false,
"panel": "dedicated",
"clear": false
},
"problemMatcher": {
"base": "$go",
"fileLocation": ["relative", "${workspaceFolder}/src"]
},
"args": [
"run", "--rm", "-v", "${workspaceFolder}/src:/app", "-w", "/app",
"golangci/golangci-lint:${input:golangci-lint-version}",
"golangci-lint", "run", "-v"
]
}
],
"inputs": [
{
"id": "golangci-lint-version",
"description": "Version of the golangci-lint Docker container to use in lint task.",
"type": "promptString",
"default": "v1.43.0"
}
]
}

View file

@ -51,5 +51,121 @@ To change *only* the background color, just omit the first color from the above
"prefix": "<,#FFFFFF>┏[</>",
```
## Palette
If your theme defined the Palette, you can use the _Palette reference_ `p:<palette key>` in places where the
__Standard color__ is expected.
### Defining a Palette
Palette is a set of named __Standard colors__. To use a Palette, define a `"palette"` object
at the top level of your theme:
```json
{
"$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
"palette": {
"git-foreground": "#193549",
"git": "#FFFB38",
"git-modified": "#FF9248",
"git-diverged": "#FF4500",
"git-ahead": "#B388FF",
"git-behind": "#B388FF",
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
"white": "#FFFFFF",
"black": "#111111"
},
"blocks": {
...
}
}
```
Color names (palette keys) can have any string value, so be creative.
Color values, on the other hand, should adhere to the __Standard color__ format.
### Using a Palette
You can now _Palette references_ in any [Segment's][segment] `foreground`, `foreground_templates`,
`background`, `background_templates` properties, and other config properties that expect __Standard color__ value.
_Palette reference_ format is `p:<palette key>`. Take a look at the [Git][git] segment using _Palette references_:
```json
{
"type": "git",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "p:git-foreground",
"background": "p:git",
"background_templates": [
"{{ if or (.Working.Changed) (.Staging.Changed) }}p:git-modified{{ end }}",
"{{ if and (gt .Ahead 0) (gt .Behind 0) }}p:git-diverged{{ end }}",
"{{ if gt .Ahead 0 }}p:git-ahead{{ end }}",
"{{ if gt .Behind 0 }}p:git-behind{{ end }}"
],
...
},
```
Having all of the colors defined in one place allows you to import existing color themes (usually with slight
tweaking to adhere to the format), easily change colors of multiple segments at once, and have a more
organized theme overall. Be creative!
### _Palette references_ and __Standard colors__
Using Palette does not interfere with using __Standard colors__ in your theme. You can still use __Standard colors__
everywhere. This can be useful if you want to use a specific color for a single segment element, or in a
_Color override_ ([Battery segment][battery]):
```json
{
"type": "battery",
"style": "powerline",
"invert_powerline": true,
"powerline_symbol": "\uE0B2",
"foreground": "p:white",
"background": "p:black",
"properties": {
"battery_icon": "<#ffa500> [II ]- </>", // icon should always be orange
"discharging_icon": "- ",
"charging_icon": "+ ",
"charged_icon": "* ",
"color_background": true,
"charged_color": "#4caf50", //
"charging_color": "#40c4ff", // battery should use specific colors for status
"discharging_color": "#ff5722", //
}
},
```
### Handling of invalid references
Should you use an invalid _Palette reference_ as a color (for example typo `p:bleu` instead of `p:blue`),
the Pallete engine will use the Transparent keyword as a fallback value. So if you see your prompt segments
rendered with incorrect colors, and you are using a Palette, be sure to check the correctness of your references.
### Recursive resolution
Palette allows for recursive _Palette reference_ resolution. You can use a _Palette reference_ as a color
value in Palette. This allows you to define named colors, and use references to those colors as Palette values.
For example, `p:foreground` and `p:background` will be correctly set to "#CAF0F80" and "#023E8A":
```json
"$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
"palette": {
"light-blue": "#CAF0F8",
"dark-blue": "#023E8A",
"foreground": "p:light-blue",
"background": "p:dark-blue"
},
"blocks": {
...
}
```
[hexcolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/
[ansicolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/
[git]: /docs/segment-git
[battery]: /docs/segment-battery

133
src/colors.go Normal file
View file

@ -0,0 +1,133 @@
package main
import (
"fmt"
"github.com/gookit/color"
)
// MakeColors creates instance of AnsiColors to use in AnsiWriter according to
// environment and configuration.
func MakeColors(env environmentInfo, cfg *Config) AnsiColors {
cacheDisabled := env.getenv("OMP_CACHE_DISABLED") == "1"
return makeColors(cfg.Palette, !cacheDisabled)
}
func makeColors(palette Palette, cacheEnabled bool) (colors AnsiColors) {
colors = &DefaultColors{}
if palette != nil {
colors = &PaletteColors{ansiColors: colors, palette: palette}
}
if cacheEnabled {
colors = &CachedColors{ansiColors: colors}
}
return
}
// DefaultColors is the default AnsiColors implementation.
type DefaultColors struct{}
var (
// Map for color names and their respective foreground [0] or background [1] color codes
ansiColorCodes = map[string][2]AnsiColor{
"black": {"30", "40"},
"red": {"31", "41"},
"green": {"32", "42"},
"yellow": {"33", "43"},
"blue": {"34", "44"},
"magenta": {"35", "45"},
"cyan": {"36", "46"},
"white": {"37", "47"},
"default": {"39", "49"},
"darkGray": {"90", "100"},
"lightRed": {"91", "101"},
"lightGreen": {"92", "102"},
"lightYellow": {"93", "103"},
"lightBlue": {"94", "104"},
"lightMagenta": {"95", "105"},
"lightCyan": {"96", "106"},
"lightWhite": {"97", "107"},
}
)
const (
foregroundIndex = 0
backgroundIndex = 1
)
func (*DefaultColors) AnsiColorFromString(colorString string, isBackground bool) AnsiColor {
if len(colorString) == 0 {
return emptyAnsiColor
}
if colorString == Transparent {
return transparentAnsiColor
}
colorFromName, err := getAnsiColorFromName(colorString, isBackground)
if err == nil {
return colorFromName
}
style := color.HEX(colorString, isBackground)
if style.IsEmpty() {
return emptyAnsiColor
}
return AnsiColor(style.String())
}
// getAnsiColorFromName returns the color code for a given color name if the name is
// known ANSI color name.
func getAnsiColorFromName(colorName string, isBackground bool) (AnsiColor, error) {
if colorCodes, found := ansiColorCodes[colorName]; found {
if isBackground {
return colorCodes[backgroundIndex], nil
}
return colorCodes[foregroundIndex], nil
}
return "", fmt.Errorf("color name %s does not exist", colorName)
}
func IsAnsiColorName(colorString string) bool {
_, ok := ansiColorCodes[colorString]
return ok
}
// PaletteColors is the AnsiColors Decorator that uses the Palette to do named color
// lookups before ANSI color code generation.
type PaletteColors struct {
ansiColors AnsiColors
palette Palette
}
func (p *PaletteColors) AnsiColorFromString(colorString string, isBackground bool) AnsiColor {
paletteColor, err := p.palette.ResolveColor(colorString)
if err != nil {
return emptyAnsiColor
}
ansiColor := p.ansiColors.AnsiColorFromString(paletteColor, isBackground)
return ansiColor
}
// CachedColors is the AnsiColors Decorator that does simple color lookup caching.
// AnsiColorFromString calls are cheap, but not free, and having a simple cache in
// has measurable positive effect on performance.
type CachedColors struct {
ansiColors AnsiColors
colorCache map[cachedColorKey]AnsiColor
}
type cachedColorKey struct {
colorString string
isBackground bool
}
func (c *CachedColors) AnsiColorFromString(colorString string, isBackground bool) AnsiColor {
if c.colorCache == nil {
c.colorCache = make(map[cachedColorKey]AnsiColor)
}
key := cachedColorKey{colorString, isBackground}
if ansiColor, hit := c.colorCache[key]; hit {
return ansiColor
}
ansiColor := c.ansiColors.AnsiColorFromString(colorString, isBackground)
c.colorCache[key] = ansiColor
return ansiColor
}

58
src/colors_test.go Normal file
View file

@ -0,0 +1,58 @@
package main
import (
"testing"
"github.com/alecthomas/assert"
)
func TestGetAnsiFromColorString(t *testing.T) {
cases := []struct {
Case string
Expected AnsiColor
Color string
Background bool
}{
{Case: "Invalid background", Expected: emptyAnsiColor, Color: "invalid", Background: true},
{Case: "Invalid background", Expected: emptyAnsiColor, Color: "invalid", Background: false},
{Case: "Hex foreground", Expected: AnsiColor("38;2;170;187;204"), Color: "#AABBCC", Background: false},
{Case: "Hex backgrond", Expected: AnsiColor("48;2;170;187;204"), Color: "#AABBCC", Background: true},
{Case: "Base 8 foreground", Expected: AnsiColor("31"), Color: "red", Background: false},
{Case: "Base 8 background", Expected: AnsiColor("41"), Color: "red", Background: true},
{Case: "Base 16 foreground", Expected: AnsiColor("91"), Color: "lightRed", Background: false},
{Case: "Base 16 backround", Expected: AnsiColor("101"), Color: "lightRed", Background: true},
}
for _, tc := range cases {
ansiColors := &DefaultColors{}
ansiColor := ansiColors.AnsiColorFromString(tc.Color, tc.Background)
assert.Equal(t, tc.Expected, ansiColor, tc.Case)
}
}
func TestMakeColors(t *testing.T) {
colors := makeColors(nil, false)
assert.IsType(t, &DefaultColors{}, colors)
colors = makeColors(nil, true)
assert.IsType(t, &CachedColors{}, colors)
assert.IsType(t, &DefaultColors{}, colors.(*CachedColors).ansiColors)
colors = makeColors(testPalette, false)
assert.IsType(t, &PaletteColors{}, colors)
assert.IsType(t, &DefaultColors{}, colors.(*PaletteColors).ansiColors)
colors = makeColors(testPalette, true)
assert.IsType(t, &CachedColors{}, colors)
assert.IsType(t, &PaletteColors{}, colors.(*CachedColors).ansiColors)
assert.IsType(t, &DefaultColors{}, colors.(*CachedColors).ansiColors.(*PaletteColors).ansiColors)
}
func BenchmarkEngineRenderPalette(b *testing.B) {
var err error
for i := 0; i < b.N; i++ {
err = engineRender("jandedobbeleer-palette.omp.json")
if err != nil {
b.Fatal(err)
}
}
}

View file

@ -29,6 +29,7 @@ type Config struct {
Blocks []*Block `config:"blocks"`
Tooltips []*Segment `config:"tooltips"`
TransientPrompt *TransientPrompt `config:"transient_prompt"`
Palette Palette `config:"palette"`
}
type TransientPrompt struct {

View file

@ -3,11 +3,17 @@ package main
import (
"testing"
"github.com/gookit/config/v2"
"github.com/stretchr/testify/assert"
)
func TestSettingsExportJSON(t *testing.T) {
defer testClearDefaultConfig()
content := exportConfig("../themes/jandedobbeleer.omp.json", "json")
assert.NotContains(t, content, "\\u003ctransparent\\u003e")
assert.Contains(t, content, "<transparent>")
}
func testClearDefaultConfig() {
config.Default().ClearAll()
}

View file

@ -2,6 +2,8 @@ package main
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
@ -41,3 +43,78 @@ func TestCanWriteRPrompt(t *testing.T) {
assert.Equal(t, tc.Expected, got, tc.Case)
}
}
func BenchmarkEngineRender(b *testing.B) {
var err error
for i := 0; i < b.N; i++ {
err = engineRender("jandedobbeleer.omp.json")
if err != nil {
b.Fatal(err)
}
}
}
func engineRender(configPath string) error {
testDir, err := os.Getwd()
if err != nil {
return err
}
configPath = filepath.Join(testDir, "testdata", configPath)
var (
debug = false
eval = false
shell = "pwsh"
plain = false
pwd = ""
pswd = ""
code = 2
execTime = 917.0
)
args := &args{
Debug: &debug,
Config: &configPath,
Eval: &eval,
Shell: &shell,
Plain: &plain,
PWD: &pwd,
PSWD: &pswd,
ErrorCode: &code,
ExecutionTime: &execTime,
}
env := &environment{}
env.init(args)
defer env.close()
cfg := GetConfig(env)
defer testClearDefaultConfig()
ansi := &ansiUtils{}
ansi.init(env.getShellName())
writerColors := MakeColors(env, cfg)
writer := &AnsiWriter{
ansi: ansi,
terminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground),
ansiColors: writerColors,
}
title := &consoleTitle{
env: env,
config: cfg,
ansi: ansi,
}
engine := &engine{
config: cfg,
env: env,
writer: writer,
consoleTitle: title,
ansi: ansi,
plain: *args.Plain,
}
engine.render()
return nil
}

View file

@ -209,9 +209,11 @@ func main() {
if *args.Plain {
writer = &PlainWriter{}
} else {
writerColors := MakeColors(env, cfg)
writer = &AnsiWriter{
ansi: ansi,
terminalBackground: getConsoleBackgroundColor(env, cfg.TerminalBackground),
ansiColors: writerColors,
}
}
title := &consoleTitle{

102
src/palette.go Normal file
View file

@ -0,0 +1,102 @@
package main
import (
"fmt"
"sort"
"strings"
)
type Palette map[string]string
const (
paletteKeyPrefix = "p:"
paletteKeyError = "palette: requested color %s does not exist in palette of colors %s"
paletteMaxRecursionDepth = 3 // allows 3 or less recusive resolutions
paletteRecursiveKeyError = "palette: recursive resolution of color %s returned palette reference %s and reached recursion depth %d"
)
// ResolveColor gets a color value from the palette using given colorName.
// If colorName is not a palette reference, it is returned as is.
func (p Palette) ResolveColor(colorName string) (string, error) {
return p.resolveColor(colorName, 1, &colorName)
}
// originalColorName is a pointer to save allocations
func (p Palette) resolveColor(colorName string, depth int, originalColorName *string) (string, error) {
key, ok := asPaletteKey(colorName)
// colorName is not a palette key, return it as is
if !ok {
return colorName, nil
}
color, ok := p[key]
if !ok {
return "", &PaletteKeyError{Key: key, palette: p}
}
if _, isKey := isPaletteKey(color); isKey {
if depth > paletteMaxRecursionDepth {
return "", &PaletteRecursiveKeyError{Key: *originalColorName, Value: color, depth: depth}
}
return p.resolveColor(color, depth+1, originalColorName)
}
return color, nil
}
func asPaletteKey(colorName string) (string, bool) {
prefix, isKey := isPaletteKey(colorName)
if !isKey {
return "", false
}
key := strings.TrimPrefix(colorName, prefix)
return key, true
}
func isPaletteKey(colorName string) (string, bool) {
return paletteKeyPrefix, strings.HasPrefix(colorName, paletteKeyPrefix)
}
// PaletteKeyError records the missing Palette key.
type PaletteKeyError struct {
Key string
palette Palette
}
func (p *PaletteKeyError) Error() string {
keys := make([]string, 0, len(p.palette))
for key := range p.palette {
keys = append(keys, key)
}
sort.Strings(keys)
allColors := strings.Join(keys, ",")
errorStr := fmt.Sprintf(paletteKeyError, p.Key, allColors)
return errorStr
}
// PaletteRecursiveKeyError records the Palette key and resolved color value (which
// is also a Palette key)
type PaletteRecursiveKeyError struct {
Key string
Value string
depth int
}
func (p *PaletteRecursiveKeyError) Error() string {
errorStr := fmt.Sprintf(paletteRecursiveKeyError, p.Key, p.Value, p.depth)
return errorStr
}
// maybeResolveColor wraps resolveColor and silences possible errors, returning
// Transparent color by default, as a Block does not know how to handle color errors.
func (p Palette) MaybeResolveColor(colorName string) string {
color, err := p.ResolveColor(colorName)
if err != nil {
return ""
}
return color
}

233
src/palette_test.go Normal file
View file

@ -0,0 +1,233 @@
package main
import (
"testing"
"github.com/alecthomas/assert"
)
var (
testPalette = Palette{
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
"white": "#FFFFFF",
"black": "#000000",
}
)
type TestPaletteRequest struct {
Case string
Request string
ExpectedError bool
Expected string
}
func TestPaletteShouldResolveColorFromTestPalette(t *testing.T) {
cases := []TestPaletteRequest{
{Case: "Palette red", Request: "p:red", Expected: "#FF0000"},
{Case: "Palette green", Request: "p:green", Expected: "#00FF00"},
{Case: "Palette blue", Request: "p:blue", Expected: "#0000FF"},
{Case: "Palette white", Request: "p:white", Expected: "#FFFFFF"},
{Case: "Palette black", Request: "p:black", Expected: "#000000"},
}
for _, tc := range cases {
testPaletteRequest(t, tc)
}
}
func testPaletteRequest(t *testing.T, tc TestPaletteRequest) {
actual, err := testPalette.ResolveColor(tc.Request)
if !tc.ExpectedError {
assert.Nil(t, err, tc.Case)
assert.Equal(t, tc.Expected, actual, "expected different color value")
} else {
assert.NotNil(t, err, tc.Case)
assert.Equal(t, tc.Expected, err.Error())
}
}
func TestPaletteShouldIgnoreNonPaletteColors(t *testing.T) {
cases := []TestPaletteRequest{
{Case: "Deep puprple", Request: "#1F1137", Expected: "#1F1137"},
{Case: "Light red", Request: "#D55252", Expected: "#D55252"},
{Case: "ANSI black", Request: "black", Expected: "black"},
{Case: "Foreground", Request: "foreground", Expected: "foreground"},
}
for _, tc := range cases {
testPaletteRequest(t, tc)
}
}
func TestPaletteShouldReturnErrorOnMissingColor(t *testing.T) {
cases := []TestPaletteRequest{
{
Case: "Palette deep purple",
Request: "p:deep-purple",
ExpectedError: true,
Expected: "palette: requested color deep-purple does not exist in palette of colors black,blue,green,red,white",
},
{
Case: "Palette cyan",
Request: "p:cyan",
ExpectedError: true,
Expected: "palette: requested color cyan does not exist in palette of colors black,blue,green,red,white",
},
{
Case: "Palette foreground",
Request: "p:foreground",
ExpectedError: true,
Expected: "palette: requested color foreground does not exist in palette of colors black,blue,green,red,white",
},
}
for _, tc := range cases {
testPaletteRequest(t, tc)
}
}
func TestPaletteShouldHandleMixedCases(t *testing.T) {
cases := []TestPaletteRequest{
{Case: "Palette red", Request: "p:red", Expected: "#FF0000"},
{Case: "ANSI black", Request: "black", Expected: "black"},
{Case: "Cyan", Request: "#05E6FA", Expected: "#05E6FA"},
{Case: "Palette black", Request: "p:black", Expected: "#000000"},
{Case: "Palette pink", Request: "p:pink", ExpectedError: true, Expected: "palette: requested color pink does not exist in palette of colors black,blue,green,red,white"},
}
for _, tc := range cases {
testPaletteRequest(t, tc)
}
}
func TestPaletteShouldUseEmptyColorByDefault(t *testing.T) {
cases := []TestPaletteRequest{
{Case: "Palette magenta", Request: "p:magenta", Expected: ""},
{Case: "Palette gray", Request: "p:gray", Expected: ""},
{Case: "Palette rose", Request: "p:rose", Expected: ""},
}
for _, tc := range cases {
actual := testPalette.MaybeResolveColor(tc.Request)
assert.Equal(t, tc.Expected, actual, "expected different color value")
}
}
func TestPaletteShouldResolveRecursiveReference(t *testing.T) {
tp := Palette{
"light-blue": "#CAF0F8",
"dark-blue": "#023E8A",
"foreground": "p:light-blue",
"background": "p:dark-blue",
"text": "p:foreground",
"icon": "p:background",
"void": "p:void", // infinite recursion - error
"1": "white",
"2": "p:1",
"3": "p:2",
"4": "p:3", // 3 recursive lookups - allowed
"5": "p:4", // 4 recursive lookups - error
}
cases := []TestPaletteRequest{
{
Case: "Palette light-blue",
Request: "p:light-blue",
Expected: "#CAF0F8",
},
{
Case: "Palette foreground",
Request: "p:foreground",
Expected: "#CAF0F8",
},
{
Case: "Palette background",
Request: "p:background",
Expected: "#023E8A",
},
{
Case: "Palette text (2 recursive lookups)",
Request: "p:text",
Expected: "#CAF0F8",
},
{
Case: "Palette icon (2 recursive lookups)",
Request: "p:icon",
Expected: "#023E8A",
},
{
Case: "Palette void (infinite recursion)",
Request: "p:void",
ExpectedError: true,
Expected: "palette: recursive resolution of color p:void returned palette reference p:void and reached recursion depth 4",
},
{
Case: "Palette p:4 (3 recursive lookups)",
Request: "p:4",
Expected: "white",
},
{
Case: "Palette p:5 (4 recursive lookups)",
Request: "p:5",
ExpectedError: true,
Expected: "palette: recursive resolution of color p:5 returned palette reference p:1 and reached recursion depth 4",
},
}
for _, tc := range cases {
actual, err := tp.ResolveColor(tc.Request)
if !tc.ExpectedError {
assert.Nil(t, err, "expected no error")
assert.Equal(t, tc.Expected, actual, "expected different color value")
} else {
assert.NotNil(t, err, "expected error")
assert.Equal(t, tc.Expected, err.Error())
}
}
}
func TestPaletteShouldHandleEmptyKey(t *testing.T) {
tp := Palette{
"": "#000000",
}
actual, err := tp.ResolveColor("p:")
assert.Nil(t, err, "expected no error")
assert.Equal(t, "#000000", actual, "expected different color value")
}
func BenchmarkPaletteMixedCaseResolution(b *testing.B) {
for i := 0; i < b.N; i++ {
benchmarkPaletteMixedCaseResolution()
}
}
func benchmarkPaletteMixedCaseResolution() {
cases := []TestPaletteRequest{
{Case: "Palette red", Request: "p:red", Expected: "#FF0000"},
{Case: "ANSI black", Request: "black", Expected: "black"},
{Case: "Cyan", Request: "#05E6FA", Expected: "#05E6FA"},
{Case: "Palette black", Request: "p:black", Expected: "#000000"},
{Case: "Palette pink", Request: "p:pink", ExpectedError: true, Expected: "palette: requested color pink does not exist in palette of colors black,blue,green,red,white"},
{Case: "Palette blue", Request: "p:blue", Expected: "#0000FF"},
// repeating the same set to have longer benchmarks
{Case: "Palette red", Request: "p:red", Expected: "#FF0000"},
{Case: "ANSI black", Request: "black", Expected: "black"},
{Case: "Cyan", Request: "#05E6FA", Expected: "#05E6FA"},
{Case: "Palette black", Request: "p:black", Expected: "#000000"},
{Case: "Palette pink", Request: "p:pink", ExpectedError: true, Expected: "palette: requested color pink does not exist in palette of colors black,blue,green,red,white"},
{Case: "Palette blue", Request: "p:blue", Expected: "#0000FF"},
}
for _, tc := range cases {
// both value and error values are irrelevant, but such assignment calms down
// golangci-lint "return value of `testPalette.ResolveColor` is not checked" error
_, _ = testPalette.ResolveColor(tc.Request)
}
}

View file

@ -71,11 +71,10 @@ func (p *properties) getColor(property Property, defaultValue string) string {
return defaultValue
}
colorString := parseString(val, defaultValue)
_, err := getColorFromName(colorString, false)
if err == nil {
if IsAnsiColorName(colorString) {
return colorString
}
values := findNamedRegexMatch(`(?P<color>#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})`, colorString)
values := findNamedRegexMatch(`(?P<color>#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|p:.*)`, colorString)
if values != nil && values["color"] != "" {
return values["color"]
}

View file

@ -78,6 +78,16 @@ func TestDefaultColorWithUnavailableProperty(t *testing.T) {
assert.Equal(t, expected, value)
}
func TestGetPaletteColor(t *testing.T) {
expected := "p:red"
values := map[Property]interface{}{Background: expected}
properties := properties{
values: values,
}
value := properties.getColor(Background, "white")
assert.Equal(t, expected, value)
}
func TestGetBool(t *testing.T) {
expected := true
values := map[Property]interface{}{DisplayHost: expected}

View file

@ -726,6 +726,7 @@ func TestGetPwd(t *testing.T) {
}
func TestParseMappedLocations(t *testing.T) {
defer testClearDefaultConfig()
cases := []struct {
Case string
JSON string

View file

@ -0,0 +1,271 @@
{
"palette": {
"session": "#C386F1",
"path": "#FF479C",
"git-foreground": "#193549",
"git": "#FFFB38",
"git-modified": "#FF9248",
"git-diverged": "#FF4500",
"git-ahead": "#B388FF",
"git-behind": "#B388FF",
"node": "#6CA35E",
"go": "#8ED1F7",
"julia": "#4063D8",
"python": "#FFDE57",
"ruby": "#AE1401",
"azfunc": "#FEAC19",
"aws-default": "#FFA400",
"aws-jan": "#F1184C",
"root": "#FFFF66",
"executiontime": "#83769C",
"exit": "#00897B",
"exit-red": "#E91E63",
"shell": "#0077C2",
"ytm": "#1BD760",
"battery": "#F36943",
"battery-charged": "#4CAF50",
"battery-charging": "#40C4FF",
"battery-discharging": "#FF5722",
"time": "#2E9599",
"white": "#FFFFFF",
"black": "#111111"
},
"blocks": [
{
"type": "prompt",
"alignment": "left",
"segments": [
{
"type": "session",
"style": "diamond",
"foreground": "palette:white",
"background": "palette:session",
"leading_diamond": "",
"trailing_diamond": "\uE0B0",
"properties": {
"postfix": " ",
"display_host": false
}
},
{
"type": "path",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:white",
"background": "palette:path",
"properties": {
"prefix": "  ",
"home_icon": "~",
"folder_separator_icon": " \uE0b1 ",
"style": "folder"
}
},
{
"type": "git",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:git-foreground",
"background": "palette:git",
"background_templates": [
"{{ if or (.Working.Changed) (.Staging.Changed) }}palette:git-modified{{ end }}",
"{{ if and (gt .Ahead 0) (gt .Behind 0) }}palette:git-diverged{{ end }}",
"{{ if gt .Ahead 0 }}palette:git-ahead{{ end }}",
"{{ if gt .Behind 0 }}palette:git-behind{{ end }}"
],
"leading_diamond": "",
"trailing_diamond": "",
"properties": {
"fetch_status": true,
"fetch_stash_count": true,
"fetch_upstream_icon": true,
"branch_max_length": 25,
"template": "{{ .UpstreamIcon }}{{ .HEAD }}{{ .BranchStatus }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0 }} \uF692 {{ .StashCount }}{{ end }}"
}
},
{
"type": "node",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:white",
"background": "palette:node",
"properties": {
"prefix": " \uF898 ",
"display_version": true
}
},
{
"type": "go",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:black",
"background": "palette:go",
"properties": {
"prefix": " \uE626 ",
"display_version": true
}
},
{
"type": "julia",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:black",
"background": "palette:julia",
"properties": {
"prefix": " \uE624 ",
"display_version": true
}
},
{
"type": "python",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:black",
"background": "palette:python",
"properties": {
"prefix": " \uE235 ",
"display_version": true,
"display_mode": "files",
"display_virtual_env": false
}
},
{
"type": "ruby",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:white",
"background": "palette:ruby",
"properties": {
"prefix": " \uE791 ",
"display_version": true,
"display_mode": "files"
}
},
{
"type": "azfunc",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:white",
"background": "palette:azfunc",
"properties": {
"prefix": " \uf0e7",
"display_version": false,
"display_mode": "files"
}
},
{
"type": "aws",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:white",
"background_templates": [
"{{if contains \"default\" .Profile}}palette:aws-default{{end}}",
"{{if contains \"jan\" .Profile}}palette:aws-jan{{end}}"
],
"properties": {
"prefix": " \uE7AD ",
"display_default": false
}
},
{
"type": "root",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "palette:black",
"background": "palette:root",
"properties": {
"root_icon": ""
}
},
{
"type": "executiontime",
"style": "plain",
"foreground": "palette:white",
"background": "palette:executiontime",
"leading_diamond": "",
"trailing_diamond": "",
"properties": {
"always_enabled": true,
"prefix": "<transparent>\uE0B0</> \ufbab",
"postfix": "\u2800"
}
},
{
"type": "exit",
"style": "diamond",
"foreground": "palette:white",
"background": "palette:exit",
"background_templates": [
"{{ if gt .Code 0 }}palette:exit-red{{ end }}"
],
"leading_diamond": "",
"trailing_diamond": "\uE0B4",
"properties": {
"always_enabled": true,
"template": "\uE23A",
"prefix": "<parentBackground>\uE0B0</> "
}
}
]
},
{
"type": "rprompt",
"segments": [
{
"type": "shell",
"style": "plain",
"foreground": "palette:white",
"background": "palette:shell",
"properties": {
"prefix": "<#0077c2,transparent>\uE0B6</>  ",
"postfix": " <transparent,#0077c2>\uE0B2</>"
}
},
{
"type": "ytm",
"style": "powerline",
"powerline_symbol": "\uE0B2",
"invert_powerline": true,
"foreground": "palette:black",
"background": "palette:ytm",
"properties": {
"prefix": " \uF167 ",
"paused_icon": " ",
"playing_icon": " "
}
},
{
"type": "battery",
"style": "powerline",
"invert_powerline": true,
"powerline_symbol": "\uE0B2",
"foreground": "palette:white",
"background": "palette:battery",
"properties": {
"battery_icon": "",
"discharging_icon": " ",
"charging_icon": " ",
"charged_icon": " ",
"color_background": true,
"charged_color": "palette:battery-charged",
"charging_color": "palette:battery-charging",
"discharging_color": "palette:battery-discharging",
"postfix": " "
}
},
{
"type": "time",
"style": "diamond",
"invert_powerline": true,
"leading_diamond": "\uE0B2",
"trailing_diamond": "\uE0B4",
"background": "palette:time",
"foreground": "palette:black"
}
]
}
],
"final_space": true,
"console_title": true,
"console_title_style": "template",
"console_title_template": "{{ .Shell }} in {{ .Folder }}"
}

238
src/testdata/jandedobbeleer.omp.json vendored Normal file
View file

@ -0,0 +1,238 @@
{
"blocks": [
{
"type": "prompt",
"alignment": "left",
"segments": [
{
"type": "session",
"style": "diamond",
"foreground": "#ffffff",
"background": "#c386f1",
"leading_diamond": "",
"trailing_diamond": "\uE0B0",
"properties": {
"postfix": " ",
"display_host": false
}
},
{
"type": "path",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background": "#ff479c",
"properties": {
"prefix": "  ",
"home_icon": "~",
"folder_separator_icon": " \uE0b1 ",
"style": "folder"
}
},
{
"type": "git",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#193549",
"background": "#fffb38",
"background_templates": [
"{{ if or (.Working.Changed) (.Staging.Changed) }}#FF9248{{ end }}",
"{{ if and (gt .Ahead 0) (gt .Behind 0) }}#ff4500{{ end }}",
"{{ if gt .Ahead 0 }}#B388FF{{ end }}",
"{{ if gt .Behind 0 }}#B388FF{{ end }}"
],
"leading_diamond": "",
"trailing_diamond": "",
"properties": {
"fetch_status": true,
"fetch_stash_count": true,
"fetch_upstream_icon": true,
"branch_max_length": 25,
"template": "{{ .UpstreamIcon }}{{ .HEAD }}{{ .BranchStatus }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0 }} \uF692 {{ .StashCount }}{{ end }}"
}
},
{
"type": "node",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background": "#6CA35E",
"properties": {
"prefix": " \uF898 ",
"display_version": true
}
},
{
"type": "go",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#111111",
"background": "#8ED1F7",
"properties": {
"prefix": " \uE626 ",
"display_version": true
}
},
{
"type": "julia",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#111111",
"background": "#4063D8",
"properties": {
"prefix": " \uE624 ",
"display_version": true
}
},
{
"type": "python",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#111111",
"background": "#FFDE57",
"properties": {
"prefix": " \uE235 ",
"display_version": true,
"display_mode": "files",
"display_virtual_env": false
}
},
{
"type": "ruby",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background": "#AE1401",
"properties": {
"prefix": " \uE791 ",
"display_version": true,
"display_mode": "files"
}
},
{
"type": "azfunc",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background": "#FEAC19",
"properties": {
"prefix": " \uf0e7",
"display_version": false,
"display_mode": "files"
}
},
{
"type": "aws",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background_templates": [
"{{if contains \"default\" .Profile}}#FFA400{{end}}",
"{{if contains \"jan\" .Profile}}#f1184c{{end}}"
],
"properties": {
"prefix": " \uE7AD ",
"display_default": false
}
},
{
"type": "root",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#111111",
"background": "#ffff66",
"properties": {
"root_icon": ""
}
},
{
"type": "executiontime",
"style": "plain",
"foreground": "#ffffff",
"background": "#83769c",
"leading_diamond": "",
"trailing_diamond": "",
"properties": {
"always_enabled": true,
"prefix": "<transparent>\uE0B0</> \ufbab",
"postfix": "\u2800"
}
},
{
"type": "exit",
"style": "diamond",
"foreground": "#ffffff",
"background": "#00897b",
"background_templates": ["{{ if gt .Code 0 }}#e91e63{{ end }}"],
"leading_diamond": "",
"trailing_diamond": "\uE0B4",
"properties": {
"always_enabled": true,
"template": "\uE23A",
"prefix": "<parentBackground>\uE0B0</> "
}
}
]
},
{
"type": "rprompt",
"segments": [
{
"type": "shell",
"style": "plain",
"foreground": "#ffffff",
"background": "#0077c2",
"properties": {
"prefix": "<#0077c2,transparent>\uE0B6</>  ",
"postfix": " <transparent,#0077c2>\uE0B2</>"
}
},
{
"type": "ytm",
"style": "powerline",
"powerline_symbol": "\uE0B2",
"invert_powerline": true,
"foreground": "#111111",
"background": "#1BD760",
"properties": {
"prefix": " \uF167 ",
"paused_icon": " ",
"playing_icon": " "
}
},
{
"type": "battery",
"style": "powerline",
"invert_powerline": true,
"powerline_symbol": "\uE0B2",
"foreground": "#ffffff",
"background": "#f36943",
"properties": {
"battery_icon": "",
"discharging_icon": " ",
"charging_icon": " ",
"charged_icon": " ",
"color_background": true,
"charged_color": "#4caf50",
"charging_color": "#40c4ff",
"discharging_color": "#ff5722",
"postfix": " "
}
},
{
"type": "time",
"style": "diamond",
"invert_powerline": true,
"leading_diamond": "\uE0B2",
"trailing_diamond": "\uE0B4",
"background": "#2e9599",
"foreground": "#111111"
}
]
}
],
"final_space": true,
"console_title": true,
"console_title_style": "template",
"console_title_template": "{{ .Shell }} in {{ .Folder }}"
}

View file

@ -1,52 +1,14 @@
package main
import (
"errors"
"fmt"
"strings"
"github.com/gookit/color"
)
var (
// Map for color names and their respective foreground [0] or background [1] color codes
colorMap = map[string][2]string{
"black": {"30", "40"},
"red": {"31", "41"},
"green": {"32", "42"},
"yellow": {"33", "43"},
"blue": {"34", "44"},
"magenta": {"35", "45"},
"cyan": {"36", "46"},
"white": {"37", "47"},
"default": {"39", "49"},
"darkGray": {"90", "100"},
"lightRed": {"91", "101"},
"lightGreen": {"92", "102"},
"lightYellow": {"93", "103"},
"lightBlue": {"94", "104"},
"lightMagenta": {"95", "105"},
"lightCyan": {"96", "106"},
"lightWhite": {"97", "107"},
}
)
const (
colorRegex = `<(?P<foreground>[^,>]+)?,?(?P<background>[^>]+)?>(?P<content>[^<]*)<\/>`
)
// 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("color name does not exist")
}
type promptWriter interface {
write(background, foreground, text string)
string() string
@ -63,6 +25,7 @@ type AnsiWriter struct {
terminalBackground string
Colors *Color
ParentColors *Color
ansiColors AnsiColors
}
type Color struct {
@ -70,6 +33,32 @@ type Color struct {
Foreground string
}
// AnsiColors is the interface that wraps AnsiColorFromString method.
//
// AnsiColorFromString 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`.
type AnsiColors interface {
AnsiColorFromString(colorString string, isBackground bool) AnsiColor
}
// AnsiColor is an ANSI color code ready to be printed to the console.
// Example: "38;2;255;255;255", "48;2;255;255;255", "31", "95".
type AnsiColor string
const (
emptyAnsiColor = AnsiColor("")
transparentAnsiColor = AnsiColor(Transparent)
)
func (c AnsiColor) IsEmpty() bool {
return c == emptyAnsiColor
}
func (c AnsiColor) IsTransparent() bool {
return c == transparentAnsiColor
}
const (
// Transparent implies a transparent color
Transparent = "transparent"
@ -101,44 +90,30 @@ func (a *AnsiWriter) clearParentColors() {
a.ParentColors = nil
}
// 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 (a *AnsiWriter) getAnsiFromColorString(colorString string, isBackground bool) string {
if colorString == Transparent || len(colorString) == 0 {
return colorString
}
colorFromName, err := getColorFromName(colorString, isBackground)
if err == nil {
return colorFromName
}
style := color.HEX(colorString, isBackground)
if style.IsEmpty() {
return ""
}
return style.String()
func (a *AnsiWriter) getAnsiFromColorString(colorString string, isBackground bool) AnsiColor {
return a.ansiColors.AnsiColorFromString(colorString, isBackground)
}
func (a *AnsiWriter) writeColoredText(background, foreground, text string) {
func (a *AnsiWriter) writeColoredText(background, foreground AnsiColor, text string) {
// Avoid emitting empty strings with color codes
if text == "" || (foreground == Transparent && background == Transparent) {
if text == "" || (foreground.IsTransparent() && background.IsTransparent()) {
return
}
// default to white fg if empty, empty backgrond is supported
if len(foreground) == 0 {
if foreground.IsEmpty() {
foreground = a.getAnsiFromColorString("white", false)
}
if foreground == Transparent && len(background) != 0 && len(a.terminalBackground) != 0 {
if foreground.IsTransparent() && !background.IsEmpty() && len(a.terminalBackground) != 0 {
fgAnsiColor := a.getAnsiFromColorString(a.terminalBackground, false)
coloredText := fmt.Sprintf(a.ansi.colorFull, background, fgAnsiColor, text)
a.builder.WriteString(coloredText)
return
}
if foreground == Transparent && len(background) != 0 {
if foreground.IsTransparent() && !background.IsEmpty() {
coloredText := fmt.Sprintf(a.ansi.colorTransparent, background, text)
a.builder.WriteString(coloredText)
return
} else if len(background) == 0 || background == Transparent {
} else if background.IsEmpty() || background.IsTransparent() {
coloredText := fmt.Sprintf(a.ansi.colorSingle, foreground, text)
a.builder.WriteString(coloredText)
return
@ -147,7 +122,7 @@ func (a *AnsiWriter) writeColoredText(background, foreground, text string) {
a.builder.WriteString(coloredText)
}
func (a *AnsiWriter) writeAndRemoveText(background, foreground, text, textToRemove, parentText string) string {
func (a *AnsiWriter) writeAndRemoveText(background, foreground AnsiColor, text, textToRemove, parentText string) string {
a.writeColoredText(background, foreground, text)
return strings.Replace(parentText, textToRemove, "", 1)
}
@ -157,60 +132,68 @@ func (a *AnsiWriter) write(background, foreground, text string) {
return
}
getAnsiColors := func(background, foreground string) (string, string) {
getColorString := func(color string) string {
if color == Background {
color = a.Colors.Background
} else if color == Foreground {
color = a.Colors.Foreground
} else if color == ParentBackground && a.ParentColors != nil {
color = a.ParentColors.Background
} else if color == ParentForeground && a.ParentColors != nil {
color = a.ParentColors.Foreground
} else if (color == ParentForeground || color == ParentBackground) && a.ParentColors == nil {
color = Transparent
}
return color
}
background = getColorString(background)
foreground = getColorString(foreground)
inverted := foreground == Transparent && len(background) != 0
background = a.getAnsiFromColorString(background, !inverted)
foreground = a.getAnsiFromColorString(foreground, false)
return background, foreground
}
bgAnsi, fgAnsi := getAnsiColors(background, foreground)
bgAnsi, fgAnsi := a.asAnsiColors(background, foreground)
text = a.ansi.escapeText(text)
text = a.ansi.formatText(text)
text = a.ansi.generateHyperlink(text)
// first we match for any potentially valid colors enclosed in <>
match := findAllNamedRegexMatch(colorRegex, text)
for i := range match {
fg := match[i]["foreground"]
bg := match[i]["background"]
if fg == Transparent && len(bg) == 0 {
bg = background
// i.e., find color overrides
overrides := findAllNamedRegexMatch(colorRegex, text)
for _, override := range overrides {
fgOverride := override["foreground"]
bgOverride := override["background"]
if fgOverride == Transparent && len(bgOverride) == 0 {
bgOverride = background
}
bg, fg = getAnsiColors(bg, fg)
bgOverrideAnsi, fgOverrideAnsi := a.asAnsiColors(bgOverride, fgOverride)
// set colors if they are empty
if len(bg) == 0 {
bg = bgAnsi
if bgOverrideAnsi.IsEmpty() {
bgOverrideAnsi = bgAnsi
}
if len(fg) == 0 {
fg = fgAnsi
if fgOverrideAnsi.IsEmpty() {
fgOverrideAnsi = fgAnsi
}
escapedTextSegment := match[i]["text"]
innerText := match[i]["content"]
escapedTextSegment := override["text"]
innerText := override["content"]
textBeforeColorOverride := strings.Split(text, escapedTextSegment)[0]
text = a.writeAndRemoveText(bgAnsi, fgAnsi, textBeforeColorOverride, textBeforeColorOverride, text)
text = a.writeAndRemoveText(bg, fg, innerText, escapedTextSegment, text)
text = a.writeAndRemoveText(bgOverrideAnsi, fgOverrideAnsi, innerText, escapedTextSegment, text)
}
// color the remaining part of text with background and foreground
a.writeColoredText(bgAnsi, fgAnsi, text)
}
func (a *AnsiWriter) asAnsiColors(background, foreground string) (AnsiColor, AnsiColor) {
if backgroundValue, ok := a.isKeyword(background); ok {
background = backgroundValue
}
if foregroundValue, ok := a.isKeyword(foreground); ok {
foreground = foregroundValue
}
inverted := foreground == Transparent && len(background) != 0
backgroundAnsi := a.getAnsiFromColorString(background, !inverted)
foregroundAnsi := a.getAnsiFromColorString(foreground, false)
return backgroundAnsi, foregroundAnsi
}
func (a *AnsiWriter) isKeyword(color string) (string, bool) {
switch {
case color == Background:
return a.Colors.Background, true
case color == Foreground:
return a.Colors.Foreground, true
case color == ParentBackground && a.ParentColors != nil:
return a.ParentColors.Background, true
case color == ParentForeground && a.ParentColors != nil:
return a.ParentColors.Foreground, true
case (color == ParentBackground || color == ParentForeground) && a.ParentColors == nil:
return Transparent, true
default:
return "", false
}
}
func (a *AnsiWriter) string() string {
return a.builder.String()
}

View file

@ -6,28 +6,6 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGetAnsiFromColorString(t *testing.T) {
cases := []struct {
Case string
Expected string
Color string
Background bool
}{
{Case: "Invalid background", Expected: "", Color: "invalid", Background: true},
{Case: "Invalid background", Expected: "", Color: "invalid", Background: false},
{Case: "Hex foreground", Expected: "48;2;170;187;204", Color: "#AABBCC", Background: false},
{Case: "Base 8 foreground", Expected: "41", Color: "red", Background: false},
{Case: "Base 8 background", Expected: "41", Color: "red", Background: true},
{Case: "Base 16 foreground", Expected: "101", Color: "lightRed", Background: false},
{Case: "Base 16 backround", Expected: "101", Color: "lightRed", Background: true},
}
for _, tc := range cases {
renderer := &AnsiWriter{}
ansiColor := renderer.getAnsiFromColorString(tc.Color, true)
assert.Equal(t, tc.Expected, ansiColor, tc.Case)
}
}
func TestWriteANSIColors(t *testing.T) {
cases := []struct {
Case string
@ -184,6 +162,12 @@ func TestWriteANSIColors(t *testing.T) {
Expected: "\x1b[40m\x1b[30mtest\x1b[0m",
Colors: &Color{Foreground: "black", Background: "white"},
},
{
Case: "Google",
Input: "<blue,white>G</><red,white>o</><yellow,white>o</><blue,white>g</><green,white>l</><red,white>e</>",
Expected: "\x1b[47m\x1b[34mG\x1b[0m\x1b[47m\x1b[31mo\x1b[0m\x1b[47m\x1b[33mo\x1b[0m\x1b[47m\x1b[34mg\x1b[0m\x1b[47m\x1b[32ml\x1b[0m\x1b[47m\x1b[31me\x1b[0m",
Colors: &Color{Foreground: "black", Background: "black"},
},
}
for _, tc := range cases {
@ -194,6 +178,7 @@ func TestWriteANSIColors(t *testing.T) {
ParentColors: tc.Parent,
Colors: tc.Colors,
terminalBackground: tc.TerminalBackground,
ansiColors: &DefaultColors{},
}
renderer.write(tc.Colors.Background, tc.Colors.Foreground, tc.Input)
got := renderer.string()

View file

@ -6,12 +6,24 @@
"description": "https://ohmyposh.dev/docs/config-overview",
"definitions": {
"color": {
"anyOf": [
{ "$ref": "#/definitions/color_string" },
{ "$ref": "#/definitions/palette_reference" }
]
},
"color_string": {
"type": "string",
"pattern": "^(#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})|black|red|green|yellow|blue|magenta|cyan|white|default|darkGray|lightRed|lightGreen|lightYellow|lightBlue|lightMagenta|lightCyan|lightWhite|transparent|parentBackground|parentForeground|background|foreground)$",
"title": "Color string",
"description": "https://ohmyposh.dev/docs/config-colors",
"format": "color"
},
"palette_reference": {
"type": "string",
"pattern": "^p:.*$",
"title": "Palette reference",
"description": "https://ohmyposh.dev/docs/config-colors#palette"
},
"color_templates": {
"type": "array",
"title": "Templates to define a color",
@ -1763,6 +1775,15 @@
"background": { "$ref": "#/definitions/color" },
"foreground": { "$ref": "#/definitions/color" }
}
},
"palette": {
"type": "object",
"title": "Palette",
"description": "https://ohmyposh.dev/docs/config-colors#palette",
"default": {},
"patternProperties": {
".*": { "$ref": "#/definitions/color" }
}
}
}
}