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