cc-context-stats 1.7.0 → 1.8.1

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.
Files changed (102) hide show
  1. package/package.json +9 -1
  2. package/scripts/context-stats.sh +1 -1
  3. package/scripts/statusline.js +128 -18
  4. package/.editorconfig +0 -60
  5. package/.eslintrc.json +0 -35
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
  8. package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
  9. package/.github/dependabot.yml +0 -44
  10. package/.github/workflows/ci.yml +0 -294
  11. package/.github/workflows/release.yml +0 -151
  12. package/.pre-commit-config.yaml +0 -74
  13. package/.prettierrc +0 -33
  14. package/.shellcheckrc +0 -10
  15. package/CHANGELOG.md +0 -163
  16. package/CLAUDE.md +0 -66
  17. package/CODE_OF_CONDUCT.md +0 -59
  18. package/CONTRIBUTING.md +0 -240
  19. package/RELEASE_NOTES.md +0 -19
  20. package/SECURITY.md +0 -44
  21. package/TODOS.md +0 -72
  22. package/assets/logo/favicon.svg +0 -19
  23. package/assets/logo/logo-black.svg +0 -24
  24. package/assets/logo/logo-full.svg +0 -40
  25. package/assets/logo/logo-icon.svg +0 -27
  26. package/assets/logo/logo-mark.svg +0 -28
  27. package/assets/logo/logo-white.svg +0 -24
  28. package/assets/logo/logo-wordmark.svg +0 -6
  29. package/config/settings-example.json +0 -7
  30. package/config/settings-node.json +0 -7
  31. package/config/settings-python.json +0 -7
  32. package/docs/ARCHITECTURE.md +0 -128
  33. package/docs/CSV_FORMAT.md +0 -42
  34. package/docs/DEPLOYMENT.md +0 -71
  35. package/docs/DEVELOPMENT.md +0 -161
  36. package/docs/configuration.md +0 -118
  37. package/docs/context-stats.md +0 -143
  38. package/docs/installation.md +0 -255
  39. package/docs/scripts.md +0 -140
  40. package/docs/troubleshooting.md +0 -278
  41. package/images/claude-statusline-token-graph.gif +0 -0
  42. package/images/claude-statusline.png +0 -0
  43. package/images/context-status-dumbzone.png +0 -0
  44. package/images/context-status.png +0 -0
  45. package/images/statusline-detail.png +0 -0
  46. package/images/token-graph.jpeg +0 -0
  47. package/images/token-graph.png +0 -0
  48. package/images/v1.6.1.png +0 -0
  49. package/install +0 -351
  50. package/install.sh +0 -298
  51. package/jest.config.js +0 -11
  52. package/pyproject.toml +0 -115
  53. package/requirements-dev.txt +0 -12
  54. package/scripts/statusline-full.sh +0 -304
  55. package/scripts/statusline-git.sh +0 -88
  56. package/scripts/statusline-minimal.sh +0 -67
  57. package/scripts/statusline.py +0 -485
  58. package/src/claude_statusline/__init__.py +0 -11
  59. package/src/claude_statusline/__main__.py +0 -6
  60. package/src/claude_statusline/cli/__init__.py +0 -1
  61. package/src/claude_statusline/cli/context_stats.py +0 -512
  62. package/src/claude_statusline/cli/explain.py +0 -228
  63. package/src/claude_statusline/cli/statusline.py +0 -169
  64. package/src/claude_statusline/core/__init__.py +0 -1
  65. package/src/claude_statusline/core/colors.py +0 -124
  66. package/src/claude_statusline/core/config.py +0 -148
  67. package/src/claude_statusline/core/git.py +0 -78
  68. package/src/claude_statusline/core/state.py +0 -323
  69. package/src/claude_statusline/formatters/__init__.py +0 -1
  70. package/src/claude_statusline/formatters/layout.py +0 -67
  71. package/src/claude_statusline/formatters/time.py +0 -50
  72. package/src/claude_statusline/formatters/tokens.py +0 -70
  73. package/src/claude_statusline/graphs/__init__.py +0 -1
  74. package/src/claude_statusline/graphs/renderer.py +0 -366
  75. package/src/claude_statusline/graphs/statistics.py +0 -92
  76. package/src/claude_statusline/ui/__init__.py +0 -1
  77. package/src/claude_statusline/ui/icons.py +0 -93
  78. package/src/claude_statusline/ui/waiting.py +0 -62
  79. package/tests/bash/test_delta_parity.bats +0 -199
  80. package/tests/bash/test_install.bats +0 -29
  81. package/tests/bash/test_parity.bats +0 -315
  82. package/tests/bash/test_statusline_full.bats +0 -139
  83. package/tests/bash/test_statusline_git.bats +0 -42
  84. package/tests/bash/test_statusline_minimal.bats +0 -37
  85. package/tests/fixtures/json/comma_in_path.json +0 -31
  86. package/tests/fixtures/json/high_usage.json +0 -17
  87. package/tests/fixtures/json/low_usage.json +0 -17
  88. package/tests/fixtures/json/medium_usage.json +0 -17
  89. package/tests/fixtures/json/valid_full.json +0 -30
  90. package/tests/fixtures/json/valid_minimal.json +0 -9
  91. package/tests/node/rotation.test.js +0 -89
  92. package/tests/node/statusline.test.js +0 -240
  93. package/tests/python/conftest.py +0 -84
  94. package/tests/python/test_colors.py +0 -105
  95. package/tests/python/test_config_colors.py +0 -78
  96. package/tests/python/test_data_pipeline.py +0 -446
  97. package/tests/python/test_explain.py +0 -177
  98. package/tests/python/test_icons.py +0 -152
  99. package/tests/python/test_layout.py +0 -127
  100. package/tests/python/test_state_rotation_validation.py +0 -232
  101. package/tests/python/test_statusline.py +0 -215
  102. package/tests/python/test_waiting.py +0 -127
@@ -1,228 +0,0 @@
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)
@@ -1,169 +0,0 @@
1
- #!/usr/bin/env python3
2
- """CLI entry point for claude-statusline command.
3
-
4
- Usage: Copy to ~/.claude/statusline.py and make executable, or install via pip.
5
-
6
- Configuration:
7
- Create/edit ~/.claude/statusline.conf and set:
8
-
9
- autocompact=true (when autocompact is enabled in Claude Code - default)
10
- autocompact=false (when you disable autocompact via /config in Claude Code)
11
-
12
- token_detail=true (show exact token count like 64,000 - default)
13
- token_detail=false (show abbreviated tokens like 64.0k)
14
-
15
- show_delta=true (show token delta since last refresh like [+2,500] - default)
16
- show_delta=false (disable delta display - saves file I/O on every refresh)
17
-
18
- show_session=true (show session_id in status line - default)
19
- show_session=false (hide session_id from status line)
20
-
21
- When AC is enabled, 22.5% of context window is reserved for autocompact buffer.
22
- """
23
-
24
- from __future__ import annotations
25
-
26
- import json
27
- import sys
28
-
29
- from claude_statusline.core.colors import ColorManager
30
- from claude_statusline.core.config import Config
31
- from claude_statusline.core.git import get_git_info
32
- from claude_statusline.core.state import StateEntry, StateFile
33
- from claude_statusline.formatters.layout import fit_to_width, get_terminal_width
34
- from claude_statusline.formatters.time import get_current_timestamp
35
- from claude_statusline.formatters.tokens import calculate_context_usage, format_tokens
36
-
37
-
38
- def main() -> None:
39
- """Main entry point for claude-statusline CLI."""
40
- try:
41
- data = json.load(sys.stdin)
42
- except json.JSONDecodeError:
43
- print("[Claude] ~")
44
- return
45
-
46
- # Extract data
47
- cwd = data.get("workspace", {}).get("current_dir", "~")
48
- project_dir = data.get("workspace", {}).get("project_dir", cwd)
49
- model = data.get("model", {}).get("display_name", "Claude")
50
- dir_name = cwd.rsplit("/", 1)[-1] if "/" in cwd else cwd or "~"
51
-
52
- # Read settings from config file
53
- config = Config.load()
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
-
61
- # Extract session_id once for reuse
62
- session_id = data.get("session_id")
63
-
64
- # Context window calculation
65
- context_info = ""
66
- ac_info = ""
67
- delta_info = ""
68
- session_info = ""
69
-
70
- total_size = data.get("context_window", {}).get("context_window_size", 0)
71
- current_usage = data.get("context_window", {}).get("current_usage")
72
- total_input_tokens = data.get("context_window", {}).get("total_input_tokens", 0)
73
- total_output_tokens = data.get("context_window", {}).get("total_output_tokens", 0)
74
- cost_usd = data.get("cost", {}).get("total_cost_usd", 0)
75
- lines_added = data.get("cost", {}).get("total_lines_added", 0)
76
- lines_removed = data.get("cost", {}).get("total_lines_removed", 0)
77
- model_id = data.get("model", {}).get("id", "")
78
- workspace_project_dir = data.get("workspace", {}).get("project_dir", "")
79
-
80
- if total_size > 0 and current_usage:
81
- # Get tokens from current_usage (includes cache)
82
- input_tokens = current_usage.get("input_tokens", 0)
83
- cache_creation = current_usage.get("cache_creation_input_tokens", 0)
84
- cache_read = current_usage.get("cache_read_input_tokens", 0)
85
-
86
- # Total used from current request
87
- used_tokens = input_tokens + cache_creation + cache_read
88
-
89
- # Calculate context usage
90
- free_tokens, free_pct, autocompact_buffer = calculate_context_usage(
91
- used_tokens,
92
- total_size,
93
- config.autocompact,
94
- )
95
-
96
- if config.autocompact:
97
- buffer_k = autocompact_buffer // 1000
98
- ac_info = f" {colors.dim}[AC:{buffer_k}k]{colors.reset}"
99
- else:
100
- ac_info = f" {colors.dim}[AC:off]{colors.reset}"
101
-
102
- # Format tokens based on token_detail setting
103
- free_display = format_tokens(free_tokens, config.token_detail)
104
-
105
- # Color based on free percentage
106
- free_pct_int = int(free_pct)
107
- if free_pct_int > 50:
108
- ctx_color = colors.green
109
- elif free_pct_int > 25:
110
- ctx_color = colors.yellow
111
- else:
112
- ctx_color = colors.red
113
-
114
- context_info = f" | {ctx_color}{free_display} free ({free_pct:.1f}%){colors.reset}"
115
-
116
- # Calculate and display token delta if enabled
117
- if config.show_delta:
118
- state_file = StateFile(session_id)
119
- prev_entry = state_file.read_last_entry()
120
-
121
- prev_tokens = prev_entry.current_used_tokens if prev_entry else 0
122
- has_prev = prev_entry is not None
123
-
124
- # Calculate delta
125
- delta = used_tokens - prev_tokens
126
-
127
- # Only show positive delta (and skip first run when no previous state)
128
- if has_prev and delta > 0:
129
- delta_display = format_tokens(delta, config.token_detail)
130
- delta_info = f" {colors.dim}[+{delta_display}]{colors.reset}"
131
-
132
- # Build current entry
133
- cur_input_tokens = current_usage.get("input_tokens", 0)
134
- cur_output_tokens = current_usage.get("output_tokens", 0)
135
-
136
- entry = StateEntry(
137
- timestamp=get_current_timestamp(),
138
- total_input_tokens=total_input_tokens,
139
- total_output_tokens=total_output_tokens,
140
- current_input_tokens=cur_input_tokens,
141
- current_output_tokens=cur_output_tokens,
142
- cache_creation=cache_creation,
143
- cache_read=cache_read,
144
- cost_usd=cost_usd,
145
- lines_added=lines_added,
146
- lines_removed=lines_removed,
147
- session_id=session_id or "",
148
- model_id=model_id,
149
- workspace_project_dir=workspace_project_dir,
150
- context_window_size=total_size,
151
- )
152
-
153
- # Only append if context usage changed (avoid duplicates from multiple refreshes)
154
- if not has_prev or used_tokens != prev_tokens:
155
- state_file.append_entry(entry)
156
-
157
- # Display session_id if enabled
158
- if config.show_session and session_id:
159
- session_info = f" {colors.dim}{session_id}{colors.reset}"
160
-
161
- # Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [session_id]
162
- base = f"{colors.dim}[{model}]{colors.reset} {colors.blue}{dir_name}{colors.reset}"
163
- max_width = get_terminal_width()
164
- parts = [base, git_info, context_info, delta_info, ac_info, session_info]
165
- print(fit_to_width(parts, max_width))
166
-
167
-
168
- if __name__ == "__main__":
169
- main()
@@ -1 +0,0 @@
1
- """Core functionality for claude-statusline."""
@@ -1,124 +0,0 @@
1
- """ANSI color constants and utilities."""
2
-
3
- from __future__ import annotations
4
-
5
- import re
6
-
7
- # ANSI color codes (defaults)
8
- BLUE = "\033[0;34m"
9
- MAGENTA = "\033[0;35m"
10
- CYAN = "\033[0;36m"
11
- GREEN = "\033[0;32m"
12
- YELLOW = "\033[0;33m"
13
- RED = "\033[0;31m"
14
- BOLD = "\033[1m"
15
- DIM = "\033[2m"
16
- RESET = "\033[0m"
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
-
70
-
71
- class ColorManager:
72
- """Manage color output based on terminal capabilities.
73
-
74
- Supports custom color overrides via a dict of {slot_name: ansi_code}.
75
- """
76
-
77
- def __init__(
78
- self,
79
- enabled: bool = True,
80
- overrides: dict[str, str] | None = None,
81
- ) -> None:
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)
89
-
90
- @property
91
- def blue(self) -> str:
92
- return self._get("blue", BLUE)
93
-
94
- @property
95
- def magenta(self) -> str:
96
- return self._get("magenta", MAGENTA)
97
-
98
- @property
99
- def cyan(self) -> str:
100
- return self._get("cyan", CYAN)
101
-
102
- @property
103
- def green(self) -> str:
104
- return self._get("green", GREEN)
105
-
106
- @property
107
- def yellow(self) -> str:
108
- return self._get("yellow", YELLOW)
109
-
110
- @property
111
- def red(self) -> str:
112
- return self._get("red", RED)
113
-
114
- @property
115
- def bold(self) -> str:
116
- return BOLD if self.enabled else ""
117
-
118
- @property
119
- def dim(self) -> str:
120
- return DIM if self.enabled else ""
121
-
122
- @property
123
- def reset(self) -> str:
124
- return RESET if self.enabled else ""
@@ -1,148 +0,0 @@
1
- """Configuration management for statusline."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from dataclasses import dataclass, field
7
- from pathlib import Path
8
- from typing import Any
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
-
22
-
23
- @dataclass
24
- class Config:
25
- """Configuration settings for the statusline."""
26
-
27
- autocompact: bool = True
28
- token_detail: bool = True
29
- show_delta: bool = True
30
- show_session: bool = True
31
- show_io_tokens: bool = True
32
- reduced_motion: bool = False
33
-
34
- # Custom color overrides (slot_name -> ANSI code)
35
- color_overrides: dict[str, str] = field(default_factory=dict)
36
-
37
- _config_path: Path = field(default_factory=lambda: Path.home() / ".claude" / "statusline.conf")
38
-
39
- @classmethod
40
- def load(cls, config_path: str | Path | None = None) -> Config:
41
- """Load configuration from file.
42
-
43
- Args:
44
- config_path: Path to config file. Defaults to ~/.claude/statusline.conf
45
-
46
- Returns:
47
- Config instance with loaded settings
48
- """
49
- config = cls()
50
- if config_path:
51
- config._config_path = Path(config_path).expanduser()
52
-
53
- if not config._config_path.exists():
54
- config._create_default()
55
- return config
56
-
57
- config._read_config()
58
- return config
59
-
60
- def _create_default(self) -> None:
61
- """Create default config file if it doesn't exist."""
62
- try:
63
- self._config_path.parent.mkdir(parents=True, exist_ok=True)
64
- self._config_path.write_text(
65
- """# Autocompact setting - sync with Claude Code's /config
66
- autocompact=true
67
-
68
- # Token display format
69
- token_detail=true
70
-
71
- # Show token delta since last refresh (adds file I/O on every refresh)
72
- # Disable if you don't need it to reduce overhead
73
- show_delta=true
74
-
75
- # Show session_id in status line
76
- show_session=true
77
-
78
- # Disable rotating text animations
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
91
- """
92
- )
93
- except OSError as e:
94
- sys.stderr.write(
95
- f"[statusline] warning: failed to create config {self._config_path}: {e}\n"
96
- )
97
-
98
- def _read_config(self) -> None:
99
- """Read settings from config file."""
100
- try:
101
- content = self._config_path.read_text()
102
- for line in content.splitlines():
103
- line = line.strip()
104
- if line.startswith("#") or "=" not in line:
105
- continue
106
- key, value = line.split("=", 1)
107
- key = key.strip()
108
- raw_value = value.strip()
109
- value_lower = raw_value.lower()
110
-
111
- if key == "autocompact":
112
- self.autocompact = value_lower != "false"
113
- elif key == "token_detail":
114
- self.token_detail = value_lower != "false"
115
- elif key == "show_delta":
116
- self.show_delta = value_lower != "false"
117
- elif key == "show_session":
118
- self.show_session = value_lower != "false"
119
- elif key == "show_io_tokens":
120
- self.show_io_tokens = value_lower != "false"
121
- elif key == "reduced_motion":
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
- )
133
- except (OSError, UnicodeDecodeError) as e:
134
- sys.stderr.write(
135
- f"[statusline] warning: failed to read config {self._config_path}: {e}\n"
136
- )
137
-
138
- def to_dict(self) -> dict[str, Any]:
139
- """Convert config to dictionary."""
140
- return {
141
- "autocompact": self.autocompact,
142
- "token_detail": self.token_detail,
143
- "show_delta": self.show_delta,
144
- "show_session": self.show_session,
145
- "show_io_tokens": self.show_io_tokens,
146
- "reduced_motion": self.reduced_motion,
147
- "color_overrides": dict(self.color_overrides),
148
- }