Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/cmd/gist/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ func printTable(io *iostreams.IOStreams, gists []shared.Gist, filter *regexp.Reg
tp.AddField(gist.ID)
tp.AddField(
text.RemoveExcessiveWhitespace(description),
tableprinter.WithColor(highlightDescription),
tableprinter.WithColor(cs.WithHyperlink(gist.HTMLURL, highlightDescription)),
)
Comment on lines 199 to 203
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.
tp.AddField(
text.Pluralize(fileCount, "file"),
Expand Down
2 changes: 2 additions & 0 deletions pkg/cmd/gist/shared/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func ListGists(client *http.Client, hostname string, limit int, filter *regexp.R
IsPublic bool
Name string
UpdatedAt time.Time
URL string
}
PageInfo struct {
HasNextPage bool
Expand Down Expand Up @@ -174,6 +175,7 @@ pagination:
Files: files,
UpdatedAt: gist.UpdatedAt,
Public: gist.IsPublic,
HTMLURL: gist.URL,
}

if filter == nil || filterFunc(&gist) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/issue/shared/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ func PrintIssues(io *iostreams.IOStreams, now time.Time, prefix string, totalCou
issueNum = "#" + issueNum
}
issueNum = prefix + issueNum
table.AddField(issueNum, tableprinter.WithColor(cs.ColorFromString(prShared.ColorForIssueState(issue))))
table.AddField(issueNum, tableprinter.WithColor(cs.WithHyperlink(issue.URL,
cs.ColorFromString(prShared.ColorForIssueState(issue)))))
if !isTTY {
table.AddField(issue.State)
}
Expand Down
12 changes: 9 additions & 3 deletions pkg/cmd/pr/checks/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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
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.
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/pr/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ func listRun(opts *ListOptions) error {
prNum = "#" + prNum
}

table.AddField(prNum, tableprinter.WithColor(cs.ColorFromString(shared.ColorForPRState(pr))))
table.AddField(prNum, tableprinter.WithColor(cs.WithHyperlink(pr.URL,
cs.ColorFromString(shared.ColorForPRState(pr)))))
table.AddField(text.RemoveExcessiveWhitespace(pr.Title))
table.AddField(pr.HeadLabel(), tableprinter.WithColor(cs.Cyan))
if !isTTY {
Expand Down
5 changes: 4 additions & 1 deletion pkg/cmd/repo/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ func listRun(opts *ListOptions) error {
if features.VisibilityField {
fields = append(defaultFields, "visibility")
}
if opts.IO.IsLinkEnabled() {
fields = append(fields, "url")
}

filter := FilterOptions{
Visibility: opts.Visibility,
Expand Down Expand Up @@ -192,7 +195,7 @@ func listRun(opts *ListOptions) error {
t = &repo.CreatedAt
}

tp.AddField(repo.NameWithOwner, tableprinter.WithColor(cs.Bold))
tp.AddField(repo.NameWithOwner, tableprinter.WithColor(cs.WithHyperlink(repo.URL, cs.Bold)))
tp.AddField(text.RemoveExcessiveWhitespace(repo.Description))
tp.AddField(info, tableprinter.WithColor(infoColor))
tp.AddTimeField(opts.Now(), *t, cs.Muted)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/search/commits/commits.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ func displayResults(io *iostreams.IOStreams, now time.Time, results search.Commi
cs := io.ColorScheme()
tp := tableprinter.New(io, tableprinter.WithHeader("Repo", "SHA", "Message", "Author", "Created"))
for _, commit := range results.Items {
tp.AddField(commit.Repo.FullName)
tp.AddField(commit.Sha)
tp.AddField(commit.Repo.FullName, tableprinter.WithColor(cs.WithHyperlink(commit.Repo.URL, nil)))
tp.AddField(commit.Sha, tableprinter.WithColor(cs.WithHyperlink(commit.URL, nil)))
tp.AddField(text.RemoveExcessiveWhitespace(commit.Info.Message))
tp.AddField(commit.Author.Login)
tp.AddTimeField(now, commit.Info.Author.Date, cs.Muted)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/search/repos/repos.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func displayResults(io *iostreams.IOStreams, now time.Time, results search.Repos
if repo.IsPrivate {
infoColor = cs.Yellow
}
tp.AddField(repo.FullName, tableprinter.WithColor(cs.Bold))
tp.AddField(repo.FullName, tableprinter.WithColor(cs.WithHyperlink(repo.URL, cs.Bold)))
tp.AddField(text.RemoveExcessiveWhitespace(repo.Description))
tp.AddField(info, tableprinter.WithColor(infoColor))
tp.AddTimeField(now, repo.UpdatedAt, cs.Muted)
Expand Down
6 changes: 4 additions & 2 deletions pkg/cmd/search/shared/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,12 @@ func displayIssueResults(io *iostreams.IOStreams, now time.Time, et EntityType,
issueNum = "#" + issueNum
}
if issue.IsPullRequest() {
color := tableprinter.WithColor(cs.ColorFromString(colorForPRState(issue.State())))
color := tableprinter.WithColor(cs.WithHyperlink(issue.URL,
cs.ColorFromString(colorForPRState(issue.State()))))
tp.AddField(issueNum, color)
} else {
color := tableprinter.WithColor(cs.ColorFromString(colorForIssueState(issue.State(), issue.StateReason)))
color := tableprinter.WithColor(cs.WithHyperlink(issue.URL,
cs.ColorFromString(colorForIssueState(issue.State(), issue.StateReason))))
tp.AddField(issueNum, color)
}
if !tp.IsTTY() {
Expand Down
34 changes: 34 additions & 0 deletions pkg/iostreams/color.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
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.
}

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))
Comment on lines +293 to +295
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 +267 to +297
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.

// Label stylizes text based on label's RGB hex color.
func (c *ColorScheme) Label(hex string, x string) string {
if !c.Enabled || !c.TrueColor || !c.ColorLabels || len(hex) != 6 {
Expand Down
7 changes: 7 additions & 0 deletions pkg/iostreams/iostreams.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type IOStreams struct {
colorEnabled bool
colorLabels bool
accessibleColorsEnabled bool
linkEnabled bool

pagerCommand string
pagerProcess *os.Process
Expand Down Expand Up @@ -110,6 +111,10 @@ func (s *IOStreams) ColorLabels() bool {
return s.colorLabels
}

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.
// DetectTerminalTheme is a utility to call before starting the output pager so that the terminal background
// can be reliably detected.
func (s *IOStreams) DetectTerminalTheme() {
Expand Down Expand Up @@ -424,6 +429,7 @@ func (s *IOStreams) ColorScheme() *ColorScheme {
TrueColor: s.HasTrueColor(),
Accessible: s.AccessibleColorsEnabled(),
ColorLabels: s.ColorLabels(),
linkEnabled: s.IsLinkEnabled(),
Theme: s.TerminalTheme(),
}
}
Expand Down Expand Up @@ -495,6 +501,7 @@ func System() *IOStreams {
In: os.Stdin,
Out: stdout,
ErrOut: stderr,
linkEnabled: os.Getenv("GH_HYPERLINK") != "",
pagerCommand: os.Getenv("PAGER"),
term: &terminal,
}
Expand Down
Loading