mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-11-09 20:44:03 -08:00
fix: correct escape sequence replacement
This commit is contained in:
parent
4046b2d154
commit
8db4d47e45
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
@ -11,7 +11,7 @@
|
|||
"prompt",
|
||||
"print",
|
||||
"primary",
|
||||
"--config=${workspaceRoot}/themes/cinnamon.omp.json",
|
||||
"--config==${workspaceRoot}/themes/cinnamon.omp.json",
|
||||
"--shell=pwsh",
|
||||
"--terminal-width=200",
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue