fix: correct escape sequence replacement

This commit is contained in:
Jan De Dobbeleer 2022-04-03 13:04:56 +02:00 committed by Jan De Dobbeleer
parent 4046b2d154
commit 8db4d47e45
15 changed files with 136 additions and 30 deletions

2
.vscode/launch.json vendored
View file

@ -11,7 +11,7 @@
"prompt", "prompt",
"print", "print",
"primary", "primary",
"--config=${workspaceRoot}/themes/cinnamon.omp.json", "--config==${workspaceRoot}/themes/cinnamon.omp.json",
"--shell=pwsh", "--shell=pwsh",
"--terminal-width=200", "--terminal-width=200",
] ]

View file

@ -52,7 +52,7 @@ You can tweak the output by using additional flags:
defer env.Close() defer env.Close()
cfg := engine.LoadConfig(env) cfg := engine.LoadConfig(env)
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init(env.Shell()) ansi.InitPlain(shell.PLAIN)
writerColors := cfg.MakeColors(env) writerColors := cfg.MakeColors(env)
writer := &color.AnsiWriter{ writer := &color.AnsiWriter{
Ansi: ansi, Ansi: ansi,

View file

@ -31,7 +31,7 @@ var debugCmd = &cobra.Command{
defer env.Close() defer env.Close()
cfg := engine.LoadConfig(env) cfg := engine.LoadConfig(env)
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init("shell") ansi.InitPlain(shell.PLAIN)
writerColors := cfg.MakeColors(env) writerColors := cfg.MakeColors(env)
writer := &color.AnsiWriter{ writer := &color.AnsiWriter{
Ansi: ansi, Ansi: ansi,

View file

@ -70,7 +70,10 @@ var printCmd = &cobra.Command{
ansi.Init(env.Shell()) ansi.Init(env.Shell())
var writer color.Writer var writer color.Writer
if plain { if plain {
writer = &color.PlainWriter{} ansi.InitPlain(env.Shell())
writer = &color.PlainWriter{
Ansi: ansi,
}
} else { } else {
writerColors := cfg.MakeColors(env) writerColors := cfg.MakeColors(env)
writer = &color.AnsiWriter{ writer = &color.AnsiWriter{

View file

@ -28,6 +28,7 @@ type Ansi struct {
escapeLeft string escapeLeft string
escapeRight string escapeRight string
hyperlink string hyperlink string
hyperlinkRegex string
osc99 string osc99 string
bold string bold string
italic string italic string
@ -37,16 +38,17 @@ type Ansi struct {
reverse string reverse string
dimmed string dimmed string
format string format string
shellReservedKeywords []shellKeyWordReplacement reservedSequences []sequenceReplacement
} }
type shellKeyWordReplacement struct { type sequenceReplacement struct {
text string text string
replacement string replacement string
} }
func (a *Ansi) Init(shellName string) { func (a *Ansi) Init(shellName string) {
a.shell = shellName a.shell = shellName
a.initEscapeSequences(shellName)
switch shellName { switch shellName {
case shell.ZSH: case shell.ZSH:
a.format = "%%{%s%%}" a.format = "%%{%s%%}"
@ -65,6 +67,7 @@ func (a *Ansi) Init(shellName string) {
a.escapeLeft = "%{" a.escapeLeft = "%{"
a.escapeRight = "%}" a.escapeRight = "%}"
a.hyperlink = "%%{\x1b]8;;%s\x1b\\\\%%}%s%%{\x1b]8;;\x1b\\\\%%}" a.hyperlink = "%%{\x1b]8;;%s\x1b\\\\%%}%s%%{\x1b]8;;\x1b\\\\%%}"
a.hyperlinkRegex = `(?P<STR>\x1b]8;;(.+)\x1b\\\\(?P<TEXT>.+)\x1b]8;;\x1b\\\\)`
a.osc99 = "%%{\x1b]9;9;\"%s\"\x1b\\%%}" a.osc99 = "%%{\x1b]9;9;\"%s\"\x1b\\%%}"
a.bold = "%%{\x1b[1m%%}%s%%{\x1b[22m%%}" a.bold = "%%{\x1b[1m%%}%s%%{\x1b[22m%%}"
a.italic = "%%{\x1b[3m%%}%s%%{\x1b[23m%%}" a.italic = "%%{\x1b[3m%%}%s%%{\x1b[23m%%}"
@ -73,8 +76,6 @@ func (a *Ansi) Init(shellName string) {
a.reverse = "%%{\x1b[7m%%}%s%%{\x1b[27m%%}" a.reverse = "%%{\x1b[7m%%}%s%%{\x1b[27m%%}"
a.dimmed = "%%{\x1b[2m%%}%s%%{\x1b[22m%%}" a.dimmed = "%%{\x1b[2m%%}%s%%{\x1b[22m%%}"
a.strikethrough = "%%{\x1b[9m%%}%s%%{\x1b[29m%%}" a.strikethrough = "%%{\x1b[9m%%}%s%%{\x1b[29m%%}"
// escape double quotes and variable expansion
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"\\", "\\\\"}, shellKeyWordReplacement{"%", "%%"})
case shell.BASH: case shell.BASH:
a.format = "\\[%s\\]" a.format = "\\[%s\\]"
a.linechange = "\\[\x1b[%d%s\\]" a.linechange = "\\[\x1b[%d%s\\]"
@ -92,6 +93,7 @@ func (a *Ansi) Init(shellName string) {
a.escapeLeft = "\\[" a.escapeLeft = "\\["
a.escapeRight = "\\]" a.escapeRight = "\\]"
a.hyperlink = "\\[\x1b]8;;%s\x1b\\\\\\]%s\\[\x1b]8;;\x1b\\\\\\]" a.hyperlink = "\\[\x1b]8;;%s\x1b\\\\\\]%s\\[\x1b]8;;\x1b\\\\\\]"
a.hyperlinkRegex = `(?P<STR>\x1b]8;;(.+)\x1b\\\\(?P<TEXT>.+)\x1b]8;;\x1b\\\\)`
a.osc99 = "\\[\x1b]9;9;\"%s\"\x1b\\\\\\]" a.osc99 = "\\[\x1b]9;9;\"%s\"\x1b\\\\\\]"
a.bold = "\\[\x1b[1m\\]%s\\[\x1b[22m\\]" a.bold = "\\[\x1b[1m\\]%s\\[\x1b[22m\\]"
a.italic = "\\[\x1b[3m\\]%s\\[\x1b[23m\\]" a.italic = "\\[\x1b[3m\\]%s\\[\x1b[23m\\]"
@ -100,9 +102,6 @@ func (a *Ansi) Init(shellName string) {
a.reverse = "\\[\x1b[7m\\]%s\\[\x1b[27m\\]" a.reverse = "\\[\x1b[7m\\]%s\\[\x1b[27m\\]"
a.dimmed = "\\[\x1b[2m\\]%s\\[\x1b[22m\\]" a.dimmed = "\\[\x1b[2m\\]%s\\[\x1b[22m\\]"
a.strikethrough = "\\[\x1b[9m\\]%s\\[\x1b[29m\\]" a.strikethrough = "\\[\x1b[9m\\]%s\\[\x1b[29m\\]"
// escape backslashes to avoid replacements
// https://tldp.org/HOWTO/Bash-Prompt-HOWTO/bash-prompt-escape-sequences.html
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"\\", "\\\\"})
default: default:
a.format = "%s" a.format = "%s"
a.linechange = "\x1b[%d%s" a.linechange = "\x1b[%d%s"
@ -120,6 +119,7 @@ func (a *Ansi) Init(shellName string) {
a.escapeLeft = "" a.escapeLeft = ""
a.escapeRight = "" a.escapeRight = ""
a.hyperlink = "\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\" a.hyperlink = "\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\"
a.hyperlinkRegex = `(?P<STR>\x1b]8;;(.+)\x1b\\(?P<TEXT>.+)\x1b]8;;\x1b\\)`
a.osc99 = "\x1b]9;9;\"%s\"\x1b\\" a.osc99 = "\x1b]9;9;\"%s\"\x1b\\"
a.bold = "\x1b[1m%s\x1b[22m" a.bold = "\x1b[1m%s\x1b[22m"
a.italic = "\x1b[3m%s\x1b[23m" a.italic = "\x1b[3m%s\x1b[23m"
@ -132,8 +132,96 @@ func (a *Ansi) Init(shellName string) {
if shellName == shell.FISH { if shellName == shell.FISH {
a.hyperlink = "\x1b]8;;%s\x1b\\\\%s\x1b]8;;\x1b\\\\" a.hyperlink = "\x1b]8;;%s\x1b\\\\%s\x1b]8;;\x1b\\\\"
} }
// common replacement for all shells }
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"`", "'"})
func (a *Ansi) InitPlain(shellName string) {
a.Init(shell.PLAIN)
a.initEscapeSequences(shellName)
}
func (a *Ansi) initEscapeSequences(shellName string) {
switch shellName {
case shell.ZSH:
// escape double quotes and variable expansion
a.reservedSequences = []sequenceReplacement{
{text: "`", replacement: "'"},
// {text: `\`, replacement: `\\`},
{text: `%l`, replacement: `%%l`},
{text: `%M`, replacement: `%%M`},
{text: `%m`, replacement: `%%m`},
{text: `%n`, replacement: `%%n`},
{text: `%y`, replacement: `%%y`},
{text: `%#`, replacement: `%%#`},
{text: `%?`, replacement: `%%?`},
{text: `%_`, replacement: `%%_`},
{text: `%^`, replacement: `%%^`},
{text: `%d`, replacement: `%%d`},
{text: `%/`, replacement: `%%/`},
{text: `%~`, replacement: `%%~`},
{text: `%e`, replacement: `%%e`},
{text: `%h`, replacement: `%%h`},
{text: `%!`, replacement: `%%!`},
{text: `%i`, replacement: `%%i`},
{text: `%I`, replacement: `%%I`},
{text: `%j`, replacement: `%%j`},
{text: `%L`, replacement: `%%L`},
{text: `%N`, replacement: `%%N`},
{text: `%x`, replacement: `%%x`},
{text: `%c`, replacement: `%%c`},
{text: `%.`, replacement: `%%.`},
{text: `%C`, replacement: `%%C`},
{text: `%D`, replacement: `%%D`},
{text: `%T`, replacement: `%%T`},
{text: `%t`, replacement: `%%t`},
{text: `%@`, replacement: `%%@`},
{text: `%*`, replacement: `%%*`},
{text: `%w`, replacement: `%%w`},
{text: `%W`, replacement: `%%W`},
{text: `%D`, replacement: `%%D`},
{text: `%B`, replacement: `%%B`},
{text: `%b`, replacement: `%%b`},
{text: `%E`, replacement: `%%E`},
{text: `%U`, replacement: `%%U`},
{text: `%S`, replacement: `%%S`},
{text: `%F`, replacement: `%%F`},
{text: `%K`, replacement: `%%K`},
{text: `%G`, replacement: `%%G`},
{text: `%v`, replacement: `%%v`},
{text: `%(`, replacement: `%%(`},
}
case shell.BASH:
a.reservedSequences = []sequenceReplacement{
{text: "`", replacement: "'"},
{text: `\a`, replacement: `\\a`},
{text: `\d`, replacement: `\\d`},
{text: `\D`, replacement: `\\D`},
{text: `\e`, replacement: `\\e`},
{text: `\h`, replacement: `\\h`},
{text: `\H`, replacement: `\\H`},
{text: `\j`, replacement: `\\j`},
{text: `\l`, replacement: `\\l`},
{text: `\n`, replacement: `\\n`},
{text: `\r`, replacement: `\\r`},
{text: `\s`, replacement: `\\s`},
{text: `\t`, replacement: `\\t`},
{text: `\T`, replacement: `\\T`},
{text: `\@`, replacement: `\\@`},
{text: `\A`, replacement: `\\A`},
{text: `\u`, replacement: `\\u`},
{text: `\v`, replacement: `\\v`},
{text: `\V`, replacement: `\\V`},
{text: `\w`, replacement: `\\w`},
{text: `\W`, replacement: `\\W`},
{text: `\!`, replacement: `\\!`},
{text: `\#`, replacement: `\\#`},
{text: `\$`, replacement: `\\$`},
{text: `\nnn`, replacement: `\\nnn`},
}
default:
a.reservedSequences = []sequenceReplacement{
{text: "`", replacement: "'"},
}
}
} }
func (a *Ansi) generateHyperlink(text string) string { func (a *Ansi) generateHyperlink(text string) string {
@ -209,7 +297,8 @@ func (a *Ansi) ClearAfter() string {
func (a *Ansi) EscapeText(text string) string { func (a *Ansi) EscapeText(text string) string {
// what to escape/replace is different per shell // what to escape/replace is different per shell
for _, s := range a.shellReservedKeywords {
for _, s := range a.reservedSequences {
text = strings.ReplaceAll(text, s.text, s.replacement) text = strings.ReplaceAll(text, s.text, s.replacement)
} }
return text return text

View file

@ -79,7 +79,7 @@ func TestFormatText(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
a := Ansi{} a := Ansi{}
a.Init("") a.InitPlain(shell.PLAIN)
formattedText := a.formatText(tc.Text) formattedText := a.formatText(tc.Text)
assert.Equal(t, tc.Expected, formattedText, tc.Case) assert.Equal(t, tc.Expected, formattedText, tc.Case)
} }

View file

@ -7,6 +7,8 @@ import (
// PlainWriter writes a plain string // PlainWriter writes a plain string
type PlainWriter struct { type PlainWriter struct {
Ansi *Ansi
builder strings.Builder builder strings.Builder
length int length int
} }
@ -20,7 +22,7 @@ func (a *PlainWriter) Write(background, foreground, text string) {
return return
} }
writeAndRemoveText := func(text, textToRemove, parentText string) string { writeAndRemoveText := func(text, textToRemove, parentText string) string {
a.length += measureText(text) a.length += a.Ansi.MeasureText(text)
a.builder.WriteString(text) a.builder.WriteString(text)
return strings.Replace(parentText, textToRemove, "", 1) return strings.Replace(parentText, textToRemove, "", 1)
} }
@ -32,7 +34,7 @@ func (a *PlainWriter) Write(background, foreground, text string) {
text = writeAndRemoveText(textBeforeColorOverride, textBeforeColorOverride, text) text = writeAndRemoveText(textBeforeColorOverride, textBeforeColorOverride, text)
text = writeAndRemoveText(innerText, escapedTextSegment, text) text = writeAndRemoveText(innerText, escapedTextSegment, text)
} }
a.length += measureText(text) a.length += a.Ansi.MeasureText(text)
a.builder.WriteString(text) a.builder.WriteString(text)
} }

View file

@ -6,24 +6,35 @@ import (
"unicode/utf8" "unicode/utf8"
) )
func measureText(text string) int { func (ansi *Ansi) MeasureText(text string) int {
// skip strings with ANSI // skip strings with ANSI
if !strings.Contains(text, "\x1b") { if !strings.Contains(text, "\x1b") {
text = ansi.TrimEscapeSequences(text)
return utf8.RuneCountInString(text) return utf8.RuneCountInString(text)
} }
if strings.Contains(text, "\x1b]8;;") { if strings.Contains(text, "\x1b]8;;") {
matches := regex.FindAllNamedRegexMatch(regex.LINK, text) matches := regex.FindAllNamedRegexMatch(ansi.hyperlinkRegex, text)
for _, match := range matches { for _, match := range matches {
text = strings.ReplaceAll(text, match["STR"], match["TEXT"]) text = strings.ReplaceAll(text, match["STR"], match["TEXT"])
} }
} }
text = TrimAnsi(text) text = ansi.TrimAnsi(text)
text = ansi.TrimEscapeSequences(text)
return utf8.RuneCountInString(text) return utf8.RuneCountInString(text)
} }
func TrimAnsi(text string) string { func (ansi *Ansi) TrimAnsi(text string) string {
if len(text) == 0 || !strings.Contains(text, "\x1b") { if len(text) == 0 || !strings.Contains(text, "\x1b") {
return text return text
} }
return regex.ReplaceAllString(AnsiRegex, text, "") return regex.ReplaceAllString(AnsiRegex, text, "")
} }
func (ansi *Ansi) TrimEscapeSequences(text string) string {
if len(text) == 0 {
return text
}
text = strings.ReplaceAll(text, ansi.escapeLeft, "")
text = strings.ReplaceAll(text, ansi.escapeRight, "")
return text
}

View file

@ -105,7 +105,7 @@ func (a *AnsiWriter) writeColoredText(background, foreground AnsiColor, text str
if text == "" || (foreground.IsTransparent() && background.IsTransparent()) { if text == "" || (foreground.IsTransparent() && background.IsTransparent()) {
return return
} }
a.length += measureText(text) a.length += a.Ansi.MeasureText(text)
// default to white fg if empty, empty backgrond is supported // default to white fg if empty, empty backgrond is supported
if foreground.IsEmpty() { if foreground.IsEmpty() {
foreground = a.getAnsiFromColorString("white", false) foreground = a.getAnsiFromColorString("white", false)
@ -140,9 +140,9 @@ func (a *AnsiWriter) Write(background, foreground, text string) {
} }
bgAnsi, fgAnsi := a.asAnsiColors(background, foreground) bgAnsi, fgAnsi := a.asAnsiColors(background, foreground)
text = a.Ansi.EscapeText(text)
text = a.Ansi.formatText(text) text = a.Ansi.formatText(text)
text = a.Ansi.generateHyperlink(text) text = a.Ansi.generateHyperlink(text)
text = a.Ansi.EscapeText(text)
// first we match for any potentially valid colors enclosed in <> // first we match for any potentially valid colors enclosed in <>
// i.e., find color overrides // i.e., find color overrides

View file

@ -1,6 +1,7 @@
package color package color
import ( import (
"oh-my-posh/shell"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -172,7 +173,7 @@ func TestWriteANSIColors(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
ansi := &Ansi{} ansi := &Ansi{}
ansi.Init("pwsh") ansi.Init(shell.PWSH)
renderer := &AnsiWriter{ renderer := &AnsiWriter{
Ansi: ansi, Ansi: ansi,
ParentColors: []*Color{tc.Parent}, ParentColors: []*Color{tc.Parent},

View file

@ -14,7 +14,7 @@ type Title struct {
func (t *Title) GetTitle() string { func (t *Title) GetTitle() string {
title := t.getTitleTemplateText() title := t.getTitleTemplateText()
title = color.TrimAnsi(title) title = t.Ansi.TrimAnsi(title)
title = t.Ansi.EscapeText(title) title = t.Ansi.EscapeText(title)
return t.Ansi.Title(title) return t.Ansi.Title(title)
} }

View file

@ -61,7 +61,7 @@ func TestGetTitle(t *testing.T) {
Folder: "vagrant", Folder: "vagrant",
}) })
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init(tc.ShellName) ansi.InitPlain(tc.ShellName)
ct := &Title{ ct := &Title{
Env: env, Env: env,
Ansi: ansi, Ansi: ansi,
@ -116,7 +116,7 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
HostName: "", HostName: "",
}) })
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init(tc.ShellName) ansi.InitPlain(tc.ShellName)
ct := &Title{ ct := &Title{
Env: env, Env: env,
Ansi: ansi, Ansi: ansi,

View file

@ -52,7 +52,7 @@ func (b *Block) init(env environment.Environment, writer color.Writer, ansi *col
func (b *Block) initPlain(env environment.Environment, config *Config) { func (b *Block) initPlain(env environment.Environment, config *Config) {
b.ansi = &color.Ansi{} b.ansi = &color.Ansi{}
b.ansi.Init(shell.PLAIN) b.ansi.InitPlain(env.Shell())
b.writer = &color.AnsiWriter{ b.writer = &color.AnsiWriter{
Ansi: b.ansi, Ansi: b.ansi,
TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground), TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground),

View file

@ -58,7 +58,7 @@ func engineRender() {
defer testClearDefaultConfig() defer testClearDefaultConfig()
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init(env.Shell()) ansi.InitPlain(env.Shell())
writerColors := cfg.MakeColors(env) writerColors := cfg.MakeColors(env)
writer := &color.AnsiWriter{ writer := &color.AnsiWriter{
Ansi: ansi, Ansi: ansi,

View file

@ -18,7 +18,7 @@ func runImageTest(content string) error {
} }
defer os.Remove(file.Name()) defer os.Remove(file.Name())
ansi := &color.Ansi{} ansi := &color.Ansi{}
ansi.Init(shell.PLAIN) ansi.InitPlain(shell.PLAIN)
image := &ImageRenderer{ image := &ImageRenderer{
AnsiString: content, AnsiString: content,
Ansi: ansi, Ansi: ansi,