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