refactor: parallel segment rendering

This commit is contained in:
Jan De Dobbeleer 2024-10-11 21:44:46 +02:00 committed by Jan De Dobbeleer
parent 87404454a4
commit d4054b04d6
22 changed files with 397 additions and 301 deletions

View file

@ -85,6 +85,7 @@ func TestAnsiRender(t *testing.T) {
env.On("TemplateCache").Return(&cache.Template{})
env.On("Getenv", "TERM_PROGRAM").Return(tc.Term)
env.On("Shell").Return("foo")
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
template.Init(env)

View file

@ -1,11 +1,5 @@
package config
import (
"sync"
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
)
// BlockType type of block
type BlockType string
@ -32,7 +26,6 @@ const (
// Block defines a part of the prompt with optional segments
type Block struct {
env runtime.Environment
Type BlockType `json:"type,omitempty" toml:"type,omitempty"`
Alignment BlockAlignment `json:"alignment,omitempty" toml:"alignment,omitempty"`
Filler string `json:"filler,omitempty" toml:"filler,omitempty"`
@ -44,39 +37,3 @@ type Block struct {
MinWidth int `json:"min_width,omitempty" toml:"min_width,omitempty"`
Newline bool `json:"newline,omitempty" toml:"newline,omitempty"`
}
func (b *Block) Init(env runtime.Environment) {
b.env = env
b.executeSegmentLogic()
}
func (b *Block) Enabled() bool {
for _, segment := range b.Segments {
if segment.Enabled {
return true
}
}
return false
}
func (b *Block) executeSegmentLogic() {
if shouldHideForWidth(b.env, b.MinWidth, b.MaxWidth) {
return
}
b.setEnabledSegments()
}
func (b *Block) setEnabledSegments() {
wg := sync.WaitGroup{}
wg.Add(len(b.Segments))
defer wg.Wait()
for _, segment := range b.Segments {
go func(s *Segment) {
defer wg.Done()
s.SetEnabled(b.env)
}(segment)
}
}

View file

@ -1,28 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBlockEnabled(t *testing.T) {
cases := []struct {
Case string
Type BlockType
Segments []*Segment
Expected bool
}{
{Case: "prompt enabled", Expected: true, Type: Prompt, Segments: []*Segment{{Enabled: true}}},
{Case: "prompt disabled", Expected: false, Type: Prompt, Segments: []*Segment{{Enabled: false}}},
{Case: "prompt enabled multiple", Expected: true, Type: Prompt, Segments: []*Segment{{Enabled: false}, {Enabled: true}}},
{Case: "rprompt enabled multiple", Expected: true, Type: RPrompt, Segments: []*Segment{{Enabled: false}, {Enabled: true}}},
}
for _, tc := range cases {
block := &Block{
Type: tc.Type,
Segments: tc.Segments,
}
assert.Equal(t, tc.Expected, block.Enabled(), tc.Case)
}
}

View file

@ -97,6 +97,7 @@ func TestGetPalette(t *testing.T) {
})
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Shell").Return("bash")
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
template.Init(env)

View file

@ -3,7 +3,6 @@ package config
import (
"encoding/json"
"fmt"
"runtime/debug"
"strings"
"time"
@ -37,8 +36,9 @@ func (s *SegmentStyle) resolve(context any) SegmentStyle {
}
type Segment struct {
writer SegmentWriter
env runtime.Environment
writer SegmentWriter
env runtime.Environment
Properties properties.Map `json:"properties,omitempty" toml:"properties,omitempty"`
Cache *cache.Config `json:"cache,omitempty" toml:"cache,omitempty"`
Filler string `json:"filler,omitempty" toml:"filler,omitempty"`
@ -83,19 +83,7 @@ func (segment *Segment) Name() string {
return name
}
func (segment *Segment) SetEnabled(env runtime.Environment) {
defer func() {
err := recover()
if err == nil {
return
}
// display a message explaining omp failed(with the err)
message := fmt.Sprintf("\noh-my-posh fatal error rendering %s segment:%s\n\n%s\n", segment.Type, err, debug.Stack())
fmt.Println(message)
segment.Enabled = true
}()
func (segment *Segment) Execute(env runtime.Environment) {
// segment timings for debug purposes
var start time.Time
if env.Flags().Debug {
@ -131,7 +119,64 @@ func (segment *Segment) SetEnabled(env runtime.Environment) {
}
}
func (segment *Segment) HasCache() bool {
func (segment *Segment) Render() {
if !segment.Enabled {
return
}
segment.Text = segment.string()
segment.Enabled = len(strings.ReplaceAll(segment.Text, " ", "")) > 0
if !segment.Enabled {
segment.env.TemplateCache().RemoveSegmentData(segment.Name())
return
}
segment.setCache()
}
func (segment *Segment) ResolveForeground() color.Ansi {
if len(segment.ForegroundTemplates) != 0 {
match := segment.ForegroundTemplates.FirstMatch(segment.writer, segment.Foreground.String())
segment.Foreground = color.Ansi(match)
}
return segment.Foreground
}
func (segment *Segment) ResolveBackground() color.Ansi {
if len(segment.BackgroundTemplates) != 0 {
match := segment.BackgroundTemplates.FirstMatch(segment.writer, segment.Background.String())
segment.Background = color.Ansi(match)
}
return segment.Background
}
func (segment *Segment) ResolveStyle() SegmentStyle {
if len(segment.styleCache) != 0 {
return segment.styleCache
}
segment.styleCache = segment.Style.resolve(segment.writer)
return segment.styleCache
}
func (segment *Segment) IsPowerline() bool {
style := segment.ResolveStyle()
return style == Powerline || style == Accordion
}
func (segment *Segment) HasEmptyDiamondAtEnd() bool {
if segment.ResolveStyle() != Diamond {
return false
}
return len(segment.TrailingDiamond) == 0
}
func (segment *Segment) hasCache() bool {
return segment.Cache != nil && !segment.Cache.Duration.IsEmpty()
}
@ -153,7 +198,7 @@ func (segment *Segment) isToggled() bool {
}
func (segment *Segment) restoreCache() bool {
if !segment.HasCache() {
if !segment.hasCache() {
return false
}
@ -182,7 +227,7 @@ func (segment *Segment) restoreCache() bool {
}
func (segment *Segment) setCache() {
if !segment.HasCache() {
if !segment.hasCache() {
return
}
@ -229,22 +274,6 @@ func (segment *Segment) folderKey() string {
return segment.env.Pwd()
}
func (segment *Segment) SetText() {
if !segment.Enabled {
return
}
segment.Text = segment.string()
segment.Enabled = len(strings.ReplaceAll(segment.Text, " ", "")) > 0
if !segment.Enabled {
segment.env.TemplateCache().RemoveSegmentData(segment.Name())
return
}
segment.setCache()
}
func (segment *Segment) string() string {
if len(segment.Template) == 0 {
segment.Template = segment.writer.Template()
@ -300,44 +329,3 @@ func (segment *Segment) cwdExcluded() bool {
list := properties.ParseStringArray(value)
return segment.env.DirMatchesOneOf(segment.env.Pwd(), list)
}
func (segment *Segment) ResolveForeground() color.Ansi {
if len(segment.ForegroundTemplates) != 0 {
match := segment.ForegroundTemplates.FirstMatch(segment.writer, segment.Foreground.String())
segment.Foreground = color.Ansi(match)
}
return segment.Foreground
}
func (segment *Segment) ResolveBackground() color.Ansi {
if len(segment.BackgroundTemplates) != 0 {
match := segment.BackgroundTemplates.FirstMatch(segment.writer, segment.Background.String())
segment.Background = color.Ansi(match)
}
return segment.Background
}
func (segment *Segment) ResolveStyle() SegmentStyle {
if len(segment.styleCache) != 0 {
return segment.styleCache
}
segment.styleCache = segment.Style.resolve(segment.writer)
return segment.styleCache
}
func (segment *Segment) IsPowerline() bool {
style := segment.ResolveStyle()
return style == Powerline || style == Accordion
}
func (segment *Segment) HasEmptyDiamondAtEnd() bool {
if segment.ResolveStyle() != Diamond {
return false
}
return len(segment.TrailingDiamond) == 0
}

View file

@ -168,12 +168,6 @@ func (e *Engine) getTitleTemplateText() string {
func (e *Engine) renderBlock(block *config.Block, cancelNewline bool) bool {
defer e.applyPowerShellBleedPatch()
block.Init(e.Env)
if !block.Enabled() {
return false
}
// do not print a newline to avoid a leading space
// when we're printing the first primary prompt in
// the shell
@ -181,7 +175,7 @@ func (e *Engine) renderBlock(block *config.Block, cancelNewline bool) bool {
e.writeNewline()
}
text, length := e.renderBlockSegments(block)
text, length := e.writeBlockSegments(block)
// do not print anything when we don't have any text
if length == 0 {
@ -262,52 +256,6 @@ func (e *Engine) applyPowerShellBleedPatch() {
e.write(terminal.ClearAfter())
}
func (e *Engine) renderBlockSegments(block *config.Block) (string, int) {
e.filterSegments(block)
for i, segment := range block.Segments {
if colors, newCycle := cycle.Loop(); colors != nil {
cycle = &newCycle
segment.Foreground = colors.Foreground
segment.Background = colors.Background
}
if i == 0 && len(block.LeadingDiamond) > 0 {
segment.LeadingDiamond = block.LeadingDiamond
}
if i == len(block.Segments)-1 && len(block.TrailingDiamond) > 0 {
segment.TrailingDiamond = block.TrailingDiamond
}
e.setActiveSegment(segment)
e.renderActiveSegment()
}
e.writeSeparator(true)
e.activeSegment = nil
e.previousActiveSegment = nil
return terminal.String()
}
func (e *Engine) filterSegments(block *config.Block) {
segments := make([]*config.Segment, 0)
for _, segment := range block.Segments {
segment.SetText()
if !segment.Enabled && segment.ResolveStyle() != config.Accordion {
continue
}
segments = append(segments, segment)
}
block.Segments = segments
}
func (e *Engine) setActiveSegment(segment *config.Segment) {
e.activeSegment = segment
terminal.Interactive = segment.Interactive
@ -341,6 +289,10 @@ func (e *Engine) renderActiveSegment() {
}
func (e *Engine) writeSeparator(final bool) {
if e.activeSegment == nil {
return
}
isCurrentDiamond := e.activeSegment.ResolveStyle() == config.Diamond
if final && isCurrentDiamond {
terminal.Write(color.Transparent, color.Background, e.activeSegment.TrailingDiamond)

View file

@ -89,6 +89,7 @@ func TestPrintPWD(t *testing.T) {
env.On("Pwd").Return(tc.Pwd)
env.On("User").Return("user")
env.On("Shell").Return(tc.Shell)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
env.On("IsCygwin").Return(tc.Cygwin)
env.On("Host").Return("host", nil)
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
@ -195,6 +196,7 @@ func TestGetTitle(t *testing.T) {
})
env.On("Getenv", "USERDOMAIN").Return("MyCompany")
env.On("Shell").Return(tc.ShellName)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
terminal.Init(shell.GENERIC)
template.Init(env)
@ -256,6 +258,7 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
})
env.On("Getenv", "USERDOMAIN").Return("MyCompany")
env.On("Shell").Return(tc.ShellName)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
terminal.Init(shell.GENERIC)
template.Init(env)

View file

@ -43,6 +43,20 @@ func (e *Engine) Primary() string {
}
func (e *Engine) writePrimaryPrompt(needsPrimaryRPrompt bool) {
// file, err := os.Create("trace.out")
// if err != nil {
// panic(err)
// }
// defer file.Close()
// err = trace.Start(file)
// if err != nil {
// panic(err)
// }
// defer trace.Stop()
if e.Config.ShellIntegration {
exitCode, _ := e.Env.StatusCodes()
e.write(terminal.CommandFinished(exitCode, e.Env.Flags().NoExitCode))
@ -97,6 +111,10 @@ func (e *Engine) writePrimaryPrompt(needsPrimaryRPrompt bool) {
}
func (e *Engine) needsPrimaryRightPrompt() bool {
if e.Env.Flags().Debug {
return true
}
switch e.Env.Shell() {
case shell.PWSH, shell.PWSH5, shell.GENERIC, shell.ZSH:
return true

View file

@ -22,13 +22,13 @@ func (e *Engine) RPrompt() string {
return ""
}
rprompt.Init(e.Env)
text, length := e.writeBlockSegments(rprompt)
if !rprompt.Enabled() {
// do not print anything when we don't have any text
if length == 0 {
return ""
}
text, length := e.renderBlockSegments(rprompt)
e.rpromptLength = length
if e.Env.Shell() == shell.ELVISH && e.Env.GOOS() != runtime.WINDOWS {

124
src/prompt/segments.go Normal file
View file

@ -0,0 +1,124 @@
package prompt
import (
"runtime"
"slices"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/regex"
"github.com/jandedobbeleer/oh-my-posh/src/terminal"
)
type result struct {
segment *config.Segment
index int
}
func (e *Engine) writeBlockSegments(block *config.Block) (string, int) {
length := len(block.Segments)
if length == 0 {
return "", 0
}
out := make(chan result, length)
for i, segment := range block.Segments {
go func(segment *config.Segment) {
segment.Execute(e.Env)
out <- result{segment, i}
}(segment)
}
e.writeSegments(out, block)
e.writeSeparator(true)
e.activeSegment = nil
e.previousActiveSegment = nil
return terminal.String()
}
func (e *Engine) writeSegments(out chan result, block *config.Block) {
count := len(block.Segments)
// store the current index
current := 0
// store the results
results := make([]*config.Segment, count)
// store the names of executed segments
executed := make([]string, count)
for {
select {
case res := <-out:
results[res.index] = res.segment
name := res.segment.Name()
if !slices.Contains(executed, name) {
executed = append(executed, name)
}
segment := results[current]
for segment != nil {
if !e.canRenderSegment(segment, executed) {
break
}
segment.Render()
e.writeSegment(current, block, segment)
if current == count-1 {
return
}
current++
segment = results[current]
}
default:
runtime.Gosched()
}
}
}
func (e *Engine) writeSegment(index int, block *config.Block, segment *config.Segment) {
if !segment.Enabled && segment.ResolveStyle() != config.Accordion {
return
}
if colors, newCycle := cycle.Loop(); colors != nil {
cycle = &newCycle
segment.Foreground = colors.Foreground
segment.Background = colors.Background
}
if index == 0 && len(block.LeadingDiamond) > 0 {
segment.LeadingDiamond = block.LeadingDiamond
}
if index == len(block.Segments)-1 && len(block.TrailingDiamond) > 0 {
segment.TrailingDiamond = block.TrailingDiamond
}
e.setActiveSegment(segment)
e.renderActiveSegment()
}
func (e *Engine) canRenderSegment(segment *config.Segment, executed []string) bool {
if !strings.Contains(segment.Template, ".Segments.") {
return true
}
matches := regex.FindNamedRegexMatch(`\.Segments\.(?P<NAME>[a-zA-Z0-9]+)`, segment.Template)
for _, name := range matches {
if slices.Contains(executed, name) {
continue
}
return false
}
return true
}

View file

@ -0,0 +1,73 @@
package prompt
import (
"testing"
"github.com/jandedobbeleer/oh-my-posh/src/config"
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
"github.com/stretchr/testify/assert"
)
func TestRenderBlock(t *testing.T) {
engine := New(&runtime.Flags{})
block := &config.Block{
Segments: []*config.Segment{
{
Type: "text",
Template: "Hello",
Foreground: "red",
Background: "blue",
},
{
Type: "text",
Template: "World",
Foreground: "red",
Background: "blue",
},
},
}
prompt, length := engine.writeBlockSegments(block)
assert.NotEmpty(t, prompt)
assert.Equal(t, 10, length)
}
func TestCanRenderSegment(t *testing.T) {
cases := []struct {
Case string
Template string
ExecutedSegments []string
Expected bool
}{
{
Case: "No cross segment dependencies",
Expected: true,
Template: "Hello",
},
{
Case: "Cross segment dependencies, nothing executed",
Expected: false,
Template: "Hello {{ .Segments.Foo.World }} {{ .Segments.Foo.Bar }}",
},
{
Case: "Cross segment dependencies, available",
Expected: true,
Template: "Hello {{ .Segments.Foo.World }}",
ExecutedSegments: []string{
"Foo",
},
},
}
for _, c := range cases {
segment := &config.Segment{
Type: "text",
Template: c.Template,
}
engine := &Engine{}
got := engine.canRenderSegment(segment, c.ExecutedSegments)
assert.Equal(t, c.Expected, got, c.Case)
}
}

View file

@ -17,7 +17,7 @@ func (e *Engine) Tooltip(tip string) string {
continue
}
tooltip.SetEnabled(e.Env)
tooltip.Execute(e.Env)
if !tooltip.Enabled {
continue
@ -36,14 +36,13 @@ func (e *Engine) Tooltip(tip string) string {
Segments: tooltips,
}
block.Init(e.Env)
text, length := e.writeBlockSegments(block)
if !block.Enabled() {
// do not print anything when we don't have any text
if length == 0 {
return ""
}
text, length := e.renderBlockSegments(block)
switch e.Env.Shell() {
case shell.PWSH, shell.PWSH5:
e.rprompt = text

View file

@ -8,6 +8,7 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/stretchr/testify/assert"
)
@ -50,7 +51,7 @@ func TestAzSegment(t *testing.T) {
{
Case: "Faulty template",
ExpectedEnabled: true,
ExpectedString: "<.Data.Burp>: can't evaluate field Burp in type template.Data",
ExpectedString: template.IncorrectTemplate,
Template: "{{ .Burp }}",
HasPowerShell: true,
},

View file

@ -6,6 +6,7 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/stretchr/testify/assert"
)
@ -105,7 +106,7 @@ func TestNSSegment(t *testing.T) {
JSONResponse: `
[{"sgv":50,"direction":"DoubleDown"}]`,
Template: "\ue2a1 {{.Sgv}}{{.Burp}}",
ExpectedString: "<.Data.Burp>: can't evaluate field Burp in type template.Data",
ExpectedString: template.IncorrectTemplate,
ExpectedEnabled: true,
},
}

View file

@ -41,6 +41,7 @@ func renderTemplateNoTrimSpace(env *mock.Environment, segmentTemplate string, co
env.On("Error", testify_.Anything)
env.On("Debug", testify_.Anything)
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
env.On("Shell").Return("foo")
template.Init(env)
@ -987,6 +988,7 @@ func TestFullPathCustomMappedLocations(t *testing.T) {
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("TemplateCache").Return(&cache.Template{})
env.On("Getenv", "HOME").Return(homeDir)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
template.Init(env)
@ -1020,6 +1022,7 @@ func TestFolderPathCustomMappedLocations(t *testing.T) {
env.On("Flags").Return(args)
env.On("Shell").Return(shell.GENERIC)
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
template.Init(env)
@ -1414,6 +1417,7 @@ func TestGetPwd(t *testing.T) {
env.On("Flags").Return(args)
env.On("Shell").Return(shell.PWSH)
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
template.Init(env)
@ -1452,6 +1456,7 @@ func TestGetFolderSeparator(t *testing.T) {
env.On("Debug", testify_.Anything)
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Shell").Return(shell.GENERIC)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
template.Init(env)
@ -1641,6 +1646,7 @@ func TestReplaceMappedLocations(t *testing.T) {
env.On("GOOS").Return(runtime.DARWIN)
env.On("Home").Return("/a/b/k")
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
template.Init(env)
@ -1769,6 +1775,7 @@ func TestGetMaxWidth(t *testing.T) {
env.On("TemplateCache").Return(&cache.Template{})
env.On("Getenv", "MAX_WIDTH").Return("120")
env.On("Shell").Return(shell.BASH)
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
template.Init(env)

View file

@ -7,6 +7,7 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
"github.com/jandedobbeleer/oh-my-posh/src/template"
"github.com/stretchr/testify/assert"
testify_ "github.com/stretchr/testify/mock"
@ -85,7 +86,7 @@ func TestStravaSegment(t *testing.T) {
},
},
Template: "{{.Ago}}{{.Burp}}",
ExpectedString: "<.Data.Burp>: can't evaluate field Burp in type template.Data",
ExpectedString: template.IncorrectTemplate,
ExpectedEnabled: true,
},
}

View file

@ -26,6 +26,7 @@ func TestGlob(t *testing.T) {
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("TemplateCache").Return(&cache.Template{})
env.On("Shell").Return("foo")
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
Init(env)

View file

@ -1,9 +1,7 @@
package template
import (
"bytes"
"sync"
"text/template"
"github.com/jandedobbeleer/oh-my-posh/src/runtime"
)
@ -21,43 +19,17 @@ const (
var (
shell string
tmplFunc *template.Template
contextPool sync.Pool
buffPool sync.Pool
env runtime.Environment
knownVariables []string
)
type buff bytes.Buffer
func (b *buff) release() {
(*bytes.Buffer)(b).Reset()
buffPool.Put(b)
}
func (b *buff) Write(p []byte) (n int, err error) {
return (*bytes.Buffer)(b).Write(p)
}
func (b *buff) String() string {
return (*bytes.Buffer)(b).String()
}
func Init(environment runtime.Environment) {
env = environment
shell = env.Shell()
tmplFunc = template.New("cache").Funcs(funcMap())
contextPool = sync.Pool{
renderPool = sync.Pool{
New: func() any {
return &context{}
},
}
buffPool = sync.Pool{
New: func() any {
return &buff{}
return newTextPoolObject()
},
}

View file

@ -27,6 +27,7 @@ func TestUrl(t *testing.T) {
env.On("Debug", testify_.Anything)
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Shell").Return("foo")
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
Init(env)

View file

@ -0,0 +1,76 @@
package template
import (
"bytes"
"errors"
"strings"
"sync"
"text/template"
"github.com/jandedobbeleer/oh-my-posh/src/cache"
)
type Data any
type context struct {
Data
Getenv func(string) string
cache.Template
}
func (c *context) init(t *Text) {
c.Data = t.Context
if c.Initialized {
return
}
c.Getenv = env.Getenv
c.Template = *env.TemplateCache()
}
var renderPool sync.Pool
type renderer struct {
template *template.Template
context *context
buffer bytes.Buffer
}
func newTextPoolObject() *renderer {
return &renderer{
template: template.New("cache").Funcs(funcMap()),
context: &context{},
}
}
func (t *renderer) release() {
t.buffer.Reset()
t.context.Data = nil
t.template.New("cache")
renderPool.Put(t)
}
func (t *renderer) execute(text *Text) (string, error) {
tmpl, err := t.template.Parse(text.Template)
if err != nil {
env.Error(err)
return "", errors.New(InvalidTemplate)
}
t.context.init(text)
err = tmpl.Execute(&t.buffer, t.context)
if err != nil {
env.Error(err)
return "", errors.New(IncorrectTemplate)
}
output := t.buffer.String()
// issue with missingkey=zero ignored for map[string]any
// https://github.com/golang/go/issues/24963
output = strings.ReplaceAll(output, "<no value>", "")
return output, nil
}

View file

@ -1,49 +1,21 @@
package template
import (
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/cache"
"github.com/jandedobbeleer/oh-my-posh/src/regex"
)
type Text struct {
Context any
Context Data
Template string
}
type Data any
type context struct {
Data
Getenv func(string) string
cache.Template
initialized bool
}
func (c *context) init(t *Text) {
c.Data = t.Context
if c.initialized {
return
}
c.Getenv = env.Getenv
c.Template = *env.TemplateCache()
c.initialized = true
}
func (c *context) release() {
c.Data = nil
contextPool.Put(c)
}
func (t *Text) Render() (string, error) {
env.DebugF("rendering template: %s", t.Template)
defer env.Trace(time.Now(), t.Template)
if !strings.Contains(t.Template, "{{") || !strings.Contains(t.Template, "}}") {
return t.Template, nil
@ -51,36 +23,10 @@ func (t *Text) Render() (string, error) {
t.patchTemplate()
tmpl, err := tmplFunc.Parse(t.Template)
if err != nil {
env.Error(err)
return "", errors.New(InvalidTemplate)
}
renderer := renderPool.Get().(*renderer)
defer renderer.release()
context := contextPool.Get().(*context)
context.init(t)
defer context.release()
buffer := buffPool.Get().(*buff)
defer buffer.release()
err = tmpl.Execute(buffer, context)
if err != nil {
env.Error(err)
msg := regex.FindNamedRegexMatch(`at (?P<MSG><.*)$`, err.Error())
if len(msg) == 0 {
return "", errors.New(IncorrectTemplate)
}
return "", errors.New(msg["MSG"])
}
text := buffer.String()
// issue with missingkey=zero ignored for map[string]any
// https://github.com/golang/go/issues/24963
text = strings.ReplaceAll(text, "<no value>", "")
return text, nil
return renderer.execute(t)
}
func (t *Text) patchTemplate() {

View file

@ -242,6 +242,7 @@ func TestRenderTemplateEnvVar(t *testing.T) {
env.On("DebugF", testify_.Anything, testify_.Anything).Return(nil)
env.On("Flags").Return(&runtime.Flags{})
env.On("Shell").Return("foo")
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
env.On("TemplateCache").Return(&cache.Template{
OS: "darwin",
})
@ -374,6 +375,7 @@ func TestSegmentContains(t *testing.T) {
Segments: segments,
})
env.On("Shell").Return("foo")
env.On("Trace", testify_.Anything, testify_.Anything).Return(nil)
Init(env)