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 {
promptText = err.Error()
}
foreground := prompt.ForegroundTemplates.Resolve(nil, e.Env, prompt.Foreground)
background := prompt.BackgroundTemplates.Resolve(nil, e.Env, prompt.Background)
foreground := prompt.ForegroundTemplates.FirstMatch(nil, e.Env, prompt.Foreground)
background := prompt.BackgroundTemplates.FirstMatch(nil, e.Env, prompt.Background)
e.Writer.SetColors(background, foreground)
e.Writer.Write(background, foreground, promptText)
switch e.Env.Shell() {

View file

@ -3,7 +3,6 @@ package engine
import (
"errors"
"fmt"
"oh-my-posh/color"
"oh-my-posh/environment"
"oh-my-posh/properties"
"oh-my-posh/segments"
@ -16,20 +15,22 @@ import (
// Segment represent a single segment and it's configuration
type Segment struct {
Type SegmentType `json:"type,omitempty"`
Tips []string `json:"tips,omitempty"`
Style SegmentStyle `json:"style,omitempty"`
PowerlineSymbol string `json:"powerline_symbol,omitempty"`
InvertPowerline bool `json:"invert_powerline,omitempty"`
Foreground string `json:"foreground,omitempty"`
ForegroundTemplates color.Templates `json:"foreground_templates,omitempty"`
Background string `json:"background,omitempty"`
BackgroundTemplates color.Templates `json:"background_templates,omitempty"`
LeadingDiamond string `json:"leading_diamond,omitempty"`
TrailingDiamond string `json:"trailing_diamond,omitempty"`
Template string `json:"template,omitempty"`
Properties properties.Map `json:"properties,omitempty"`
Interactive bool `json:"interactive,omitempty"`
Type SegmentType `json:"type,omitempty"`
Tips []string `json:"tips,omitempty"`
Style SegmentStyle `json:"style,omitempty"`
PowerlineSymbol string `json:"powerline_symbol,omitempty"`
InvertPowerline bool `json:"invert_powerline,omitempty"`
Foreground string `json:"foreground,omitempty"`
ForegroundTemplates template.List `json:"foreground_templates,omitempty"`
Background string `json:"background,omitempty"`
BackgroundTemplates template.List `json:"background_templates,omitempty"`
LeadingDiamond string `json:"leading_diamond,omitempty"`
TrailingDiamond string `json:"trailing_diamond,omitempty"`
Template string `json:"template,omitempty"`
Templates template.List `json:"templates,omitempty"`
TemplatesLogic template.Logic `json:"templates_logic,omitempty"`
Properties properties.Map `json:"properties,omitempty"`
Interactive bool `json:"interactive,omitempty"`
writer SegmentWriter
Enabled bool `json:"-"`
@ -239,14 +240,14 @@ func (segment *Segment) shouldInvokeWithTip(tip string) bool {
func (segment *Segment) foreground() string {
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
}
func (segment *Segment) background() string {
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
}
@ -325,13 +326,21 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error
}
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 {
segment.Template = segment.writer.Template()
}
tmpl := &template.Text{
Template: segment.Template,
Context: segment.writer,
Env: segment.env,
Template: segment.Template,
Context: segment.writer,
Env: segment.env,
TemplatesResult: templatesResult,
}
text, err := tmpl.Render()
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 {
Template string
Context interface{}
Env environment.Environment
Template string
Context interface{}
Env environment.Environment
TemplatesResult string
}
type Data interface{}
@ -29,10 +30,12 @@ type Context struct {
// Simple container to hold ANY object
Data
Templates string
}
func (c *Context) init(t *Text) {
c.Data = t.Context
c.Templates = t.TemplatesResult
if cache := t.Env.TemplateCache(); cache != nil {
c.TemplateCache = cache
return
@ -78,7 +81,22 @@ func (t *Text) cleanTemplate() {
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)
for _, match := range matches {
if variable, OK := unknownVariable(match["VAR"], &knownVariables); OK {

View file

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

View file

@ -22,6 +22,7 @@ understand how to configure a segment.
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background": "#61AFEF",
"template": " {{ .Path }}} ",
"properties": {
...
}
@ -38,11 +39,13 @@ understand how to configure a segment.
- invert_powerline: `boolean`
- leading_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_templates: foreground [color templates][color-templates]
- background: `string` [color][colors]
- 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`
- interactive: `boolean`
@ -52,7 +55,7 @@ Takes the `string` value referencing which segment logic it needs to run (see [s
## 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
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.
## 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
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
- `.Root`: `boolean` - is the current user root/admin or not
- `.PWD`: `string` - the current working directory
- `.Folder`: `string` - the current working folder
- `.Shell`: `string` - the current shell name
- `.ShellVersion`: `string` - the current shell version
- `.UserName`: `string` - the current user name
- `.HostName`: `string` - the host name
- `.Code`: `int` - the last exit code
- `.OS`: `string` - the operating system
- `.WSL`: `boolean` - in WSL yes/no
| Name | Type | Description |
| --------------- | --------- | ------------------------------------- |
| `.Root` | `boolean` | is the current user root/admin or not |
| `.PWD` | `string` | the current working directory |
| `.Folder` | `string` | the current working folder |
| `.Shell` | `string` | the current shell name |
| `.ShellVersion` | `string` | the current shell version |
| `.UserName` | `string` | the current user name |
| `.HostName` | `string` | the host name |
| `.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
@ -47,9 +50,11 @@ in the template.
```json
"template": " \\.NET \uE77F "
```
:::
<!-- markdownlint-disable MD013 -->
| Template | Description |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{{.}}` | 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. |
| `{{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). |
<!-- markdownlint-enable MD013 -->
## Helper functions
@ -74,12 +80,14 @@ use them.
### Custom
<!-- markdownlint-disable MD013 -->
| Template | Description |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `{{ 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]). |
| `{{ 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. |
<!-- markdownlint-enable MD013 -->
## 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:
- `<b>bold</b>`: renders `bold` as bold text
- `<u>underline</u>`: renders `underline` as underlined text
- `<o>overline</o>`: renders `overline` as overlined text
- `<i>italic</i>`: renders `italic` as italic text
- `<s>strikethrough</s>`: renders `strikethrough` as strikethrough text
- `<d>dimmed</d>`: renders `dimmed` as dimmed text
- `<f>blink</f>`: renders `blink` as blinking (flashing) text
- `<r>reversed</r>`: renders `reversed` as reversed text
| Syntax | Description |
| ---------------------- | --------------------------------------------- |
| `<b>bold</b>` | `bold` as bold text |
| `<u>underline</u>` | `underline` as underlined text |
| `<o>overline</o>` | `overline` as overlined text |
| `<i>italic</i>` | `italic` as italic text |
| `<s>strikethrough</s>` | `strikethrough` as strikethrough 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.
@ -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
[git]: /docs/segments/git
[exit]: /docs/segments/exit
[templates]: /docs/configuration/segment#templates