From d8b979ec3b3023c2d51793141988a8253033d0f5 Mon Sep 17 00:00:00 2001 From: Jan De Dobbeleer Date: Sun, 12 Jun 2022 19:55:57 +0200 Subject: [PATCH] fix(bash): escape rprompt entirely closes #2398 --- src/color/text.go | 6 ++-- src/engine/engine.go | 72 ++++++++++++++++++++++++++++----------- src/engine/engine_test.go | 7 ++-- 3 files changed, 60 insertions(+), 25 deletions(-) diff --git a/src/color/text.go b/src/color/text.go index b1dc2a71..44c809e0 100644 --- a/src/color/text.go +++ b/src/color/text.go @@ -10,7 +10,8 @@ func (ansi *Ansi) MeasureText(text string) int { // skip strings with ANSI if !strings.Contains(text, "\x1b") { text = ansi.TrimEscapeSequences(text) - return utf8.RuneCountInString(text) + length := utf8.RuneCountInString(text) + return length } if strings.Contains(text, "\x1b]8;;") { matches := regex.FindAllNamedRegexMatch(ansi.hyperlinkRegex, text) @@ -20,7 +21,8 @@ func (ansi *Ansi) MeasureText(text string) int { } text = ansi.TrimAnsi(text) text = ansi.TrimEscapeSequences(text) - return utf8.RuneCountInString(text) + length := utf8.RuneCountInString(text) + return length } func (ansi *Ansi) TrimAnsi(text string) string { diff --git a/src/engine/engine.go b/src/engine/engine.go index 33b48976..ae24dbbc 100644 --- a/src/engine/engine.go +++ b/src/engine/engine.go @@ -41,6 +41,9 @@ func (e *Engine) string() string { } func (e *Engine) canWriteRPrompt() bool { + if e.rprompt == "" || e.Plain { + return false + } consoleWidth, err := e.Env.TerminalWidth() if err != nil || consoleWidth == 0 { return true @@ -86,7 +89,7 @@ func (e *Engine) shouldFill(block *Block, length int) (string, bool) { return "", false } terminalWidth, err := e.Env.TerminalWidth() - if err != nil && terminalWidth == 0 { + if err != nil || terminalWidth == 0 { return "", false } padLength := terminalWidth - e.currentLineLength - length @@ -106,7 +109,7 @@ func (e *Engine) shouldFill(block *Block, length int) (string, bool) { func (e *Engine) renderBlock(block *Block) { // when in bash, for rprompt blocks we need to write plain // and wrap in escaped mode or the prompt will not render correctly - if block.Type == RPrompt && e.Env.Shell() == shell.BASH { + if e.Env.Shell() == shell.BASH && (block.Type == RPrompt || block.Alignment == Right) { block.InitPlain(e.Env, e.Config) } else { block.Init(e.Env, e.Writer, e.Ansi) @@ -127,28 +130,42 @@ func (e *Engine) renderBlock(block *Block) { if block.VerticalOffset != 0 { e.writeANSI(e.Ansi.ChangeLine(block.VerticalOffset)) } - switch block.Alignment { - case Right: - text, length := block.RenderSegments() - if padText, OK := e.shouldFill(block, length); OK { - e.write(padText) - } - e.writeANSI(e.Ansi.CarriageForward()) - e.writeANSI(e.Ansi.GetCursorForRightWrite(length, block.HorizontalOffset)) - e.currentLineLength = 0 - e.write(text) - case Left: + + if block.Alignment == Left { text, length := block.RenderSegments() e.currentLineLength += length e.write(text) + break } - case RPrompt: + + if block.Alignment != Right { + break + } + text, length := block.RenderSegments() - e.rpromptLength = length - if e.Env.Shell() == shell.BASH { - text = e.Ansi.FormatText(text) + padText, hasPadText := e.shouldFill(block, length) + if hasPadText { + // in this case we can print plain + e.write(padText) + e.write(text) + break } - e.rprompt = text + // this can contain ANSI escape sequences + ansi := e.Ansi + if e.Env.Shell() == shell.BASH { + ansi = &color.Ansi{} + ansi.InitPlain() + } + prompt := ansi.CarriageForward() + prompt += ansi.GetCursorForRightWrite(length, block.HorizontalOffset) + prompt += text + e.currentLineLength = 0 + if e.Env.Shell() == shell.BASH { + prompt = e.Ansi.FormatText(prompt) + } + e.write(prompt) + case RPrompt: + e.rprompt, e.rpromptLength = block.RenderSegments() } // Due to a bug in Powershell, the end of the line needs to be cleared. // If this doesn't happen, the portion after the prompt gets colored in the background @@ -213,8 +230,8 @@ func (e *Engine) print() string { prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`)) prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt) return prompt - case shell.PWSH, shell.PWSH5, shell.BASH, shell.PLAIN, shell.NU: - if e.rprompt == "" || !e.canWriteRPrompt() || e.Plain { + case shell.PWSH, shell.PWSH5, shell.PLAIN, shell.NU: + if !e.canWriteRPrompt() { break } e.write(e.Ansi.SaveCursorPosition()) @@ -222,6 +239,21 @@ func (e *Engine) print() string { e.write(e.Ansi.GetCursorForRightWrite(e.rpromptLength, 0)) e.write(e.rprompt) e.write(e.Ansi.RestoreCursorPosition()) + case shell.BASH: + if !e.canWriteRPrompt() { + 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 + ansi := &color.Ansi{} + ansi.InitPlain() + prompt := ansi.SaveCursorPosition() + prompt += ansi.CarriageForward() + prompt += ansi.GetCursorForRightWrite(e.rpromptLength, 0) + prompt += e.rprompt + prompt += ansi.RestoreCursorPosition() + prompt = e.Ansi.FormatText(prompt) + e.write(prompt) } return e.string() } diff --git a/src/engine/engine_test.go b/src/engine/engine_test.go index 3f39d20d..81034969 100644 --- a/src/engine/engine_test.go +++ b/src/engine/engine_test.go @@ -34,10 +34,11 @@ func TestCanWriteRPrompt(t *testing.T) { env := new(mock.MockedEnvironment) env.On("TerminalWidth").Return(tc.TerminalWidth, tc.TerminalWidthError) engine := &Engine{ - Env: env, + Env: env, + rpromptLength: tc.RPromptLength, + currentLineLength: tc.PromptLength, + rprompt: "hello", } - engine.rpromptLength = tc.RPromptLength - engine.currentLineLength = tc.PromptLength got := engine.canWriteRPrompt() assert.Equal(t, tc.Expected, got, tc.Case) }