mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-11-14 15:04:03 -08:00
refactor: parallel segment rendering
This commit is contained in:
parent
87404454a4
commit
d4054b04d6
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
124
src/prompt/segments.go
Normal 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
|
||||
}
|
73
src/prompt/segments_test.go
Normal file
73
src/prompt/segments_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
76
src/template/render_pool.go
Normal file
76
src/template/render_pool.go
Normal 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
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue