refactor: split engine logic

This commit is contained in:
Jan De Dobbeleer 2023-04-22 21:15:04 +02:00 committed by Jan De Dobbeleer
parent 6c181c1120
commit a1b95a0274
7 changed files with 341 additions and 302 deletions

View file

@ -75,7 +75,7 @@ Exports the config to an image file using customized output options.`,
Writer: writer,
}
prompt := eng.PrintPrimary()
prompt := eng.Primary()
imageCreator := &engine.ImageRenderer{
AnsiString: prompt,

View file

@ -67,21 +67,21 @@ var printCmd = &cobra.Command{
switch args[0] {
case "debug":
fmt.Print(eng.PrintExtraPrompt(engine.Debug))
fmt.Print(eng.ExtraPrompt(engine.Debug))
case "primary":
fmt.Print(eng.PrintPrimary())
fmt.Print(eng.Primary())
case "secondary":
fmt.Print(eng.PrintExtraPrompt(engine.Secondary))
fmt.Print(eng.ExtraPrompt(engine.Secondary))
case "transient":
fmt.Print(eng.PrintExtraPrompt(engine.Transient))
fmt.Print(eng.ExtraPrompt(engine.Transient))
case "right":
fmt.Print(eng.PrintRPrompt())
fmt.Print(eng.RPrompt())
case "tooltip":
fmt.Print(eng.PrintTooltip(command))
fmt.Print(eng.Tooltip(command))
case "valid":
fmt.Print(eng.PrintExtraPrompt(engine.Valid))
fmt.Print(eng.ExtraPrompt(engine.Valid))
case "error":
fmt.Print(eng.PrintExtraPrompt(engine.Error))
fmt.Print(eng.ExtraPrompt(engine.Error))
default:
_ = cmd.Help()
}

65
src/engine/debug.go Normal file
View file

@ -0,0 +1,65 @@
package engine
import (
"fmt"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/log"
)
// debug will loop through your config file and output the timings for each segments
func (e *Engine) PrintDebug(startTime time.Time, version string) string {
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Version:").Green().Bold().Plain(), version))
sh := e.Env.Shell()
shellVersion := e.Env.Getenv("POSH_SHELL_VERSION")
if len(shellVersion) != 0 {
sh += fmt.Sprintf(" (%s)", shellVersion)
}
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Shell:").Green().Bold().Plain(), sh))
e.write(log.Text("\nSegments:\n\n").Green().Bold().Plain().String())
// console title timing
titleStartTime := time.Now()
e.Env.Debug("Segment: Title")
title := e.getTitleTemplateText()
consoleTitleTiming := &SegmentTiming{
name: "ConsoleTitle",
nameLength: 12,
active: len(e.Config.ConsoleTitleTemplate) > 0,
text: title,
duration: time.Since(titleStartTime),
}
largestSegmentNameLength := consoleTitleTiming.nameLength
var segmentTimings []*SegmentTiming
segmentTimings = append(segmentTimings, consoleTitleTiming)
// cache a pointer to the color cycle
cycle = &e.Config.Cycle
// loop each segments of each blocks
for _, block := range e.Config.Blocks {
block.Init(e.Env, e.Writer)
longestSegmentName, timings := block.Debug()
segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength {
largestSegmentNameLength = longestSegmentName
}
}
// 22 is the color for false/true and 7 is the reset color
largestSegmentNameLength += 22 + 7
for _, segment := range segmentTimings {
duration := segment.duration.Milliseconds()
var active log.Text
if segment.active {
active = log.Text("true").Yellow()
} else {
active = log.Text("false").Purple()
}
segmentName := fmt.Sprintf("%s(%s)", segment.name, active.Plain())
e.write(fmt.Sprintf("%-*s - %3d ms - %s\n", largestSegmentNameLength, segmentName, duration, segment.text))
}
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Run duration:").Green().Bold().Plain(), time.Since(startTime)))
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Cache path:").Green().Bold().Plain(), e.Env.CachePath()))
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Config path:").Green().Bold().Plain(), e.Env.Flags().Config))
e.write(log.Text("\nLogs:\n\n").Green().Bold().Plain().String())
e.write(e.Env.Logs())
return e.string()
}

View file

@ -1,12 +1,9 @@
package engine
import (
"fmt"
"strings"
"time"
"github.com/jandedobbeleer/oh-my-posh/src/ansi"
"github.com/jandedobbeleer/oh-my-posh/src/log"
"github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template"
@ -61,29 +58,7 @@ func (e *Engine) canWriteRightBlock(rprompt bool) bool {
return canWrite
}
func (e *Engine) PrintPrimary() string {
// cache a pointer to the color cycle
cycle = &e.Config.Cycle
for i, block := range e.Config.Blocks {
var cancelNewline bool
if i == 0 {
row, _ := e.Env.CursorPosition()
cancelNewline = e.Env.Flags().Cleared || e.Env.Flags().PromptCount == 1 || row == 1
}
e.renderBlock(block, cancelNewline)
}
if len(e.Config.ConsoleTitleTemplate) > 0 {
title := e.getTitleTemplateText()
e.write(e.Writer.FormatTitle(title))
}
if e.Config.FinalSpace {
e.write(" ")
}
e.printPWD()
return e.print()
}
func (e *Engine) printPWD() {
func (e *Engine) pwd() {
// only print when supported
sh := e.Env.Shell()
if sh == shell.ELVISH || sh == shell.XONSH {
@ -250,267 +225,3 @@ func (e *Engine) renderBlock(block *Block, cancelNewline bool) {
e.rprompt, e.rpromptLength = block.RenderSegments()
}
}
// debug will loop through your config file and output the timings for each segments
func (e *Engine) PrintDebug(startTime time.Time, version string) string {
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Version:").Green().Bold().Plain(), version))
sh := e.Env.Shell()
shellVersion := e.Env.Getenv("POSH_SHELL_VERSION")
if len(shellVersion) != 0 {
sh += fmt.Sprintf(" (%s)", shellVersion)
}
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Shell:").Green().Bold().Plain(), sh))
e.write(log.Text("\nSegments:\n\n").Green().Bold().Plain().String())
// console title timing
titleStartTime := time.Now()
e.Env.Debug("Segment: Title")
title := e.getTitleTemplateText()
consoleTitleTiming := &SegmentTiming{
name: "ConsoleTitle",
nameLength: 12,
active: len(e.Config.ConsoleTitleTemplate) > 0,
text: title,
duration: time.Since(titleStartTime),
}
largestSegmentNameLength := consoleTitleTiming.nameLength
var segmentTimings []*SegmentTiming
segmentTimings = append(segmentTimings, consoleTitleTiming)
// cache a pointer to the color cycle
cycle = &e.Config.Cycle
// loop each segments of each blocks
for _, block := range e.Config.Blocks {
block.Init(e.Env, e.Writer)
longestSegmentName, timings := block.Debug()
segmentTimings = append(segmentTimings, timings...)
if longestSegmentName > largestSegmentNameLength {
largestSegmentNameLength = longestSegmentName
}
}
// 22 is the color for false/true and 7 is the reset color
largestSegmentNameLength += 22 + 7
for _, segment := range segmentTimings {
duration := segment.duration.Milliseconds()
var active log.Text
if segment.active {
active = log.Text("true").Yellow()
} else {
active = log.Text("false").Purple()
}
segmentName := fmt.Sprintf("%s(%s)", segment.name, active.Plain())
e.write(fmt.Sprintf("%-*s - %3d ms - %s\n", largestSegmentNameLength, segmentName, duration, segment.text))
}
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Run duration:").Green().Bold().Plain(), time.Since(startTime)))
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Cache path:").Green().Bold().Plain(), e.Env.CachePath()))
e.write(fmt.Sprintf("\n%s %s\n", log.Text("Config path:").Green().Bold().Plain(), e.Env.Flags().Config))
e.write(log.Text("\nLogs:\n\n").Green().Bold().Plain().String())
e.write(e.Env.Logs())
return e.string()
}
func (e *Engine) print() string {
switch e.Env.Shell() {
case shell.ZSH:
if !e.Env.Flags().Eval {
break
}
// Warp doesn't support RPROMPT so we need to write it manually
if e.isWarp() {
e.write(e.Writer.SaveCursorPosition())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.rprompt)
e.write(e.Writer.RestoreCursorPosition())
// escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
return prompt
}
// escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
return prompt
case shell.PWSH, shell.PWSH5, shell.GENERIC, shell.NU:
if !e.canWriteRightBlock(true) {
break
}
e.write(e.Writer.SaveCursorPosition())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.rprompt)
e.write(e.Writer.RestoreCursorPosition())
case shell.BASH:
if !e.canWriteRightBlock(true) {
break
}
// in bash, the entire rprompt needs to be escaped for the prompt to be interpreted correctly
// see https://github.com/jandedobbeleer/oh-my-posh/pull/2398
writer := &ansi.Writer{
TrueColor: e.Env.Flags().TrueColor,
}
writer.Init(shell.GENERIC)
prompt := writer.SaveCursorPosition()
prompt += writer.CarriageForward()
prompt += writer.GetCursorForRightWrite(e.rpromptLength, 0)
prompt += e.rprompt
prompt += writer.RestoreCursorPosition()
prompt = e.Writer.FormatText(prompt)
e.write(prompt)
}
return e.string()
}
func (e *Engine) PrintTooltip(tip string) string {
tip = strings.Trim(tip, " ")
var tooltip *Segment
for _, tp := range e.Config.Tooltips {
if !tp.shouldInvokeWithTip(tip) {
continue
}
tooltip = tp
}
if tooltip == nil {
return ""
}
if err := tooltip.mapSegmentWithWriter(e.Env); err != nil {
return ""
}
if !tooltip.writer.Enabled() {
return ""
}
tooltip.Enabled = true
// little hack to reuse the current logic
block := &Block{
Alignment: Right,
Segments: []*Segment{tooltip},
}
switch e.Env.Shell() {
case shell.ZSH, shell.CMD, shell.FISH, shell.GENERIC:
block.Init(e.Env, e.Writer)
if !block.Enabled() {
return ""
}
text, _ := block.RenderSegments()
return text
case shell.PWSH, shell.PWSH5:
block.InitPlain(e.Env, e.Config)
if !block.Enabled() {
return ""
}
text, length := block.RenderSegments()
// clear from cursor to the end of the line in case a previous tooltip is cut off and partially preserved,
// if the new one is shorter
e.write(e.Writer.ClearAfter())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(length, 0))
e.write(text)
return e.string()
}
return ""
}
type ExtraPromptType int
const (
Transient ExtraPromptType = iota
Valid
Error
Secondary
Debug
)
func (e *Engine) PrintExtraPrompt(promptType ExtraPromptType) string {
// populate env with latest context
e.Env.LoadTemplateCache()
var prompt *Segment
switch promptType {
case Debug:
prompt = e.Config.DebugPrompt
case Transient:
prompt = e.Config.TransientPrompt
case Valid:
prompt = e.Config.ValidLine
case Error:
prompt = e.Config.ErrorLine
case Secondary:
prompt = e.Config.SecondaryPrompt
}
if prompt == nil {
prompt = &Segment{}
}
getTemplate := func(template string) string {
if len(template) != 0 {
return template
}
switch promptType { //nolint: exhaustive
case Debug:
return "[DBG]: "
case Transient:
return "{{ .Shell }}> "
case Secondary:
return "> "
default:
return ""
}
}
tmpl := &template.Text{
Template: getTemplate(prompt.Template),
Env: e.Env,
}
promptText, err := tmpl.Render()
if err != nil {
promptText = err.Error()
}
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)
str, length := e.Writer.String()
if promptType == Transient {
if padText, OK := e.shouldFill(prompt.Filler, length); OK {
str += padText
}
}
switch e.Env.Shell() {
case shell.ZSH:
// escape double quotes contained in the prompt
if promptType == Transient {
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(str, "\"", "\"\""))
// empty RPROMPT
prompt += "\nRPROMPT=\"\""
return prompt
}
return str
case shell.PWSH, shell.PWSH5:
// Return the string and empty our buffer
// clear the line afterwards to prevent text from being written on the same line
// see https://github.com/JanDeDobbeleer/oh-my-posh/issues/3628
return str + e.Writer.ClearAfter()
case shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.GENERIC:
// Return the string and empty our buffer
return str
}
return ""
}
func (e *Engine) PrintRPrompt() string {
filterRPromptBlock := func(blocks []*Block) *Block {
for _, block := range blocks {
if block.Type == RPrompt {
return block
}
}
return nil
}
block := filterRPromptBlock(e.Config.Blocks)
if block == nil {
return ""
}
block.Init(e.Env, e.Writer)
if !block.Enabled() {
return ""
}
text, length := block.RenderSegments()
e.rpromptLength = length
return text
}

View file

@ -81,8 +81,8 @@ func TestPrintPWD(t *testing.T) {
},
Writer: writer,
}
engine.printPWD()
got := engine.print()
engine.pwd()
got := engine.string()
assert.Equal(t, tc.Expected, got, tc.Case)
}
}
@ -114,7 +114,7 @@ func engineRender() {
Writer: writer,
}
engine.PrintPrimary()
engine.Primary()
}
func BenchmarkEngineRenderPalette(b *testing.B) {

200
src/engine/prompt.go Normal file
View file

@ -0,0 +1,200 @@
package engine
import (
"fmt"
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/ansi"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template"
)
type ExtraPromptType int
const (
Transient ExtraPromptType = iota
Valid
Error
Secondary
Debug
)
func (e *Engine) Primary() string {
// cache a pointer to the color cycle
cycle = &e.Config.Cycle
for i, block := range e.Config.Blocks {
var cancelNewline bool
if i == 0 {
row, _ := e.Env.CursorPosition()
cancelNewline = e.Env.Flags().Cleared || e.Env.Flags().PromptCount == 1 || row == 1
}
e.renderBlock(block, cancelNewline)
}
if len(e.Config.ConsoleTitleTemplate) > 0 {
title := e.getTitleTemplateText()
e.write(e.Writer.FormatTitle(title))
}
if e.Config.FinalSpace {
e.write(" ")
}
e.pwd()
switch e.Env.Shell() {
case shell.ZSH:
if !e.Env.Flags().Eval {
break
}
// Warp doesn't support RPROMPT so we need to write it manually
if e.isWarp() {
e.write(e.Writer.SaveCursorPosition())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.rprompt)
e.write(e.Writer.RestoreCursorPosition())
// escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
return prompt
}
// escape double quotes contained in the prompt
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
return prompt
case shell.PWSH, shell.PWSH5, shell.GENERIC, shell.NU:
if !e.canWriteRightBlock(true) {
break
}
e.write(e.Writer.SaveCursorPosition())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(e.rpromptLength, 0))
e.write(e.rprompt)
e.write(e.Writer.RestoreCursorPosition())
case shell.BASH:
if !e.canWriteRightBlock(true) {
break
}
// in bash, the entire rprompt needs to be escaped for the prompt to be interpreted correctly
// see https://github.com/jandedobbeleer/oh-my-posh/pull/2398
writer := &ansi.Writer{
TrueColor: e.Env.Flags().TrueColor,
}
writer.Init(shell.GENERIC)
prompt := writer.SaveCursorPosition()
prompt += writer.CarriageForward()
prompt += writer.GetCursorForRightWrite(e.rpromptLength, 0)
prompt += e.rprompt
prompt += writer.RestoreCursorPosition()
prompt = e.Writer.FormatText(prompt)
e.write(prompt)
}
return e.string()
}
func (e *Engine) ExtraPrompt(promptType ExtraPromptType) string {
// populate env with latest context
e.Env.LoadTemplateCache()
var prompt *Segment
switch promptType {
case Debug:
prompt = e.Config.DebugPrompt
case Transient:
prompt = e.Config.TransientPrompt
case Valid:
prompt = e.Config.ValidLine
case Error:
prompt = e.Config.ErrorLine
case Secondary:
prompt = e.Config.SecondaryPrompt
}
if prompt == nil {
prompt = &Segment{}
}
getTemplate := func(template string) string {
if len(template) != 0 {
return template
}
switch promptType { //nolint: exhaustive
case Debug:
return "[DBG]: "
case Transient:
return "{{ .Shell }}> "
case Secondary:
return "> "
default:
return ""
}
}
tmpl := &template.Text{
Template: getTemplate(prompt.Template),
Env: e.Env,
}
promptText, err := tmpl.Render()
if err != nil {
promptText = err.Error()
}
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)
str, length := e.Writer.String()
if promptType == Transient {
if padText, OK := e.shouldFill(prompt.Filler, length); OK {
str += padText
}
}
switch e.Env.Shell() {
case shell.ZSH:
// escape double quotes contained in the prompt
if promptType == Transient {
prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(str, "\"", "\"\""))
// empty RPROMPT
prompt += "\nRPROMPT=\"\""
return prompt
}
return str
case shell.PWSH, shell.PWSH5:
// Return the string and empty our buffer
// clear the line afterwards to prevent text from being written on the same line
// see https://github.com/JanDeDobbeleer/oh-my-posh/issues/3628
return str + e.Writer.ClearAfter()
case shell.CMD, shell.BASH, shell.FISH, shell.NU, shell.GENERIC:
// Return the string and empty our buffer
return str
}
return ""
}
func (e *Engine) RPrompt() string {
filterRPromptBlock := func(blocks []*Block) *Block {
for _, block := range blocks {
if block.Type == RPrompt {
return block
}
}
return nil
}
block := filterRPromptBlock(e.Config.Blocks)
if block == nil {
return ""
}
block.Init(e.Env, e.Writer)
if !block.Enabled() {
return ""
}
text, length := block.RenderSegments()
e.rpromptLength = length
return text
}

63
src/engine/tooltip.go Normal file
View file

@ -0,0 +1,63 @@
package engine
import (
"strings"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
)
func (e *Engine) Tooltip(tip string) string {
tip = strings.Trim(tip, " ")
var tooltip *Segment
for _, tp := range e.Config.Tooltips {
if !tp.shouldInvokeWithTip(tip) {
continue
}
tooltip = tp
}
if tooltip == nil {
return ""
}
if err := tooltip.mapSegmentWithWriter(e.Env); err != nil {
return ""
}
if !tooltip.writer.Enabled() {
return ""
}
tooltip.Enabled = true
// little hack to reuse the current logic
block := &Block{
Alignment: Right,
Segments: []*Segment{tooltip},
}
switch e.Env.Shell() {
case shell.ZSH, shell.CMD, shell.FISH, shell.GENERIC:
block.Init(e.Env, e.Writer)
if !block.Enabled() {
return ""
}
text, _ := block.RenderSegments()
return text
case shell.PWSH, shell.PWSH5:
block.InitPlain(e.Env, e.Config)
if !block.Enabled() {
return ""
}
text, length := block.RenderSegments()
// clear from cursor to the end of the line in case a previous tooltip
// is cut off and partially preserved, if the new one is shorter
e.write(e.Writer.ClearAfter())
e.write(e.Writer.CarriageForward())
e.write(e.Writer.GetCursorForRightWrite(length, 0))
e.write(text)
return e.string()
}
return ""
}