Repository: JanDeDobbeleer/oh-my-posh
Pull request #2090 (2022-04-14 19:21:44)

fix(zsh): escape hyperlinks correctly
### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)

### Description

@JanDeDobbeleer Hi, Jan. I have done a test in zsh using a modified version of your theme (`jandedobbeleer.omp.json`): (with a `git` segment and a `git` tooltip added)

```json
{
  "background": "#87cefa",
  "foreground": "black",
  "powerline_symbol": "\ue0b0",
  "properties": {
    "branch_max_length": 20,
    "fetch_status": true,
    "fetch_upstream_icon": true
  },
  "style": "powerline",
  "template": " {{if .UpstreamGone}}{{.HEAD}}{{else}}{{url (cat .UpstreamIcon .HEAD) .UpstreamURL}}{{end}} ",
  "type": "git"
}
```

```json
{
  "background": "#87cefa",
  "foreground": "black",
  "leading_diamond": "\ue0b6",
  "properties": {
    "branch_max_length": 20,
    "fetch_status": true,
    "fetch_upstream_icon": true
  },
  "style": "diamond",
  "template": " {{if .UpstreamGone}}{{.HEAD}}{{else}}{{url (cat .UpstreamIcon .HEAD) .UpstreamURL}}{{end}} ",
  "tips": [
    "git"
  ],
  "trailing_diamond": "\ue0b4",
  "type": "git"
}
```

It turned out to be like this:

![screenshot](https://user-images.githubusercontent.com/83903009/163410174-25e2bce3-6357-47d1-9ac1-c40a1bfc2dc8.png)

**This never happens when there is no hyperlink wrapped in the text.**

The hyperlink format string defined for zsh: (in [color/ansi.go](https://github.com/JanDeDobbeleer/oh-my-posh/blob/0c7afaa7f769c48d20ad3fa7c1ced52c15605d43/src/color/ansi.go#L69))

```go
a.hyperlink = "%%{\x1b]8;;%s\x1b\\\\%%}%s%%{\x1b]8;;\x1b\\\\%%}"
```

It works well for a _**non-tooltip segment**_ because of this: (in [shell/scripts/omp.zsh](https://github.com/JanDeDobbeleer/oh-my-posh/blob/0c7afaa7f769c48d20ad3fa7c1ced52c15605d43/src/shell/scripts/omp.zsh#L20))

```zsh
eval "$(::OMP:: print primary --config="$POSH_THEME" --error="$omp_last_error" --execution-time="$omp_elapsed" --stack-count="$omp_stack_count" --eval --shell=zsh)"
```

E.g., if I want to make a prompt that includes a hyperlink of [`google`](https://www.google.com), zsh will actually run: (using `\x1b` as a representation of the control character `ESC` as it cannot be displayed correctly on webpage, similarly hereinafter)

```zsh
PS1="...%{\x1b]8;;https://www.google.com\x1b\\%}google%{\x1b]8;;\x1b\\%}..."
RPROMPT="..."
```

The `\\` is used as an escape sequence for a single backslash.

While for a _**tooltip**_, the output of OMP is firstly stored in a variable and then assigned to `$RPROMPT`: (in [shell/scripts/omp.zsh](https://github.com/JanDeDobbeleer/oh-my-posh/blob/0c7afaa7f769c48d20ad3fa7c1ced52c15605d43/src/shell/scripts/omp.zsh#L51))

```zsh
tooltip=$(::OMP:: print tooltip --config="$POSH_THEME" --shell=zsh --command="$BUFFER")
...
RPROMPT=$tooltip
```

In this case `\\` is not used as an escape sequence and it flows into `$RPROMPT` directly, which cause the problem, as we can see in the screenshot: two extra backslashes and line break.

To solve it, we should use this for zsh:

```go
a.hyperlink = "%%{\x1b]8;;%s\x1b\\%%}%s%%{\x1b]8;;\x1b\\%%}"
a.hyperlinkRegex = `(?P<STR>%{\x1b]8;;(.+)\x1b\\%}(?P<TEXT>.+)%{\x1b]8;;\x1b\\%})`
```

Eventually, it works for either a tooltip or a non-tooltip segment, as both `\` and `\\` will be evaluated to a single backslash when in a [literal escape sequence](https://zsh.sourceforge.io/Doc/Release/Prompt-Expansion.html#Visual-effects). If I do `RPROMPT="A%{\%}B"` or `RPROMPT="A%{\\%}B"`, `echo $RPROMPT` will show `A%{\%}B`, and `A\B` is displayed in the right prompt.

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


JanDeDobbeleer: Amazing PR! And yes, reading through it you're right!

Patch:
diff --git a/src/color/ansi.go b/src/color/ansi.go
index 66b66880..11e7777a 100644
--- a/src/color/ansi.go
+++ b/src/color/ansi.go
@@ -66,8 +66,8 @@ func (a *Ansi) Init(shellName string) {
 		a.colorTransparent = "%%{\x1b[%s;49m\x1b[7m%%}%s%%{\x1b[0m%%}"
 		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.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%%}"
diff --git a/src/engine/engine.go b/src/engine/engine.go
index 494dcac8..26546c9f 100644
--- a/src/engine/engine.go
+++ b/src/engine/engine.go
@@ -211,7 +211,7 @@ func (e *Engine) print() string {
 			break
 		}
 		// escape double quotes contained in the prompt
-		prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), "\"", "\"\""))
+		prompt := fmt.Sprintf("PS1=\"%s\"", strings.ReplaceAll(e.string(), `"`, `\"`))
 		prompt += fmt.Sprintf("\nRPROMPT=\"%s\"", e.rprompt)
 		return prompt
 	case shell.PWSH, shell.PWSH5, shell.BASH, shell.PLAIN, shell.NU:


Test patch:
diff --git a/src/color/ansi_test.go b/src/color/ansi_test.go
index 92afad2e..1b5b0e4f 100644
--- a/src/color/ansi_test.go
+++ b/src/color/ansi_test.go
@@ -31,7 +31,7 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
 		ShellName string
 		Expected  string
 	}{
-		{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.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\\\\\\]"},
 	}