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
 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}