-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Add supports for terminal hyperlinks in output (v2) #12682
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
base: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ import ( | |
|
|
||
| func addRow(tp *tableprinter.TablePrinter, io *iostreams.IOStreams, o check) { | ||
| cs := io.ColorScheme() | ||
| isLinkEnabled := io.IsLinkEnabled() | ||
| elapsed := "" | ||
|
|
||
| if !o.StartedAt.IsZero() && !o.CompletedAt.IsZero() { | ||
|
|
@@ -43,10 +44,12 @@ func addRow(tp *tableprinter.TablePrinter, io *iostreams.IOStreams, o check) { | |
| name += fmt.Sprintf(" (%s)", o.Event) | ||
| } | ||
| tp.AddField(mark, tableprinter.WithColor(markColor)) | ||
| tp.AddField(name) | ||
| tp.AddField(name, tableprinter.WithColor(cs.WithHyperlink(o.Link, nil))) | ||
| tp.AddField(o.Description) | ||
| tp.AddField(elapsed) | ||
| tp.AddField(o.Link) | ||
| if !isLinkEnabled { | ||
| tp.AddField(o.Link) | ||
| } | ||
| } else { | ||
| tp.AddField(o.Name) | ||
| if o.Bucket == "cancel" { | ||
|
|
@@ -94,7 +97,10 @@ func printSummary(io *iostreams.IOStreams, counts checkCounts) { | |
| func printTable(io *iostreams.IOStreams, checks []check) error { | ||
| var headers []string | ||
| if io.IsStdoutTTY() { | ||
| headers = []string{"", "NAME", "DESCRIPTION", "ELAPSED", "URL"} | ||
| headers = []string{"", "NAME", "DESCRIPTION", "ELAPSED"} | ||
| if !io.IsLinkEnabled() { | ||
| headers = append(headers, "URL") | ||
| } | ||
| } else { | ||
| headers = []string{"NAME", "STATUS", "ELAPSED", "URL", "DESCRIPTION"} | ||
| } | ||
|
Comment on lines
97
to
106
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -53,6 +53,8 @@ type ColorScheme struct { | |||||||||||||||||||||||||||||||||||||||||
| Accessible bool | ||||||||||||||||||||||||||||||||||||||||||
| // ColorLabels is whether labels are colored based on their truecolor RGB hex color. | ||||||||||||||||||||||||||||||||||||||||||
| ColorLabels bool | ||||||||||||||||||||||||||||||||||||||||||
| // linkEnabled is whether terminal hyperlinks should be rendered. | ||||||||||||||||||||||||||||||||||||||||||
| linkEnabled bool | ||||||||||||||||||||||||||||||||||||||||||
| // Theme is the terminal background color theme used to contextually color text for light, dark, or none at all. | ||||||||||||||||||||||||||||||||||||||||||
| Theme string | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -262,6 +264,38 @@ func (c *ColorScheme) ColorFromString(s string) func(string) string { | |||||||||||||||||||||||||||||||||||||||||
| return fn | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func (c *ColorScheme) Hyperlink(text, url string) string { | ||||||||||||||||||||||||||||||||||||||||||
| if !c.linkEnabled { | ||||||||||||||||||||||||||||||||||||||||||
| return text | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Make trailing spaces not to be part of the link as it looks ugly, ... | ||||||||||||||||||||||||||||||||||||||||||
| link_text := strings.TrimRight(text, " ") | ||||||||||||||||||||||||||||||||||||||||||
| trailing_spaces := text[len(link_text):] | ||||||||||||||||||||||||||||||||||||||||||
| if link_text == "" { | ||||||||||||||||||||||||||||||||||||||||||
| // ... but still allow spaces-only text to be clickable. | ||||||||||||||||||||||||||||||||||||||||||
| link_text = text | ||||||||||||||||||||||||||||||||||||||||||
| trailing_spaces = "" | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda | ||||||||||||||||||||||||||||||||||||||||||
| return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\%s", url, link_text, trailing_spaces) | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+273
to
+282
|
||||||||||||||||||||||||||||||||||||||||||
| link_text := strings.TrimRight(text, " ") | |
| trailing_spaces := text[len(link_text):] | |
| if link_text == "" { | |
| // ... but still allow spaces-only text to be clickable. | |
| link_text = text | |
| trailing_spaces = "" | |
| } | |
| // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda | |
| return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\%s", url, link_text, trailing_spaces) | |
| linkText := strings.TrimRight(text, " ") | |
| trailingSpaces := text[len(linkText):] | |
| if linkText == "" { | |
| // ... but still allow spaces-only text to be clickable. | |
| linkText = text | |
| trailingSpaces = "" | |
| } | |
| // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda | |
| return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\%s", url, linkText, trailingSpaces) |
Copilot
AI
Feb 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WithHyperlink currently calls Hyperlink before invoking the provided colorize function. This is unsafe for colorizers that transform the input based on string indices/content (e.g., highlightMatch in gist list), because the injected OSC 8 bytes will shift indices and can corrupt both highlighting and the hyperlink escape sequence. Consider restructuring so you first split/truncate trailing padding from the original text, then apply colorize to the non-padding portion, and finally wrap that result in the OSC 8 sequence while appending the original trailing spaces outside the link.
| // Call c.Hyperlink first, then colorize. | |
| // Otherwise space-trimming logic in c.Hyperlink wouldn't work. | |
| return colorize(c.Hyperlink(text, url)) | |
| // Make trailing spaces not to be part of the link as it looks ugly, ... | |
| link_text := strings.TrimRight(text, " ") | |
| trailing_spaces := text[len(link_text):] | |
| if link_text == "" { | |
| // ... but still allow spaces-only text to be clickable. | |
| link_text = text | |
| trailing_spaces = "" | |
| } | |
| // Apply colorization only to the part that will be inside the hyperlink, | |
| // so index-based colorizers operate on the original, unmodified text. | |
| colored_link_text := colorize(link_text) | |
| // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda | |
| return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\%s", url, colored_link_text, trailing_spaces) |
Copilot
AI
Feb 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New hyperlink rendering logic in Hyperlink/WithHyperlink isn’t covered by unit tests. Since pkg/iostreams/color_test.go already exists, please add tests for: links disabled (returns input), links enabled (OSC 8 wrapper), and the trailing-space handling (spaces excluded from link but preserved in output).
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -75,6 +75,7 @@ type IOStreams struct { | |||||||||||
| colorEnabled bool | ||||||||||||
| colorLabels bool | ||||||||||||
| accessibleColorsEnabled bool | ||||||||||||
| linkEnabled bool | ||||||||||||
|
|
||||||||||||
| pagerCommand string | ||||||||||||
| pagerProcess *os.Process | ||||||||||||
|
|
@@ -110,6 +111,10 @@ func (s *IOStreams) ColorLabels() bool { | |||||||||||
| return s.colorLabels | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func (s *IOStreams) IsLinkEnabled() bool { | ||||||||||||
| return s.linkEnabled | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
|
||||||||||||
| func (s *IOStreams) SetLinkEnabled(enabled bool) { | |
| s.linkEnabled = enabled | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
highlightDescriptionuseshighlightMatch(regex + index-based slicing) to produce colored output. Passing it tocs.WithHyperlinkmeans the hyperlink escape sequence is injected before highlighting runs, which can break match highlighting and may corrupt the OSC 8 sequence. After adjustingWithHyperlink, ensure this call site highlights the plain description first (or otherwise avoids running index-based transforms on already-escaped text).