capabilities.go

  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}