1package common
2
3import (
4 "slices"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8 "github.com/charmbracelet/colorprofile"
9 uv "github.com/charmbracelet/ultraviolet"
10 "github.com/charmbracelet/x/ansi"
11 xstrings "github.com/charmbracelet/x/exp/strings"
12)
13
14// Capabilities define different terminal capabilities supported.
15type Capabilities struct {
16 // Profile is the terminal color profile used to determine how colors are
17 // rendered.
18 Profile colorprofile.Profile
19 // Columns is the number of character columns in the terminal.
20 Columns int
21 // Rows is the number of character rows in the terminal.
22 Rows int
23 // PixelX is the width of the terminal in pixels.
24 PixelX int
25 // PixelY is the height of the terminal in pixels.
26 PixelY int
27 // KittyGraphics indicates whether the terminal supports the Kitty graphics
28 // protocol.
29 KittyGraphics bool
30 // SixelGraphics indicates whether the terminal supports Sixel graphics.
31 SixelGraphics bool
32 // Env is the terminal environment variables.
33 Env uv.Environ
34 // TerminalVersion is the terminal version string.
35 TerminalVersion string
36 // ReportFocusEvents indicates whether the terminal supports focus events.
37 ReportFocusEvents bool
38}
39
40// Update updates the capabilities based on the given message.
41func (c *Capabilities) Update(msg any) {
42 switch m := msg.(type) {
43 case tea.EnvMsg:
44 c.Env = uv.Environ(m)
45 case tea.ColorProfileMsg:
46 c.Profile = m.Profile
47 case tea.WindowSizeMsg:
48 c.Columns = m.Width
49 c.Rows = m.Height
50 case uv.WindowPixelSizeEvent:
51 c.PixelX = m.Width
52 c.PixelY = m.Height
53 case uv.KittyGraphicsEvent:
54 c.KittyGraphics = true
55 case uv.PrimaryDeviceAttributesEvent:
56 if slices.Contains(m, 4) {
57 c.SixelGraphics = true
58 }
59 case tea.TerminalVersionMsg:
60 c.TerminalVersion = m.Name
61 case uv.ModeReportEvent:
62 switch m.Mode {
63 case ansi.ModeFocusEvent:
64 c.ReportFocusEvents = modeSupported(m.Value)
65 }
66 }
67}
68
69// QueryCmd returns a [tea.Cmd] that queries the terminal for different
70// capabilities.
71func QueryCmd(env uv.Environ) tea.Cmd {
72 var sb strings.Builder
73 sb.WriteString(ansi.RequestPrimaryDeviceAttributes)
74
75 // Queries that should only be sent to "smart" normal terminals.
76 shouldQueryFor := shouldQueryCapabilities(env)
77 if shouldQueryFor {
78 sb.WriteString(ansi.RequestNameVersion)
79 // sb.WriteString(ansi.RequestModeFocusEvent) // TODO: re-enable when we need notifications.
80 sb.WriteString(ansi.WindowOp(14)) // Window size in pixels
81 kittyReq := ansi.KittyGraphics([]byte("AAAA"), "i=31", "s=1", "v=1", "a=q", "t=d", "f=24")
82 if _, isTmux := env.LookupEnv("TMUX"); isTmux {
83 kittyReq = ansi.TmuxPassthrough(kittyReq)
84 }
85 sb.WriteString(kittyReq)
86 }
87
88 return tea.Raw(sb.String())
89}
90
91// SupportsTrueColor returns true if the terminal supports true color.
92func (c Capabilities) SupportsTrueColor() bool {
93 return c.Profile == colorprofile.TrueColor
94}
95
96// SupportsKittyGraphics returns true if the terminal supports Kitty graphics.
97func (c Capabilities) SupportsKittyGraphics() bool {
98 return c.KittyGraphics
99}
100
101// SupportsSixelGraphics returns true if the terminal supports Sixel graphics.
102func (c Capabilities) SupportsSixelGraphics() bool {
103 return c.SixelGraphics
104}
105
106// CellSize returns the size of a single terminal cell in pixels.
107func (c Capabilities) CellSize() (width, height int) {
108 if c.Columns == 0 || c.Rows == 0 {
109 return 0, 0
110 }
111 return c.PixelX / c.Columns, c.PixelY / c.Rows
112}
113
114func modeSupported(v ansi.ModeSetting) bool {
115 return v.IsSet() || v.IsReset()
116}
117
118// kittyTerminals defines terminals supporting querying capabilities.
119var kittyTerminals = []string{"alacritty", "ghostty", "kitty", "rio", "wezterm"}
120
121func shouldQueryCapabilities(env uv.Environ) bool {
122 const osVendorTypeApple = "Apple"
123 termType := env.Getenv("TERM")
124 termProg, okTermProg := env.LookupEnv("TERM_PROGRAM")
125 _, okSSHTTY := env.LookupEnv("SSH_TTY")
126 if okTermProg && strings.Contains(termProg, osVendorTypeApple) {
127 return false
128 }
129 return (!okTermProg && !okSSHTTY) ||
130 (!strings.Contains(termProg, osVendorTypeApple) && !okSSHTTY) ||
131 // Terminals that do support XTVERSION.
132 xstrings.ContainsAnyOf(termType, kittyTerminals...)
133}