Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make URLs in confirmation panels clickable, and underline them #3446

Merged
merged 5 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion pkg/gui/controllers/helpers/confirmation_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
confirmationView.RenderTextArea()
} else {
self.c.ResetViewOrigin(confirmationView)
self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt))
self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(underlineLinks(opts.Prompt)))
}

if err := self.setKeyBindings(cancel, opts); err != nil {
Expand All @@ -228,6 +228,32 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
return self.c.PushContext(self.c.Contexts().Confirmation)
}

func underlineLinks(text string) string {
result := ""
remaining := text
for {
linkStart := strings.Index(remaining, "https://")
if linkStart == -1 {
break
}

linkEnd := strings.IndexAny(remaining[linkStart:], " \n>")
if linkEnd == -1 {
linkEnd = len(remaining)
} else {
linkEnd += linkStart
}
underlinedLink := style.AttrUnderline.Sprint(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"
}
Comment on lines +247 to +250
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty hacky, but we need it so that we don't cancel the bold text. I don't see a better way of doing this except by making changes to gookit/color.

result += remaining[:linkStart] + underlinedLink
remaining = remaining[linkEnd:]
}
return result + remaining
}

func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) error {
var onConfirm func() error
if opts.HandleConfirmPrompt != nil {
Expand Down
63 changes: 63 additions & 0 deletions pkg/gui/controllers/helpers/confirmation_helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package helpers

import (
"testing"

"github.com/gookit/color"
"github.com/stretchr/testify/assert"
"github.com/xo/terminfo"
)

func Test_underlineLinks(t *testing.T) {
scenarios := []struct {
name string
text string
expectedResult string
}{
{
name: "empty string",
text: "",
expectedResult: "",
},
{
name: "no links",
text: "abc",
expectedResult: "abc",
},
{
name: "entire string is a link",
text: "https://example.com",
expectedResult: "\x1b[4mhttps://example.com\x1b[24m",
},
{
name: "link preceeded and followed by text",
text: "bla https://example.com xyz",
expectedResult: "bla \x1b[4mhttps://example.com\x1b[24m xyz",
},
{
name: "more than one link",
text: "bla https://link1 blubb https://link2 xyz",
expectedResult: "bla \x1b[4mhttps://link1\x1b[24m blubb \x1b[4mhttps://link2\x1b[24m xyz",
},
{
name: "link in angle brackets",
text: "See <https://example.com> for details",
expectedResult: "See <\x1b[4mhttps://example.com\x1b[24m> for details",
},
{
name: "link followed by newline",
text: "URL: https://example.com\nNext line",
expectedResult: "URL: \x1b[4mhttps://example.com\x1b[24m\nNext line",
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)

for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
result := underlineLinks(s.text)
assert.Equal(t, s.expectedResult, result)
})
}
}
13 changes: 1 addition & 12 deletions pkg/gui/controllers/status_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,7 @@ func (self *StatusController) GetMouseKeybindings(opts types.KeybindingsOpts) []
}

func (self *StatusController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
view := self.c.Views().Main

cx, cy := view.Cursor()
url, err := view.Word(cx, cy)
if err == nil && strings.HasPrefix(url, "https://") {
// 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.)
_ = self.c.OS().OpenLink(url)
}

return nil
return self.c.HandleGenericClick(self.c.Views().Main)
}

func (self *StatusController) GetOnRenderToMain() func() error {
Expand Down
8 changes: 8 additions & 0 deletions pkg/gui/global_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ func (gui *Gui) scrollDownConfirmationPanel() error {
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 {
return gui.handleCopySelectedSideContextItemToClipboardWithTruncation(-1)
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/gui/gui_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ func (self *guiCommon) PostRefreshUpdate(context types.Context) error {
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 {
return self.gui.runSubprocessWithSuspenseAndRefresh(cmdObj)
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/gui/keybindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Modifier: gocui.ModNone,
Handler: self.scrollDownConfirmationPanel,
},
{
ViewName: "confirmation",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: self.handleConfirmationClick,
},
{
ViewName: "confirmation",
Key: gocui.MouseWheelUp,
Expand Down
5 changes: 5 additions & 0 deletions pkg/gui/presentation/branches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import (
"testing"
"time"

"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/xo/terminfo"
)

func Test_getBranchDisplayStrings(t *testing.T) {
Expand Down Expand Up @@ -223,6 +225,9 @@ func Test_getBranchDisplayStrings(t *testing.T) {
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
defer color.ForceSetColorLevel(oldColorLevel)

c := utils.NewDummyCommon()

for i, s := range scenarios {
Expand Down
7 changes: 3 additions & 4 deletions pkg/gui/presentation/commits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ import (
"github.com/xo/terminfo"
)

func init() {
color.ForceSetColorLevel(terminfo.ColorLevelNone)
}

func formatExpected(expected string) string {
return strings.TrimSpace(strings.ReplaceAll(expected, "\t", ""))
}
Expand Down Expand Up @@ -385,6 +381,9 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
defer color.ForceSetColorLevel(oldColorLevel)

os.Setenv("TZ", "UTC")

focusing := false
Expand Down
10 changes: 6 additions & 4 deletions pkg/gui/presentation/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ import (
"github.com/xo/terminfo"
)

func init() {
color.ForceSetColorLevel(terminfo.ColorLevelNone)
}

func toStringSlice(str string) []string {
return strings.Split(strings.TrimSpace(str), "\n")
}
Expand Down Expand Up @@ -66,6 +62,9 @@ M file1
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
defer color.ForceSetColorLevel(oldColorLevel)

for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
Expand Down Expand Up @@ -125,6 +124,9 @@ M file1
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelNone)
defer color.ForceSetColorLevel(oldColorLevel)

for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
Expand Down
17 changes: 12 additions & 5 deletions pkg/gui/presentation/graph/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ import (
"github.com/xo/terminfo"
)

func init() {
// on CI we've got no color capability so we're forcing it here
color.ForceSetColorLevel(terminfo.ColorLevelMillions)
}

func TestRenderCommitGraph(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -218,6 +213,9 @@ func TestRenderCommitGraph(t *testing.T) {
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
Expand Down Expand Up @@ -452,6 +450,9 @@ func TestRenderPipeSet(t *testing.T) {
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
Expand Down Expand Up @@ -523,6 +524,9 @@ func TestGetNextPipes(t *testing.T) {
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)

for _, test := range tests {
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
pipes := getNextPipes(test.prevPipes, test.commit, getStyle)
Expand All @@ -538,6 +542,9 @@ func TestGetNextPipes(t *testing.T) {
}

func BenchmarkRenderCommitGraph(b *testing.B) {
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)

commits := generateCommits(50)
getStyle := func(commit *models.Commit) style.TextStyle {
return authors.AuthorStyle(commit.AuthorName)
Expand Down
11 changes: 6 additions & 5 deletions pkg/gui/style/style_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import (
"github.com/xo/terminfo"
)

func init() {
// on CI we've got no color capability so we're forcing it here
color.ForceSetColorLevel(terminfo.ColorLevelMillions)
}

func TestMerge(t *testing.T) {
type scenario struct {
name string
Expand Down Expand Up @@ -162,6 +157,9 @@ func TestMerge(t *testing.T) {
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)

for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
Expand Down Expand Up @@ -210,6 +208,9 @@ func TestTemplateFuncMapAddColors(t *testing.T) {
},
}

oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
defer color.ForceSetColorLevel(oldColorLevel)

for _, s := range scenarios {
s := s
t.Run(s.name, func(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions pkg/gui/types/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ type IGuiCommon interface {
// case would be overkill, although refresh will internally call 'PostRefreshUpdate'
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
SetViewContent(view *gocui.View, content string)
// resets cursor and origin of view. Often used before calling SetViewContent
Expand Down
26 changes: 26 additions & 0 deletions pkg/gui/view_helpers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gui

import (
"regexp"
"time"

"github.com/jesseduffield/gocui"
Expand Down Expand Up @@ -148,3 +149,28 @@ func (gui *Gui) postRefreshUpdate(c types.Context) error {

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
}
Loading