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",
"print",
"primary",
"--config=${workspaceRoot}/themes/cinnamon.omp.json",
"--config==${workspaceRoot}/themes/cinnamon.omp.json",
"--shell=pwsh",
"--terminal-width=200",
]

View file

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

View file

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

View file

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

View file

@ -28,6 +28,7 @@ type Ansi struct {
escapeLeft string
escapeRight string
hyperlink string
hyperlinkRegex string
osc99 string
bold string
italic string
@ -37,16 +38,17 @@ type Ansi struct {
reverse string
dimmed string
format string
shellReservedKeywords []shellKeyWordReplacement
reservedSequences []sequenceReplacement
}
type shellKeyWordReplacement struct {
type sequenceReplacement struct {
text string
replacement string
}
func (a *Ansi) Init(shellName string) {
a.shell = shellName
a.initEscapeSequences(shellName)
switch shellName {
case shell.ZSH:
a.format = "%%{%s%%}"
@ -65,6 +67,7 @@ func (a *Ansi) Init(shellName string) {
a.escapeLeft = "%{"
a.escapeRight = "%}"
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.bold = "%%{\x1b[1m%%}%s%%{\x1b[22m%%}"
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.dimmed = "%%{\x1b[2m%%}%s%%{\x1b[22m%%}"
a.strikethrough = "%%{\x1b[9m%%}%s%%{\x1b[29m%%}"
// escape double quotes and variable expansion
a.shellReservedKeywords = append(a.shellReservedKeywords, shellKeyWordReplacement{"\\", "\\\\"}, shellKeyWordReplacement{"%", "%%"})
case shell.BASH:
a.format = "\\[%s\\]"
a.linechange = "\\[\x1b[%d%s\\]"
@ -92,6 +93,7 @@ func (a *Ansi) Init(shellName string) {
a.escapeLeft = "\\["
a.escapeRight = "\\]"
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.bold = "\\[\x1b[1m\\]%s\\[\x1b[22m\\]"
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.dimmed = "\\[\x1b[2m\\]%s\\[\x1b[22m\\]"
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:
a.format = "%s"
a.linechange = "\x1b[%d%s"
@ -120,6 +119,7 @@ func (a *Ansi) Init(shellName string) {
a.escapeLeft = ""
a.escapeRight = ""
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.bold = "\x1b[1m%s\x1b[22m"
a.italic = "\x1b[3m%s\x1b[23m"
@ -132,8 +132,96 @@ func (a *Ansi) Init(shellName string) {
if shellName == shell.FISH {
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 {
@ -209,7 +297,8 @@ func (a *Ansi) ClearAfter() string {
func (a *Ansi) EscapeText(text string) string {
// 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)
}
return text

View file

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

View file

@ -7,6 +7,8 @@ import (
// PlainWriter writes a plain string
type PlainWriter struct {
Ansi *Ansi
builder strings.Builder
length int
}
@ -20,7 +22,7 @@ func (a *PlainWriter) Write(background, foreground, text string) {
return
}
writeAndRemoveText := func(text, textToRemove, parentText string) string {
a.length += measureText(text)
a.length += a.Ansi.MeasureText(text)
a.builder.WriteString(text)
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(innerText, escapedTextSegment, text)
}
a.length += measureText(text)
a.length += a.Ansi.MeasureText(text)
a.builder.WriteString(text)
}

View file

@ -6,24 +6,35 @@ import (
"unicode/utf8"
)
func measureText(text string) int {
func (ansi *Ansi) MeasureText(text string) int {
// skip strings with ANSI
if !strings.Contains(text, "\x1b") {
text = ansi.TrimEscapeSequences(text)
return utf8.RuneCountInString(text)
}
if strings.Contains(text, "\x1b]8;;") {
matches := regex.FindAllNamedRegexMatch(regex.LINK, text)
matches := regex.FindAllNamedRegexMatch(ansi.hyperlinkRegex, text)
for _, match := range matches {
text = strings.ReplaceAll(text, match["STR"], match["TEXT"])
}
}
text = TrimAnsi(text)
text = ansi.TrimAnsi(text)
text = ansi.TrimEscapeSequences(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") {
return 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()) {
return
}
a.length += measureText(text)
a.length += a.Ansi.MeasureText(text)
// default to white fg if empty, empty backgrond is supported
if foreground.IsEmpty() {
foreground = a.getAnsiFromColorString("white", false)
@ -140,9 +140,9 @@ func (a *AnsiWriter) Write(background, foreground, text string) {
}
bgAnsi, fgAnsi := a.asAnsiColors(background, foreground)
text = a.Ansi.EscapeText(text)
text = a.Ansi.formatText(text)
text = a.Ansi.generateHyperlink(text)
text = a.Ansi.EscapeText(text)
// first we match for any potentially valid colors enclosed in <>
// i.e., find color overrides

View file

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

View file

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

View file

@ -61,7 +61,7 @@ func TestGetTitle(t *testing.T) {
Folder: "vagrant",
})
ansi := &color.Ansi{}
ansi.Init(tc.ShellName)
ansi.InitPlain(tc.ShellName)
ct := &Title{
Env: env,
Ansi: ansi,
@ -116,7 +116,7 @@ func TestGetConsoleTitleIfGethostnameReturnsError(t *testing.T) {
HostName: "",
})
ansi := &color.Ansi{}
ansi.Init(tc.ShellName)
ansi.InitPlain(tc.ShellName)
ct := &Title{
Env: env,
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) {
b.ansi = &color.Ansi{}
b.ansi.Init(shell.PLAIN)
b.ansi.InitPlain(env.Shell())
b.writer = &color.AnsiWriter{
Ansi: b.ansi,
TerminalBackground: shell.ConsoleBackgroundColor(env, config.TerminalBackground),

View file

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

View file

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