Repository: JanDeDobbeleer/oh-my-posh
Pull request #2944 (2022-10-14 18:20:16)

feat(palette): conditional palettes
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md]
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added/updated (for bug fixes/features)

<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/cli/config_export_image.go b/src/cli/config_export_image.go
index bf588d95..6d30865e 100644
--- a/src/cli/config_export_image.go
+++ b/src/cli/config_export_image.go
@@ -59,7 +59,7 @@ Exports the config to an image file using customized output options.`,
 		cfg := engine.LoadConfig(env)
 		ansi := &color.Ansi{}
 		ansi.InitPlain()
-		writerColors := cfg.MakeColors(env)
+		writerColors := cfg.MakeColors()
 		writer := &color.AnsiWriter{
 			Ansi:               ansi,
 			TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
diff --git a/src/cli/debug.go b/src/cli/debug.go
index 640219ff..36dcdc6f 100644
--- a/src/cli/debug.go
+++ b/src/cli/debug.go
@@ -32,7 +32,7 @@ var debugCmd = &cobra.Command{
 		cfg := engine.LoadConfig(env)
 		ansi := &color.Ansi{}
 		ansi.InitPlain()
-		writerColors := cfg.MakeColors(env)
+		writerColors := cfg.MakeColors()
 		writer := &color.AnsiWriter{
 			Ansi:               ansi,
 			TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
diff --git a/src/cli/print.go b/src/cli/print.go
index 7d08efde..866b0286 100644
--- a/src/cli/print.go
+++ b/src/cli/print.go
@@ -73,7 +73,7 @@ var printCmd = &cobra.Command{
 				Ansi: ansi,
 			}
 		} else {
-			writerColors := cfg.MakeColors(env)
+			writerColors := cfg.MakeColors()
 			writer = &color.AnsiWriter{
 				Ansi:               ansi,
 				TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),
diff --git a/src/color/palettes.go b/src/color/palettes.go
new file mode 100644
index 00000000..a6019dc3
--- /dev/null
+++ b/src/color/palettes.go
@@ -0,0 +1,6 @@
+package color
+
+type Palettes struct {
+	Template string             `json:"template,omitempty"`
+	List     map[string]Palette `json:"list,omitempty"`
+}
diff --git a/src/engine/block.go b/src/engine/block.go
index 83c37e31..b13c4bfa 100644
--- a/src/engine/block.go
+++ b/src/engine/block.go
@@ -67,7 +67,7 @@ func (b *Block) InitPlain(env environment.Environment, config *Config) {
 	b.writer = &color.AnsiWriter{
 		Ansi:               b.ansi,
 		TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground),
-		AnsiColors:         config.MakeColors(env),
+		AnsiColors:         config.MakeColors(),
 	}
 	b.env = env
 	b.executeSegmentLogic()
diff --git a/src/engine/config.go b/src/engine/config.go
index 1fbeb5b6..615c5e8d 100644
--- a/src/engine/config.go
+++ b/src/engine/config.go
@@ -9,6 +9,7 @@ import (
 	"oh-my-posh/environment"
 	"oh-my-posh/properties"
 	"oh-my-posh/segments"
+	"oh-my-posh/template"
 	"os"
 	"path/filepath"
 	"strings"
@@ -31,20 +32,21 @@ const (
 
 // Config holds all the theme for rendering the prompt
 type Config struct {
-	Version              int           `json:"version"`
-	FinalSpace           bool          `json:"final_space,omitempty"`
-	ConsoleTitleTemplate string        `json:"console_title_template,omitempty"`
-	TerminalBackground   string        `json:"terminal_background,omitempty"`
-	AccentColor          string        `json:"accent_color,omitempty"`
-	Blocks               []*Block      `json:"blocks,omitempty"`
-	Tooltips             []*Segment    `json:"tooltips,omitempty"`
-	TransientPrompt      *Segment      `json:"transient_prompt,omitempty"`
-	ValidLine            *Segment      `json:"valid_line,omitempty"`
-	ErrorLine            *Segment      `json:"error_line,omitempty"`
-	SecondaryPrompt      *Segment      `json:"secondary_prompt,omitempty"`
-	DebugPrompt          *Segment      `json:"debug_prompt,omitempty"`
-	Palette              color.Palette `json:"palette,omitempty"`
-	PWD                  string        `json:"pwd,omitempty"`
+	Version              int             `json:"version"`
+	FinalSpace           bool            `json:"final_space,omitempty"`
+	ConsoleTitleTemplate string          `json:"console_title_template,omitempty"`
+	TerminalBackground   string          `json:"terminal_background,omitempty"`
+	AccentColor          string          `json:"accent_color,omitempty"`
+	Blocks               []*Block        `json:"blocks,omitempty"`
+	Tooltips             []*Segment      `json:"tooltips,omitempty"`
+	TransientPrompt      *Segment        `json:"transient_prompt,omitempty"`
+	ValidLine            *Segment        `json:"valid_line,omitempty"`
+	ErrorLine            *Segment        `json:"error_line,omitempty"`
+	SecondaryPrompt      *Segment        `json:"secondary_prompt,omitempty"`
+	DebugPrompt          *Segment        `json:"debug_prompt,omitempty"`
+	Palette              color.Palette   `json:"palette,omitempty"`
+	Palettes             *color.Palettes `json:"palettes,omitempty"`
+	PWD                  string          `json:"pwd,omitempty"`
 
 	// Deprecated
 	OSC99 bool `json:"osc99,omitempty"`
@@ -55,13 +57,30 @@ type Config struct {
 	origin  string
 	eval    bool
 	updated bool
+	env     environment.Environment
 }
 
 // MakeColors creates instance of AnsiColors to use in AnsiWriter according to
 // environment and configuration.
-func (cfg *Config) MakeColors(env environment.Environment) color.AnsiColors {
-	cacheDisabled := env.Getenv("OMP_CACHE_DISABLED") == "1"
-	return color.MakeColors(cfg.Palette, !cacheDisabled, cfg.AccentColor, env)
+func (cfg *Config) MakeColors() color.AnsiColors {
+	cacheDisabled := cfg.env.Getenv("OMP_CACHE_DISABLED") == "1"
+	return color.MakeColors(cfg.getPalette(), !cacheDisabled, cfg.AccentColor, cfg.env)
+}
+
+func (cfg *Config) getPalette() color.Palette {
+	if cfg.Palettes == nil {
+		return cfg.Palette
+	}
+	tmpl := &template.Text{
+		Template: cfg.Palettes.Template,
+		Env:      cfg.env,
+	}
+	if palette, err := tmpl.Render(); err == nil {
+		if p, ok := cfg.Palettes.List[palette]; ok {
+			return p
+		}
+	}
+	return cfg.Palette
 }
 
 func (cfg *Config) print(message string) {
@@ -88,6 +107,7 @@ func (cfg *Config) exitWithError(err error) {
 // LoadConfig returns the default configuration including possible user overrides
 func LoadConfig(env environment.Environment) *Config {
 	cfg := loadConfig(env)
+	cfg.env = env
 	// only migrate automatically when the switch isn't set
 	if !env.Flags().Migrate && cfg.Version < configVersion {
 		cfg.BackupAndMigrate(env)
diff --git a/themes/schema.json b/themes/schema.json
index 32b96691..a0847a9e 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -2900,6 +2900,27 @@
         }
       }
     },
+    "palettes": {
+      "type": "object",
+      "title": "Palettes",
+      "description": "https://ohmyposh.dev/docs/configuration/colors#palettes",
+      "default": {},
+      "properties": {
+        "template": {
+          "type": "string",
+          "title": "Prompt Template"
+        },
+        "list": {
+          "type": "object",
+          "title": "List of palettes",
+          "patternProperties": {
+            ".*": {
+              "$ref": "#/definitions/palette"
+            }
+          }
+        }
+      }
+    },
     "accent_color": {
       "title": "Accent color",
       "$ref": "#/definitions/color"
diff --git a/website/docs/configuration/colors.mdx b/website/docs/configuration/colors.mdx
index a7cffd97..5b1f89a8 100644
--- a/website/docs/configuration/colors.mdx
+++ b/website/docs/configuration/colors.mdx
@@ -17,6 +17,7 @@ Oh My Posh supports multiple different color references, being:
   as well as 8 extended ANSI colors:
 
   `darkGray` `lightRed` `lightGreen` `lightYellow` `lightBlue` `lightMagenta` `lightCyan` `lightWhite`
+
 - 256 color palette using their number representation.
   For example `0` is black, `1` is red, `2` is green, etc.
 - The `transparent` keyword which can be used to create either a transparent foreground override
@@ -190,6 +191,51 @@ For example, `p:foreground` and `p:background` will be correctly set to "#CAF0F8
     }
 ```
 
+## Palettes
+
+If you want to use a `palette` conditionally, for example for **light or dark mode**, you can define multiple
+`palettes` and a [template][templates] that resolves to the `palette` key. The `template` is evaluated at
+runtime so your prompt can change at any time based on the outcome of the `template`.
+
+Take the following configuration:
+
+```json
+{
+  "$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
+  "palettes": {
+    "template": "{{ if eq .Shell \"pwsh\" }}latte{{ else }}frappe{{ end }}",
+    "list": {
+      "latte": {
+        "black": "#262B44",
+        "green": "#59C9A5",
+        "orange": "#F07623",
+        "red": "#e64553",
+        "white": "#E0DEF4",
+        "yellow": "#df8e1d",
+        "blue": "#7287fd"
+      },
+      "frappe": {
+        "black": "#262B44",
+        "green": "#59C9A5",
+        "orange": "#F07623",
+        "red": "#D81E5B",
+        "white": "#E0DEF4",
+        "yellow": "#F3AE35",
+        "blue": "#4B95E9"
+      }
+    }
+  },
+  "blocks": [
+    ...
+  ]
+}
+```
+
+In this case, when the shell is `pwsh`, the `latte` palette will be used, otherwise it uses the `frappe` palette. If you want,
+you could also add `frappe` as the default `palette`, given that one is used as a fallback when not match can be found based on what
+the `template` resolves to. In case no match is available and no `palette` is defined, it will also fallback to `transparent`
+for any palette color reference in templates/colors.
+
 [hexcolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/
 [ansicolors]: https://htmlcolorcodes.com/color-chart/material-design-color-chart/
 [git]: /docs/segments/git


Test patch:
diff --git a/src/engine/config_test.go b/src/engine/config_test.go
index 2b67d88a..11b69345 100644
--- a/src/engine/config_test.go
+++ b/src/engine/config_test.go
@@ -1,6 +1,9 @@
 package engine
 
 import (
+	"oh-my-posh/color"
+	"oh-my-posh/environment"
+	"oh-my-posh/mock"
 	"oh-my-posh/segments"
 	"testing"
 
@@ -55,3 +58,77 @@ func TestEscapeGlyphs(t *testing.T) {
 		assert.Equal(t, tc.Expected, escapeGlyphs(tc.Input), tc.Input)
 	}
 }
+
+func TestGetPalette(t *testing.T) {
+	palette := color.Palette{
+		"red":  "#ff0000",
+		"blue": "#0000ff",
+	}
+	cases := []struct {
+		Case            string
+		Palettes        *color.Palettes
+		Palette         color.Palette
+		ExpectedPalette color.Palette
+	}{
+		{
+			Case: "match",
+			Palettes: &color.Palettes{
+				Template: "{{ .Shell }}",
+				List: map[string]color.Palette{
+					"bash": palette,
+					"zsh": {
+						"red":  "#ff0001",
+						"blue": "#0000fb",
+					},
+				},
+			},
+			ExpectedPalette: palette,
+		},
+		{
+			Case: "no match, no fallback",
+			Palettes: &color.Palettes{
+				Template: "{{ .Shell }}",
+				List: map[string]color.Palette{
+					"fish": palette,
+					"zsh": {
+						"red":  "#ff0001",
+						"blue": "#0000fb",
+					},
+				},
+			},
+			ExpectedPalette: nil,
+		},
+		{
+			Case: "no match, default",
+			Palettes: &color.Palettes{
+				Template: "{{ .Shell }}",
+				List: map[string]color.Palette{
+					"zsh": {
+						"red":  "#ff0001",
+						"blue": "#0000fb",
+					},
+				},
+			},
+			Palette:         palette,
+			ExpectedPalette: palette,
+		},
+		{
+			Case:            "no palettes",
+			ExpectedPalette: nil,
+		},
+	}
+	for _, tc := range cases {
+		env := &mock.MockedEnvironment{}
+		env.On("TemplateCache").Return(&environment.TemplateCache{
+			Env:   map[string]string{},
+			Shell: "bash",
+		})
+		cfg := &Config{
+			env:      env,
+			Palette:  tc.Palette,
+			Palettes: tc.Palettes,
+		}
+		got := cfg.getPalette()
+		assert.Equal(t, tc.ExpectedPalette, got, tc.Case)
+	}
+}
diff --git a/src/engine/engine_test.go b/src/engine/engine_test.go
index 782c8b6b..e5dabec0 100644
--- a/src/engine/engine_test.go
+++ b/src/engine/engine_test.go
@@ -100,7 +100,7 @@ func engineRender() {
 
 	ansi := &color.Ansi{}
 	ansi.InitPlain()
-	writerColors := cfg.MakeColors(env)
+	writerColors := cfg.MakeColors()
 	writer := &color.AnsiWriter{
 		Ansi:               ansi,
 		TerminalBackground: shell.ConsoleBackgroundColor(env, cfg.TerminalBackground),