Skip to content

Add supports for terminal hyperlinks in output (v2)#12682

Open
xzfc wants to merge 3 commits intocli:trunkfrom
xzfc:terminal-hyperlinks
Open

Add supports for terminal hyperlinks in output (v2)#12682
xzfc wants to merge 3 commits intocli:trunkfrom
xzfc:terminal-hyperlinks

Conversation

@xzfc
Copy link

@xzfc xzfc commented Feb 14, 2026

This is a resurrection of #3309.

Why revisit this PR again

The previous PR got closed due to lack of support from terminals. Situation improved since then.

Chronology: In 2021 and 2022 the team decided not to merge it due to lack of support in terminal emulators. In 2024, @williammartin suggested that it could be revisited.

Referring to points from the original PR:

Details

In this PR, hyperlinks are enabled when GH_HYPERLINK is non-empty. When enabled, the following arbitrarily chosen1 tables are updated:

  • gh pr checks: NAME is link to the job; URL is removed
  • gh issue/pr list: ID is link to the issue/pr
  • gh gist list: DESCRIPTION is link to the gist
  • gh repo list: NAME is link to the repo
  • gh search commits: REPO and SHA are links to the repo and commit
  • gh search issues/prs: ID is link to the issue/pr
  • gh search repos: REPO is link to the repo

Footnotes

  1. I think there are still a lot of remaining tables that could be augmented by hyperlinks.

xzfc and others added 3 commits February 14, 2026 01:46
In supporting terminals (e.g. iTerm), the identifier field of
table-based output like `issue/pr list` and `pr checks` is now a link
that can be followed in a browser by Cmd-clicking it.

This commit is a modernized version of cli#3309/
ca3f730.

The following commands have updated:
- `gh pr checks`: `NAME` is link to the job; `URL` is removed
- `gh issue/pr list`: `ID` is link to the issue/pr

Co-authored-by: Mislav Marohnić <[email protected]>
The following commands have updated:
- `gh gist list`: `DESCRIPTION` is link to the gist
- `gh repo list`: `NAME` is link to the repo
- `gh search commits`: `REPO` and `SHA` are links to the repo and commit
- `gh search issues/prs`: `ID` is link to the issue/pr
- `gh search repos`: `REPO` is link to the repo
@xzfc xzfc requested a review from a team as a code owner February 14, 2026 01:51
@xzfc xzfc temporarily deployed to cli-automation February 14, 2026 01:51 — with GitHub Actions Inactive
@cliAutomation cliAutomation added the external pull request originating outside of the CLI core team label Feb 14, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds opt-in OSC 8 terminal hyperlink support across several table-based command outputs, gated by the GH_HYPERLINK environment variable.

Changes:

  • Introduces linkEnabled on IOStreams / ColorScheme and adds hyperlink helper methods.
  • Updates multiple table renderers (PR checks, PR/issue list, repo list, search results, gist list) to render key columns as hyperlinks and, in some cases, remove redundant URL columns.
  • Extends gist GraphQL query mapping to include the gist URL for hyperlinking.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pkg/iostreams/iostreams.go Adds linkEnabled to IOStreams, getter, and wires env var GH_HYPERLINK into system IOStreams.
pkg/iostreams/color.go Adds OSC 8 hyperlink rendering helpers to ColorScheme.
pkg/cmd/search/shared/shared.go Makes issue/PR IDs clickable in search results when enabled.
pkg/cmd/search/repos/repos.go Makes repo names clickable in repo search results.
pkg/cmd/search/commits/commits.go Makes repo and SHA clickable in commit search results.
pkg/cmd/repo/list/list.go Makes repo names clickable in gh repo list and conditionally fetches url.
pkg/cmd/pr/list/list.go Makes PR IDs clickable in gh pr list.
pkg/cmd/pr/checks/output.go Makes check NAME clickable and drops URL column in TTY output when enabled.
pkg/cmd/issue/shared/display.go Makes issue IDs clickable in issue list output.
pkg/cmd/gist/shared/shared.go Fetches gist URL via GraphQL for hyperlinking in list output.
pkg/cmd/gist/list/list.go Makes gist description clickable in gh gist list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +293 to +295
// Call c.Hyperlink first, then colorize.
// Otherwise space-trimming logic in c.Hyperlink wouldn't work.
return colorize(c.Hyperlink(text, url))
Copy link

Copilot AI Feb 14, 2026

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.

Suggested change
// 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 uses AI. Check for mistakes.
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)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Go style in this package uses camelCase for local variables; link_text and trailing_spaces stand out and make the new logic harder to read/grep consistently. Please rename to linkText / trailingSpaces (and similar) for idiomatic Go.

Suggested change
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 uses AI. Check for mistakes.
Comment on lines +267 to +297
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)
}

func (c *ColorScheme) WithHyperlink(url string, colorize func(string) string) func(string) string {
if colorize == nil {
colorize = func(s string) string { return s }
}
if !c.linkEnabled {
return colorize
}
return func(text string) string {
// Call c.Hyperlink first, then colorize.
// Otherwise space-trimming logic in c.Hyperlink wouldn't work.
return colorize(c.Hyperlink(text, url))
}
}
Copy link

Copilot AI Feb 14, 2026

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).

Copilot uses AI. Check for mistakes.
Comment on lines 199 to 203
tp.AddField(gist.ID)
tp.AddField(
text.RemoveExcessiveWhitespace(description),
tableprinter.WithColor(highlightDescription),
tableprinter.WithColor(cs.WithHyperlink(gist.HTMLURL, highlightDescription)),
)
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

highlightDescription uses highlightMatch (regex + index-based slicing) to produce colored output. Passing it to cs.WithHyperlink means the hyperlink escape sequence is injected before highlighting runs, which can break match highlighting and may corrupt the OSC 8 sequence. After adjusting WithHyperlink, ensure this call site highlights the plain description first (or otherwise avoids running index-based transforms on already-escaped text).

Copilot uses AI. Check for mistakes.
func (s *IOStreams) IsLinkEnabled() bool {
return s.linkEnabled
}

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

IsLinkEnabled is currently read-only, and iostreams.Test() doesn’t set linkEnabled from GH_HYPERLINK. That makes it difficult for command-level tests (outside the iostreams package) to enable and assert hyperlink output. Consider adding a SetLinkEnabled(bool) helper (similar to SetColorEnabled) or having Test() honor the env var so the new behavior can be exercised in existing command tests.

Suggested change
func (s *IOStreams) SetLinkEnabled(enabled bool) {
s.linkEnabled = enabled
}

Copilot uses AI. Check for mistakes.
Comment on lines 97 to 106
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"}
}
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

When IsLinkEnabled() is true, the TTY table drops the URL column and turns NAME into a hyperlink. There are extensive tests for the TTY output in this command, but none cover the link-enabled header/row shape; please add a test case that enables hyperlinks and asserts the URL column is omitted and NAME contains the OSC 8 sequence.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external pull request originating outside of the CLI core team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants