diff --git a/get.go b/get.go index 9c2f06fe..aa41c001 100644 --- a/get.go +++ b/get.go @@ -414,6 +414,18 @@ func (s Style) GetTransform() func(string) string { return s.getAsTransform(transformKey) } +// GetHyperlink returns the hyperlink URL set on the style. If no hyperlink is +// set the empty string and nil is returned. +func (s Style) GetHyperlink() string { + return s.getAsHyperlinkURL(hyperlinkURLKey) +} + +// GetHyperlink returns the hyperlink URL set on the style. If no hyperlink is +// set the empty string and nil is returned. +func (s Style) GetHyperlinkParams() map[string]string { + return s.getAsHyperlinkParams(hyperlinkParamsKey) +} + // Returns whether or not the given property is set. func (s Style) isSet(k propKey) bool { return s.props.has(k) @@ -519,13 +531,29 @@ func (s Style) getBorderStyle() Border { return s.borderStyle } -func (s Style) getAsTransform(propKey) func(string) string { - if !s.isSet(transformKey) { +func (s Style) getAsTransform(k propKey) func(string) string { + if !s.isSet(k) { return nil } return s.transform } +func (s Style) getAsHyperlinkURL(k propKey) string { + if !s.isSet(k) { + return "" + } + + return s.hyperlinkURL +} + +func (s Style) getAsHyperlinkParams(k propKey) map[string]string { + if !s.isSet(k) { + return nil + } + + return decodeHyperlinkParams(s.hyperlinkParams) +} + // Split a string into lines, additionally returning the size of the widest // line. func getLines(s string) (lines []string, widest int) { @@ -540,3 +568,38 @@ func getLines(s string) (lines []string, widest int) { return lines, widest } + +func encodeHyperlinkParams(m map[string]string) string { + if len(m) == 0 { + return "" + } + + var parts []string + for k, v := range m { + parts = append(parts, k+"="+v) + } + + return strings.Join(parts, ";") +} + +func decodeHyperlinkParams(s string) map[string]string { + if len(s) == 0 { + return nil + } + + parts := strings.Split(s, ";") + if len(parts) == 0 { + return nil + } + + m := make(map[string]string) + for _, p := range parts { + kv := strings.Split(p, "=") + if len(kv) != 2 { + continue + } + m[kv[0]] = kv[1] + } + + return m +} diff --git a/set.go b/set.go index ed6e272c..ada43e26 100644 --- a/set.go +++ b/set.go @@ -65,6 +65,10 @@ func (s *Style) set(key propKey, value interface{}) { s.tabWidth = value.(int) case transformKey: s.transform = value.(func(string) string) + case hyperlinkURLKey: + s.hyperlinkURL = value.(string) + case hyperlinkParamsKey: + s.hyperlinkParams = value.(string) default: if v, ok := value.(bool); ok { //nolint:nestif if v { @@ -145,6 +149,10 @@ func (s *Style) setFrom(key propKey, i Style) { s.set(tabWidthKey, i.tabWidth) case transformKey: s.set(transformKey, i.transform) + case hyperlinkURLKey: + s.set(hyperlinkURLKey, i.hyperlinkURL) + case hyperlinkParamsKey: + s.set(hyperlinkParamsKey, i.hyperlinkParams) default: // Set attributes for set bool properties s.set(key, i.attrs) @@ -685,6 +693,35 @@ func (s Style) Transform(fn func(string) string) Style { return s } +// Hyperlink sets a hyperlink on the style. You're responsible for setting +// a style on the on the text so that it visually identifiable as a hyperlink +// (or not). +// +// Note that hyperlinks are not available in all terminal emulators and there's +// a no way to detect if hyperlinks are supported. If hyperlinks are not +// the text will still render as normal. +// +// Hyperlinks in terminal emulators can optionally contain parameters. To set +// those see [Style.hyperlinkParams]. +func (s Style) Hyperlink(url string) Style { + if url == "" { + return s + } + + s.set(hyperlinkURLKey, url) + return s +} + +// HyperlinkParams sets parameters for a hyperlink, such as an ID. If no +// URL is set via [Style.Hyperlink], hyperlink parameters will be ignored. +func (s Style) HyperlinkParams(p map[string]string) Style { + if p == nil { + return s + } + s.set(hyperlinkParamsKey, encodeHyperlinkParams(p)) + return s +} + // Renderer sets the renderer for the style. This is useful for changing the // renderer for a style that is being used in a different context. func (s Style) Renderer(r *Renderer) Style { diff --git a/style.go b/style.go index 28ddccbe..d9c44475 100644 --- a/style.go +++ b/style.go @@ -75,6 +75,9 @@ const ( tabWidthKey transformKey + + hyperlinkURLKey + hyperlinkParamsKey ) // props is a set of properties. @@ -157,6 +160,9 @@ type Style struct { tabWidth int transform func(string) string + + hyperlinkURL string + hyperlinkParams string } // joinString joins a list of strings into a single string separated with a @@ -283,12 +289,23 @@ func (s Style) Render(strs ...string) string { useSpaceStyler = (underline && !underlineSpaces) || (strikethrough && !strikethroughSpaces) || underlineSpaces || strikethroughSpaces transform = s.getAsTransform(transformKey) + + hyperlinkURL = s.getAsHyperlinkURL(hyperlinkURLKey) + hyperlinkParams = s.getAsHyperlinkParams(hyperlinkParamsKey) ) if transform != nil { str = transform(str) } + if hyperlinkURL != "" { + var params []string + for k, v := range hyperlinkParams { + params = append(params, k, v) + } + str = ansi.SetHyperlink(hyperlinkURL, params...) + str + } + if s.props == 0 { return s.maybeConvertTabs(str) } @@ -465,6 +482,10 @@ func (s Style) Render(strs ...string) string { } } + if hyperlinkURL != "" { + str += ansi.ResetHyperlink() + } + return str }