capabilities.go 4.1 KB

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