From 9d3e8e40df91a9876382fd84bba985c36c3f5a79 Mon Sep 17 00:00:00 2001 From: drew Date: Thu, 13 Aug 2026 15:16:46 +0400 Subject: [PATCH] feat: hyperlinks Signed-off-by: drew --- go.sum | 2 -- main.go | 9 +++++++-- ui/pager.go | 6 ++++++ utils/utils.go | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/go.sum b/go.sum index 52a572a..5dc57a1 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ charm.land/bubbles/v2 v2.1.1 h1:7r55WzBxpo/R3z98hGmY7KKPd3ET6vsf0Fb9sDHOV60= charm.land/bubbles/v2 v2.1.1/go.mod h1:GE6M31gaWZVXzGw73OeuTTgy4lX+OtkH0E5ymnNsHxo= charm.land/bubbletea/v2 v2.0.8 h1:SxTJMhCAI3lbPmy4SgX5LWZ24AdINr4I6UEqzZvYJuY= charm.land/bubbletea/v2 v2.0.8/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss= -charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c= -charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k= charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ= charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= diff --git a/main.go b/main.go index cfcf1a8..d4f885b 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "mvdan.cc/sh/v3/shell" "charm.land/glamour/v2" + glamsi "charm.land/glamour/v2/ansi" "charm.land/glamour/v2/styles" "charm.land/glow/v3/ui" "charm.land/glow/v3/utils" @@ -290,12 +291,16 @@ func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error { isCode := !utils.IsMarkdownFile(src.URL) // initialize glamour - r, err := glamour.NewTermRenderer( + options := []glamour.TermRendererOption{ utils.GlamourStyle(style, isCode), glamour.WithWordWrap(int(width)), //nolint:gosec glamour.WithBaseURL(baseURL), glamour.WithPreservedNewLines(), - ) + } + if utils.SupportsHyperlinks(os.Environ()) { + options = append(options, glamour.WithHyperlinkMode(glamsi.HyperlinkModeInline)) + } + r, err := glamour.NewTermRenderer(options...) if err != nil { return fmt.Errorf("unable to create renderer: %w", err) } diff --git a/ui/pager.go b/ui/pager.go index 62816b4..ba10ed8 100644 --- a/ui/pager.go +++ b/ui/pager.go @@ -3,6 +3,7 @@ package ui import ( "fmt" "math" + "os" "path/filepath" "strings" "time" @@ -10,6 +11,7 @@ import ( "charm.land/bubbles/v2/viewport" tea "charm.land/bubbletea/v2" "charm.land/glamour/v2" + glamsi "charm.land/glamour/v2/ansi" "charm.land/glow/v3/utils" "charm.land/lipgloss/v2" "github.com/atotto/clipboard" @@ -369,6 +371,10 @@ func glamourRender(m pagerModel, markdown string) (string, error) { glamour.WithWordWrap(width), } + if utils.SupportsHyperlinks(os.Environ()) { + options = append(options, glamour.WithHyperlinkMode(glamsi.HyperlinkModeInline)) + } + if m.common.cfg.PreserveNewLines { options = append(options, glamour.WithPreservedNewLines()) } diff --git a/utils/utils.go b/utils/utils.go index 7690b07..1842de6 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -111,3 +111,39 @@ func GlamourStyle(style string, isCode bool) glamour.TermRendererOption { return glamour.WithStyles(styleConfig) } + +// SupportsHyperlinks reports whether the terminal is known to support OSC 8 +// hyperlinks, based on environment variables. +func SupportsHyperlinks(env []string) bool { + get := func(key string) string { + for _, e := range env { + if k, v, ok := strings.Cut(e, "="); ok && k == key { + return v + } + } + return "" + } + + // Explicit opt-out. + if get("TERM_PROGRAM") == "Apple_Terminal" { + return false + } + // Terminals known to support OSC 8 hyperlinks. + switch get("TERM_PROGRAM") { + case "iTerm.app", "WezTerm", "kitty", "alacritty", "Hyper", "ghostty", "vscode", "Tabby", " Rio": + return true + } + if get("WT_SESSION") != "" { // Windows Terminal + return true + } + if get("KONSOLE_VERSION") != "" { // Konsole + return true + } + if get("VTE_VERSION") != "" { // GNOME Terminal, Tilix, etc. + return true + } + if get("TERM") == "xterm-kitty" { + return true + } + return false +}