fix(ansi): escape prompt sequences correctly

resolves #5090
This commit is contained in:
Jan De Dobbeleer 2024-06-16 09:27:28 +02:00 committed by Jan De Dobbeleer
parent 4ed8104d2d
commit 48a2dff7c2
5 changed files with 62 additions and 22 deletions

View file

@ -82,8 +82,10 @@ type Writer struct {
Colors *Colors Colors *Colors
ParentColors []*Colors ParentColors []*Colors
AnsiColors ColorString AnsiColors ColorString
Plain bool Plain bool
TrueColor bool TrueColor bool
Interactive bool
builder strings.Builder builder strings.Builder
length int length int
@ -111,6 +113,9 @@ type Writer struct {
osc7 string osc7 string
osc51 string osc51 string
lastRune rune
escapeSequences map[rune]rune
hyperlinkStart string hyperlinkStart string
hyperlinkCenter string hyperlinkCenter string
hyperlinkEnd string hyperlinkEnd string
@ -144,6 +149,10 @@ func (w *Writer) Init(shellName string) {
w.iTermPromptMark = "\\[$(iterm2_prompt_mark)\\]" w.iTermPromptMark = "\\[$(iterm2_prompt_mark)\\]"
w.iTermCurrentDir = "\\[\x1b]1337;CurrentDir=%s\x07\\]" w.iTermCurrentDir = "\\[\x1b]1337;CurrentDir=%s\x07\\]"
w.iTermRemoteHost = "\\[\x1b]1337;RemoteHost=%s@%s\x07\\]" w.iTermRemoteHost = "\\[\x1b]1337;RemoteHost=%s@%s\x07\\]"
w.escapeSequences = map[rune]rune{
96: 92, // backtick
92: 92, // backslash
}
case shell.ZSH, shell.TCSH: case shell.ZSH, shell.TCSH:
w.format = "%%{%s%%}" w.format = "%%{%s%%}"
w.linechange = "%%{\x1b[%d%s%%}" w.linechange = "%%{\x1b[%d%s%%}"
@ -184,6 +193,13 @@ func (w *Writer) Init(shellName string) {
w.iTermCurrentDir = "\x1b]1337;CurrentDir=%s\x07" w.iTermCurrentDir = "\x1b]1337;CurrentDir=%s\x07"
w.iTermRemoteHost = "\x1b]1337;RemoteHost=%s@%s\x07" w.iTermRemoteHost = "\x1b]1337;RemoteHost=%s@%s\x07"
} }
if shellName == shell.ZSH {
w.escapeSequences = map[rune]rune{
96: 92, // backtick
37: 37, // %
}
}
} }
func (w *Writer) SetColors(background, foreground string) { func (w *Writer) SetColors(background, foreground string) {
@ -207,11 +223,14 @@ func (w *Writer) ChangeLine(numberOfLines int) string {
if w.Plain { if w.Plain {
return "" return ""
} }
position := "B" position := "B"
if numberOfLines < 0 { if numberOfLines < 0 {
position = "F" position = "F"
numberOfLines = -numberOfLines numberOfLines = -numberOfLines
} }
return fmt.Sprintf(w.linechange, numberOfLines, position) return fmt.Sprintf(w.linechange, numberOfLines, position)
} }
@ -219,9 +238,11 @@ func (w *Writer) ConsolePwd(pwdType, userName, hostName, pwd string) string {
if w.Plain { if w.Plain {
return "" return ""
} }
if strings.HasSuffix(pwd, ":") { if strings.HasSuffix(pwd, ":") {
pwd += "\\" pwd += "\\"
} }
switch pwdType { switch pwdType {
case OSC7: case OSC7:
return fmt.Sprintf(w.osc7, hostName, pwd) return fmt.Sprintf(w.osc7, hostName, pwd)
@ -238,6 +259,7 @@ func (w *Writer) ClearAfter() string {
if w.Plain { if w.Plain {
return "" return ""
} }
return w.clearLine + w.clearBelow return w.clearLine + w.clearBelow
} }
@ -246,6 +268,7 @@ func (w *Writer) FormatTitle(title string) string {
if w.Plain { if w.Plain {
return title return title
} }
// we have to do this to prevent bash/zsh from misidentifying escape sequences // we have to do this to prevent bash/zsh from misidentifying escape sequences
switch w.shell { switch w.shell {
case shell.BASH: case shell.BASH:
@ -256,6 +279,7 @@ func (w *Writer) FormatTitle(title string) string {
// these shells don't support setting the title // these shells don't support setting the title
return "" return ""
} }
return fmt.Sprintf(w.title, title) return fmt.Sprintf(w.title, title)
} }
@ -416,10 +440,21 @@ func (w *Writer) write(s rune) {
return return
} }
if !w.hyperlink { if w.hyperlink {
w.length += runewidth.RuneWidth(s) w.builder.WriteRune(s)
return
} }
if !w.Interactive {
for special, escape := range w.escapeSequences {
if s == special && w.lastRune != escape {
w.builder.WriteRune(escape)
}
}
}
w.length += runewidth.RuneWidth(s)
w.lastRune = s
w.builder.WriteRune(s) w.builder.WriteRune(s)
} }

View file

@ -70,6 +70,7 @@ func (b *Block) InitPlain(env platform.Environment, config *Config) {
AnsiColors: config.MakeColors(), AnsiColors: config.MakeColors(),
TrueColor: env.Flags().TrueColor, TrueColor: env.Flags().TrueColor,
} }
b.writer.Init(shell.GENERIC) b.writer.Init(shell.GENERIC)
b.env = env b.env = env
b.executeSegmentLogic() b.executeSegmentLogic()
@ -79,12 +80,14 @@ func (b *Block) executeSegmentLogic() {
if shouldHideForWidth(b.env, b.MinWidth, b.MaxWidth) { if shouldHideForWidth(b.env, b.MinWidth, b.MaxWidth) {
return return
} }
b.setEnabledSegments() b.setEnabledSegments()
b.setSegmentsText() b.setSegmentsText()
} }
func (b *Block) setActiveSegment(segment *Segment) { func (b *Block) setActiveSegment(segment *Segment) {
b.activeSegment = segment b.activeSegment = segment
b.writer.Interactive = segment.Interactive
b.writer.SetColors(segment.background(), segment.foreground()) b.writer.SetColors(segment.background(), segment.foreground())
} }

View file

@ -92,16 +92,12 @@ func (e *Engine) pwd() {
cwd := e.Env.Pwd() cwd := e.Env.Pwd()
// in BASH, we need to escape the path
if e.Env.Shell() == shell.BASH {
cwd = strings.ReplaceAll(cwd, `\`, `\\`)
}
// Backwards compatibility for deprecated OSC99 // Backwards compatibility for deprecated OSC99
if e.Config.OSC99 { if e.Config.OSC99 {
e.write(e.Writer.ConsolePwd(ansi.OSC99, "", "", cwd)) e.write(e.Writer.ConsolePwd(ansi.OSC99, "", "", cwd))
return return
} }
// Allow template logic to define when to enable the PWD (when supported) // Allow template logic to define when to enable the PWD (when supported)
tmpl := &template.Text{ tmpl := &template.Text{
Template: e.Config.PWD, Template: e.Config.PWD,

View file

@ -66,7 +66,7 @@ func TestPrintPWD(t *testing.T) {
Pwd: `C:\Users\user\Documents\GitHub\oh-my-posh`, Pwd: `C:\Users\user\Documents\GitHub\oh-my-posh`,
Config: ansi.OSC99, Config: ansi.OSC99,
Shell: shell.BASH, Shell: shell.BASH,
Expected: "\x1b]9;9;C:\\\\Users\\\\user\\\\Documents\\\\GitHub\\\\oh-my-posh\x1b\\", Expected: "\x1b]9;9;C:\\Users\\user\\Documents\\GitHub\\oh-my-posh\x1b\\",
}, },
} }

View file

@ -11,7 +11,6 @@ import (
"github.com/jandedobbeleer/oh-my-posh/src/platform" "github.com/jandedobbeleer/oh-my-posh/src/platform"
"github.com/jandedobbeleer/oh-my-posh/src/properties" "github.com/jandedobbeleer/oh-my-posh/src/properties"
"github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/jandedobbeleer/oh-my-posh/src/segments"
"github.com/jandedobbeleer/oh-my-posh/src/shell"
"github.com/jandedobbeleer/oh-my-posh/src/template" "github.com/jandedobbeleer/oh-my-posh/src/template"
c "golang.org/x/text/cases" c "golang.org/x/text/cases"
@ -371,7 +370,9 @@ func (segment *Segment) style() SegmentStyle {
if len(segment.styleCache) != 0 { if len(segment.styleCache) != 0 {
return segment.styleCache return segment.styleCache
} }
segment.styleCache = segment.Style.Resolve(segment.env, segment.writer) segment.styleCache = segment.Style.Resolve(segment.env, segment.writer)
return segment.styleCache return segment.styleCache
} }
@ -379,8 +380,10 @@ func (segment *Segment) shouldIncludeFolder() bool {
if segment.env == nil { if segment.env == nil {
return true return true
} }
cwdIncluded := segment.cwdIncluded() cwdIncluded := segment.cwdIncluded()
cwdExcluded := segment.cwdExcluded() cwdExcluded := segment.cwdExcluded()
return cwdIncluded && !cwdExcluded return cwdIncluded && !cwdExcluded
} }
@ -419,6 +422,7 @@ func (segment *Segment) cwdExcluded() bool {
if !ok { if !ok {
value = segment.Properties[properties.IgnoreFolders] value = segment.Properties[properties.IgnoreFolders]
} }
list := properties.ParseStringArray(value) list := properties.ParseStringArray(value)
return segment.env.DirMatchesOneOf(segment.env.Pwd(), list) return segment.env.DirMatchesOneOf(segment.env.Pwd(), list)
} }
@ -429,6 +433,7 @@ func (segment *Segment) shouldInvokeWithTip(tip string) bool {
return true return true
} }
} }
return false return false
} }
@ -436,9 +441,11 @@ func (segment *Segment) foreground() string {
if segment.colors == nil { if segment.colors == nil {
segment.colors = &ansi.Colors{} segment.colors = &ansi.Colors{}
} }
if len(segment.colors.Foreground) == 0 { if len(segment.colors.Foreground) == 0 {
segment.colors.Foreground = segment.ForegroundTemplates.FirstMatch(segment.writer, segment.env, segment.Foreground) segment.colors.Foreground = segment.ForegroundTemplates.FirstMatch(segment.writer, segment.env, segment.Foreground)
} }
return segment.colors.Foreground return segment.colors.Foreground
} }
@ -446,9 +453,11 @@ func (segment *Segment) background() string {
if segment.colors == nil { if segment.colors == nil {
segment.colors = &ansi.Colors{} segment.colors = &ansi.Colors{}
} }
if len(segment.colors.Background) == 0 { if len(segment.colors.Background) == 0 {
segment.colors.Background = segment.BackgroundTemplates.FirstMatch(segment.writer, segment.env, segment.Background) segment.colors.Background = segment.BackgroundTemplates.FirstMatch(segment.writer, segment.env, segment.Background)
} }
return segment.colors.Background return segment.colors.Background
} }
@ -481,19 +490,23 @@ func (segment *Segment) string() string {
return templatesResult return templatesResult
} }
} }
if len(segment.Template) == 0 { if len(segment.Template) == 0 {
segment.Template = segment.writer.Template() segment.Template = segment.writer.Template()
} }
tmpl := &template.Text{ tmpl := &template.Text{
Template: segment.Template, Template: segment.Template,
Context: segment.writer, Context: segment.writer,
Env: segment.env, Env: segment.env,
TemplatesResult: templatesResult, TemplatesResult: templatesResult,
} }
text, err := tmpl.Render() text, err := tmpl.Render()
if err != nil { if err != nil {
return err.Error() return err.Error()
} }
return text return text
} }
@ -501,10 +514,12 @@ func (segment *Segment) Name() string {
if len(segment.name) != 0 { if len(segment.name) != 0 {
return segment.name return segment.name
} }
name := segment.Alias name := segment.Alias
if len(name) == 0 { if len(name) == 0 {
name = c.Title(language.English).String(string(segment.Type)) name = c.Title(language.English).String(string(segment.Type))
} }
segment.name = name segment.name = name
return name return name
} }
@ -562,20 +577,11 @@ func (segment *Segment) SetText() {
if !segment.Enabled { if !segment.Enabled {
return return
} }
segment.text = segment.string() segment.text = segment.string()
segment.Enabled = len(strings.ReplaceAll(segment.text, " ", "")) > 0 segment.Enabled = len(strings.ReplaceAll(segment.text, " ", "")) > 0
if !segment.Enabled { if !segment.Enabled {
segment.env.TemplateCache().RemoveSegmentData(segment.Name()) segment.env.TemplateCache().RemoveSegmentData(segment.Name())
} }
if segment.Interactive {
return
}
// we have to do this to prevent bash/zsh from misidentifying escape sequences
switch segment.env.Shell() {
case shell.BASH:
segment.text = strings.NewReplacer("`", "\\`", `\`, `\\`).Replace(segment.text)
case shell.ZSH:
segment.text = strings.NewReplacer("`", "\\`", `%`, `%%`).Replace(segment.text)
}
} }