mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-11 20:36:03 +02:00
Support hyperlinks from pagers (#3825)
- **PR Description** Allows to use `delta --hyperlinks` as a pager, which turns line numbers in the diff into clickable links that take you to the respective file. For VS Code users, I recommend to combine this with `--hyperlinks-file-link-format="vscode://file/{path}:{line}"` so that it jumps to the right line. In addition, I added a few commits that replaces our old, manual ad-hoc handling of links in various places (status view, confirmation panels, information view) with the new hyperlinks feature, which cleans up the code a bit. Fixes #3817.
This commit is contained in:
commit
c28ecabfd8
23 changed files with 164 additions and 148 deletions
|
@ -26,6 +26,8 @@ git:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `pager:` config to `delta --dark --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
|
||||||
|
|
||||||
## Diff-so-fancy
|
## Diff-so-fancy
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -16,7 +16,7 @@ require (
|
||||||
github.com/integrii/flaggy v1.4.0
|
github.com/integrii/flaggy v1.4.0
|
||||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
|
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
|
||||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
|
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20240824081936-a3adeb73f602
|
github.com/jesseduffield/gocui v0.3.1-0.20240824083442-15b7fbca7ae9
|
||||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -188,8 +188,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
|
||||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
||||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
|
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
|
||||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20240824081936-a3adeb73f602 h1:nzGt/sRT0WCancALG5Q9e4DlQWGo7QUMc35rApdt+aM=
|
github.com/jesseduffield/gocui v0.3.1-0.20240824083442-15b7fbca7ae9 h1:1muwCO0cmCGHpOvNz1qTOrCFPECnBAV87yDE9Fgwy6U=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20240824081936-a3adeb73f602/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
|
github.com/jesseduffield/gocui v0.3.1-0.20240824083442-15b7fbca7ae9/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
|
||||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
|
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
|
||||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
|
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
|
||||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||||
|
|
|
@ -259,11 +259,7 @@ func underlineLinks(text string) string {
|
||||||
} else {
|
} else {
|
||||||
linkEnd += linkStart
|
linkEnd += linkStart
|
||||||
}
|
}
|
||||||
underlinedLink := style.AttrUnderline.Sprint(remaining[linkStart:linkEnd])
|
underlinedLink := style.PrintSimpleHyperlink(remaining[linkStart:linkEnd])
|
||||||
if strings.HasSuffix(underlinedLink, "\x1b[0m") {
|
|
||||||
// Replace the "all styles off" code with "underline off" code
|
|
||||||
underlinedLink = underlinedLink[:len(underlinedLink)-2] + "24m"
|
|
||||||
}
|
|
||||||
result += remaining[:linkStart] + underlinedLink
|
result += remaining[:linkStart] + underlinedLink
|
||||||
remaining = remaining[linkEnd:]
|
remaining = remaining[linkEnd:]
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,27 +27,27 @@ func Test_underlineLinks(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "entire string is a link",
|
name: "entire string is a link",
|
||||||
text: "https://example.com",
|
text: "https://example.com",
|
||||||
expectedResult: "\x1b[4mhttps://example.com\x1b[24m",
|
expectedResult: "\x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "link preceeded and followed by text",
|
name: "link preceeded and followed by text",
|
||||||
text: "bla https://example.com xyz",
|
text: "bla https://example.com xyz",
|
||||||
expectedResult: "bla \x1b[4mhttps://example.com\x1b[24m xyz",
|
expectedResult: "bla \x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\ xyz",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "more than one link",
|
name: "more than one link",
|
||||||
text: "bla https://link1 blubb https://link2 xyz",
|
text: "bla https://link1 blubb https://link2 xyz",
|
||||||
expectedResult: "bla \x1b[4mhttps://link1\x1b[24m blubb \x1b[4mhttps://link2\x1b[24m xyz",
|
expectedResult: "bla \x1b]8;;https://link1\x1b\\https://link1\x1b]8;;\x1b\\ blubb \x1b]8;;https://link2\x1b\\https://link2\x1b]8;;\x1b\\ xyz",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "link in angle brackets",
|
name: "link in angle brackets",
|
||||||
text: "See <https://example.com> for details",
|
text: "See <https://example.com> for details",
|
||||||
expectedResult: "See <\x1b[4mhttps://example.com\x1b[24m> for details",
|
expectedResult: "See <\x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\> for details",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "link followed by newline",
|
name: "link followed by newline",
|
||||||
text: "URL: https://example.com\nNext line",
|
text: "URL: https://example.com\nNext line",
|
||||||
expectedResult: "URL: \x1b[4mhttps://example.com\x1b[24m\nNext line",
|
expectedResult: "URL: \x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\\nNext line",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,11 +71,6 @@ func (self *StatusController) GetKeybindings(opts types.KeybindingsOpts) []*type
|
||||||
|
|
||||||
func (self *StatusController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
func (self *StatusController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
||||||
return []*gocui.ViewMouseBinding{
|
return []*gocui.ViewMouseBinding{
|
||||||
{
|
|
||||||
ViewName: "main",
|
|
||||||
Key: gocui.MouseLeft,
|
|
||||||
Handler: self.onClickMain,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ViewName: self.Context().GetViewName(),
|
ViewName: self.Context().GetViewName(),
|
||||||
Key: gocui.MouseLeft,
|
Key: gocui.MouseLeft,
|
||||||
|
@ -84,10 +79,6 @@ func (self *StatusController) GetMouseKeybindings(opts types.KeybindingsOpts) []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *StatusController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
|
|
||||||
return self.c.HandleGenericClick(self.c.Views().Main)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *StatusController) GetOnRenderToMain() func() error {
|
func (self *StatusController) GetOnRenderToMain() func() error {
|
||||||
return func() error {
|
return func() error {
|
||||||
switch self.c.UserConfig().Gui.StatusPanelView {
|
switch self.c.UserConfig().Gui.StatusPanelView {
|
||||||
|
@ -219,12 +210,12 @@ func (self *StatusController) showDashboard() error {
|
||||||
[]string{
|
[]string{
|
||||||
lazygitTitle(),
|
lazygitTitle(),
|
||||||
fmt.Sprintf("Copyright %d Jesse Duffield", time.Now().Year()),
|
fmt.Sprintf("Copyright %d Jesse Duffield", time.Now().Year()),
|
||||||
fmt.Sprintf("Keybindings: %s", style.AttrUnderline.Sprint(fmt.Sprintf(constants.Links.Docs.Keybindings, versionStr))),
|
fmt.Sprintf("Keybindings: %s", style.PrintSimpleHyperlink(fmt.Sprintf(constants.Links.Docs.Keybindings, versionStr))),
|
||||||
fmt.Sprintf("Config Options: %s", style.AttrUnderline.Sprint(fmt.Sprintf(constants.Links.Docs.Config, versionStr))),
|
fmt.Sprintf("Config Options: %s", style.PrintSimpleHyperlink(fmt.Sprintf(constants.Links.Docs.Config, versionStr))),
|
||||||
fmt.Sprintf("Tutorial: %s", style.AttrUnderline.Sprint(constants.Links.Docs.Tutorial)),
|
fmt.Sprintf("Tutorial: %s", style.PrintSimpleHyperlink(constants.Links.Docs.Tutorial)),
|
||||||
fmt.Sprintf("Raise an Issue: %s", style.AttrUnderline.Sprint(constants.Links.Issues)),
|
fmt.Sprintf("Raise an Issue: %s", style.PrintSimpleHyperlink(constants.Links.Issues)),
|
||||||
fmt.Sprintf("Release Notes: %s", style.AttrUnderline.Sprint(constants.Links.Releases)),
|
fmt.Sprintf("Release Notes: %s", style.PrintSimpleHyperlink(constants.Links.Releases)),
|
||||||
style.FgMagenta.Sprintf("Become a sponsor: %s", style.AttrUnderline.Sprint(constants.Links.Donate)), // caffeine ain't free
|
style.FgMagenta.Sprintf("Become a sponsor: %s", style.PrintSimpleHyperlink(constants.Links.Donate)), // caffeine ain't free
|
||||||
}, "\n\n") + "\n"
|
}, "\n\n") + "\n"
|
||||||
|
|
||||||
return self.c.RenderToMainViews(types.RefreshMainOpts{
|
return self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||||
|
|
|
@ -109,14 +109,6 @@ func (gui *Gui) scrollDownConfirmationPanel() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleConfirmationClick() error {
|
|
||||||
if gui.Views.Confirmation.Editable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.handleGenericClick(gui.Views.Confirmation)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
|
func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
|
||||||
return gui.handleCopySelectedSideContextItemToClipboardWithTruncation(-1)
|
return gui.handleCopySelectedSideContextItemToClipboardWithTruncation(-1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -359,6 +360,28 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
gui.g.SetOpenHyperlinkFunc(func(url string) error {
|
||||||
|
if strings.HasPrefix(url, "lazygit-edit:") {
|
||||||
|
re := regexp.MustCompile(`^lazygit-edit://(.+?)(?::(\d+))?$`)
|
||||||
|
matches := re.FindStringSubmatch(url)
|
||||||
|
if matches == nil {
|
||||||
|
return fmt.Errorf(gui.Tr.InvalidLazygitEditURL, url)
|
||||||
|
}
|
||||||
|
filepath := matches[1]
|
||||||
|
if matches[2] != "" {
|
||||||
|
lineNumber := utils.MustConvertToInt(matches[2])
|
||||||
|
return gui.helpers.Files.EditFileAtLine(filepath, lineNumber)
|
||||||
|
}
|
||||||
|
return gui.helpers.Files.EditFiles([]string{filepath})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gui.os.OpenLink(url); err != nil {
|
||||||
|
return fmt.Errorf(gui.Tr.FailedToOpenURL, url, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// if a context key has been given, push that instead, and set its index to 0
|
// if a context key has been given, push that instead, and set its index to 0
|
||||||
if contextKey != context.NO_CONTEXT {
|
if contextKey != context.NO_CONTEXT {
|
||||||
contextToPush = gui.c.ContextForKey(contextKey)
|
contextToPush = gui.c.ContextForKey(contextKey)
|
||||||
|
|
|
@ -33,10 +33,6 @@ func (self *guiCommon) PostRefreshUpdate(context types.Context) error {
|
||||||
return self.gui.postRefreshUpdate(context)
|
return self.gui.postRefreshUpdate(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *guiCommon) HandleGenericClick(view *gocui.View) error {
|
|
||||||
return self.gui.handleGenericClick(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *guiCommon) RunSubprocessAndRefresh(cmdObj oscommands.ICmdObj) error {
|
func (self *guiCommon) RunSubprocessAndRefresh(cmdObj oscommands.ICmdObj) error {
|
||||||
return self.gui.runSubprocessWithSuspenseAndRefresh(cmdObj)
|
return self.gui.runSubprocessWithSuspenseAndRefresh(cmdObj)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ func (gui *Gui) informationStr() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if gui.g.Mouse {
|
if gui.g.Mouse {
|
||||||
donate := style.FgMagenta.SetUnderline().Sprint(gui.c.Tr.Donate)
|
donate := style.FgMagenta.Sprint(style.PrintHyperlink(gui.c.Tr.Donate, constants.Links.Donate))
|
||||||
askQuestion := style.FgYellow.SetUnderline().Sprint(gui.c.Tr.AskQuestion)
|
askQuestion := style.FgYellow.Sprint(style.PrintHyperlink(gui.c.Tr.AskQuestion, constants.Links.Discussions))
|
||||||
return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion())
|
return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion())
|
||||||
} else {
|
} else {
|
||||||
return gui.Config.GetVersion()
|
return gui.Config.GetVersion()
|
||||||
|
@ -39,28 +39,5 @@ func (gui *Gui) handleInfoClick() error {
|
||||||
return activeMode.Reset()
|
return activeMode.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
var title, url string
|
|
||||||
|
|
||||||
// if we're not in an active mode we show the donate button
|
|
||||||
if cx <= utils.StringWidth(gui.c.Tr.Donate) {
|
|
||||||
url = constants.Links.Donate
|
|
||||||
title = gui.c.Tr.Donate
|
|
||||||
} else if cx <= utils.StringWidth(gui.c.Tr.Donate)+1+utils.StringWidth(gui.c.Tr.AskQuestion) {
|
|
||||||
url = constants.Links.Discussions
|
|
||||||
title = gui.c.Tr.AskQuestion
|
|
||||||
}
|
|
||||||
err := gui.os.OpenLink(url)
|
|
||||||
if err != nil {
|
|
||||||
// Opening the link via the OS failed for some reason. (For example, this
|
|
||||||
// can happen if the `os.openLink` config key references a command that
|
|
||||||
// doesn't exist, or that errors when called.)
|
|
||||||
//
|
|
||||||
// In that case, rather than crash the app, fall back to simply showing a
|
|
||||||
// dialog asking the user to visit the URL.
|
|
||||||
placeholders := map[string]string{"url": url}
|
|
||||||
message := utils.ResolvePlaceholderString(gui.c.Tr.PleaseGoToURL, placeholders)
|
|
||||||
return gui.c.Alert(title, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,12 +248,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: self.scrollDownConfirmationPanel,
|
Handler: self.scrollDownConfirmationPanel,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
ViewName: "confirmation",
|
|
||||||
Key: gocui.MouseLeft,
|
|
||||||
Modifier: gocui.ModNone,
|
|
||||||
Handler: self.handleConfirmationClick,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
ViewName: "confirmation",
|
ViewName: "confirmation",
|
||||||
Key: gocui.MouseWheelUp,
|
Key: gocui.MouseWheelUp,
|
||||||
|
|
13
pkg/gui/style/hyperlink.go
Normal file
13
pkg/gui/style/hyperlink.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package style
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Render the given text as an OSC 8 hyperlink
|
||||||
|
func PrintHyperlink(text string, link string) string {
|
||||||
|
return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", link, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render a link where the text is the same as a link
|
||||||
|
func PrintSimpleHyperlink(link string) string {
|
||||||
|
return fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", link, link)
|
||||||
|
}
|
|
@ -35,10 +35,6 @@ type IGuiCommon interface {
|
||||||
// case would be overkill, although refresh will internally call 'PostRefreshUpdate'
|
// case would be overkill, although refresh will internally call 'PostRefreshUpdate'
|
||||||
PostRefreshUpdate(Context) error
|
PostRefreshUpdate(Context) error
|
||||||
|
|
||||||
// a generic click handler that can be used for any view; it handles opening
|
|
||||||
// URLs in the browser when the user clicks on one
|
|
||||||
HandleGenericClick(view *gocui.View) error
|
|
||||||
|
|
||||||
// renders string to a view without resetting its origin
|
// renders string to a view without resetting its origin
|
||||||
SetViewContent(view *gocui.View, content string)
|
SetViewContent(view *gocui.View, content string)
|
||||||
// resets cursor and origin of view. Often used before calling SetViewContent
|
// resets cursor and origin of view. Often used before calling SetViewContent
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
@ -149,28 +148,3 @@ func (gui *Gui) postRefreshUpdate(c types.Context) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGenericClick is a generic click handler that can be used for any view.
|
|
||||||
// It handles opening URLs in the browser when the user clicks on one.
|
|
||||||
func (gui *Gui) handleGenericClick(view *gocui.View) error {
|
|
||||||
cx, cy := view.Cursor()
|
|
||||||
word, err := view.Word(cx, cy)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow URLs to be wrapped in angle brackets, and the closing bracket to
|
|
||||||
// be followed by punctuation:
|
|
||||||
re := regexp.MustCompile(`^<?(https://.+?)(>[,.;!]*)?$`)
|
|
||||||
matches := re.FindStringSubmatch(word)
|
|
||||||
if matches == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore errors (opening the link via the OS can fail if the
|
|
||||||
// `os.openLink` config key references a command that doesn't exist, or
|
|
||||||
// that errors when called.)
|
|
||||||
_ = gui.c.OS().OpenLink(matches[1])
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -784,7 +784,8 @@ type TranslationSet struct {
|
||||||
MarkAsBaseCommit string
|
MarkAsBaseCommit string
|
||||||
MarkAsBaseCommitTooltip string
|
MarkAsBaseCommitTooltip string
|
||||||
MarkedCommitMarker string
|
MarkedCommitMarker string
|
||||||
PleaseGoToURL string
|
FailedToOpenURL string
|
||||||
|
InvalidLazygitEditURL string
|
||||||
NoCopiedCommits string
|
NoCopiedCommits string
|
||||||
DisabledMenuItemPrefix string
|
DisabledMenuItemPrefix string
|
||||||
QuickStartInteractiveRebase string
|
QuickStartInteractiveRebase string
|
||||||
|
@ -1770,7 +1771,8 @@ func EnglishTranslationSet() *TranslationSet {
|
||||||
MarkAsBaseCommit: "Mark as base commit for rebase",
|
MarkAsBaseCommit: "Mark as base commit for rebase",
|
||||||
MarkAsBaseCommitTooltip: "Select a base commit for the next rebase. When you rebase onto a branch, only commits above the base commit will be brought across. This uses the `git rebase --onto` command.",
|
MarkAsBaseCommitTooltip: "Select a base commit for the next rebase. When you rebase onto a branch, only commits above the base commit will be brought across. This uses the `git rebase --onto` command.",
|
||||||
MarkedCommitMarker: "↑↑↑ Will rebase from here ↑↑↑",
|
MarkedCommitMarker: "↑↑↑ Will rebase from here ↑↑↑",
|
||||||
PleaseGoToURL: "Please go to {{.url}}",
|
FailedToOpenURL: "Failed to open URL %s\n\nError: %v",
|
||||||
|
InvalidLazygitEditURL: "Invalid lazygit-edit URL format: %s",
|
||||||
DisabledMenuItemPrefix: "Disabled: ",
|
DisabledMenuItemPrefix: "Disabled: ",
|
||||||
NoCopiedCommits: "No copied commits",
|
NoCopiedCommits: "No copied commits",
|
||||||
QuickStartInteractiveRebase: "Start interactive rebase",
|
QuickStartInteractiveRebase: "Start interactive rebase",
|
||||||
|
|
|
@ -17,8 +17,8 @@ var OpenLinkFailure = NewIntegrationTest(NewIntegrationTestArgs{
|
||||||
t.Views().Information().Click(0, 0)
|
t.Views().Information().Click(0, 0)
|
||||||
|
|
||||||
t.ExpectPopup().Confirmation().
|
t.ExpectPopup().Confirmation().
|
||||||
Title(Equals("Donate")).
|
Title(Equals("Error")).
|
||||||
Content(Equals("Please go to https://github.com/sponsors/jesseduffield")).
|
Content(Equals("Failed to open URL https://github.com/sponsors/jesseduffield\n\nError: exit status 42")).
|
||||||
Confirm()
|
Confirm()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,7 +25,9 @@ func Decolorise(str string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
re := regexp.MustCompile(`\x1B\[([0-9]{1,3}(;[0-9]{1,3})*)?[mGK]`)
|
re := regexp.MustCompile(`\x1B\[([0-9]{1,3}(;[0-9]{1,3})*)?[mGK]`)
|
||||||
|
linkRe := regexp.MustCompile(`\x1B]8;[^;]*;(.*?)(\x1B.|\x07)`)
|
||||||
ret := re.ReplaceAllString(str, "")
|
ret := re.ReplaceAllString(str, "")
|
||||||
|
ret = linkRe.ReplaceAllString(ret, "")
|
||||||
|
|
||||||
decoloriseMutex.Lock()
|
decoloriseMutex.Lock()
|
||||||
decoloriseCache[str] = ret
|
decoloriseCache[str] = ret
|
||||||
|
|
|
@ -2,6 +2,8 @@ package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecolorise(t *testing.T) {
|
func TestDecolorise(t *testing.T) {
|
||||||
|
@ -189,6 +191,10 @@ func TestDecolorise(t *testing.T) {
|
||||||
input: "\x1b[38;2;157;205;18mta\x1b[0m",
|
input: "\x1b[38;2;157;205;18mta\x1b[0m",
|
||||||
output: "ta",
|
output: "ta",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: "a_" + style.PrintSimpleHyperlink("xyz") + "_b",
|
||||||
|
output: "a_xyz_b",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
|
|
79
vendor/github.com/jesseduffield/gocui/escape.go
generated
vendored
79
vendor/github.com/jesseduffield/gocui/escape.go
generated
vendored
|
@ -17,6 +17,7 @@ type escapeInterpreter struct {
|
||||||
curFgColor, curBgColor Attribute
|
curFgColor, curBgColor Attribute
|
||||||
mode OutputMode
|
mode OutputMode
|
||||||
instruction instruction
|
instruction instruction
|
||||||
|
hyperlink string
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -40,7 +41,11 @@ const (
|
||||||
stateCSI
|
stateCSI
|
||||||
stateParams
|
stateParams
|
||||||
stateOSC
|
stateOSC
|
||||||
stateOSCEscape
|
stateOSCWaitForParams
|
||||||
|
stateOSCParams
|
||||||
|
stateOSCHyperlink
|
||||||
|
stateOSCEndEscape
|
||||||
|
stateOSCSkipUnknown
|
||||||
|
|
||||||
bold fontEffect = 1
|
bold fontEffect = 1
|
||||||
faint fontEffect = 2
|
faint fontEffect = 2
|
||||||
|
@ -60,6 +65,7 @@ var (
|
||||||
errNotCSI = errors.New("Not a CSI escape sequence")
|
errNotCSI = errors.New("Not a CSI escape sequence")
|
||||||
errCSIParseError = errors.New("CSI escape sequence parsing error")
|
errCSIParseError = errors.New("CSI escape sequence parsing error")
|
||||||
errCSITooLong = errors.New("CSI escape sequence is too long")
|
errCSITooLong = errors.New("CSI escape sequence is too long")
|
||||||
|
errOSCParseError = errors.New("OSC escape sequence parsing error")
|
||||||
)
|
)
|
||||||
|
|
||||||
// runes in case of error will output the non-parsed runes as a string.
|
// runes in case of error will output the non-parsed runes as a string.
|
||||||
|
@ -78,6 +84,7 @@ func (ei *escapeInterpreter) runes() []rune {
|
||||||
ret = append(ret, ';')
|
ret = append(ret, ';')
|
||||||
}
|
}
|
||||||
return append(ret, ei.curch)
|
return append(ret, ei.curch)
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -191,15 +198,47 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
|
||||||
return false, errCSIParseError
|
return false, errCSIParseError
|
||||||
}
|
}
|
||||||
case stateOSC:
|
case stateOSC:
|
||||||
switch ch {
|
if ch == '8' {
|
||||||
case 0x1b:
|
ei.state = stateOSCWaitForParams
|
||||||
ei.state = stateOSCEscape
|
ei.hyperlink = ""
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ei.state = stateOSCSkipUnknown
|
||||||
return true, nil
|
return true, nil
|
||||||
case stateOSCEscape:
|
case stateOSCWaitForParams:
|
||||||
|
if ch != ';' {
|
||||||
|
return true, errOSCParseError
|
||||||
|
}
|
||||||
|
|
||||||
|
ei.state = stateOSCParams
|
||||||
|
return true, nil
|
||||||
|
case stateOSCParams:
|
||||||
|
if ch == ';' {
|
||||||
|
ei.state = stateOSCHyperlink
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
case stateOSCHyperlink:
|
||||||
|
switch ch {
|
||||||
|
case 0x07:
|
||||||
|
ei.state = stateNone
|
||||||
|
case 0x1b:
|
||||||
|
ei.state = stateOSCEndEscape
|
||||||
|
default:
|
||||||
|
ei.hyperlink += string(ch)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
case stateOSCEndEscape:
|
||||||
ei.state = stateNone
|
ei.state = stateNone
|
||||||
return true, nil
|
return true, nil
|
||||||
|
case stateOSCSkipUnknown:
|
||||||
|
switch ch {
|
||||||
|
case 0x07:
|
||||||
|
ei.state = stateNone
|
||||||
|
case 0x1b:
|
||||||
|
ei.state = stateOSCEndEscape
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -267,58 +306,48 @@ func (ei *escapeInterpreter) outputCSI() error {
|
||||||
|
|
||||||
func (ei *escapeInterpreter) csiColor(param []string) (color Attribute, skip int, err error) {
|
func (ei *escapeInterpreter) csiColor(param []string) (color Attribute, skip int, err error) {
|
||||||
if len(param) < 2 {
|
if len(param) < 2 {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch param[1] {
|
switch param[1] {
|
||||||
case "2":
|
case "2":
|
||||||
// 24-bit color
|
// 24-bit color
|
||||||
if ei.mode < OutputTrue {
|
if ei.mode < OutputTrue {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if len(param) < 5 {
|
if len(param) < 5 {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var red, green, blue int
|
var red, green, blue int
|
||||||
red, err = strconv.Atoi(param[2])
|
red, err = strconv.Atoi(param[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
green, err = strconv.Atoi(param[3])
|
green, err = strconv.Atoi(param[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
blue, err = strconv.Atoi(param[4])
|
blue, err = strconv.Atoi(param[4])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
return NewRGBColor(int32(red), int32(green), int32(blue)), 5, nil
|
return NewRGBColor(int32(red), int32(green), int32(blue)), 5, nil
|
||||||
case "5":
|
case "5":
|
||||||
// 8-bit color
|
// 8-bit color
|
||||||
if ei.mode < Output256 {
|
if ei.mode < Output256 {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if len(param) < 3 {
|
if len(param) < 3 {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var hex int
|
var hex int
|
||||||
hex, err = strconv.Atoi(param[2])
|
hex, err = strconv.Atoi(param[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
return Get256Color(int32(hex)), 3, nil
|
return Get256Color(int32(hex)), 3, nil
|
||||||
default:
|
default:
|
||||||
err = errCSIParseError
|
return 0, 0, errCSIParseError
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
vendor/github.com/jesseduffield/gocui/gui.go
generated
vendored
43
vendor/github.com/jesseduffield/gocui/gui.go
generated
vendored
|
@ -130,6 +130,7 @@ type Gui struct {
|
||||||
managers []Manager
|
managers []Manager
|
||||||
keybindings []*keybinding
|
keybindings []*keybinding
|
||||||
focusHandler func(bool) error
|
focusHandler func(bool) error
|
||||||
|
openHyperlink func(string) error
|
||||||
maxX, maxY int
|
maxX, maxY int
|
||||||
outputMode OutputMode
|
outputMode OutputMode
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
|
@ -624,6 +625,10 @@ func (g *Gui) SetFocusHandler(handler func(bool) error) {
|
||||||
g.focusHandler = handler
|
g.focusHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Gui) SetOpenHyperlinkFunc(openHyperlinkFunc func(string) error) {
|
||||||
|
g.openHyperlink = openHyperlinkFunc
|
||||||
|
}
|
||||||
|
|
||||||
// getKey takes an empty interface with a key and returns the corresponding
|
// getKey takes an empty interface with a key and returns the corresponding
|
||||||
// typed Key or rune.
|
// typed Key or rune.
|
||||||
func getKey(key interface{}) (Key, rune, error) {
|
func getKey(key interface{}) (Key, rune, error) {
|
||||||
|
@ -1302,7 +1307,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||||
switch ev.Type {
|
switch ev.Type {
|
||||||
case eventKey:
|
case eventKey:
|
||||||
|
|
||||||
_, err := g.execKeybindings(g.currentView, ev)
|
err := g.execKeybindings(g.currentView, ev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1367,6 +1372,14 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ev.Key == MouseLeft && !v.Editable && g.openHyperlink != nil {
|
||||||
|
if newY >= 0 && newY <= len(v.viewLines)-1 && newX >= 0 && newX <= len(v.viewLines[newY].line)-1 {
|
||||||
|
if link := v.viewLines[newY].line[newX].hyperlink; link != "" {
|
||||||
|
return g.openHyperlink(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if IsMouseKey(ev.Key) {
|
if IsMouseKey(ev.Key) {
|
||||||
opts := ViewMouseBindingOpts{X: newX, Y: newY}
|
opts := ViewMouseBindingOpts{X: newX, Y: newY}
|
||||||
matched, err := g.execMouseKeybindings(v, ev, opts)
|
matched, err := g.execMouseKeybindings(v, ev, opts)
|
||||||
|
@ -1378,9 +1391,11 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := g.execKeybindings(v, ev); err != nil {
|
if err := g.execKeybindings(v, ev); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -1440,25 +1455,25 @@ func IsMouseScrollKey(key interface{}) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// execKeybindings executes the keybinding handlers that match the passed view
|
// execKeybindings executes the keybinding handlers that match the passed view
|
||||||
// and event. The value of matched is true if there is a match and no errors.
|
// and event.
|
||||||
func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) (matched bool, err error) {
|
func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) error {
|
||||||
var globalKb *keybinding
|
var globalKb *keybinding
|
||||||
var matchingParentViewKb *keybinding
|
var matchingParentViewKb *keybinding
|
||||||
|
|
||||||
// if we're searching, and we've hit n/N/Esc, we ignore the default keybinding
|
// if we're searching, and we've hit n/N/Esc, we ignore the default keybinding
|
||||||
if v != nil && v.IsSearching() && ev.Mod == ModNone {
|
if v != nil && v.IsSearching() && ev.Mod == ModNone {
|
||||||
if eventMatchesKey(ev, g.NextSearchMatchKey) {
|
if eventMatchesKey(ev, g.NextSearchMatchKey) {
|
||||||
return true, v.gotoNextMatch()
|
return v.gotoNextMatch()
|
||||||
} else if eventMatchesKey(ev, g.PrevSearchMatchKey) {
|
} else if eventMatchesKey(ev, g.PrevSearchMatchKey) {
|
||||||
return true, v.gotoPreviousMatch()
|
return v.gotoPreviousMatch()
|
||||||
} else if eventMatchesKey(ev, g.SearchEscapeKey) {
|
} else if eventMatchesKey(ev, g.SearchEscapeKey) {
|
||||||
v.searcher.clearSearch()
|
v.searcher.clearSearch()
|
||||||
if g.OnSearchEscape != nil {
|
if g.OnSearchEscape != nil {
|
||||||
if err := g.OnSearchEscape(); err != nil {
|
if err := g.OnSearchEscape(); err != nil {
|
||||||
return true, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true, nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1486,26 +1501,26 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) (matched bool, err error)
|
||||||
if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil {
|
if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil {
|
||||||
matched := g.currentView.Editor.Edit(g.currentView, ev.Key, ev.Ch, ev.Mod)
|
matched := g.currentView.Editor.Edit(g.currentView, ev.Key, ev.Ch, ev.Mod)
|
||||||
if matched {
|
if matched {
|
||||||
return true, nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalKb != nil {
|
if globalKb != nil {
|
||||||
return g.execKeybinding(v, globalKb)
|
return g.execKeybinding(v, globalKb)
|
||||||
}
|
}
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// execKeybinding executes a given keybinding
|
// execKeybinding executes a given keybinding
|
||||||
func (g *Gui) execKeybinding(v *View, kb *keybinding) (bool, error) {
|
func (g *Gui) execKeybinding(v *View, kb *keybinding) error {
|
||||||
if g.isBlacklisted(kb.key) {
|
if g.isBlacklisted(kb.key) {
|
||||||
return true, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := kb.handler(g, v); err != nil {
|
if err := kb.handler(g, v); err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
return true, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Gui) onFocus(ev *GocuiEvent) error {
|
func (g *Gui) onFocus(ev *GocuiEvent) error {
|
||||||
|
|
3
vendor/github.com/jesseduffield/gocui/tcell_driver.go
generated
vendored
3
vendor/github.com/jesseduffield/gocui/tcell_driver.go
generated
vendored
|
@ -363,6 +363,7 @@ func (g *Gui) pollEvent() GocuiEvent {
|
||||||
mouseKey = MouseRight
|
mouseKey = MouseRight
|
||||||
case tcell.ButtonMiddle:
|
case tcell.ButtonMiddle:
|
||||||
mouseKey = MouseMiddle
|
mouseKey = MouseMiddle
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,11 +375,13 @@ func (g *Gui) pollEvent() GocuiEvent {
|
||||||
dragState = NOT_DRAGGING
|
dragState = NOT_DRAGGING
|
||||||
case tcell.ButtonSecondary:
|
case tcell.ButtonSecondary:
|
||||||
case tcell.ButtonMiddle:
|
case tcell.ButtonMiddle:
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
mouseMod = Modifier(lastMouseMod)
|
mouseMod = Modifier(lastMouseMod)
|
||||||
lastMouseMod = tcell.ModNone
|
lastMouseMod = tcell.ModNone
|
||||||
lastMouseKey = tcell.ButtonNone
|
lastMouseKey = tcell.ButtonNone
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
if !wheeling {
|
if !wheeling {
|
||||||
|
|
11
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
11
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
|
@ -378,6 +378,7 @@ type viewLine struct {
|
||||||
type cell struct {
|
type cell struct {
|
||||||
chr rune
|
chr rune
|
||||||
bgColor, fgColor Attribute
|
bgColor, fgColor Attribute
|
||||||
|
hyperlink string
|
||||||
}
|
}
|
||||||
|
|
||||||
type lineType []cell
|
type lineType []cell
|
||||||
|
@ -851,9 +852,10 @@ func (v *View) parseInput(ch rune, x int, _ int) (bool, []cell) {
|
||||||
repeatCount = tabStop - (x % tabStop)
|
repeatCount = tabStop - (x % tabStop)
|
||||||
}
|
}
|
||||||
c := cell{
|
c := cell{
|
||||||
fgColor: v.ei.curFgColor,
|
fgColor: v.ei.curFgColor,
|
||||||
bgColor: v.ei.curBgColor,
|
bgColor: v.ei.curBgColor,
|
||||||
chr: ch,
|
hyperlink: v.ei.hyperlink,
|
||||||
|
chr: ch,
|
||||||
}
|
}
|
||||||
for i := 0; i < repeatCount; i++ {
|
for i := 0; i < repeatCount; i++ {
|
||||||
cells = append(cells, c)
|
cells = append(cells, c)
|
||||||
|
@ -1188,6 +1190,9 @@ func (v *View) draw() error {
|
||||||
if bgColor == ColorDefault {
|
if bgColor == ColorDefault {
|
||||||
bgColor = v.BgColor
|
bgColor = v.BgColor
|
||||||
}
|
}
|
||||||
|
if c.hyperlink != "" {
|
||||||
|
fgColor |= AttrUnderline
|
||||||
|
}
|
||||||
|
|
||||||
if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
|
if err := v.setRune(x, y, c.chr, fgColor, bgColor); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
|
@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
|
||||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
|
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
|
||||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
||||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||||
# github.com/jesseduffield/gocui v0.3.1-0.20240824081936-a3adeb73f602
|
# github.com/jesseduffield/gocui v0.3.1-0.20240824083442-15b7fbca7ae9
|
||||||
## explicit; go 1.12
|
## explicit; go 1.12
|
||||||
github.com/jesseduffield/gocui
|
github.com/jesseduffield/gocui
|
||||||
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue