1// Package terminalcap owns pure terminal capability values and detection policy.
2package terminalcap
3
4import (
5 "fmt"
6 "strings"
7)
8
9// ColorMode is the color output mode selected for one attachment.
10type ColorMode uint8
11
12const (
13 // TrueColor is the zero value so manually constructed attachments retain the
14 // historical renderer behavior; live Hello paths always use the detector.
15 TrueColor ColorMode = iota
16 ANSI256
17 ANSI16
18 Monochrome
19)
20
21// Valid reports whether m is one of the defined color modes.
22func (m ColorMode) Valid() bool { return m <= Monochrome }
23
24// ColorCapabilities is the color output capability selected for one attachment
25// and how confidently it was selected.
26type ColorCapabilities struct {
27 Mode ColorMode
28 Source Source
29}
30
31// Valid reports whether both the mode and the source are defined values.
32func (c ColorCapabilities) Valid() bool { return c.Mode.Valid() && c.Source.Valid() }
33
34// RGB reports whether the attachment can receive RGB ANSI output.
35func (c ColorCapabilities) RGB() bool { return c.Mode == TrueColor }
36
37// Source records how confidently a terminal capability was selected.
38type Source uint8
39
40const (
41 SourceUnknown Source = iota
42 SourceHeuristic
43 SourceDeclared
44 // SourceForced is an explicit user override that detection must not
45 // second-guess and that never warrants a downgrade notice.
46 SourceForced
47)
48
49// Valid reports whether s is one of the defined sources.
50func (s Source) Valid() bool { return s <= SourceForced }
51
52// Application identifies a known terminal application when its environment
53// provides a trustworthy origin signal.
54type Application uint8
55
56const (
57 ApplicationUnknown Application = iota
58 ApplicationKitty
59)
60
61// Capabilities describes the output features selected for one client attachment.
62type Capabilities struct {
63 Color ColorCapabilities
64 Application Application
65 KittyGraphics bool
66}
67
68// SupportsKittyGraphics reports whether the active outer-terminal probe
69// accepted the Kitty graphics protocol. Environment detection never sets it.
70func (c Capabilities) SupportsKittyGraphics() bool { return c.KittyGraphics }
71
72// Detect derives conservative attachment capabilities from a client environment.
73func Detect(env []string) Capabilities {
74 values := environmentValues(env)
75 term := strings.ToLower(strings.TrimSpace(values["TERM"]))
76 colorTerm := strings.ToLower(strings.TrimSpace(values["COLORTERM"]))
77 caps := Capabilities{Color: ColorCapabilities{Mode: ANSI256}}
78
79 if values["KITTY_WINDOW_ID"] != "" || values["KITTY_PID"] != "" || values["KITTY_LISTEN_ON"] != "" {
80 caps.Application = ApplicationKitty
81 }
82
83 kittyIdentity := term == "xterm-kitty" && caps.Application == ApplicationKitty
84 switch colorTerm {
85 case "truecolor", "24bit":
86 caps.Color.Mode = TrueColor
87 caps.Color.Source = SourceDeclared
88 }
89 if term == "xterm-direct" || strings.HasSuffix(term, "-direct") {
90 caps.Color.Mode = TrueColor
91 caps.Color.Source = SourceDeclared
92 }
93 if kittyIdentity {
94 if caps.Color.Source == SourceUnknown {
95 caps.Color.Source = SourceHeuristic
96 }
97 caps.Color.Mode = TrueColor
98 return caps
99 }
100 if caps.Color.Source == SourceDeclared {
101 return caps
102 }
103 if mode, ok := termColorMode(term); ok {
104 caps.Color.Mode = mode
105 caps.Color.Source = SourceDeclared
106 }
107 return caps
108}
109
110// termColorMode classifies a lowercase TERM value that carries no truecolor
111// signal. It is deliberately conservative: only well-known terminfo names map
112// to a constrained mode, and anything else reports ok=false so the caller keeps
113// its unverified 256-color default. The rules apply in this order:
114//
115// - monochrome: dumb, vt52, vt100, vt102, vt220, or a name ending in -m or -mono;
116// - 256 colors: any name containing "256color";
117// - 16 colors: linux, ansi, cons25, or a name ending in -16color or -color.
118func termColorMode(term string) (ColorMode, bool) {
119 switch term {
120 case "dumb", "vt52", "vt100", "vt102", "vt220":
121 return Monochrome, true
122 case "linux", "ansi", "cons25":
123 return ANSI16, true
124 }
125 switch {
126 case strings.HasSuffix(term, "-m"), strings.HasSuffix(term, "-mono"):
127 return Monochrome, true
128 case strings.Contains(term, "256color"):
129 return ANSI256, true
130 case strings.HasSuffix(term, "-16color"), strings.HasSuffix(term, "-color"):
131 return ANSI16, true
132 }
133 return 0, false
134}
135
136// ParseColorMode parses a user color override: auto, truecolor, 256, 16, or
137// mono, case-insensitively and ignoring surrounding space. An empty value is
138// auto. When auto is true the caller must use detection and mode is
139// meaningless; an unknown value returns an error.
140func ParseColorMode(value string) (mode ColorMode, auto bool, err error) {
141 switch strings.ToLower(strings.TrimSpace(value)) {
142 case "", "auto":
143 return TrueColor, true, nil
144 case "truecolor":
145 return TrueColor, false, nil
146 case "256":
147 return ANSI256, false, nil
148 case "16":
149 return ANSI16, false, nil
150 case "mono":
151 return Monochrome, false, nil
152 }
153 return TrueColor, false, fmt.Errorf("invalid color mode %q (want auto, truecolor, 256, 16, or mono)", value)
154}
155
156func environmentValues(env []string) map[string]string {
157 values := make(map[string]string, len(env))
158 for _, entry := range env {
159 key, value, ok := strings.Cut(entry, "=")
160 if ok {
161 values[key] = value
162 }
163 }
164 return values
165}
166
167// Resolve selects attachment capabilities from the client environment and the
168// client's color claim. A declared or forced client color is used verbatim. A
169// heuristic truecolor claim upgrades a weaker daemon-side detection, because
170// the daemon may not see the client environment that produced the inference.
171// Any other claim falls back to environment detection.
172func Resolve(env []string, declared ColorCapabilities) Capabilities {
173 caps := Detect(env)
174 switch {
175 case declared.Source == SourceDeclared || declared.Source == SourceForced:
176 caps.Color = declared
177 case declared.Source == SourceHeuristic && declared.RGB() && !caps.Color.RGB():
178 caps.Color = declared
179 }
180 return caps
181}