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.PixelSizeEvent:
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 sb.WriteString(ansi.QueryModifyOtherKeys)
75
76 // Queries that should only be sent to "smart" normal terminals.
77 shouldQueryFor := shouldQueryCapabilities(env)
78 if shouldQueryFor {
79 sb.WriteString(ansi.RequestNameVersion)
80 // sb.WriteString(ansi.RequestModeFocusEvent) // TODO: re-enable when we need notifications.
81 sb.WriteString(ansi.WindowOp(14)) // Window size in pixels
82 kittyReq := ansi.KittyGraphics([]byte("AAAA"), "i=31", "s=1", "v=1", "a=q", "t=d", "f=24")
83 if _, isTmux := env.LookupEnv("TMUX"); isTmux {
84 kittyReq = ansi.TmuxPassthrough(kittyReq)
85 }
86 sb.WriteString(kittyReq)
87 }
88
89 return tea.Raw(sb.String())
90}
91
92// SupportsTrueColor returns true if the terminal supports true color.
93func (c Capabilities) SupportsTrueColor() bool {
94 return c.Profile == colorprofile.TrueColor
95}
96
97// SupportsKittyGraphics returns true if the terminal supports Kitty graphics.
98func (c Capabilities) SupportsKittyGraphics() bool {
99 return c.KittyGraphics
100}
101
102// SupportsSixelGraphics returns true if the terminal supports Sixel graphics.
103func (c Capabilities) SupportsSixelGraphics() bool {
104 return c.SixelGraphics
105}
106
107// CellSize returns the size of a single terminal cell in pixels.
108func (c Capabilities) CellSize() (width, height int) {
109 if c.Columns == 0 || c.Rows == 0 {
110 return 0, 0
111 }
112 return c.PixelX / c.Columns, c.PixelY / c.Rows
113}
114
115func modeSupported(v ansi.ModeSetting) bool {
116 return v.IsSet() || v.IsReset()
117}
118
119// kittyTerminals defines terminals supporting querying capabilities.
120var kittyTerminals = []string{"alacritty", "ghostty", "kitty", "rio", "wezterm"}
121
122func shouldQueryCapabilities(env uv.Environ) bool {
123 const osVendorTypeApple = "Apple"
124 termType := env.Getenv("TERM")
125 termProg, okTermProg := env.LookupEnv("TERM_PROGRAM")
126 _, okSSHTTY := env.LookupEnv("SSH_TTY")
127 if okTermProg && strings.Contains(termProg, osVendorTypeApple) {
128 return false
129 }
130 return (!okTermProg && !okSSHTTY) ||
131 (!strings.Contains(termProg, osVendorTypeApple) && !okSSHTTY) ||
132 // Terminals that do support XTVERSION.
133 xstrings.ContainsAnyOf(termType, kittyTerminals...)
134}