mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-11-09 20:44:03 -08:00
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:
parent
862d37bb7b
commit
9ecd7c09a4
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -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
35
.vscode/tasks.json
vendored
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
133
src/colors.go
Normal 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
58
src/colors_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
102
src/palette.go
Normal 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
233
src/palette_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -726,6 +726,7 @@ func TestGetPwd(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseMappedLocations(t *testing.T) {
|
||||
defer testClearDefaultConfig()
|
||||
cases := []struct {
|
||||
Case string
|
||||
JSON string
|
||||
|
|
271
src/testdata/jandedobbeleer-palette.omp.json
vendored
Normal file
271
src/testdata/jandedobbeleer-palette.omp.json
vendored
Normal 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
238
src/testdata/jandedobbeleer.omp.json
vendored
Normal 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 }}"
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue