refactor: move hyperlink logic intro writer loop

This commit is contained in:
Jan De Dobbeleer 2023-01-05 07:45:03 +01:00 committed by Jan De Dobbeleer
parent 59ddfc2ba1
commit f1b1a72868
3 changed files with 138 additions and 116 deletions

View file

@ -59,9 +59,16 @@ const (
AnsiRegex = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
OSC99 string = "osc99"
OSC7 string = "osc7"
OSC51 string = "osc51"
OSC99 = "osc99"
OSC7 = "osc7"
OSC51 = "osc51"
LINK = "link"
TEXT = "text"
OTHER = "plain"
ANCHOR = "ANCHOR"
BG = "BG"
FG = "FG"
)
// Writer writes colorized ANSI strings
@ -98,9 +105,16 @@ type Writer struct {
osc99 string
osc7 string
osc51 string
// hyperlink
hasHyperlink bool
hyperlinkBuilder strings.Builder
squareIndex, roundCount int
state string
}
func (w *Writer) Init(shellName string) {
w.state = OTHER
w.shell = shellName
switch w.shell {
case shell.BASH:
@ -223,6 +237,9 @@ func (w *Writer) ClearAfter() string {
func (w *Writer) FormatTitle(title string) string {
title = w.trimAnsi(title)
if w.Plain {
return title
}
// we have to do this to prevent bash/zsh from misidentifying escape sequences
switch w.shell {
case shell.BASH:
@ -256,10 +273,6 @@ func (w *Writer) Write(background, foreground, text string) {
return
}
if !w.Plain {
text = w.GenerateHyperlink(text)
}
w.background, w.foreground = w.asAnsiColors(background, foreground)
// default to white foreground
if w.foreground.IsEmpty() {
@ -270,27 +283,30 @@ func (w *Writer) Write(background, foreground, text string) {
if len(match) != 0 {
colorOverride := true
for _, style := range knownStyles {
if match["ANCHOR"] != style.AnchorStart {
if match[ANCHOR] != style.AnchorStart {
continue
}
w.printEscapedAnsiString(style.Start)
w.writeEscapedAnsiString(style.Start)
colorOverride = false
}
if colorOverride {
w.currentBackground, w.currentForeground = w.asAnsiColors(match["BG"], match["FG"])
w.currentBackground, w.currentForeground = w.asAnsiColors(match[BG], match[FG])
}
}
w.writeSegmentColors()
text = text[len(match["ANCHOR"]):]
text = text[len(match[ANCHOR]):]
w.runes = []rune(text)
// only run hyperlink logic when we have to
w.hasHyperlink = strings.Count(text, "[")+strings.Count(text, "]")+strings.Count(text, "(")+strings.Count(text, ")") >= 4
for i := 0; i < len(w.runes); i++ {
s := w.runes[i]
// ignore everything which isn't overriding
if s != '<' {
w.length += runewidth.RuneWidth(s)
w.builder.WriteRune(s)
w.write(i, s)
continue
}
@ -303,10 +319,10 @@ func (w *Writer) Write(background, foreground, text string) {
}
w.length += runewidth.RuneWidth(s)
w.builder.WriteRune(s)
w.write(i, s)
}
w.printEscapedAnsiString(colorStyle.End)
w.writeEscapedAnsiString(colorStyle.End)
// reset current
w.currentBackground = ""
@ -322,7 +338,7 @@ func (w *Writer) String() (string, int) {
return w.builder.String(), w.length
}
func (w *Writer) printEscapedAnsiString(text string) {
func (w *Writer) writeEscapedAnsiString(text string) {
if w.Plain {
return
}
@ -350,16 +366,16 @@ func (w *Writer) writeSegmentColors() {
if fg.IsTransparent() && len(w.TerminalBackground) != 0 {
background := w.getAnsiFromColorString(w.TerminalBackground, false)
w.printEscapedAnsiString(fmt.Sprintf(colorise, background))
w.printEscapedAnsiString(fmt.Sprintf(colorise, bg.ToForeground()))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, background))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, bg.ToForeground()))
} else if fg.IsTransparent() && !bg.IsEmpty() {
w.printEscapedAnsiString(fmt.Sprintf(transparent, bg))
w.writeEscapedAnsiString(fmt.Sprintf(transparent, bg))
} else {
if !bg.IsEmpty() && !bg.IsTransparent() {
w.printEscapedAnsiString(fmt.Sprintf(colorise, bg))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, bg))
}
if !fg.IsEmpty() {
w.printEscapedAnsiString(fmt.Sprintf(colorise, fg))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, fg))
}
}
@ -371,7 +387,7 @@ func (w *Writer) writeSegmentColors() {
func (w *Writer) writeColorOverrides(match map[string]string, background string, i int) (position int) {
position = i
// check color reset first
if match["ANCHOR"] == colorStyle.AnchorEnd {
if match[ANCHOR] == colorStyle.AnchorEnd {
// make sure to reset the colors if needed
position += len([]rune(colorStyle.AnchorEnd)) - 1
// do not restore colors at the end of the string, we print it anyways
@ -379,31 +395,31 @@ func (w *Writer) writeColorOverrides(match map[string]string, background string,
return
}
if w.currentBackground != w.background {
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.background))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.background))
}
if w.currentForeground != w.foreground {
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.foreground))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.foreground))
}
return
}
position += len([]rune(match["ANCHOR"])) - 1
position += len([]rune(match[ANCHOR])) - 1
for _, style := range knownStyles {
if style.AnchorEnd == match["ANCHOR"] {
w.printEscapedAnsiString(style.End)
if style.AnchorEnd == match[ANCHOR] {
w.writeEscapedAnsiString(style.End)
return
}
if style.AnchorStart == match["ANCHOR"] {
w.printEscapedAnsiString(style.Start)
if style.AnchorStart == match[ANCHOR] {
w.writeEscapedAnsiString(style.Start)
return
}
}
if match["FG"] == Transparent && len(match["BG"]) == 0 {
match["BG"] = background
if match[FG] == Transparent && len(match[BG]) == 0 {
match[BG] = background
}
w.currentBackground, w.currentForeground = w.asAnsiColors(match["BG"], match["FG"])
w.currentBackground, w.currentForeground = w.asAnsiColors(match[BG], match[FG])
// make sure we have colors
if w.currentForeground.IsEmpty() {
@ -415,27 +431,27 @@ func (w *Writer) writeColorOverrides(match map[string]string, background string,
if w.currentForeground.IsTransparent() && len(w.TerminalBackground) != 0 {
background := w.getAnsiFromColorString(w.TerminalBackground, false)
w.printEscapedAnsiString(fmt.Sprintf(colorise, background))
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.currentBackground.ToForeground()))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, background))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentBackground.ToForeground()))
return
}
if w.currentForeground.IsTransparent() && !w.currentBackground.IsTransparent() {
w.printEscapedAnsiString(fmt.Sprintf(transparent, w.currentBackground))
w.writeEscapedAnsiString(fmt.Sprintf(transparent, w.currentBackground))
return
}
if w.currentBackground != w.background {
// end the colors in case we have a transparent background
if w.currentBackground.IsTransparent() {
w.printEscapedAnsiString(colorStyle.End)
w.writeEscapedAnsiString(colorStyle.End)
} else {
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.currentBackground))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentBackground))
}
}
if w.currentForeground != w.foreground || w.currentBackground.IsTransparent() {
w.printEscapedAnsiString(fmt.Sprintf(colorise, w.currentForeground))
w.writeEscapedAnsiString(fmt.Sprintf(colorise, w.currentForeground))
}
return position

View file

@ -8,66 +8,51 @@ import (
"github.com/jandedobbeleer/oh-my-posh/shell"
)
func (w *Writer) GenerateHyperlink(text string) string {
const (
LINK = "link"
TEXT = "text"
OTHER = "plain"
)
// do not do this when we do not need to
anchorCount := strings.Count(text, "[") + strings.Count(text, "]") + strings.Count(text, "(") + strings.Count(text, ")")
if anchorCount < 4 {
return text
func (w *Writer) write(i int, s rune) {
// ignore the logic when there is no hyperlink
if !w.hasHyperlink {
w.builder.WriteRune(s)
return
}
var result, hyperlink strings.Builder
var squareIndex, roundCount int
state := OTHER
for i, s := range text {
if s == '[' && state == OTHER {
state = TEXT
hyperlink.WriteRune(s)
continue
}
if state == OTHER {
result.WriteRune(s)
continue
}
hyperlink.WriteRune(s)
switch s {
case ']':
// potential end of text part of hyperlink
squareIndex = i
case '(':
// split into link part
if squareIndex == i-1 {
state = LINK
}
if state == LINK {
roundCount++
}
case ')':
if state != LINK {
continue
}
roundCount--
if roundCount != 0 {
continue
}
// end of link part
result.WriteString(w.replaceHyperlink(hyperlink.String()))
hyperlink.Reset()
state = OTHER
}
if s == '[' && w.state == OTHER {
w.state = TEXT
w.hyperlinkBuilder.WriteRune(s)
return
}
result.WriteString(hyperlink.String())
return result.String()
if w.state == OTHER {
w.builder.WriteRune(s)
return
}
w.hyperlinkBuilder.WriteRune(s)
switch s {
case ']':
// potential end of text part of hyperlink
w.squareIndex = i
case '(':
// split into link part
if w.squareIndex == i-1 {
w.state = LINK
}
if w.state == LINK {
w.roundCount++
}
case ')':
if w.state != LINK {
return
}
w.roundCount--
if w.roundCount != 0 {
return
}
// end of link part
w.builder.WriteString(w.replaceHyperlink(w.hyperlinkBuilder.String()))
w.hyperlinkBuilder.Reset()
w.state = OTHER
}
}
func (w *Writer) replaceHyperlink(text string) string {
@ -76,10 +61,15 @@ func (w *Writer) replaceHyperlink(text string) string {
if len(results) != 3 {
return text
}
if w.Plain {
return results["TEXT"]
}
linkText := w.escapeLinkTextForFishShell(results["TEXT"])
// build hyperlink ansi
hyperlink := fmt.Sprintf(w.hyperlink, results["URL"], linkText)
// replace original text by the new onex
// replace original text by the new ones
return strings.Replace(text, results["ALL"], hyperlink, 1)
}

View file

@ -14,14 +14,17 @@ func TestGenerateHyperlinkNoUrl(t *testing.T) {
ShellName string
Expected string
}{
{Text: "sample text with no url", ShellName: shell.ZSH, Expected: "sample text with no url"},
{Text: "sample text with no url", ShellName: shell.PWSH, Expected: "sample text with no url"},
{Text: "sample text with no url", ShellName: shell.BASH, Expected: "sample text with no url"},
{Text: "sample text with no url", ShellName: shell.ZSH, Expected: "%{\x1b[47m%}%{\x1b[30m%}sample text with no url%{\x1b[0m%}"},
{Text: "sample text with no url", ShellName: shell.PWSH, Expected: "\x1b[47m\x1b[30msample text with no url\x1b[0m"},
{Text: "sample text with no url", ShellName: shell.BASH, Expected: "\\[\x1b[47m\\]\\[\x1b[30m\\]sample text with no url\\[\x1b[0m\\]"},
}
for _, tc := range cases {
a := Writer{}
a := Writer{
AnsiColors: &DefaultColors{},
}
a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text)
a.Write("white", "black", tc.Text)
hyperlinkText, _ := a.String()
assert.Equal(t, tc.Expected, hyperlinkText)
}
}
@ -35,26 +38,33 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
{
Text: "[google](http://www.google.be) [maps (2/2)](http://maps.google.be)",
ShellName: shell.FISH,
Expected: "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps (2/2)\x1b]8;;\x1b\\",
Expected: "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps (2/2)\x1b]8;;\x1b\\\x1b[0m",
},
{
Text: "in <accent>\x1b[1mpwsh \x1b[22m</> ",
ShellName: shell.PWSH,
Expected: "in <accent>\x1b[1mpwsh \x1b[22m</> ",
Expected: "\x1b[47m\x1b[30min \x1b[1mpwsh \x1b[22m \x1b[0m",
},
{Text: "[google](http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b[47m%}%{\x1b[30m%}%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}%{\x1b[0m%}"},
{Text: "[google](http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\\x1b[0m"},
{
Text: "[google](http://www.google.be)",
ShellName: shell.BASH,
Expected: "\\[\x1b[47m\\]\\[\x1b[30m\\]\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]\\[\x1b[0m\\]",
},
{Text: "[google](http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}"},
{Text: "[google](http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\"},
{Text: "[google](http://www.google.be)", ShellName: shell.BASH, Expected: "\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]"},
{
Text: "[google](http://www.google.be) [maps](http://maps.google.be)",
ShellName: shell.FISH,
Expected: "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps\x1b]8;;\x1b\\",
Expected: "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps\x1b]8;;\x1b\\\x1b[0m",
},
}
for _, tc := range cases {
a := Writer{}
a := Writer{
AnsiColors: &DefaultColors{},
}
a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text)
a.Write("white", "black", tc.Text)
hyperlinkText, _ := a.String()
assert.Equal(t, tc.Expected, hyperlinkText)
}
}
@ -65,14 +75,17 @@ func TestGenerateHyperlinkWithUrlNoName(t *testing.T) {
ShellName string
Expected string
}{
{Text: "[](http://www.google.be)", ShellName: shell.ZSH, Expected: "[](http://www.google.be)"},
{Text: "[](http://www.google.be)", ShellName: shell.PWSH, Expected: "[](http://www.google.be)"},
{Text: "[](http://www.google.be)", ShellName: shell.BASH, Expected: "[](http://www.google.be)"},
{Text: "[](http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b[47m%}%{\x1b[30m%}[](http://www.google.be)%{\x1b[0m%}"},
{Text: "[](http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b[47m\x1b[30m[](http://www.google.be)\x1b[0m"},
{Text: "[](http://www.google.be)", ShellName: shell.BASH, Expected: "\\[\x1b[47m\\]\\[\x1b[30m\\][](http://www.google.be)\\[\x1b[0m\\]"},
}
for _, tc := range cases {
a := Writer{}
a := Writer{
AnsiColors: &DefaultColors{},
}
a.Init(tc.ShellName)
hyperlinkText := a.GenerateHyperlink(tc.Text)
a.Write("white", "black", tc.Text)
hyperlinkText, _ := a.String()
assert.Equal(t, tc.Expected, hyperlinkText)
}
}
@ -84,14 +97,17 @@ func TestGenerateFileLink(t *testing.T) {
}{
{
Text: `[Posh](file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh)`,
Expected: "\x1b]8;;file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh\x1b\\Posh\x1b]8;;\x1b\\",
Expected: "\x1b[47m\x1b[30m\x1b]8;;file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh\x1b\\Posh\x1b]8;;\x1b\\\x1b[0m",
},
{Text: `[Windows](file:C:/Windows)`, Expected: "\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\"},
{Text: `[Windows](file:C:/Windows)`, Expected: "\x1b[47m\x1b[30m\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\\x1b[0m"},
}
for _, tc := range cases {
a := Writer{}
a := Writer{
AnsiColors: &DefaultColors{},
}
a.Init(shell.PWSH)
hyperlinkText := a.GenerateHyperlink(tc.Text)
a.Write("white", "black", tc.Text)
hyperlinkText, _ := a.String()
assert.Equal(t, tc.Expected, hyperlinkText)
}
}