cc-context-stats 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,228 @@
1
+ """Diagnostic command that dumps the raw JSON context from Claude Code.
2
+
3
+ Usage:
4
+ echo '{"model":...}' | context-stats explain
5
+ echo '{"model":...}' | context-stats explain --no-color
6
+
7
+ Reads the same JSON that Claude Code pipes to the statusline script,
8
+ pretty-prints every field with labels, and shows how cc-context-stats
9
+ interprets them. Useful for debugging blank or missing modules.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import sys
16
+
17
+ from claude_statusline.core.colors import ColorManager
18
+ from claude_statusline.core.config import Config
19
+ from claude_statusline.formatters.tokens import format_tokens
20
+
21
+
22
+ def _pct_color(colors: ColorManager, pct: float) -> str:
23
+ """Return ANSI color based on free-space percentage."""
24
+ if pct > 50:
25
+ return colors.green
26
+ if pct > 25:
27
+ return colors.yellow
28
+ return colors.red
29
+
30
+
31
+ def _fv(colors: ColorManager, value: object) -> str:
32
+ """Format a value for display, handling None gracefully."""
33
+ if value is None:
34
+ return f"{colors.dim}(absent){colors.reset}"
35
+ if isinstance(value, float):
36
+ return f"{value:.4f}"
37
+ return str(value)
38
+
39
+
40
+ def _render_model(data: dict, colors: ColorManager) -> None:
41
+ model = data.get("model", {})
42
+ print(f"{colors.bold}Model{colors.reset}")
43
+ print(f" display_name: {_fv(colors, model.get('display_name'))}")
44
+ print(f" id: {_fv(colors, model.get('id'))}")
45
+ print(f" api_name: {_fv(colors, model.get('api_name'))}")
46
+ print()
47
+
48
+
49
+ def _render_workspace(data: dict, colors: ColorManager) -> None:
50
+ workspace = data.get("workspace", {})
51
+ print(f"{colors.bold}Workspace{colors.reset}")
52
+ print(f" current_dir: {_fv(colors, workspace.get('current_dir'))}")
53
+ print(f" project_dir: {_fv(colors, workspace.get('project_dir'))}")
54
+ print()
55
+
56
+
57
+ def _render_context_window(data: dict, colors: ColorManager, config: Config) -> None:
58
+ cw = data.get("context_window", {})
59
+ print(f"{colors.bold}Context Window{colors.reset}")
60
+ total_size = cw.get("context_window_size", 0)
61
+ print(
62
+ f" window_size: "
63
+ f"{format_tokens(total_size, config.token_detail) if total_size else _fv(colors, None)}"
64
+ )
65
+
66
+ total_in = cw.get("total_input_tokens")
67
+ total_out = cw.get("total_output_tokens")
68
+ print(
69
+ f" total_input: "
70
+ f"{format_tokens(total_in, config.token_detail) if total_in else _fv(colors, total_in)}"
71
+ )
72
+ print(
73
+ f" total_output: "
74
+ f"{format_tokens(total_out, config.token_detail) if total_out else _fv(colors, total_out)}"
75
+ )
76
+
77
+ used_pct = cw.get("used_percentage")
78
+ remaining_pct = cw.get("remaining_percentage")
79
+ print(f" used_pct: {_fv(colors, used_pct)}")
80
+ print(f" remaining_pct: {_fv(colors, remaining_pct)}")
81
+
82
+ cu = cw.get("current_usage")
83
+ if cu:
84
+ _render_current_usage(cu, total_size, colors, config)
85
+ else:
86
+ print(f" current_usage: {colors.dim}(absent — no API call yet this session){colors.reset}")
87
+ print()
88
+
89
+
90
+ def _render_current_usage(cu: dict, total_size: int, colors: ColorManager, config: Config) -> None:
91
+ input_tok = cu.get("input_tokens", 0)
92
+ output_tok = cu.get("output_tokens", 0)
93
+ cache_create = cu.get("cache_creation_input_tokens", 0)
94
+ cache_read = cu.get("cache_read_input_tokens", 0)
95
+ used_tokens = input_tok + cache_create + cache_read
96
+
97
+ print(f"\n {colors.bold}Current Usage{colors.reset}")
98
+ print(f" input_tokens: {format_tokens(input_tok, config.token_detail)}")
99
+ print(f" output_tokens: {format_tokens(output_tok, config.token_detail)}")
100
+ print(f" cache_creation_tokens: {format_tokens(cache_create, config.token_detail)}")
101
+ print(f" cache_read_tokens: {format_tokens(cache_read, config.token_detail)}")
102
+
103
+ print(f"\n {colors.bold}Derived Values{colors.reset}")
104
+ print(
105
+ f" context_used (in+cache): "
106
+ f"{colors.cyan}{format_tokens(used_tokens, config.token_detail)}{colors.reset}"
107
+ )
108
+
109
+ if total_size > 0:
110
+ free = total_size - used_tokens
111
+ free_pct = (free * 100.0) / total_size
112
+ color = _pct_color(colors, free_pct)
113
+ print(
114
+ f" free_tokens: "
115
+ f"{color}{format_tokens(max(0, free), config.token_detail)} ({free_pct:.1f}%){colors.reset}"
116
+ )
117
+
118
+ if config.autocompact:
119
+ ac_buffer = int(total_size * 0.225)
120
+ effective_free = max(0, free - ac_buffer)
121
+ eff_pct = (effective_free * 100.0) / total_size
122
+ eff_color = _pct_color(colors, eff_pct)
123
+ print(
124
+ f" autocompact_buffer: "
125
+ f"{colors.dim}{format_tokens(ac_buffer, config.token_detail)}{colors.reset}"
126
+ )
127
+ print(
128
+ f" effective_free (w/ AC): "
129
+ f"{eff_color}{format_tokens(effective_free, config.token_detail)}"
130
+ f" ({eff_pct:.1f}%){colors.reset}"
131
+ )
132
+ else:
133
+ print(f" autocompact: {colors.dim}disabled{colors.reset}")
134
+
135
+
136
+ def _render_cost(data: dict, colors: ColorManager) -> None:
137
+ cost = data.get("cost", {})
138
+ if not cost:
139
+ return
140
+ print(f"{colors.bold}Cost{colors.reset}")
141
+ cost_usd = cost.get("total_cost_usd")
142
+ print(
143
+ f" total_cost_usd: {f'${cost_usd:.4f}' if cost_usd is not None else _fv(colors, None)}"
144
+ )
145
+ print(f" total_duration_ms: {_fv(colors, cost.get('total_duration_ms'))}")
146
+ print(f" lines_added: {_fv(colors, cost.get('total_lines_added'))}")
147
+ print(f" lines_removed: {_fv(colors, cost.get('total_lines_removed'))}")
148
+ print()
149
+
150
+
151
+ def _render_session(data: dict, colors: ColorManager) -> None:
152
+ print(f"{colors.bold}Session{colors.reset}")
153
+ print(f" session_id: {_fv(colors, data.get('session_id'))}")
154
+ print(f" version: {_fv(colors, data.get('version'))}")
155
+ print(f" transcript_path: {_fv(colors, data.get('transcript_path'))}")
156
+ print(f" exceeds_200k: {_fv(colors, data.get('exceeds_200k_tokens'))}")
157
+ print()
158
+
159
+
160
+ def _render_extensions(data: dict, colors: ColorManager) -> None:
161
+ vim = data.get("vim")
162
+ agent = data.get("agent")
163
+ output_style = data.get("output_style")
164
+ if vim is None and agent is None and output_style is None:
165
+ return
166
+ print(f"{colors.bold}Extensions{colors.reset}")
167
+ if vim is not None:
168
+ vim_mode = vim.get("mode") if isinstance(vim, dict) else vim
169
+ print(f" vim_mode: {_fv(colors, vim_mode)}")
170
+ if agent is not None:
171
+ agent_name = agent.get("name") if isinstance(agent, dict) else agent
172
+ print(f" agent: {_fv(colors, agent_name)}")
173
+ if output_style is not None:
174
+ style_name = output_style.get("name") if isinstance(output_style, dict) else output_style
175
+ print(f" output_style: {_fv(colors, style_name)}")
176
+ print()
177
+
178
+
179
+ def _render_config(config: Config, colors: ColorManager) -> None:
180
+ print(
181
+ f"{colors.bold}Active Config{colors.reset} "
182
+ f"{colors.dim}(~/.claude/statusline.conf){colors.reset}"
183
+ )
184
+ for k, v in config.to_dict().items():
185
+ if k == "color_overrides":
186
+ if v:
187
+ print(f" {k}:")
188
+ for slot, ansi_code in v.items():
189
+ print(f" {slot}: {ansi_code}████{colors.reset}")
190
+ continue
191
+ print(f" {k}: {v}")
192
+ print()
193
+
194
+
195
+ def _render_raw_json(data: dict, colors: ColorManager) -> None:
196
+ print(f"{colors.bold}Raw JSON{colors.reset}")
197
+ print(f"{colors.dim}{json.dumps(data, indent=2)}{colors.reset}")
198
+ print()
199
+
200
+
201
+ def run_explain(data: dict, no_color: bool = False) -> None:
202
+ """Print a diagnostic dump of the Claude Code session JSON.
203
+
204
+ Args:
205
+ data: Parsed JSON dict from stdin.
206
+ no_color: If True, suppress ANSI color codes.
207
+ """
208
+ config = Config.load()
209
+
210
+ # Respect --no-color flag and non-TTY output
211
+ color_enabled = not no_color and sys.stdout.isatty()
212
+ colors = ColorManager(enabled=color_enabled, overrides=config.color_overrides)
213
+
214
+ print(f"\n{colors.bold}cc-context-stats explain{colors.reset}")
215
+ print(f"{colors.dim}{'─' * 60}{colors.reset}")
216
+ print(
217
+ f"{colors.dim}Shows how cc-context-stats interprets Claude Code's JSON context."
218
+ f"{colors.reset}\n"
219
+ )
220
+
221
+ _render_model(data, colors)
222
+ _render_workspace(data, colors)
223
+ _render_context_window(data, colors, config)
224
+ _render_cost(data, colors)
225
+ _render_session(data, colors)
226
+ _render_extensions(data, colors)
227
+ _render_config(config, colors)
228
+ _render_raw_json(data, colors)
@@ -26,14 +26,7 @@ from __future__ import annotations
26
26
  import json
27
27
  import sys
28
28
 
29
- from claude_statusline.core.colors import (
30
- BLUE,
31
- DIM,
32
- GREEN,
33
- RED,
34
- RESET,
35
- YELLOW,
36
- )
29
+ from claude_statusline.core.colors import ColorManager
37
30
  from claude_statusline.core.config import Config
38
31
  from claude_statusline.core.git import get_git_info
39
32
  from claude_statusline.core.state import StateEntry, StateFile
@@ -56,12 +49,15 @@ def main() -> None:
56
49
  model = data.get("model", {}).get("display_name", "Claude")
57
50
  dir_name = cwd.rsplit("/", 1)[-1] if "/" in cwd else cwd or "~"
58
51
 
59
- # Git info
60
- git_info = get_git_info(project_dir)
61
-
62
52
  # Read settings from config file
63
53
  config = Config.load()
64
54
 
55
+ # Build color manager with any user overrides
56
+ colors = ColorManager(enabled=True, overrides=config.color_overrides)
57
+
58
+ # Git info (pass color manager for configurable branch/change colors)
59
+ git_info = get_git_info(project_dir, color_manager=colors)
60
+
65
61
  # Extract session_id once for reuse
66
62
  session_id = data.get("session_id")
67
63
 
@@ -99,9 +95,9 @@ def main() -> None:
99
95
 
100
96
  if config.autocompact:
101
97
  buffer_k = autocompact_buffer // 1000
102
- ac_info = f" {DIM}[AC:{buffer_k}k]{RESET}"
98
+ ac_info = f" {colors.dim}[AC:{buffer_k}k]{colors.reset}"
103
99
  else:
104
- ac_info = f" {DIM}[AC:off]{RESET}"
100
+ ac_info = f" {colors.dim}[AC:off]{colors.reset}"
105
101
 
106
102
  # Format tokens based on token_detail setting
107
103
  free_display = format_tokens(free_tokens, config.token_detail)
@@ -109,13 +105,13 @@ def main() -> None:
109
105
  # Color based on free percentage
110
106
  free_pct_int = int(free_pct)
111
107
  if free_pct_int > 50:
112
- ctx_color = GREEN
108
+ ctx_color = colors.green
113
109
  elif free_pct_int > 25:
114
- ctx_color = YELLOW
110
+ ctx_color = colors.yellow
115
111
  else:
116
- ctx_color = RED
112
+ ctx_color = colors.red
117
113
 
118
- context_info = f" | {ctx_color}{free_display} free ({free_pct:.1f}%){RESET}"
114
+ context_info = f" | {ctx_color}{free_display} free ({free_pct:.1f}%){colors.reset}"
119
115
 
120
116
  # Calculate and display token delta if enabled
121
117
  if config.show_delta:
@@ -131,7 +127,7 @@ def main() -> None:
131
127
  # Only show positive delta (and skip first run when no previous state)
132
128
  if has_prev and delta > 0:
133
129
  delta_display = format_tokens(delta, config.token_detail)
134
- delta_info = f" {DIM}[+{delta_display}]{RESET}"
130
+ delta_info = f" {colors.dim}[+{delta_display}]{colors.reset}"
135
131
 
136
132
  # Build current entry
137
133
  cur_input_tokens = current_usage.get("input_tokens", 0)
@@ -160,10 +156,10 @@ def main() -> None:
160
156
 
161
157
  # Display session_id if enabled
162
158
  if config.show_session and session_id:
163
- session_info = f" {DIM}{session_id}{RESET}"
159
+ session_info = f" {colors.dim}{session_id}{colors.reset}"
164
160
 
165
161
  # Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [session_id]
166
- base = f"{DIM}[{model}]{RESET} {BLUE}{dir_name}{RESET}"
162
+ base = f"{colors.dim}[{model}]{colors.reset} {colors.blue}{dir_name}{colors.reset}"
167
163
  max_width = get_terminal_width()
168
164
  parts = [base, git_info, context_info, delta_info, ac_info, session_info]
169
165
  print(fit_to_width(parts, max_width))
@@ -1,6 +1,10 @@
1
1
  """ANSI color constants and utilities."""
2
2
 
3
- # ANSI color codes
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ # ANSI color codes (defaults)
4
8
  BLUE = "\033[0;34m"
5
9
  MAGENTA = "\033[0;35m"
6
10
  CYAN = "\033[0;36m"
@@ -11,36 +15,101 @@ BOLD = "\033[1m"
11
15
  DIM = "\033[2m"
12
16
  RESET = "\033[0m"
13
17
 
18
+ # Mapping from color names to ANSI codes
19
+ COLOR_NAMES: dict[str, str] = {
20
+ "black": "\033[0;30m",
21
+ "red": "\033[0;31m",
22
+ "green": "\033[0;32m",
23
+ "yellow": "\033[0;33m",
24
+ "blue": "\033[0;34m",
25
+ "magenta": "\033[0;35m",
26
+ "cyan": "\033[0;36m",
27
+ "white": "\033[0;37m",
28
+ "bright_black": "\033[0;90m",
29
+ "bright_red": "\033[0;91m",
30
+ "bright_green": "\033[0;92m",
31
+ "bright_yellow": "\033[0;93m",
32
+ "bright_blue": "\033[0;94m",
33
+ "bright_magenta": "\033[0;95m",
34
+ "bright_cyan": "\033[0;96m",
35
+ "bright_white": "\033[0;97m",
36
+ }
37
+
38
+ _HEX_RE = re.compile(r"^#([0-9a-fA-F]{6})$")
39
+
40
+
41
+ def parse_color(value: str) -> str | None:
42
+ """Parse a color value into an ANSI escape code.
43
+
44
+ Accepts:
45
+ - Named colors: "red", "green", "bright_cyan", etc.
46
+ - Hex colors: "#ff5733" (converted to 24-bit ANSI)
47
+
48
+ Returns:
49
+ ANSI escape code string, or None if the value is not recognized.
50
+ """
51
+ value = value.strip().lower()
52
+ if not value:
53
+ return None
54
+
55
+ # Named color
56
+ if value in COLOR_NAMES:
57
+ return COLOR_NAMES[value]
58
+
59
+ # Hex color (#rrggbb)
60
+ m = _HEX_RE.match(value)
61
+ if m:
62
+ hex_str = m.group(1)
63
+ r = int(hex_str[0:2], 16)
64
+ g = int(hex_str[2:4], 16)
65
+ b = int(hex_str[4:6], 16)
66
+ return f"\033[38;2;{r};{g};{b}m"
67
+
68
+ return None
69
+
14
70
 
15
71
  class ColorManager:
16
- """Manage color output based on terminal capabilities."""
72
+ """Manage color output based on terminal capabilities.
73
+
74
+ Supports custom color overrides via a dict of {slot_name: ansi_code}.
75
+ """
17
76
 
18
- def __init__(self, enabled: bool = True) -> None:
77
+ def __init__(
78
+ self,
79
+ enabled: bool = True,
80
+ overrides: dict[str, str] | None = None,
81
+ ) -> None:
19
82
  self.enabled = enabled
83
+ self._overrides = overrides or {}
84
+
85
+ def _get(self, slot: str, default: str) -> str:
86
+ if not self.enabled:
87
+ return ""
88
+ return self._overrides.get(slot, default)
20
89
 
21
90
  @property
22
91
  def blue(self) -> str:
23
- return BLUE if self.enabled else ""
92
+ return self._get("blue", BLUE)
24
93
 
25
94
  @property
26
95
  def magenta(self) -> str:
27
- return MAGENTA if self.enabled else ""
96
+ return self._get("magenta", MAGENTA)
28
97
 
29
98
  @property
30
99
  def cyan(self) -> str:
31
- return CYAN if self.enabled else ""
100
+ return self._get("cyan", CYAN)
32
101
 
33
102
  @property
34
103
  def green(self) -> str:
35
- return GREEN if self.enabled else ""
104
+ return self._get("green", GREEN)
36
105
 
37
106
  @property
38
107
  def yellow(self) -> str:
39
- return YELLOW if self.enabled else ""
108
+ return self._get("yellow", YELLOW)
40
109
 
41
110
  @property
42
111
  def red(self) -> str:
43
- return RED if self.enabled else ""
112
+ return self._get("red", RED)
44
113
 
45
114
  @property
46
115
  def bold(self) -> str:
@@ -7,6 +7,18 @@ from dataclasses import dataclass, field
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
+ from claude_statusline.core.colors import parse_color
11
+
12
+ # Color config keys and which ColorManager slot they map to
13
+ _COLOR_KEYS: dict[str, str] = {
14
+ "color_green": "green",
15
+ "color_yellow": "yellow",
16
+ "color_red": "red",
17
+ "color_blue": "blue",
18
+ "color_magenta": "magenta",
19
+ "color_cyan": "cyan",
20
+ }
21
+
10
22
 
11
23
  @dataclass
12
24
  class Config:
@@ -19,6 +31,9 @@ class Config:
19
31
  show_io_tokens: bool = True
20
32
  reduced_motion: bool = False
21
33
 
34
+ # Custom color overrides (slot_name -> ANSI code)
35
+ color_overrides: dict[str, str] = field(default_factory=dict)
36
+
22
37
  _config_path: Path = field(default_factory=lambda: Path.home() / ".claude" / "statusline.conf")
23
38
 
24
39
  @classmethod
@@ -62,10 +77,23 @@ show_session=true
62
77
 
63
78
  # Disable rotating text animations
64
79
  reduced_motion=false
80
+
81
+ # Custom colors - use named colors or hex (#rrggbb)
82
+ # Available color slots: color_green, color_yellow, color_red,
83
+ # color_blue, color_magenta, color_cyan
84
+ # Named colors: black, red, green, yellow, blue, magenta, cyan, white,
85
+ # bright_black, bright_red, bright_green, bright_yellow,
86
+ # bright_blue, bright_magenta, bright_cyan, bright_white
87
+ # Examples:
88
+ # color_green=#7dcfff
89
+ # color_yellow=bright_yellow
90
+ # color_red=#f7768e
65
91
  """
66
92
  )
67
93
  except OSError as e:
68
- sys.stderr.write(f"[statusline] warning: failed to create config {self._config_path}: {e}\n")
94
+ sys.stderr.write(
95
+ f"[statusline] warning: failed to create config {self._config_path}: {e}\n"
96
+ )
69
97
 
70
98
  def _read_config(self) -> None:
71
99
  """Read settings from config file."""
@@ -77,22 +105,35 @@ reduced_motion=false
77
105
  continue
78
106
  key, value = line.split("=", 1)
79
107
  key = key.strip()
80
- value = value.strip().lower()
108
+ raw_value = value.strip()
109
+ value_lower = raw_value.lower()
81
110
 
82
111
  if key == "autocompact":
83
- self.autocompact = value != "false"
112
+ self.autocompact = value_lower != "false"
84
113
  elif key == "token_detail":
85
- self.token_detail = value != "false"
114
+ self.token_detail = value_lower != "false"
86
115
  elif key == "show_delta":
87
- self.show_delta = value != "false"
116
+ self.show_delta = value_lower != "false"
88
117
  elif key == "show_session":
89
- self.show_session = value != "false"
118
+ self.show_session = value_lower != "false"
90
119
  elif key == "show_io_tokens":
91
- self.show_io_tokens = value != "false"
120
+ self.show_io_tokens = value_lower != "false"
92
121
  elif key == "reduced_motion":
93
- self.reduced_motion = value != "false"
122
+ self.reduced_motion = value_lower != "false"
123
+ elif key in _COLOR_KEYS:
124
+ slot = _COLOR_KEYS[key]
125
+ ansi = parse_color(raw_value)
126
+ if ansi:
127
+ self.color_overrides[slot] = ansi
128
+ else:
129
+ sys.stderr.write(
130
+ f"[statusline] warning: unrecognized color value "
131
+ f"'{raw_value}' for {key}\n"
132
+ )
94
133
  except (OSError, UnicodeDecodeError) as e:
95
- sys.stderr.write(f"[statusline] warning: failed to read config {self._config_path}: {e}\n")
134
+ sys.stderr.write(
135
+ f"[statusline] warning: failed to read config {self._config_path}: {e}\n"
136
+ )
96
137
 
97
138
  def to_dict(self) -> dict[str, Any]:
98
139
  """Convert config to dictionary."""
@@ -103,4 +144,5 @@ reduced_motion=false
103
144
  "show_session": self.show_session,
104
145
  "show_io_tokens": self.show_io_tokens,
105
146
  "reduced_motion": self.reduced_motion,
147
+ "color_overrides": dict(self.color_overrides),
106
148
  }
@@ -5,15 +5,22 @@ from __future__ import annotations
5
5
  import subprocess
6
6
  from pathlib import Path
7
7
 
8
- from claude_statusline.core.colors import CYAN, MAGENTA, RESET
8
+ from claude_statusline.core.colors import CYAN, MAGENTA, RESET, ColorManager
9
9
 
10
10
 
11
- def get_git_info(project_dir: str | Path, colors_enabled: bool = True) -> str:
11
+ def get_git_info(
12
+ project_dir: str | Path,
13
+ colors_enabled: bool = True,
14
+ color_manager: ColorManager | None = None,
15
+ ) -> str:
12
16
  """Get git branch and change count for a directory.
13
17
 
14
18
  Args:
15
19
  project_dir: Path to the project directory
16
- colors_enabled: Whether to include ANSI color codes
20
+ colors_enabled: Whether to include ANSI color codes. Deprecated —
21
+ prefer passing a ColorManager via color_manager instead.
22
+ color_manager: Optional ColorManager for custom colors. If provided,
23
+ colors_enabled is ignored (the manager handles that).
17
24
 
18
25
  Returns:
19
26
  Formatted string with branch and change count, or empty string if not a git repo
@@ -53,8 +60,12 @@ def get_git_info(project_dir: str | Path, colors_enabled: bool = True) -> str:
53
60
  else:
54
61
  changes = len([line for line in result.stdout.split("\n") if line.strip()])
55
62
 
56
- # Format output
57
- if colors_enabled:
63
+ # Format output — use ColorManager if provided, else fallback to constants
64
+ if color_manager is not None:
65
+ magenta = color_manager.magenta
66
+ cyan = color_manager.cyan
67
+ reset = color_manager.reset
68
+ elif colors_enabled:
58
69
  magenta, cyan, reset = MAGENTA, CYAN, RESET
59
70
  else:
60
71
  magenta = cyan = reset = ""