feat: add hyperlink rendering

An hyperlink can be added using markdown syntax and will be detected by the engine.
Initial implementation for path segments.
This commit is contained in:
lnu 2021-01-09 16:18:37 +01:00 committed by Jan De Dobbeleer
parent 7f3b2dd882
commit 866c44e42d
8 changed files with 113 additions and 9 deletions

View file

@ -291,6 +291,19 @@ Oh my Posh mainly supports three different color types being
`darkGray` `lightRed` `lightGreen` `lightYellow` `lightBlue` `lightMagenta` `lightCyan` `lightWhite`
### Hyperlinks
The engine has the ability to render hyperlinks. Your terminal has to support it and the option
has to be enabled at the segment level. Hyperlink generation is disabled by default.
#### Supported segments
- [Path](segment-path.md)
#### Supported terminals
- [Terminal list](thttps://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
## Full Sample
```json
@ -341,7 +354,8 @@ Oh my Posh mainly supports three different color types being
"style": "folder",
"ignore_folders": [
"/super/secret/project"
]
],
"enable_hyperlink": false
}
},
{

View file

@ -37,6 +37,7 @@ Display the current path.
is set to `true`)
- mapped_locations_enabled: `boolean` - replace known locations in the path with the replacements before applying the
style. defaults to `true`
- enable_hyperlink: `boolean` - displays an hyperlink for the path - defaults to `false`
## Style

View file

@ -1,6 +1,7 @@
package main
import (
"fmt"
"strings"
"golang.org/x/text/unicode/norm"
@ -21,6 +22,7 @@ type ansiFormats struct {
colorTransparent string
escapeLeft string
escapeRight string
hyperlink string
}
func (a *ansiFormats) init(shell string) {
@ -40,6 +42,7 @@ func (a *ansiFormats) init(shell string) {
a.colorTransparent = "%%{\x1b[%s;49m\x1b[7m%%}%s%%{\x1b[m\x1b[0m%%}"
a.escapeLeft = "%{"
a.escapeRight = "%}"
a.hyperlink = "%%{\x1b]8;;%s\x1b\\%%}%s%%{\x1b]8;;\x1b\\%%}"
case bash:
a.linechange = "\\[\x1b[%d%s\\]"
a.left = "\\[\x1b[%dC\\]"
@ -54,6 +57,7 @@ func (a *ansiFormats) init(shell string) {
a.colorTransparent = "\\[\x1b[%s;49m\x1b[7m\\]%s\\[\x1b[m\x1b[0m\\]"
a.escapeLeft = "\\["
a.escapeRight = "\\]"
a.hyperlink = "\\[\x1b]8;;%s\x1b\\\\\\]%s\\[\x1b]8;;\x1b\\\\\\]"
default:
a.linechange = "\x1b[%d%s"
a.left = "\x1b[%dC"
@ -68,6 +72,7 @@ func (a *ansiFormats) init(shell string) {
a.colorTransparent = "\x1b[%s;49m\x1b[7m%s\x1b[m\x1b[0m"
a.escapeLeft = ""
a.escapeRight = ""
a.hyperlink = "\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\"
}
}
@ -85,3 +90,15 @@ func (a *ansiFormats) lenWithoutANSI(text string) int {
}
return count
}
func (a *ansiFormats) generateHyperlink(text string) string {
// hyperlink matching
results := findNamedRegexMatch("(?P<all>(?:\\[(?P<name>.+)\\])(?:\\((?P<url>.*)\\)))", text)
if len(results) != 3 {
return text
}
// build hyperlink ansi
hyperlink := fmt.Sprintf(a.hyperlink, results["url"], results["name"])
// replace original text by the new one
return strings.Replace(text, results["all"], hyperlink, 1)
}

View file

@ -23,3 +23,57 @@ func TestLenWithoutAnsi(t *testing.T) {
assert.Equal(t, 5, strippedLength)
}
}
func TestGenerateHyperlinkNoUrl(t *testing.T) {
cases := []struct {
Text string
ShellName string
Expected string
}{
{Text: "sample text with no url", ShellName: zsh, Expected: "sample text with no url"},
{Text: "sample text with no url", ShellName: pwsh, Expected: "sample text with no url"},
{Text: "sample text with no url", ShellName: bash, Expected: "sample text with no url"},
}
for _, tc := range cases {
a := ansiFormats{}
a.init(tc.ShellName)
hyperlinkText := a.generateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText)
}
}
func TestGenerateHyperlinkWithUrl(t *testing.T) {
cases := []struct {
Text string
ShellName string
Expected string
}{
{Text: "[google](http://www.google.be)", ShellName: zsh, Expected: "%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}"},
{Text: "[google](http://www.google.be)", ShellName: pwsh, Expected: "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\"},
{Text: "[google](http://www.google.be)", ShellName: bash, Expected: "\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]"},
}
for _, tc := range cases {
a := ansiFormats{}
a.init(tc.ShellName)
hyperlinkText := a.generateHyperlink(tc.Text)
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: zsh, Expected: "[](http://www.google.be)"},
{Text: "[](http://www.google.be)", ShellName: pwsh, Expected: "[](http://www.google.be)"},
{Text: "[](http://www.google.be)", ShellName: bash, Expected: "[](http://www.google.be)"},
}
for _, tc := range cases {
a := ansiFormats{}
a.init(tc.ShellName)
hyperlinkText := a.generateHyperlink(tc.Text)
assert.Equal(t, tc.Expected, hyperlinkText)
}
}

View file

@ -82,6 +82,9 @@ func (e *engine) renderText(text string) {
if e.activeSegment.Background != "" {
defaultValue = fmt.Sprintf("<%s>\u2588</>", e.activeSegment.Background)
}
text = e.color.formats.generateHyperlink(text)
prefix := e.activeSegment.getValue(Prefix, defaultValue)
postfix := e.activeSegment.getValue(Postfix, defaultValue)
e.color.write(e.activeSegment.Background, e.activeSegment.Foreground, fmt.Sprintf("%s%s%s", prefix, text, postfix))
@ -109,10 +112,9 @@ func (e *engine) renderBlockSegments(block *Block) string {
}
e.activeSegment = segment
e.endPowerline()
text := segment.stringValue
e.activeSegment.Background = segment.props.background
e.activeSegment.Foreground = segment.props.foreground
e.renderSegmentText(text)
e.renderSegmentText(segment.stringValue)
}
if e.previousActiveSegment != nil && e.previousActiveSegment.Style == Powerline {
e.writePowerLineSeparator(Transparent, e.previousActiveSegment.Background, true)
@ -166,7 +168,7 @@ func (e *engine) render() {
e.write()
}
// debug will lool through your config file and output the timings for each segments
// debug will loop through your config file and output the timings for each segments
func (e *engine) debug() {
var segmentTimings []SegmentTiming
largestSegmentNameLength := 0

View file

@ -44,23 +44,31 @@ func (pt *path) enabled() bool {
}
func (pt *path) string() string {
cwd := pt.env.getcwd()
var formattedPath string
switch style := pt.props.getString(Style, Agnoster); style {
case Agnoster:
return pt.getAgnosterPath()
formattedPath = pt.getAgnosterPath()
case AgnosterFull:
return pt.getAgnosterFullPath()
formattedPath = pt.getAgnosterFullPath()
case AgnosterShort:
return pt.getAgnosterShortPath()
formattedPath = pt.getAgnosterShortPath()
case Short:
// "short" is a duplicate of "full", just here for backwards compatibility
fallthrough
case Full:
return pt.getFullPath()
formattedPath = pt.getFullPath()
case Folder:
return pt.getFolderPath()
formattedPath = pt.getFolderPath()
default:
return fmt.Sprintf("Path style: %s is not available", style)
}
if pt.props.getBool(EnableHyperlink, false) {
return fmt.Sprintf("[%s](file://%s)", formattedPath, cwd)
}
return formattedPath
}
func (pt *path) init(props *properties, env environmentInfo) {

View file

@ -34,6 +34,8 @@ const (
Left BlockAlignment = "left"
// Right aligns right
Right BlockAlignment = "right"
// EnableHyperlink enable hyperlink
EnableHyperlink Property = "enable_hyperlink"
)
// Block defines a part of the prompt with optional segments

View file

@ -944,6 +944,12 @@
"title": "Enable the Mapped Locations feature",
"description": "Replace known locations in the path with the replacements before applying the style.",
"default": true
},
"enable_hyperlink": {
"type": "string",
"title": "Enable hyperlink",
"description": "Displays an hyperlink for the current path",
"default": false
}
}
}