Repository: JanDeDobbeleer/oh-my-posh
Pull request #4574 (2024-01-01 19:33:35)

fix hyperlink
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md].
- [x] The commit message follows the [conventional commits][cc] guidelines.
- [x] Tests for the changes have been added (for bug fixes / features).
- [x] Docs have been added/updated (for bug fixes / features).

### Description

- fix(hyperlink): use built-in format to avoid collisions
- fix(nba): adjust test to solve season offset on year end

<!---

Tips:

If you're not comfortable with working with Git,
we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/configurations/configuration.dsc.yaml b/configurations/configuration.dsc.yaml
new file mode 100644
index 00000000..e9af15ca
--- /dev/null
+++ b/configurations/configuration.dsc.yaml
@@ -0,0 +1,21 @@
+# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
+properties:
+  resources:
+    - resource: Microsoft.WinGet.DSC/WinGetPackage
+      id: ohmyposh
+      directives:
+        description: Install Oh My Posh
+        allowPrerelease: true
+      settings:
+        id: JanDeDobbeleer.OhMyPosh
+        source: winget
+    # - resource: PSDesiredStateConfiguration/Script
+    #   dependsOn:
+    #     - ohmyposh
+    #   id: fonts
+    #   directives:
+    #     description: Install Nerd Font
+    #   settings:
+    #     TestScript: return $false
+    #     GetScript: oh-my-posh font install
+  configurationVersion: 0.2.0
diff --git a/src/ansi/ansi_writer.go b/src/ansi/ansi_writer.go
index eb2f0181..658f06c0 100644
--- a/src/ansi/ansi_writer.go
+++ b/src/ansi/ansi_writer.go
@@ -66,12 +66,14 @@ const (
 	OSC7  = "osc7"
 	OSC51 = "osc51"
 
-	LINK   = "link"
-	TEXT   = "text"
-	OTHER  = "other"
 	ANCHOR = "ANCHOR"
 	BG     = "BG"
 	FG     = "FG"
+
+	hyperLinkStart   = "<LINK>"
+	hyperLinkEnd     = "</LINK>"
+	hyperLinkText    = "<TEXT>"
+	hyperLinkTextEnd = "</TEXT>"
 )
 
 // Writer writes colorized ANSI strings
@@ -92,6 +94,7 @@ type Writer struct {
 	runes       []rune
 	transparent bool
 	invisible   bool
+	hyperlink   bool
 
 	shell                 string
 	format                string
@@ -104,21 +107,16 @@ type Writer struct {
 	restoreCursorPosition string
 	escapeLeft            string
 	escapeRight           string
-	hyperlink             string
-	hyperlinkRegex        string
 	osc99                 string
 	osc7                  string
 	osc51                 string
 
-	// hyperlink
-	hasHyperlink             bool
-	hyperlinkBuilder         strings.Builder
-	bracketIndex, roundCount int
-	hyperlinkState           string
+	hyperlinkStart  string
+	hyperlinkCenter string
+	hyperlinkEnd    string
 }
 
 func (w *Writer) Init(shellName string) {
-	w.hyperlinkState = OTHER
 	w.shell = shellName
 	w.format = "%s"
 	switch w.shell {
@@ -133,8 +131,9 @@ func (w *Writer) Init(shellName string) {
 		w.title = "\\[\x1b]0;%s\007\\]"
 		w.escapeLeft = "\\["
 		w.escapeRight = "\\]"
-		w.hyperlink = "\\[\x1b]8;;%s\x1b\\\\\\]%s\\[\x1b]8;;\x1b\\\\\\]"
-		w.hyperlinkRegex = `(?P<STR>\\\[\x1b\]8;;(.+)\x1b\\\\\\\](?P<TEXT>.+)\\\[\x1b\]8;;\x1b\\\\\\\])`
+		w.hyperlinkStart = "\\[\x1b]8;;"
+		w.hyperlinkCenter = "\x1b\\\\\\]"
+		w.hyperlinkEnd = "\\[\x1b]8;;\x1b\\\\\\]"
 		w.osc99 = "\\[\x1b]9;9;%s\x1b\\\\\\]"
 		w.osc7 = "\\[\x1b]7;file://%s/%s\x1b\\\\\\]"
 		w.osc51 = "\\[\x1b]51;A;%s@%s:%s\x1b\\\\\\]"
@@ -149,8 +148,9 @@ func (w *Writer) Init(shellName string) {
 		w.title = "%%{\x1b]0;%s\007%%}"
 		w.escapeLeft = "%{"
 		w.escapeRight = "%}"
-		w.hyperlink = "%%{\x1b]8;;%s\x1b\\%%}%s%%{\x1b]8;;\x1b\\%%}"
-		w.hyperlinkRegex = `(?P<STR>%{\x1b]8;;(.+)\x1b\\%}(?P<TEXT>.+)%{\x1b]8;;\x1b\\%})`
+		w.hyperlinkStart = "%{\x1b]8;;"
+		w.hyperlinkCenter = "\x1b\\%}"
+		w.hyperlinkEnd = "%{\x1b]8;;\x1b\\%}"
 		w.osc99 = "%%{\x1b]9;9;%s\x1b\\%%}"
 		w.osc7 = "%%{\x1b]7;file://%s/%s\x1b\\%%}"
 		w.osc51 = "%%{\x1b]51;A%s@%s:%s\x1b\\%%}"
@@ -165,8 +165,9 @@ func (w *Writer) Init(shellName string) {
 		// when in fish on Linux, it seems hyperlinks ending with \\ print a \
 		// unlike on macOS. However, this is a fish bug, so do not try to fix it here:
 		// https://github.com/JanDeDobbeleer/oh-my-posh/pull/3288#issuecomment-1369137068
-		w.hyperlink = "\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\"
-		w.hyperlinkRegex = "(?P<STR>\x1b]8;;(.+)\x1b\\\\\\\\?(?P<TEXT>.+)\x1b]8;;\x1b\\\\)"
+		w.hyperlinkStart = "\x1b]8;;"
+		w.hyperlinkCenter = "\x1b\\"
+		w.hyperlinkEnd = "\x1b]8;;\x1b\\"
 		w.osc99 = "\x1b]9;9;%s\x1b\\"
 		w.osc7 = "\x1b]7;file://%s/%s\x1b\\"
 		w.osc51 = "\x1b]51;A%s@%s:%s\x1b\\"
@@ -292,7 +293,7 @@ func (w *Writer) Write(background, foreground, text string) {
 	}
 	// validate if we start with a color override
 	match := regex.FindNamedRegexMatch(AnchorRegex, text)
-	if len(match) != 0 {
+	if len(match) != 0 && match[ANCHOR] != hyperLinkStart {
 		colorOverride := true
 		for _, style := range knownStyles {
 			if match[ANCHOR] != style.AnchorStart {
@@ -301,24 +302,29 @@ func (w *Writer) Write(background, foreground, text string) {
 			w.writeEscapedAnsiString(style.Start)
 			colorOverride = false
 		}
+
 		if colorOverride {
 			w.current.Add(w.asAnsiColors(match[BG], match[FG]))
-			// w.current.Background(), w.current.Foreground() = w.asAnsiColors(match[BG], match[FG])
 		}
 	}
+
 	w.writeSegmentColors()
 
+	// print the hyperlink part AFTER the coloring
+	if match[ANCHOR] == hyperLinkStart {
+		w.hyperlink = true
+		w.builder.WriteString(w.hyperlinkStart)
+	}
+
 	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
+	hyperlinkTextPosition := 0
 
 	for i := 0; i < len(w.runes); i++ {
 		s := w.runes[i]
 		// ignore everything which isn't overriding
 		if s != '<' {
-			w.write(i, s)
+			w.write(s)
 			continue
 		}
 
@@ -326,19 +332,41 @@ func (w *Writer) Write(background, foreground, text string) {
 		text = string(w.runes[i:])
 		match = regex.FindNamedRegexMatch(AnchorRegex, text)
 		if len(match) > 0 {
-			i = w.writeColorOverrides(match, background, i)
+			// check for hyperlinks first
+			switch match[ANCHOR] {
+			case hyperLinkStart:
+				w.hyperlink = true
+				i += len([]rune(match[ANCHOR])) - 1
+				w.builder.WriteString(w.hyperlinkStart)
+				continue
+			case hyperLinkText:
+				w.hyperlink = false
+				i += len([]rune(match[ANCHOR])) - 1
+				hyperlinkTextPosition = i
+				w.builder.WriteString(w.hyperlinkCenter)
+				continue
+			case hyperLinkTextEnd:
+				// this implies there's no text in the hyperlink
+				if hyperlinkTextPosition+1 == i {
+					w.builder.WriteString("link")
+					w.length += 4
+				}
+				i += len([]rune(match[ANCHOR])) - 1
+				continue
+			case hyperLinkEnd:
+				i += len([]rune(match[ANCHOR])) - 1
+				w.builder.WriteString(w.hyperlinkEnd)
+				continue
+			}
+
+			i = w.writeArchorOverride(match, background, i)
 			continue
 		}
 
 		w.length += runewidth.RuneWidth(s)
-		w.write(i, s)
+		w.write(s)
 	}
 
-	// append remnant hyperlink
-	w.builder.WriteString(w.hyperlinkBuilder.String())
-	w.hyperlinkBuilder.Reset()
-	w.hyperlinkState = OTHER
-
 	// reset colors
 	w.writeEscapedAnsiString(resetStyle.End)
 
@@ -359,20 +387,25 @@ func (w *Writer) writeEscapedAnsiString(text string) {
 	if w.Plain {
 		return
 	}
+
 	if len(w.format) != 0 {
 		text = fmt.Sprintf(w.format, text)
 	}
-	if w.hyperlinkState == OTHER {
-		w.builder.WriteString(text)
-		return
-	}
-	w.hyperlinkBuilder.WriteString(text)
+
+	w.builder.WriteString(text)
 }
 
 func (w *Writer) getAnsiFromColorString(colorString string, isBackground bool) Color {
 	return w.AnsiColors.ToColor(colorString, isBackground, w.TrueColor)
 }
 
+func (w *Writer) write(s rune) {
+	if !w.hyperlink {
+		w.length += runewidth.RuneWidth(s)
+	}
+	w.builder.WriteRune(s)
+}
+
 func (w *Writer) writeSegmentColors() {
 	// use correct starting colors
 	bg := w.background
@@ -410,7 +443,7 @@ func (w *Writer) writeSegmentColors() {
 	w.current.Add(bg, fg)
 }
 
-func (w *Writer) writeColorOverrides(match map[string]string, background string, i int) int {
+func (w *Writer) writeArchorOverride(match map[string]string, background string, i int) int {
 	position := i
 	// check color reset first
 	if match[ANCHOR] == resetStyle.AnchorEnd {
diff --git a/src/ansi/ansi_writer_hyperlink.go b/src/ansi/ansi_writer_hyperlink.go
deleted file mode 100644
index 7a27c166..00000000
--- a/src/ansi/ansi_writer_hyperlink.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package ansi
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/jandedobbeleer/oh-my-posh/src/regex"
-	"github.com/jandedobbeleer/oh-my-posh/src/shell"
-	"github.com/mattn/go-runewidth"
-)
-
-func (w *Writer) write(i int, s rune) {
-	// ignore processing when invisible (<transparent,transparent>)
-	if w.invisible {
-		return
-	}
-	// ignore the logic when there is no hyperlink or things arent't visible
-	if !w.hasHyperlink {
-		w.length += runewidth.RuneWidth(s)
-		w.builder.WriteRune(s)
-		return
-	}
-
-	if s == '«' && w.hyperlinkState == OTHER {
-		w.hyperlinkState = TEXT
-		w.hyperlinkBuilder.WriteRune(s)
-		return
-	}
-
-	if w.hyperlinkState == OTHER {
-		w.length += runewidth.RuneWidth(s)
-		w.builder.WriteRune(s)
-		return
-	}
-
-	w.hyperlinkBuilder.WriteRune(s)
-
-	switch s {
-	case '»':
-		// potential end of text part of hyperlink
-		w.bracketIndex = i
-	case '(':
-		// split into link part
-		if w.bracketIndex == i-1 {
-			w.hyperlinkState = LINK
-		}
-		if w.hyperlinkState == LINK {
-			w.roundCount++
-		}
-	case ')':
-		if w.hyperlinkState != 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.hyperlinkState = OTHER
-	}
-}
-
-func (w *Writer) replaceHyperlink(text string) string {
-	// hyperlink matching
-	results := regex.FindNamedRegexMatch("(?P<ALL>(?:«(?P<TEXT>.+)»)(?:\\((?P<URL>.*)\\)))", text)
-	if len(results) != 3 {
-		return text
-	}
-
-	linkText := results["TEXT"]
-
-	// this isn't supported for elvish and xonsh
-	if w.shell == shell.ELVISH || w.shell == shell.XONSH {
-		return strings.Replace(text, results["ALL"], linkText, 1)
-	}
-
-	// we only care about the length of the actual text part
-	characters := w.trimAnsi(linkText)
-	w.length += runewidth.StringWidth(characters)
-
-	if w.Plain {
-		return linkText
-	}
-
-	// build hyperlink ansi
-	hyperlink := fmt.Sprintf(w.hyperlink, results["URL"], linkText)
-	// replace original text by the new ones
-	return strings.Replace(text, results["ALL"], hyperlink, 1)
-}
diff --git a/src/segments/nba.go b/src/segments/nba.go
index 7ba753b2..6d5bcd27 100644
--- a/src/segments/nba.go
+++ b/src/segments/nba.go
@@ -51,11 +51,13 @@ const (
 	InProgressTemplate properties.Property = "in_progress_template"
 	FinishedTemplate   properties.Property = "finished_template"
 
-	NBAScoreURL      string = "https://cdn.nba.com/static/json/liveData/scoreboard/todaysScoreboard_00.json"
-	NBASchedURLPart1 string = "https://stats.nba.com/stats/internationalbroadcasterschedule?LeagueID=00&Season="
-	NBASchedURLPart2 string = "&RegionID=1&EST=Y"
+	NBAScoreURL    string = "https://cdn.nba.com/static/json/liveData/scoreboard/todaysScoreboard_00.json"
+	NBAScheduleURL string = "https://stats.nba.com/stats/internationalbroadcasterschedule?LeagueID=00&Season=%s&Date=%s&RegionID=1&EST=Y"
 
 	Unknown = "Unknown"
+
+	currentNBASeason = "2023"
+	NBADateFormat    = "02/01/2006"
 )
 
 // Custom type for GameStatus
@@ -209,7 +211,7 @@ func (nba *Nba) retrieveScoreData(teamName string, httpTimeout int) (*NBAData, e
 		return nil, err
 	}
 
-	var scoreboardResponse *ScoreboardResponse
+	var scoreboardResponse ScoreboardResponse
 	err = json.Unmarshal(body, &scoreboardResponse)
 	if err != nil {
 		return nil, err
@@ -240,15 +242,15 @@ func (nba *Nba) retrieveScoreData(teamName string, httpTimeout int) (*NBAData, e
 func (nba *Nba) retrieveScheduleData(teamName string, httpTimeout int) (*NBAData, error) {
 	// How many days into the future should we look for a game.
 	numDaysToSearch := nba.props.GetInt(DaysOffset, 8)
-	nbaSeason := nba.props.GetString(NBASeason, "2023")
+	nbaSeason := nba.props.GetString(NBASeason, currentNBASeason)
 	// Get the current date in America/New_York
-	t := time.Now().In(time.FixedZone("America/New_York", -5*60*60))
+	nowNYC := time.Now().In(time.FixedZone("America/New_York", -5*60*60))
 
 	// Check to see if a game is scheduled while the numDaysToSearch is greater than 0
 	for numDaysToSearch > 0 {
-		// convert t into the format that the API expects "MM/YYYY/DD"
-		dateStr := fmt.Sprintf("%02d/%02d/%d", t.Month(), t.Day(), t.Year())
-		urlEndpoint := NBASchedURLPart1 + nbaSeason + "&Date=" + dateStr + NBASchedURLPart2
+		dateStr := nowNYC.Format(NBADateFormat)
+		urlEndpoint := fmt.Sprintf(NBAScheduleURL, nbaSeason, dateStr)
+
 		body, err := nba.env.HTTPRequest(urlEndpoint, nil, httpTimeout)
 		if err != nil {
 			return nil, err
@@ -264,7 +266,7 @@ func (nba *Nba) retrieveScheduleData(teamName string, httpTimeout int) (*NBAData
 		gameInfo, err := nba.findGameSchedulebyTeamTricode(scheduleResponse.ResultSets[1].CompleteGameList, teamName)
 		if err != nil {
 			// We didn't find a game for the team on this day, so we need to check the next day
-			t = t.AddDate(0, 0, 1)
+			nowNYC = nowNYC.AddDate(0, 0, 1)
 			numDaysToSearch--
 			continue
 		}
diff --git a/src/template/link.go b/src/template/link.go
index e4c543dd..1fef434e 100644
--- a/src/template/link.go
+++ b/src/template/link.go
@@ -14,9 +14,9 @@ func url(text, url string) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	return fmt.Sprintf("«%s»(%s)", text, url), nil
+	return fmt.Sprintf("<LINK>%s<TEXT>%s</TEXT></LINK>", url, text), nil
 }
 
 func path(text, path string) (string, error) {
-	return fmt.Sprintf("«%s»(file:%s)", text, path), nil
+	return fmt.Sprintf("<LINK>file:%s<TEXT>%s</TEXT></LINK>", path, text), nil
 }


Test patch:
diff --git a/src/ansi/ansi_writer_hyperlink_test.go b/src/ansi/ansi_writer_hyperlink_test.go
index 7e5067c7..17248282 100644
--- a/src/ansi/ansi_writer_hyperlink_test.go
+++ b/src/ansi/ansi_writer_hyperlink_test.go
@@ -37,7 +37,7 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
 		Expected  string
 	}{
 		{
-			Text:      "«google»(http://www.google.be) «maps (2/2)»(http://maps.google.be)",
+			Text:      "<LINK>http://www.google.be<TEXT>google</TEXT></LINK> <LINK>http://maps.google.be<TEXT>maps (2/2)</TEXT></LINK>",
 			ShellName: shell.FISH,
 			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",
 		},
@@ -46,44 +46,51 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
 			ShellName: shell.PWSH,
 			Expected:  "\x1b[47m\x1b[30min \x1b[49m\x1b[1mpwsh \x1b[22m\x1b[47m \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)",
+			Text:      "<LINK>http://www.google.be<TEXT>google</TEXT></LINK>",
+			ShellName: shell.ZSH,
+			Expected:  "%{\x1b[47m%}%{\x1b[30m%}%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}%{\x1b[0m%}",
+		},
+		{
+			Text:      "<LINK>http://www.google.be<TEXT>google</TEXT></LINK>",
+			ShellName: shell.PWSH,
+			Expected:  "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\\x1b[0m",
+		},
+		{
+			Text:      "<LINK>http://www.google.be<TEXT>google</TEXT></LINK>",
 			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) «maps»(http://maps.google.be)",
+			Text:      "<LINK>http://www.google.be<TEXT>google</TEXT></LINK> <LINK>http://maps.google.be<TEXT>maps</TEXT></LINK>",
 			ShellName: shell.FISH,
 			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",
 		},
 		{
-			Text:      "[]«google»(http://www.google.be)[]",
+			Text:      "[]<LINK>http://www.google.be<TEXT>google</TEXT></LINK>[]",
 			ShellName: shell.FISH,
 			Expected:  "\x1b[47m\x1b[30m[]\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\[]\x1b[0m",
 		},
-	}
-	for _, tc := range cases {
-		a := Writer{
-			AnsiColors: &DefaultColors{},
-		}
-		a.Init(tc.ShellName)
-		a.Write("white", "black", tc.Text)
-		hyperlinkText, _ := a.String()
-		assert.Equal(t, tc.Expected, hyperlinkText)
-	}
-}
-
-func TestGenerateHyperlinkWithUrlNoName(t *testing.T) {
-	cases := []struct {
-		Text      string
-		ShellName string
-		Expected  string
-	}{
-		{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\\]"},
+		{
+			Text:      "<LINK>http://www.google.be<TEXT><blue>google</></TEXT></LINK>",
+			ShellName: shell.FISH,
+			Expected:  "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\\x1b[49m\x1b[34mgoogle\x1b[47m\x1b[30m\x1b]8;;\x1b\\\x1b[0m",
+		},
+		{
+			Text:      "<LINK>http://www.google.be<TEXT></TEXT></LINK>",
+			ShellName: shell.ZSH,
+			Expected:  "%{\x1b[47m%}%{\x1b[30m%}%{\x1b]8;;http://www.google.be\x1b\\%}link%{\x1b]8;;\x1b\\%}%{\x1b[0m%}",
+		},
+		{
+			Text:      "<LINK>http://www.google.be<TEXT></TEXT></LINK>",
+			ShellName: shell.PWSH,
+			Expected:  "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\link\x1b]8;;\x1b\\\x1b[0m",
+		},
+		{
+			Text:      "<LINK>http://www.google.be<TEXT></TEXT></LINK>",
+			ShellName: shell.BASH,
+			Expected:  "\\[\x1b[47m\\]\\[\x1b[30m\\]\\[\x1b]8;;http://www.google.be\x1b\\\\\\]link\\[\x1b]8;;\x1b\\\\\\]\\[\x1b[0m\\]",
+		},
 	}
 	for _, tc := range cases {
 		a := Writer{
@@ -102,10 +109,10 @@ func TestGenerateFileLink(t *testing.T) {
 		Expected string
 	}{
 		{
-			Text:     `«Posh»(file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh)`,
+			Text:     `<LINK>file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh<TEXT>Posh</TEXT></LINK>`,
 			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[47m\x1b[30m\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\\x1b[0m"},
+		{Text: `<LINK>file:C:/Windows<TEXT>Windows</TEXT></LINK>`, Expected: "\x1b[47m\x1b[30m\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\\x1b[0m"},
 	}
 	for _, tc := range cases {
 		a := Writer{
diff --git a/src/ansi/ansi_writer_test.go b/src/ansi/ansi_writer_test.go
index a4584c8b..b5b8a2ab 100644
--- a/src/ansi/ansi_writer_test.go
+++ b/src/ansi/ansi_writer_test.go
@@ -244,16 +244,22 @@ func TestWriteLength(t *testing.T) {
 		},
 		{
 			Case:     "Bold with color override and link",
-			Input:    "<b><#ffffff>test</></b> «url»(https://example.com)",
+			Input:    "<b><#ffffff>test</></b> <LINK>https://example.com<TEXT>url</TEXT></LINK>",
 			Expected: 8,
 			Colors:   &Colors{Foreground: "black", Background: ParentBackground},
 		},
 		{
 			Case:     "Bold with color override and link and leading/trailing spaces",
-			Input:    " <b><#ffffff>test</></b> «url»(https://example.com) ",
+			Input:    " <b><#ffffff>test</></b> <LINK>https://example.com<TEXT>url</TEXT></LINK> ",
 			Expected: 10,
 			Colors:   &Colors{Foreground: "black", Background: ParentBackground},
 		},
+		{
+			Case:     "Bold with color override and link without text and leading/trailing spaces",
+			Input:    " <b><#ffffff>test</></b> <LINK>https://example.com<TEXT></TEXT></LINK> ",
+			Expected: 11,
+			Colors:   &Colors{Foreground: "black", Background: ParentBackground},
+		},
 	}
 
 	for _, tc := range cases {
diff --git a/src/segments/bazel_test.go b/src/segments/bazel_test.go
index 5e91dc58..243cdd7d 100644
--- a/src/segments/bazel_test.go
+++ b/src/segments/bazel_test.go
@@ -7,8 +7,6 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-const icon = "\ue63a"
-
 func TestBazel(t *testing.T) {
 	cases := []struct {
 		Case           string
@@ -16,9 +14,9 @@ func TestBazel(t *testing.T) {
 		Version        string
 		Template       string
 	}{
-		{Case: "bazel 6.4.0", ExpectedString: fmt.Sprintf("«%s»(https://bazel.build/versions/6.4.0) 6.4.0", icon), Version: "bazel 6.4.0", Template: ""},
-		{Case: "bazel 10.11.12", ExpectedString: fmt.Sprintf("«%s»(https://docs.bazel.build/versions/3.7.0) 3.7.0", icon), Version: "bazel 3.7.0"},
-		{Case: "", ExpectedString: fmt.Sprintf("%s err parsing info from bazel with", icon), Version: ""},
+		{Case: "bazel 6.4.0", ExpectedString: "<LINK>https://bazel.build/versions/6.4.0<TEXT>\ue63a</TEXT></LINK> 6.4.0", Version: "bazel 6.4.0", Template: ""},
+		{Case: "bazel 10.11.12", ExpectedString: "<LINK>https://docs.bazel.build/versions/3.7.0<TEXT>\ue63a</TEXT></LINK> 3.7.0", Version: "bazel 3.7.0"},
+		{Case: "", ExpectedString: "\ue63a err parsing info from bazel with", Version: ""},
 	}
 	for _, tc := range cases {
 		params := &mockedLanguageParams{
@@ -28,7 +26,7 @@ func TestBazel(t *testing.T) {
 			extension:     "*.bazel",
 		}
 		env, props := getMockedLanguageEnv(params)
-		props[Icon] = icon
+		props[Icon] = "\ue63a"
 		b := &Bazel{}
 		b.Init(props, env)
 		failMsg := fmt.Sprintf("Failed in case: %s", tc.Case)
diff --git a/src/segments/nba_test.go b/src/segments/nba_test.go
index dc1e32dd..6fd6c000 100644
--- a/src/segments/nba_test.go
+++ b/src/segments/nba_test.go
@@ -90,9 +90,8 @@ func TestNBASegment(t *testing.T) {
 			currTime := time.Now().In(time.FixedZone("America/New_York", -5*60*60))
 			// add offset days to currTime so we can query for games in the future
 			currTime = currTime.AddDate(0, 0, i)
-			dateStr := fmt.Sprintf("%02d/%02d/%d", currTime.Month(), currTime.Day(), currTime.Year())
-			nbaSeason := fmt.Sprintf("%d", currTime.Year())
-			scheduleURLEndpoint := NBASchedURLPart1 + nbaSeason + "&Date=" + dateStr + NBASchedURLPart2
+			dateStr := currTime.Format(NBADateFormat)
+			scheduleURLEndpoint := fmt.Sprintf(NBAScheduleURL, currentNBASeason, dateStr)
 			env.On("HTTPRequest", scheduleURLEndpoint).Return([]byte(tc.JSONResponse), tc.Error)
 		}
 
diff --git a/src/template/link_test.go b/src/template/link_test.go
index f0349092..8e7807cc 100644
--- a/src/template/link_test.go
+++ b/src/template/link_test.go
@@ -17,7 +17,7 @@ func TestUrl(t *testing.T) {
 		Template    string
 		ShouldError bool
 	}{
-		{Case: "valid url", Expected: "«link»(https://ohmyposh.dev)", Template: `{{ url "link" "https://ohmyposh.dev" }}`},
+		{Case: "valid url", Expected: "<LINK>https://ohmyposh.dev<TEXT>link</TEXT></LINK>", Template: `{{ url "link" "https://ohmyposh.dev" }}`},
 		{Case: "invalid url", Expected: "", Template: `{{ url "link" "Foo" }}`, ShouldError: true},
 	}
 
@@ -49,7 +49,7 @@ func TestPath(t *testing.T) {
 		Expected string
 		Template string
 	}{
-		{Case: "valid path", Expected: "«link»(file:/test/test)", Template: `{{ path "link" "/test/test" }}`},
+		{Case: "valid path", Expected: "<LINK>file:/test/test<TEXT>link</TEXT></LINK>", Template: `{{ path "link" "/test/test" }}`},
 	}
 
 	env := &mock.MockedEnvironment{}