Skip to content

Commit

Permalink
feat: add hyperlink rendering
Browse files Browse the repository at this point in the history
An hyperlink can be added using markdown syntax and will be
detected by the engine.
Initial implementation for git and path segments.
  • Loading branch information
lnu committed Jan 9, 2021
1 parent c7bbed1 commit 78acc70
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 22 deletions.
4 changes: 4 additions & 0 deletions docs/docs/segment-git.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@ foreground/background (see `color_background`)
foreground/background (see `color_background`)

[colors]: /docs/configure#colors

### Hyperlink

- enable_hyperlink: `boolean` - displays an hyperlink for the repository - defaults to `false`
1 change: 1 addition & 0 deletions docs/docs/segment-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/ansi_formats.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type ansiFormats struct {
colorTransparent string
escapeLeft string
escapeRight string
hyperlink string
}

func (a *ansiFormats) init(shell string) {
Expand Down Expand Up @@ -69,6 +70,7 @@ func (a *ansiFormats) init(shell string) {
a.escapeLeft = ""
a.escapeRight = ""
}
a.hyperlink = "\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\"
}

func (a *ansiFormats) lenWithoutANSI(text string) int {
Expand Down
17 changes: 14 additions & 3 deletions src/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"strings"
"sync"
"time"
)
Expand Down Expand Up @@ -82,6 +83,16 @@ func (e *engine) renderText(text string) {
if e.activeSegment.Background != "" {
defaultValue = fmt.Sprintf("<%s>\u2588</>", e.activeSegment.Background)
}

// hyperlink matching
results := findNamedRegexMatch("(?P<all>(?:\\[(?P<name>.+)\\])(?:\\((?P<url>.*)\\)))", text)
if len(results) > 0 {
// build hyperlink ansi
hyperlink := fmt.Sprintf(e.color.formats.hyperlink, results["url"], results["name"])
// replace original text by the new one
text = strings.Replace(text, results["all"], hyperlink, 1)
}

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))
Expand Down Expand Up @@ -109,10 +120,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)
Expand Down Expand Up @@ -166,7 +176,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
Expand Down Expand Up @@ -217,6 +227,7 @@ func (e *engine) debug() {
segmentName := fmt.Sprintf("%s(%t)", segment.name, segment.enabled)
e.renderer.print(fmt.Sprintf("%-*s - %3d ms - %s\n", largestSegmentNameLength, segmentName, duration, segment.stringValue))
}

fmt.Print(e.renderer.string())
}

Expand Down
69 changes: 60 additions & 9 deletions src/segment_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,34 @@ package main
import (
"bytes"
"fmt"
"net/url"
"strconv"
"strings"
)

var (
// Map for git service providers
// generate url to commits list of the current branch
gitProvidersMap map[string]string = map[string]string{
"github.com": "%s/commits/%s",
"azure.com": "%s/commits?itemVersion=GB%s",
"visualstudio.com": "%s/commits?itemVersion=GB%s",
"gitlab.com": "%s/-/commits/%s",
"bitbucket.org": "%s/commits/branch/%s",
}
)

type gitRepo struct {
working *gitStatus
staging *gitStatus
ahead int
behind int
HEAD string
HEADPRETTY string
upstream string
stashCount string
gitFolder string
url string
}

type gitStatus struct {
Expand Down Expand Up @@ -147,11 +162,20 @@ func (g *git) string() string {
g.SetStatusColor()
}
buffer := new(bytes.Buffer)

// url
upstream := replaceAllString("/.*", g.repo.upstream, "")
g.repo.url = g.getGitCommandOutput("remote", "get-url", upstream)

// branchName
if g.repo.upstream != "" && g.props.getBool(DisplayUpstreamIcon, false) {
fmt.Fprintf(buffer, "%s", g.getUpstreamSymbol())
fmt.Fprintf(buffer, "%s", g.getUpstreamSymbol(g.repo.url))
}
fmt.Fprintf(buffer, "%s", g.repo.HEAD)

// default name
repoHEAD := g.getHEAD(g.repo)
fmt.Fprintf(buffer, "%s", repoHEAD)

displayStatus := g.props.getBool(DisplayStatus, true)
if !displayStatus {
return buffer.String()
Expand Down Expand Up @@ -198,16 +222,14 @@ func (g *git) getStatusDetailString(status *gitStatus, color, icon Property, def
return status.string(prefix, foregroundColor)
}

func (g *git) getUpstreamSymbol() string {
upstream := replaceAllString("/.*", g.repo.upstream, "")
url := g.getGitCommandOutput("remote", "get-url", upstream)
if strings.Contains(url, "github") {
func (g *git) getUpstreamSymbol(repoURL string) string {
if strings.Contains(repoURL, "github") {
return g.props.getString(GithubIcon, "\uF408 ")
}
if strings.Contains(url, "gitlab") {
if strings.Contains(repoURL, "gitlab") {
return g.props.getString(GitlabIcon, "\uF296 ")
}
if strings.Contains(url, "bitbucket") {
if strings.Contains(repoURL, "bitbucket") {
return g.props.getString(BitbucketIcon, "\uF171 ")
}
return g.props.getString(GitIcon, "\uE5FB ")
Expand All @@ -226,7 +248,8 @@ func (g *git) setGitStatus() {
g.repo.upstream = status["upstream"]
}
}
g.repo.HEAD = g.getGitHEADContext(status["local"])
g.repo.HEAD = status["local"]
g.repo.HEADPRETTY = g.getGitHEADContext(status["local"])
if g.props.getBool(DisplayStashCount, false) {
g.repo.stashCount = g.getStashContext()
}
Expand Down Expand Up @@ -376,3 +399,31 @@ func (g *git) parseGitStatusInfo(branchInfo string) map[string]string {
var branchRegex = `^## (?P<local>\S+?)(\.{3}(?P<upstream>\S+?)( \[(?P<upstream_status>(ahead (?P<ahead>\d+)(, )?)?(behind (?P<behind>\d+))?(gone)?)])?)?$`
return findNamedRegexMatch(branchRegex, branchInfo)
}

func (g *git) getHEAD(repo *gitRepo) string {
if repo.upstream == "" || !g.props.getBool(EnableHyperlink, false) {
return repo.HEADPRETTY
}
// parse url to and strips extra info
baseURL, err := url.Parse(repo.url)
if err != nil {
return repo.HEADPRETTY
}
var urlTemplate string
// check for a match in the providers map
for i, val := range gitProvidersMap {
if strings.Contains(baseURL.Host, i) {
urlTemplate = val
break
}
}
// construct url(remove .git from url(github))
// removing extra data could be done using a regex stored in the map
repoURL := fmt.Sprintf("%s://%s%s", baseURL.Scheme, baseURL.Host, strings.ReplaceAll(baseURL.Path, ".git", ""))
if urlTemplate != "" {
// if provider found, open the branch
return fmt.Sprintf("[%s](%s)", repo.HEADPRETTY, fmt.Sprintf(urlTemplate, repoURL, repo.HEAD))
}
// if no provider found, open the base url
return fmt.Sprintf("[%s](%s)", repo.HEADPRETTY, repoURL)
}
8 changes: 4 additions & 4 deletions src/segment_git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,25 +420,25 @@ func bootstrapUpstreamTest(upstream string) *git {

func TestGetUpstreamSymbolGitHub(t *testing.T) {
g := bootstrapUpstreamTest("github.com/test")
upstreamIcon := g.getUpstreamSymbol()
upstreamIcon := g.getUpstreamSymbol("github.com/test")
assert.Equal(t, "GH", upstreamIcon)
}

func TestGetUpstreamSymbolGitLab(t *testing.T) {
g := bootstrapUpstreamTest("gitlab.com/test")
upstreamIcon := g.getUpstreamSymbol()
upstreamIcon := g.getUpstreamSymbol("gitlab.com/test")
assert.Equal(t, "GL", upstreamIcon)
}

func TestGetUpstreamSymbolBitBucket(t *testing.T) {
g := bootstrapUpstreamTest("bitbucket.org/test")
upstreamIcon := g.getUpstreamSymbol()
upstreamIcon := g.getUpstreamSymbol("bitbucket.org/test")
assert.Equal(t, "BB", upstreamIcon)
}

func TestGetUpstreamSymbolGit(t *testing.T) {
g := bootstrapUpstreamTest("gitstash.com/test")
upstreamIcon := g.getUpstreamSymbol()
upstreamIcon := g.getUpstreamSymbol("gitstash.com/test")
assert.Equal(t, "G", upstreamIcon)
}

Expand Down
18 changes: 13 additions & 5 deletions src/segment_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions src/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Settings struct {
ConsoleTitle bool `json:"console_title"`
ConsoleTitleStyle ConsoleTitleStyle `json:"console_title_style"`
ConsoleTitleTemplate string `json:"console_title_template"`
EnableHyperlink bool `json:"enable_hyperlink"`
Blocks []*Block `json:"blocks"`
}

Expand All @@ -34,6 +35,8 @@ const (
Left BlockAlignment = "left"
// Right aligns right
Right BlockAlignment = "right"
// EnableHyperlink if set, an hyperlink will be generated for the repo name
EnableHyperlink Property = "enable_hyperlink"
)

// Block defines a part of the prompt with optional segments
Expand Down
14 changes: 13 additions & 1 deletion themes/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,13 @@
"$ref": "#/definitions/color"
},
"behind_color": { "$ref": "#/definitions/color" },
"ahead_color": { "$ref": "#/definitions/color" }
"ahead_color": { "$ref": "#/definitions/color" },
"enable_hyperlink": {
"type": "string",
"title": "Display an hyperlink for the current branch",
"description": "https://ohmyposh.dev/docs/configure#console-title-template",
"default": false
}
}
}
}
Expand Down Expand Up @@ -944,6 +950,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": "Display an hyperlink for the path",
"description": "https://ohmyposh.dev/docs/configure#console-title-template",
"default": false
}
}
}
Expand Down

0 comments on commit 78acc70

Please sign in to comment.