feat(templates): advanced templates logic

resolves #2491
This commit is contained in:
Jan De Dobbeleer 2022-07-08 11:47:08 +02:00 committed by Jan De Dobbeleer
parent 5cc12558ac
commit 24d2ef4767
8 changed files with 191 additions and 80 deletions

View file

@ -1,27 +0,0 @@
package color
import (
"oh-my-posh/environment"
"oh-my-posh/template"
)
type Templates []string
func (t Templates) Resolve(context interface{}, env environment.Environment, defaultColor string) string {
if len(t) == 0 {
return defaultColor
}
txtTemplate := &template.Text{
Context: context,
Env: env,
}
for _, tmpl := range t {
txtTemplate.Template = tmpl
value, err := txtTemplate.Render()
if err != nil || value == "" {
continue
}
return value
}
return defaultColor
}

View file

@ -374,8 +374,8 @@ func (e *Engine) PrintExtraPrompt(promptType ExtraPromptType) string {
if err != nil { if err != nil {
promptText = err.Error() promptText = err.Error()
} }
foreground := prompt.ForegroundTemplates.Resolve(nil, e.Env, prompt.Foreground) foreground := prompt.ForegroundTemplates.FirstMatch(nil, e.Env, prompt.Foreground)
background := prompt.BackgroundTemplates.Resolve(nil, e.Env, prompt.Background) background := prompt.BackgroundTemplates.FirstMatch(nil, e.Env, prompt.Background)
e.Writer.SetColors(background, foreground) e.Writer.SetColors(background, foreground)
e.Writer.Write(background, foreground, promptText) e.Writer.Write(background, foreground, promptText)
switch e.Env.Shell() { switch e.Env.Shell() {

View file

@ -3,7 +3,6 @@ package engine
import ( import (
"errors" "errors"
"fmt" "fmt"
"oh-my-posh/color"
"oh-my-posh/environment" "oh-my-posh/environment"
"oh-my-posh/properties" "oh-my-posh/properties"
"oh-my-posh/segments" "oh-my-posh/segments"
@ -16,20 +15,22 @@ import (
// Segment represent a single segment and it's configuration // Segment represent a single segment and it's configuration
type Segment struct { type Segment struct {
Type SegmentType `json:"type,omitempty"` Type SegmentType `json:"type,omitempty"`
Tips []string `json:"tips,omitempty"` Tips []string `json:"tips,omitempty"`
Style SegmentStyle `json:"style,omitempty"` Style SegmentStyle `json:"style,omitempty"`
PowerlineSymbol string `json:"powerline_symbol,omitempty"` PowerlineSymbol string `json:"powerline_symbol,omitempty"`
InvertPowerline bool `json:"invert_powerline,omitempty"` InvertPowerline bool `json:"invert_powerline,omitempty"`
Foreground string `json:"foreground,omitempty"` Foreground string `json:"foreground,omitempty"`
ForegroundTemplates color.Templates `json:"foreground_templates,omitempty"` ForegroundTemplates template.List `json:"foreground_templates,omitempty"`
Background string `json:"background,omitempty"` Background string `json:"background,omitempty"`
BackgroundTemplates color.Templates `json:"background_templates,omitempty"` BackgroundTemplates template.List `json:"background_templates,omitempty"`
LeadingDiamond string `json:"leading_diamond,omitempty"` LeadingDiamond string `json:"leading_diamond,omitempty"`
TrailingDiamond string `json:"trailing_diamond,omitempty"` TrailingDiamond string `json:"trailing_diamond,omitempty"`
Template string `json:"template,omitempty"` Template string `json:"template,omitempty"`
Properties properties.Map `json:"properties,omitempty"` Templates template.List `json:"templates,omitempty"`
Interactive bool `json:"interactive,omitempty"` TemplatesLogic template.Logic `json:"templates_logic,omitempty"`
Properties properties.Map `json:"properties,omitempty"`
Interactive bool `json:"interactive,omitempty"`
writer SegmentWriter writer SegmentWriter
Enabled bool `json:"-"` Enabled bool `json:"-"`
@ -239,14 +240,14 @@ func (segment *Segment) shouldInvokeWithTip(tip string) bool {
func (segment *Segment) foreground() string { func (segment *Segment) foreground() string {
if len(segment.foregroundCache) == 0 { if len(segment.foregroundCache) == 0 {
segment.foregroundCache = segment.ForegroundTemplates.Resolve(segment.writer, segment.env, segment.Foreground) segment.foregroundCache = segment.ForegroundTemplates.FirstMatch(segment.writer, segment.env, segment.Foreground)
} }
return segment.foregroundCache return segment.foregroundCache
} }
func (segment *Segment) background() string { func (segment *Segment) background() string {
if len(segment.backgroundCache) == 0 { if len(segment.backgroundCache) == 0 {
segment.backgroundCache = segment.BackgroundTemplates.Resolve(segment.writer, segment.env, segment.Background) segment.backgroundCache = segment.BackgroundTemplates.FirstMatch(segment.writer, segment.env, segment.Background)
} }
return segment.backgroundCache return segment.backgroundCache
} }
@ -325,13 +326,21 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error
} }
func (segment *Segment) string() string { func (segment *Segment) string() string {
var templatesResult string
if !segment.Templates.Empty() {
templatesResult = segment.Templates.Resolve(segment.writer, segment.env, "", segment.TemplatesLogic)
if len(templatesResult) != 0 && len(segment.Template) == 0 {
return templatesResult
}
}
if len(segment.Template) == 0 { if len(segment.Template) == 0 {
segment.Template = segment.writer.Template() segment.Template = segment.writer.Template()
} }
tmpl := &template.Text{ tmpl := &template.Text{
Template: segment.Template, Template: segment.Template,
Context: segment.writer, Context: segment.writer,
Env: segment.env, Env: segment.env,
TemplatesResult: templatesResult,
} }
text, err := tmpl.Render() text, err := tmpl.Render()
if err != nil { if err != nil {

69
src/template/list.go Normal file
View file

@ -0,0 +1,69 @@
package template
import (
"oh-my-posh/environment"
"strings"
)
type Logic string
const (
FirstMatch Logic = "first_match"
Join Logic = "join"
)
type List []string
func (l List) Empty() bool {
return len(l) == 0
}
func (l List) Resolve(context interface{}, env environment.Environment, defaultValue string, logic Logic) string {
switch logic {
case FirstMatch:
return l.FirstMatch(context, env, defaultValue)
case Join:
fallthrough
default:
return l.Join(context, env)
}
}
func (l List) Join(context interface{}, env environment.Environment) string {
if len(l) == 0 {
return ""
}
txtTemplate := &Text{
Context: context,
Env: env,
}
var buffer strings.Builder
for _, tmpl := range l {
txtTemplate.Template = tmpl
value, err := txtTemplate.Render()
if err != nil || len(strings.TrimSpace(value)) == 0 {
continue
}
buffer.WriteString(value)
}
return buffer.String()
}
func (l List) FirstMatch(context interface{}, env environment.Environment, defaultValue string) string {
if len(l) == 0 {
return defaultValue
}
txtTemplate := &Text{
Context: context,
Env: env,
}
for _, tmpl := range l {
txtTemplate.Template = tmpl
value, err := txtTemplate.Render()
if err != nil || len(strings.TrimSpace(value)) == 0 {
continue
}
return value
}
return defaultValue
}

View file

@ -17,9 +17,10 @@ const (
) )
type Text struct { type Text struct {
Template string Template string
Context interface{} Context interface{}
Env environment.Environment Env environment.Environment
TemplatesResult string
} }
type Data interface{} type Data interface{}
@ -29,10 +30,12 @@ type Context struct {
// Simple container to hold ANY object // Simple container to hold ANY object
Data Data
Templates string
} }
func (c *Context) init(t *Text) { func (c *Context) init(t *Text) {
c.Data = t.Context c.Data = t.Context
c.Templates = t.TemplatesResult
if cache := t.Env.TemplateCache(); cache != nil { if cache := t.Env.TemplateCache(); cache != nil {
c.TemplateCache = cache c.TemplateCache = cache
return return
@ -78,7 +81,22 @@ func (t *Text) cleanTemplate() {
return splitted[0], true return splitted[0], true
} }
knownVariables := []string{"Root", "PWD", "Folder", "Shell", "ShellVersion", "UserName", "HostName", "Env", "Data", "Code", "OS", "WSL", "Segments"} knownVariables := []string{
"Root",
"PWD",
"Folder",
"Shell",
"ShellVersion",
"UserName",
"HostName",
"Env",
"Data",
"Code",
"OS",
"WSL",
"Segments",
"Templates",
}
matches := regex.FindAllNamedRegexMatch(`(?: |{|\()(?P<VAR>(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template) matches := regex.FindAllNamedRegexMatch(`(?: |{|\()(?P<VAR>(\.[a-zA-Z_][a-zA-Z0-9]*)+)`, t.Template)
for _, match := range matches { for _, match := range matches {
if variable, OK := unknownVariable(match["VAR"], &knownVariables); OK { if variable, OK := unknownVariable(match["VAR"], &knownVariables); OK {

View file

@ -28,10 +28,9 @@
"title": "Palette reference", "title": "Palette reference",
"description": "https://ohmyposh.dev/docs/configuration/colors#palette" "description": "https://ohmyposh.dev/docs/configuration/colors#palette"
}, },
"color_templates": { "templates": {
"type": "array", "type": "array",
"title": "Templates to define a color", "title": "An array of templates",
"description": "https://ohmyposh.dev/docs/configuration/colors#color-templates",
"default": [], "default": [],
"items": { "items": {
"$ref": "#/definitions/segment/properties/template" "$ref": "#/definitions/segment/properties/template"
@ -86,13 +85,15 @@
"$ref": "#/definitions/color" "$ref": "#/definitions/color"
}, },
"foreground_templates": { "foreground_templates": {
"$ref": "#/definitions/color_templates" "$ref": "#/definitions/templates",
"description": "https://ohmyposh.dev/docs/configuration/colors#color-templates"
}, },
"background": { "background": {
"$ref": "#/definitions/color" "$ref": "#/definitions/color"
}, },
"background_templates": { "background_templates": {
"$ref": "#/definitions/color_templates" "$ref": "#/definitions/templates",
"description": "https://ohmyposh.dev/docs/configuration/colors#color-templates"
} }
} }
}, },
@ -261,13 +262,15 @@
"$ref": "#/definitions/color" "$ref": "#/definitions/color"
}, },
"foreground_templates": { "foreground_templates": {
"$ref": "#/definitions/color_templates" "$ref": "#/definitions/templates",
"description": "https://ohmyposh.dev/docs/configuration/colors#color-templates"
}, },
"background": { "background": {
"$ref": "#/definitions/color" "$ref": "#/definitions/color"
}, },
"background_templates": { "background_templates": {
"$ref": "#/definitions/color_templates" "$ref": "#/definitions/templates",
"description": "https://ohmyposh.dev/docs/configuration/colors#color-templates"
}, },
"template": { "template": {
"type": "string", "type": "string",
@ -275,6 +278,16 @@
"description": "https://ohmyposh.dev/docs/configuration/templates", "description": "https://ohmyposh.dev/docs/configuration/templates",
"default": "" "default": ""
}, },
"templates": {
"$ref": "#/definitions/templates",
"description": "https://ohmyposh.dev/docs/configuration/segment#templates"
},
"templates_logic": {
"type": "string",
"title": "Templates Logic",
"description": "https://ohmyposh.dev/docs/configuration/segment#templates",
"enum": ["first_match", "join"]
},
"properties": { "properties": {
"type": "object", "type": "object",
"title": "Segment Properties, used to change behavior/displaying", "title": "Segment Properties, used to change behavior/displaying",

View file

@ -22,6 +22,7 @@ understand how to configure a segment.
"powerline_symbol": "\uE0B0", "powerline_symbol": "\uE0B0",
"foreground": "#ffffff", "foreground": "#ffffff",
"background": "#61AFEF", "background": "#61AFEF",
"template": " {{ .Path }}} ",
"properties": { "properties": {
... ...
} }
@ -38,11 +39,13 @@ understand how to configure a segment.
- invert_powerline: `boolean` - invert_powerline: `boolean`
- leading_diamond: `string` - leading_diamond: `string`
- trailing_diamond: `string` - trailing_diamond: `string`
- template: `string` a go [text/template][go-text-template] [template][templates] to render the prompt
- foreground: `string` [color][colors] - foreground: `string` [color][colors]
- foreground_templates: foreground [color templates][color-templates] - foreground_templates: foreground [color templates][color-templates]
- background: `string` [color][colors] - background: `string` [color][colors]
- background_templates: background [color templates][color-templates] - background_templates: background [color templates][color-templates]
- template: `string` a go [text/template][go-text-template] [template][templates] to render the prompt
- templates: `array` of `template`
- templates_logic: `first_match` | `join`
- properties: `array` of `Property`: `string` - properties: `array` of `Property`: `string`
- interactive: `boolean` - interactive: `boolean`
@ -52,7 +55,7 @@ Takes the `string` value referencing which segment logic it needs to run (see [s
## Style ## Style
Oh Hi! You made it to a really interesting part, great! Style defines how a prompt is rendered. Looking at the most prompt Style defines how a prompt is rendered. Looking at the most prompt
themes out there, we identified 3 types. All of these require a different configuration and depending on the look themes out there, we identified 3 types. All of these require a different configuration and depending on the look
you want to achieve you might need to understand/use them all. you want to achieve you might need to understand/use them all.
@ -96,6 +99,21 @@ its foreground color.
Text character to use at the end of the segment. Will take the background color of the segment as its foreground color. Text character to use at the end of the segment. Will take the background color of the segment as its foreground color.
## Template
A single line [template][templates] string leveraging a segment's properties to render the text.
## Templates
In some cases having a single [template][templates] string is a bit cumbersome. Templates allows you to span the
segment's [template][templates] string multiple lines where every [template][templates] is evaluated and depending
on what you aim to achieve, there are two possible outcomes based on `templates_logic`:
- `first_match`: return the first non-whitespace string and skip everything else
- `join`: evaluate all templates and join all non-whitespace strings
The default is `join`.
## Properties ## Properties
An array of **Properties** with a value. This is used inside of the segment logic to tweak what the output of the segment An array of **Properties** with a value. This is used inside of the segment logic to tweak what the output of the segment

View file

@ -10,16 +10,19 @@ offers a few standard properties to work with.
## Global properties ## Global properties
- `.Root`: `boolean` - is the current user root/admin or not | Name | Type | Description |
- `.PWD`: `string` - the current working directory | --------------- | --------- | ------------------------------------- |
- `.Folder`: `string` - the current working folder | `.Root` | `boolean` | is the current user root/admin or not |
- `.Shell`: `string` - the current shell name | `.PWD` | `string` | the current working directory |
- `.ShellVersion`: `string` - the current shell version | `.Folder` | `string` | the current working folder |
- `.UserName`: `string` - the current user name | `.Shell` | `string` | the current shell name |
- `.HostName`: `string` - the host name | `.ShellVersion` | `string` | the current shell version |
- `.Code`: `int` - the last exit code | `.UserName` | `string` | the current user name |
- `.OS`: `string` - the operating system | `.HostName` | `string` | the host name |
- `.WSL`: `boolean` - in WSL yes/no | `.Code` | `int` | the last exit code |
| `.OS` | `string` | the operating system |
| `.WSL` | `boolean` | in WSL yes/no |
| `.Templates` | `string` | the [templates][templates] result |
## Environment variables ## Environment variables
@ -47,9 +50,11 @@ in the template.
```json ```json
"template": " \\.NET \uE77F " "template": " \\.NET \uE77F "
``` ```
::: :::
<!-- markdownlint-disable MD013 --> <!-- markdownlint-disable MD013 -->
| Template | Description | | Template | Description |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{{.}}` | Root element. | | `{{.}}` | Root element. |
@ -61,6 +66,7 @@ in the template.
| `{{with .Var}} {{end}}` | With statement, where Var is a variable. It skips the block if the variable is absent. | | `{{with .Var}} {{end}}` | With statement, where Var is a variable. It skips the block if the variable is absent. |
| `{{range .Array}} {{end}}` | Range statement, where Array is an array, slice, map, or channel. | | `{{range .Array}} {{end}}` | Range statement, where Array is an array, slice, map, or channel. |
| `{{ lt 3 4 }}` | This lt comparison function evaluates to true since 3 is less than 4 (other boolean operators: eq, ne, lt, le, gt, ge). | | `{{ lt 3 4 }}` | This lt comparison function evaluates to true since 3 is less than 4 (other boolean operators: eq, ne, lt, le, gt, ge). |
<!-- markdownlint-enable MD013 --> <!-- markdownlint-enable MD013 -->
## Helper functions ## Helper functions
@ -74,12 +80,14 @@ use them.
### Custom ### Custom
<!-- markdownlint-disable MD013 --> <!-- markdownlint-disable MD013 -->
| Template | Description | | Template | Description |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `{{ url .UpstreamIcon .UpstreamURL }}` | Create a hyperlink to a website to open your default browser (needs terminal [support][terminal-list-hyperlinks]). | | `{{ url .UpstreamIcon .UpstreamURL }}` | Create a hyperlink to a website to open your default browser (needs terminal [support][terminal-list-hyperlinks]). |
| `{{ path .Path .Location }}` | Create a link to a folder to open your file explorer (needs terminal [support][terminal-list-hyperlinks]). | | `{{ path .Path .Location }}` | Create a link to a folder to open your file explorer (needs terminal [support][terminal-list-hyperlinks]). |
| `{{ secondsRound 3600 }}` | Round seconds to a time indication. In this case the output is `1h`. | | `{{ secondsRound 3600 }}` | Round seconds to a time indication. In this case the output is `1h`. |
| `{{ if glob "*.go" }}OK{{ else }}NOK{{ end }}` | Exposes [filepath.Glob][glob] as a boolean template function. | | `{{ if glob "*.go" }}OK{{ else }}NOK{{ end }}` | Exposes [filepath.Glob][glob] as a boolean template function. |
<!-- markdownlint-enable MD013 --> <!-- markdownlint-enable MD013 -->
## Cross segment template properties ## Cross segment template properties
@ -103,14 +111,16 @@ your config does not contain a git segment as Oh My Posh only populates the prop
You can make use of the following syntax to decorate text: You can make use of the following syntax to decorate text:
- `<b>bold</b>`: renders `bold` as bold text | Syntax | Description |
- `<u>underline</u>`: renders `underline` as underlined text | ---------------------- | --------------------------------------------- |
- `<o>overline</o>`: renders `overline` as overlined text | `<b>bold</b>` | `bold` as bold text |
- `<i>italic</i>`: renders `italic` as italic text | `<u>underline</u>` | `underline` as underlined text |
- `<s>strikethrough</s>`: renders `strikethrough` as strikethrough text | `<o>overline</o>` | `overline` as overlined text |
- `<d>dimmed</d>`: renders `dimmed` as dimmed text | `<i>italic</i>` | `italic` as italic text |
- `<f>blink</f>`: renders `blink` as blinking (flashing) text | `<s>strikethrough</s>` | `strikethrough` as strikethrough text |
- `<r>reversed</r>`: renders `reversed` as reversed text | `<d>dimmed</d>` | `dimmed` as dimmed text |
| `<f>blink</f>` | `blink` as blinking (flashing) text |
| `<r>reversed</r>` | `reversed` as reversed text |
This can be used in templates and icons/text inside your config. This can be used in templates and icons/text inside your config.
@ -142,3 +152,4 @@ Only spaces are excluded, meaning you can still add line breaks and tabs if that
[glob]: https://pkg.go.dev/path/filepath#Glob [glob]: https://pkg.go.dev/path/filepath#Glob
[git]: /docs/segments/git [git]: /docs/segments/git
[exit]: /docs/segments/exit [exit]: /docs/segments/exit
[templates]: /docs/configuration/segment#templates