mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2025-03-05 20:49:04 -08:00
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:
parent
7f3b2dd882
commit
866c44e42d
|
@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue