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/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) { 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 "" +}