Skip to content

Commit 1d65641

Browse files
committed
feat: add hyperlink support
This commit adds support for hyperlinks in lipgloss. Hyperlinks are useful for rendering text that can be clicked on in a terminal emulator that supports hyperlinks.
1 parent aa91bd6 commit 1d65641

File tree

5 files changed

+120
-1
lines changed

5 files changed

+120
-1
lines changed

get.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,18 @@ func (s Style) GetTransform() func(string) string {
415415
return s.getAsTransform(transformKey)
416416
}
417417

418+
// GetHyperlink returns the hyperlink along with its parameters. If no
419+
// hyperlink is set, empty strings are returned.
420+
func (s Style) GetHyperlink() (link, params string) {
421+
if s.isSet(linkKey) {
422+
link = s.link
423+
}
424+
if s.isSet(linkParamsKey) {
425+
params = s.linkParams
426+
}
427+
return
428+
}
429+
418430
// Returns whether or not the given property is set.
419431
func (s Style) isSet(k propKey) bool {
420432
return s.props.has(k)

set.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package lipgloss
22

3-
import "image/color"
3+
import (
4+
"image/color"
5+
"strings"
6+
)
47

58
// Set a value on the underlying rules map.
69
func (s *Style) set(key propKey, value interface{}) {
@@ -67,6 +70,10 @@ func (s *Style) set(key propKey, value interface{}) {
6770
s.tabWidth = value.(int)
6871
case transformKey:
6972
s.transform = value.(func(string) string)
73+
case linkKey:
74+
s.link = value.(string)
75+
case linkParamsKey:
76+
s.linkParams = value.(string)
7077
default:
7178
if v, ok := value.(bool); ok { //nolint:nestif
7279
if v {
@@ -688,6 +695,21 @@ func (s Style) Transform(fn func(string) string) Style {
688695
return s
689696
}
690697

698+
// Hyperlink sets a hyperlink on a style. This is useful for rendering text that
699+
// can be clicked on in a terminal emulator that supports hyperlinks.
700+
//
701+
// Example:
702+
//
703+
// s := lipgloss.NewStyle().Hyperlink("https://charm.sh")
704+
// s := lipgloss.NewStyle().Hyperlink("https://charm.sh", "id=1")
705+
func (s Style) Hyperlink(link string, params ...string) Style {
706+
s.set(linkKey, link)
707+
if len(params) > 0 {
708+
s.set(linkParamsKey, strings.Join(params, ":"))
709+
}
710+
return s
711+
}
712+
691713
// whichSidesInt is a helper method for setting values on sides of a block based
692714
// on the number of arguments. It follows the CSS shorthand rules for blocks
693715
// like margin, padding. and borders. Here are how the rules work:

style.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ const (
7575
tabWidthKey
7676

7777
transformKey
78+
79+
// Hyperlink.
80+
linkKey
81+
linkParamsKey
7882
)
7983

8084
// props is a set of properties.
@@ -107,6 +111,9 @@ type Style struct {
107111
props props
108112
value string
109113

114+
// hyperlink
115+
link, linkParams string
116+
110117
// we store bool props values here
111118
attrs int
112119

@@ -271,6 +278,8 @@ func (s Style) Render(strs ...string) string {
271278
useSpaceStyler = (underline && !underlineSpaces) || (strikethrough && !strikethroughSpaces) || underlineSpaces || strikethroughSpaces
272279

273280
transform = s.getAsTransform(transformKey)
281+
282+
link, linkParams = s.GetHyperlink()
274283
)
275284

276285
if transform != nil {
@@ -379,6 +388,10 @@ func (s Style) Render(strs ...string) string {
379388
}
380389

381390
str = b.String()
391+
392+
if len(link) > 0 {
393+
str = ansi.SetHyperlink(link, linkParams) + str + ansi.ResetHyperlink()
394+
}
382395
}
383396

384397
// Padding

style_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,67 @@ func TestHeight(t *testing.T) {
562562
}
563563
}
564564
}
565+
566+
func TestHyperlink(t *testing.T) {
567+
tests := []struct {
568+
name string
569+
style Style
570+
expected string
571+
}{
572+
{
573+
name: "hyperlink",
574+
style: NewStyle().Hyperlink("https://example.com").SetString("https://example.com"),
575+
expected: "\x1b]8;;https://example.com\x07https://example.com\x1b]8;;\x07",
576+
},
577+
{
578+
name: "hyperlink with text",
579+
style: NewStyle().Hyperlink("https://example.com", "id=123").SetString("example"),
580+
expected: "\x1b]8;id=123;https://example.com\x07example\x1b]8;;\x07",
581+
},
582+
{
583+
name: "hyperlink with text and style",
584+
style: NewStyle().Hyperlink("https://example.com", "id=123").SetString("example").
585+
Bold(true).Foreground(Color("234")),
586+
expected: "\x1b]8;id=123;https://example.com\x07\x1b[1;38;5;234mexample\x1b[m\x1b]8;;\x07",
587+
},
588+
}
589+
for _, tc := range tests {
590+
t.Run(tc.name, func(t *testing.T) {
591+
if tc.style.String() != tc.expected {
592+
t.Fatalf("got: %q, want: %q", tc.style.String(), tc.expected)
593+
}
594+
})
595+
}
596+
}
597+
598+
func TestUnsetHyperlink(t *testing.T) {
599+
tests := []struct {
600+
name string
601+
style Style
602+
expected string
603+
}{
604+
{
605+
name: "unset hyperlink",
606+
style: NewStyle().Hyperlink("https://example.com").SetString("https://example.com").UnsetHyperlink(),
607+
expected: "https://example.com",
608+
},
609+
{
610+
name: "unset hyperlink with text",
611+
style: NewStyle().Hyperlink("https://example.com", "id=123").SetString("example").UnsetHyperlink(),
612+
expected: "example",
613+
},
614+
{
615+
name: "unset hyperlink with text and style",
616+
style: NewStyle().Hyperlink("https://example.com", "id=123").SetString("example").
617+
Bold(true).Foreground(Color("234")).UnsetHyperlink(),
618+
expected: "\x1b[1;38;5;234mexample\x1b[m",
619+
},
620+
}
621+
for _, tc := range tests {
622+
t.Run(tc.name, func(t *testing.T) {
623+
if tc.style.String() != tc.expected {
624+
t.Fatalf("got: %q, want: %q", tc.style.String(), tc.expected)
625+
}
626+
})
627+
}
628+
}

unset.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,14 @@ func (s Style) UnsetTransform() Style {
324324
return s
325325
}
326326

327+
// UnsetHyperlink removes the value set by Hyperlink.
328+
func (s Style) UnsetHyperlink() Style {
329+
s.unset(linkKey)
330+
s.unset(linkParamsKey)
331+
s.link, s.linkParams = "", "" // save memory
332+
return s
333+
}
334+
327335
// UnsetString sets the underlying string value to the empty string.
328336
func (s Style) UnsetString() Style {
329337
s.value = ""

0 commit comments

Comments
 (0)