-
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathhud_rainbow.py
More file actions
221 lines (173 loc) · 6.74 KB
/
Copy pathhud_rainbow.py
File metadata and controls
221 lines (173 loc) · 6.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
"""Mode rainbow ANSI coloring for CodingBuddy statusLine (#1326, Wave 2-D).
Renders the statusLine mode label with per-mode ANSI coloring and a
tier-specific glyph so users can tell at a glance which workflow
mode is active:
PLAN → ◇ blue (planning — cool, deliberate)
ACT → ◆ green (executing — go)
EVAL → ◈ purple (evaluating — reflective)
AUTO → ◊ rainbow (cycling — energetic gradient)
Color output honours the ``NO_COLOR`` environment variable
(https://no-color.org) — when it is set to any non-empty value,
:func:`render_mode_rainbow` emits plain text without escape codes so
redirected/logged output stays clean.
Primary entry points:
- :func:`is_color_enabled` — check the NO_COLOR env var
- :func:`mode_glyph` — per-mode Unicode glyph
- :func:`gradient_ansi` — character-by-character RGB gradient
- :func:`render_mode_rainbow` — end-to-end mode label renderer
"""
from __future__ import annotations
import os
from typing import Dict, List, Tuple
# ------------------------------------------------------------------------
# Constants
# ------------------------------------------------------------------------
# ANSI escape codes
_CSI = "\x1b["
_RESET = f"{_CSI}0m"
# Basic 8-bit color codes (38;5;N) for simple modes.
_FG_BLUE = 33 # bright blue
_FG_GREEN = 42 # bright green
_FG_PURPLE = 135 # bright purple
_FG_RED = 196
_FG_ORANGE = 208
_FG_YELLOW = 226
_FG_CYAN = 51
# Per-mode glyphs (mirror AGENT_GLYPHS in codingbuddy-hud.py).
_MODE_GLYPHS: Dict[str, str] = {
"PLAN": "\u25c7", # ◇
"ACT": "\u25c6", # ◆
"EVAL": "\u25c8", # ◈
"AUTO": "\u25ca", # ◊
}
# Per-mode RGB anchor pairs for gradient rendering. AUTO uses a
# 6-stop rainbow; PLAN/ACT/EVAL use a single solid hue applied to
# every character.
MODE_PALETTE: Dict[str, List[Tuple[int, int, int]]] = {
"PLAN": [(64, 128, 255)], # steady blue
"ACT": [(64, 200, 96)], # steady green
"EVAL": [(180, 96, 220)], # steady purple
"AUTO": [
(255, 64, 64), # red
(255, 144, 32), # orange
(255, 224, 32), # yellow
(64, 200, 96), # green
(64, 128, 255), # blue
(180, 96, 220), # purple
],
}
# Reset sequence exposed for callers (tests).
RESET: str = _RESET
# ------------------------------------------------------------------------
# Environment detection
# ------------------------------------------------------------------------
def is_color_enabled(env: Dict[str, str] = None) -> bool: # type: ignore[assignment]
"""Return True when ANSI color output should be produced.
Honours the ``NO_COLOR`` standard (https://no-color.org): any
non-empty value disables color. When ``env`` is omitted, reads
from :data:`os.environ`.
"""
if env is None:
env = os.environ
value = env.get("NO_COLOR", "")
return not value
# ------------------------------------------------------------------------
# Glyph helper
# ------------------------------------------------------------------------
def mode_glyph(mode: str) -> str:
"""Return the Unicode glyph for a mode, or an empty string.
Mode matching is case-insensitive. Unknown modes return
an empty string so callers can render just the text label.
"""
if not mode:
return ""
return _MODE_GLYPHS.get(mode.upper(), "")
# ------------------------------------------------------------------------
# Gradient renderer
# ------------------------------------------------------------------------
def _rgb_escape(r: int, g: int, b: int) -> str:
"""Return the ANSI truecolor foreground escape for ``(r, g, b)``."""
return f"{_CSI}38;2;{r};{g};{b}m"
def gradient_ansi(text: str, palette: List[Tuple[int, int, int]]) -> str:
"""Apply a character-by-character gradient to ``text``.
When ``palette`` has a single color, every character is rendered
in that color. When it has multiple colors, each character is
linearly mapped to a stop on the palette so the first character
is ``palette[0]`` and the last character is ``palette[-1]``.
Returns ``text`` unchanged (without escapes) when ``palette`` is
empty or ``text`` is empty.
"""
if not text or not palette:
return text
if len(palette) == 1:
r, g, b = palette[0]
return f"{_rgb_escape(r, g, b)}{text}{_RESET}"
n_chars = len(text)
n_stops = len(palette)
out: List[str] = []
for i, ch in enumerate(text):
# Map character index to palette stop (0..n_stops-1)
stop = int(i * n_stops / max(n_chars, 1))
stop = min(stop, n_stops - 1)
r, g, b = palette[stop]
out.append(f"{_rgb_escape(r, g, b)}{ch}")
out.append(_RESET)
return "".join(out)
# ------------------------------------------------------------------------
# End-to-end renderer
# ------------------------------------------------------------------------
def render_mode_rainbow(
mode: str,
*,
enabled: bool = None, # type: ignore[assignment]
env: Dict[str, str] = None, # type: ignore[assignment]
) -> str:
"""Render the mode label with glyph + ANSI color.
Output format:
``<glyph> <MODE>``
The label text is the uppercased mode name. When color is
enabled, the whole string (glyph + space + label) is wrapped
in the mode's palette via :func:`gradient_ansi`. When color
is disabled, plain text is returned.
Args:
mode: Workflow mode name (case-insensitive: PLAN/ACT/EVAL/AUTO).
enabled: Explicit override. ``None`` (default) defers to
:func:`is_color_enabled` which honours ``NO_COLOR``.
env: Optional environment override for testing.
Returns an empty string when ``mode`` is empty.
"""
if not mode:
return ""
mode_upper = mode.upper()
glyph = mode_glyph(mode_upper)
label = f"{glyph} {mode_upper}" if glyph else mode_upper
if enabled is None:
enabled = is_color_enabled(env)
if not enabled:
return label
palette = MODE_PALETTE.get(mode_upper)
if not palette:
return label # unknown mode: plain text
return gradient_ansi(label, palette)
def strip_ansi(s: str) -> str:
"""Remove ANSI CSI sequences from ``s`` (helper for tests and layout).
Simple state machine — recognises ``ESC [`` ... ``m`` escape
runs. Sufficient for the escapes this module emits.
"""
if not s or "\x1b" not in s:
return s
out: List[str] = []
i = 0
n = len(s)
while i < n:
ch = s[i]
if ch == "\x1b" and i + 1 < n and s[i + 1] == "[":
# Skip until 'm'
j = i + 2
while j < n and s[j] != "m":
j += 1
i = j + 1
continue
out.append(ch)
i += 1
return "".join(out)