Repository: JanDeDobbeleer/oh-my-posh
Pull request #3746 (2023-04-22 19:19:17)

refactor: split engine logic
refactor: split engine logic



Patch:
diff --git a/src/ansi/ansi_writer_hyperlink.go b/src/ansi/ansi_writer_hyperlink.go
index aa111d40..3846678b 100644
--- a/src/ansi/ansi_writer_hyperlink.go
+++ b/src/ansi/ansi_writer_hyperlink.go
@@ -71,8 +71,8 @@ func (w *Writer) replaceHyperlink(text string) string {
 
 	linkText := results["TEXT"]
 
-	// this isn't supported for elvish
-	if w.shell == shell.ELVISH {
+	// this isn't supported for elvish and xonsh
+	if w.shell == shell.ELVISH || w.shell == shell.XONSH {
 		return strings.Replace(text, results["ALL"], linkText, 1)
 	}
 
diff --git a/src/cli/config_export_image.go b/src/cli/config_export_image.go
index 5d495df1..43661c72 100644
--- a/src/cli/config_export_image.go
+++ b/src/cli/config_export_image.go
@@ -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,
diff --git a/src/cli/print.go b/src/cli/print.go
index 8444a772..b15ac64b 100644
--- a/src/cli/print.go
+++ b/src/cli/print.go
@@ -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()
 		}
diff --git a/src/engine/debug.go b/src/engine/debug.go
new file mode 100644
index 00000000..5f213ad2
--- /dev/null
+++ b/src/engine/debug.go
@@ -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()
+}
diff --git a/src/engine/engine.go b/src/engine/engine.go
index 5ee07391..340e887f 100644
--- a/src/engine/engine.go
+++ b/src/engine/engine.go
@@ -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
-}
diff --git a/src/engine/prompt.go b/src/engine/prompt.go
new file mode 100644
index 00000000..6101ebac
--- /dev/null
+++ b/src/engine/prompt.go
@@ -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
+}
diff --git a/src/engine/tooltip.go b/src/engine/tooltip.go
new file mode 100644
index 00000000..d1de330d
--- /dev/null
+++ b/src/engine/tooltip.go
@@ -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 ""
+}
diff --git a/src/shell/scripts/omp.py b/src/shell/scripts/omp.py
index 0dbaa46f..a817cc56 100644
--- a/src/shell/scripts/omp.py
+++ b/src/shell/scripts/omp.py
@@ -1,7 +1,7 @@
 import uuid
 
 $POWERLINE_COMMAND = "oh-my-posh"
-$POSH_THEME = "::CONFIG::"
+$POSH_THEME = r"::CONFIG::"
 $POSH_PID = uuid.uuid4().hex
 $POSH_SHELL_VERSION = $XONSH_VERSION
 


Test patch:
diff --git a/src/engine/engine_test.go b/src/engine/engine_test.go
index ec38192e..0e1f9d2c 100644
--- a/src/engine/engine_test.go
+++ b/src/engine/engine_test.go
@@ -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) {