diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 8a61a86e14e..6cf5a1e9015 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -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 { @@ -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" + } + 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 { diff --git a/pkg/gui/controllers/helpers/confirmation_helper_test.go b/pkg/gui/controllers/helpers/confirmation_helper_test.go new file mode 100644 index 00000000000..488c72710ef --- /dev/null +++ b/pkg/gui/controllers/helpers/confirmation_helper_test.go @@ -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 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) + }) + } +}