cc-context-stats 1.3.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.
Files changed (72) hide show
  1. package/.claude/commands/context-stats.md +17 -0
  2. package/.claude/settings.local.json +85 -0
  3. package/.editorconfig +60 -0
  4. package/.eslintrc.json +35 -0
  5. package/.github/dependabot.yml +44 -0
  6. package/.github/workflows/ci.yml +255 -0
  7. package/.github/workflows/release.yml +149 -0
  8. package/.pre-commit-config.yaml +74 -0
  9. package/.prettierrc +33 -0
  10. package/.shellcheckrc +10 -0
  11. package/CHANGELOG.md +100 -0
  12. package/CONTRIBUTING.md +240 -0
  13. package/PUBLISHING_GUIDE.md +69 -0
  14. package/README.md +179 -0
  15. package/config/settings-example.json +7 -0
  16. package/config/settings-node.json +7 -0
  17. package/config/settings-python.json +7 -0
  18. package/docs/configuration.md +83 -0
  19. package/docs/context-stats.md +132 -0
  20. package/docs/installation.md +195 -0
  21. package/docs/scripts.md +116 -0
  22. package/docs/troubleshooting.md +189 -0
  23. package/images/claude-statusline-token-graph.gif +0 -0
  24. package/images/claude-statusline.png +0 -0
  25. package/images/context-status-dumbzone.png +0 -0
  26. package/images/context-status.png +0 -0
  27. package/images/statusline-detail.png +0 -0
  28. package/images/token-graph.jpeg +0 -0
  29. package/images/token-graph.png +0 -0
  30. package/install +344 -0
  31. package/install.sh +272 -0
  32. package/jest.config.js +11 -0
  33. package/npm-publish.sh +33 -0
  34. package/package.json +36 -0
  35. package/publish.sh +24 -0
  36. package/pyproject.toml +113 -0
  37. package/requirements-dev.txt +12 -0
  38. package/scripts/context-stats.sh +970 -0
  39. package/scripts/statusline-full.sh +241 -0
  40. package/scripts/statusline-git.sh +32 -0
  41. package/scripts/statusline-minimal.sh +11 -0
  42. package/scripts/statusline.js +350 -0
  43. package/scripts/statusline.py +312 -0
  44. package/show_raw_claude_code_api.js +11 -0
  45. package/src/claude_statusline/__init__.py +11 -0
  46. package/src/claude_statusline/__main__.py +6 -0
  47. package/src/claude_statusline/cli/__init__.py +1 -0
  48. package/src/claude_statusline/cli/context_stats.py +379 -0
  49. package/src/claude_statusline/cli/statusline.py +172 -0
  50. package/src/claude_statusline/core/__init__.py +1 -0
  51. package/src/claude_statusline/core/colors.py +55 -0
  52. package/src/claude_statusline/core/config.py +98 -0
  53. package/src/claude_statusline/core/git.py +67 -0
  54. package/src/claude_statusline/core/state.py +266 -0
  55. package/src/claude_statusline/formatters/__init__.py +1 -0
  56. package/src/claude_statusline/formatters/time.py +50 -0
  57. package/src/claude_statusline/formatters/tokens.py +70 -0
  58. package/src/claude_statusline/graphs/__init__.py +1 -0
  59. package/src/claude_statusline/graphs/renderer.py +346 -0
  60. package/src/claude_statusline/graphs/statistics.py +58 -0
  61. package/tests/bash/test_install.bats +29 -0
  62. package/tests/bash/test_statusline_full.bats +109 -0
  63. package/tests/bash/test_statusline_git.bats +42 -0
  64. package/tests/bash/test_statusline_minimal.bats +37 -0
  65. package/tests/fixtures/json/high_usage.json +17 -0
  66. package/tests/fixtures/json/low_usage.json +17 -0
  67. package/tests/fixtures/json/medium_usage.json +17 -0
  68. package/tests/fixtures/json/valid_full.json +30 -0
  69. package/tests/fixtures/json/valid_minimal.json +9 -0
  70. package/tests/node/statusline.test.js +199 -0
  71. package/tests/python/conftest.py +84 -0
  72. package/tests/python/test_statusline.py +154 -0
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env python3
2
+ """Context Stats Visualizer for Claude Code.
3
+
4
+ Displays ASCII graphs of token consumption over time.
5
+
6
+ Usage:
7
+ context-stats [session_id] [options]
8
+
9
+ Options:
10
+ --type <cumulative|delta|io|both|all> Graph type to display (default: both)
11
+ --watch, -w [interval] Real-time monitoring mode (default: 2s)
12
+ --no-color Disable color output
13
+ --help Show this help
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import signal
20
+ import sys
21
+ import time
22
+ from pathlib import Path
23
+
24
+ from claude_statusline import __version__
25
+ from claude_statusline.core.colors import ColorManager
26
+ from claude_statusline.core.config import Config
27
+ from claude_statusline.core.state import StateFile
28
+ from claude_statusline.graphs.renderer import GraphDimensions, GraphRenderer
29
+ from claude_statusline.graphs.statistics import calculate_deltas
30
+
31
+ # Cursor control sequences
32
+ CURSOR_HOME = "\033[H"
33
+ CLEAR_SCREEN = "\033[2J"
34
+ HIDE_CURSOR = "\033[?25l"
35
+ SHOW_CURSOR = "\033[?25h"
36
+ CLEAR_TO_END = "\033[J"
37
+
38
+
39
+ def show_help() -> None:
40
+ """Show help message."""
41
+ print(
42
+ """Context Stats Visualizer for Claude Code
43
+
44
+ USAGE:
45
+ context-stats [session_id] [options]
46
+
47
+ ARGUMENTS:
48
+ session_id Optional session ID. If not provided, uses the latest session.
49
+
50
+ OPTIONS:
51
+ --type <type> Graph type to display:
52
+ - delta: Context growth per interaction (default)
53
+ - cumulative: Total context usage over time
54
+ - io: Input/output tokens over time
55
+ - both: Show cumulative and delta graphs
56
+ - all: Show all graphs including I/O
57
+ -w [interval] Set refresh interval in seconds (default: 2)
58
+ --no-watch Show graphs once and exit (disable live monitoring)
59
+ --no-color Disable color output
60
+ --help Show this help message
61
+
62
+ NOTE:
63
+ By default, context-stats runs in live monitoring mode, refreshing every 2 seconds.
64
+ Press Ctrl+C to exit. Use --no-watch to display graphs once and exit.
65
+
66
+ EXAMPLES:
67
+ # Live monitoring (default, refreshes every 2s)
68
+ context-stats
69
+
70
+ # Live monitoring with custom interval
71
+ context-stats -w 5
72
+
73
+ # Show graphs once and exit
74
+ context-stats --no-watch
75
+
76
+ # Show graphs for specific session
77
+ context-stats abc123def
78
+
79
+ # Show only cumulative graph
80
+ context-stats --type cumulative
81
+
82
+ # Combine options
83
+ context-stats abc123 --type cumulative -w 3
84
+
85
+ # Output to file (no colors, single run)
86
+ context-stats --no-watch --no-color > output.txt
87
+
88
+ DATA SOURCE:
89
+ Reads token history from ~/.claude/statusline/statusline.<session_id>.state
90
+ """
91
+ )
92
+
93
+
94
+ def parse_args() -> argparse.Namespace:
95
+ """Parse command-line arguments."""
96
+ parser = argparse.ArgumentParser(
97
+ description="Context Stats Visualizer for Claude Code",
98
+ add_help=False,
99
+ )
100
+ parser.add_argument("session_id", nargs="?", default=None, help="Session ID")
101
+ parser.add_argument(
102
+ "--type",
103
+ choices=["cumulative", "delta", "io", "both", "all"],
104
+ default="delta",
105
+ help="Graph type to display (default: delta)",
106
+ )
107
+ parser.add_argument(
108
+ "--watch",
109
+ "-w",
110
+ nargs="?",
111
+ const=2,
112
+ type=int,
113
+ default=2,
114
+ help="Watch mode interval in seconds (default: 2, use --no-watch to disable)",
115
+ )
116
+ parser.add_argument(
117
+ "--no-watch",
118
+ action="store_true",
119
+ help="Disable watch mode (show graphs once and exit)",
120
+ )
121
+ parser.add_argument(
122
+ "--no-color",
123
+ action="store_true",
124
+ help="Disable color output",
125
+ )
126
+ parser.add_argument(
127
+ "--help",
128
+ "-h",
129
+ action="store_true",
130
+ help="Show help message",
131
+ )
132
+
133
+ args = parser.parse_args()
134
+
135
+ if args.help:
136
+ show_help()
137
+ sys.exit(0)
138
+
139
+ return args
140
+
141
+
142
+ def render_once(
143
+ state_file: StateFile,
144
+ graph_type: str,
145
+ renderer: GraphRenderer,
146
+ colors: ColorManager,
147
+ watch_mode: bool = False,
148
+ ) -> bool:
149
+ """Render graphs once.
150
+
151
+ Args:
152
+ state_file: StateFile instance
153
+ graph_type: Type of graphs to render
154
+ renderer: GraphRenderer instance
155
+ colors: ColorManager instance
156
+ watch_mode: Whether running in watch mode
157
+
158
+ Returns:
159
+ True if rendering was successful, False if not enough data
160
+ """
161
+ entries = state_file.read_history()
162
+
163
+ if len(entries) < 2:
164
+ print(f"\n{colors.yellow}Need at least 2 data points to generate graphs.{colors.reset}")
165
+ print(
166
+ f"{colors.dim}Found: {len(entries)} entry. Use Claude Code to accumulate more data.{colors.reset}"
167
+ )
168
+ return False
169
+
170
+ # Extract data for graphs
171
+ timestamps = [e.timestamp for e in entries]
172
+ # Current context window usage (what's actually in the context)
173
+ # This is: cache_read + cache_creation + current_input_tokens
174
+ context_used = [e.current_used_tokens for e in entries]
175
+ # Per-request I/O tokens from current_usage
176
+ current_input = [e.current_input_tokens for e in entries]
177
+ current_output = [e.current_output_tokens for e in entries]
178
+ deltas = calculate_deltas(context_used)
179
+ delta_times = timestamps[1:] # Deltas start from second entry
180
+
181
+ # Get session name and project from entries
182
+ file_path = state_file.find_latest_state_file()
183
+ session_name = file_path.stem.replace("statusline.", "") if file_path else "unknown"
184
+
185
+ # Get project name from the last entry (most recent)
186
+ last_entry = entries[-1]
187
+ project_name = ""
188
+ if last_entry.workspace_project_dir:
189
+ # Extract just the project folder name from the path
190
+ project_name = Path(last_entry.workspace_project_dir).name
191
+
192
+ # Header
193
+ if not watch_mode:
194
+ print()
195
+ if project_name:
196
+ print(
197
+ f"{colors.bold}{colors.magenta}Context Stats{colors.reset} "
198
+ f"{colors.dim}({colors.cyan}{project_name}{colors.dim} • {session_name}){colors.reset}"
199
+ )
200
+ else:
201
+ print(
202
+ f"{colors.bold}{colors.magenta}Context Stats{colors.reset} "
203
+ f"{colors.dim}(Session: {session_name}){colors.reset}"
204
+ )
205
+
206
+ # Render requested graphs
207
+ if graph_type in ("cumulative", "both", "all"):
208
+ renderer.render_timeseries(
209
+ context_used, timestamps, "Context Usage Over Time", colors.green
210
+ )
211
+
212
+ if graph_type in ("delta", "both", "all"):
213
+ renderer.render_timeseries(
214
+ deltas, delta_times, "Context Growth Per Interaction", colors.cyan
215
+ )
216
+
217
+ if graph_type in ("io", "all"):
218
+ renderer.render_timeseries(
219
+ current_input, timestamps, "Input Tokens (per request)", colors.blue
220
+ )
221
+ renderer.render_timeseries(
222
+ current_output, timestamps, "Output Tokens (per request)", colors.magenta
223
+ )
224
+
225
+ # Summary and footer
226
+ renderer.render_summary(entries, deltas)
227
+ renderer.render_footer(__version__)
228
+
229
+ return True
230
+
231
+
232
+ def run_watch_mode(
233
+ state_file: StateFile,
234
+ graph_type: str,
235
+ interval: int,
236
+ renderer: GraphRenderer,
237
+ colors: ColorManager,
238
+ ) -> None:
239
+ """Run in watch mode with continuous refresh.
240
+
241
+ Args:
242
+ state_file: StateFile instance
243
+ graph_type: Type of graphs to render
244
+ interval: Refresh interval in seconds
245
+ renderer: GraphRenderer instance
246
+ colors: ColorManager instance
247
+ """
248
+
249
+ # Signal handler for clean exit
250
+ def handle_signal(_signum: int, _frame: object) -> None:
251
+ sys.stdout.write(SHOW_CURSOR)
252
+ sys.stdout.flush()
253
+ print(f"\n{colors.dim}Watch mode stopped.{colors.reset}")
254
+ sys.exit(0)
255
+
256
+ signal.signal(signal.SIGINT, handle_signal)
257
+ signal.signal(signal.SIGTERM, handle_signal)
258
+
259
+ # Hide cursor and initial clear in one write
260
+ sys.stdout.write(f"{HIDE_CURSOR}{CLEAR_SCREEN}{CURSOR_HOME}")
261
+ sys.stdout.flush()
262
+
263
+ try:
264
+ while True:
265
+ # Move cursor to home
266
+ sys.stdout.write(CURSOR_HOME)
267
+ sys.stdout.flush()
268
+
269
+ # Update dimensions in case of terminal resize
270
+ renderer.dimensions = GraphDimensions.detect()
271
+
272
+ # Watch mode indicator
273
+ current_time = time.strftime("%H:%M:%S")
274
+ print(
275
+ f"{colors.dim}[LIVE {current_time}] Refresh: {interval}s | Ctrl+C to exit{colors.reset}"
276
+ )
277
+
278
+ # Check if state file exists now (may have been created since start)
279
+ file_path = state_file.find_latest_state_file()
280
+ if not file_path or not file_path.exists():
281
+ # Show waiting message for new session
282
+ show_waiting_message(
283
+ colors,
284
+ state_file.session_id,
285
+ "Waiting for session data...",
286
+ )
287
+ else:
288
+ # Render graphs
289
+ render_once(state_file, graph_type, renderer, colors, watch_mode=True)
290
+
291
+ # Clear any remaining content
292
+ sys.stdout.write(CLEAR_TO_END)
293
+ sys.stdout.flush()
294
+
295
+ time.sleep(interval)
296
+ finally:
297
+ sys.stdout.write(SHOW_CURSOR)
298
+ sys.stdout.flush()
299
+
300
+
301
+ def show_waiting_message(
302
+ colors: ColorManager,
303
+ session_id: str | None,
304
+ message: str = "Waiting for session data...",
305
+ ) -> None:
306
+ """Show a friendly waiting message for new sessions.
307
+
308
+ Args:
309
+ colors: ColorManager instance
310
+ session_id: Session ID if specified
311
+ message: Message to display
312
+ """
313
+ print()
314
+ if session_id:
315
+ print(
316
+ f"{colors.bold}{colors.magenta}Context Stats{colors.reset} "
317
+ f"{colors.dim}(Session: {session_id}){colors.reset}"
318
+ )
319
+ else:
320
+ print(f"{colors.bold}{colors.magenta}Context Stats{colors.reset}")
321
+
322
+ print()
323
+ print(f" {colors.cyan}⏳ {message}{colors.reset}")
324
+ print()
325
+ print(
326
+ f" {colors.dim}The session has just started and no data has been recorded yet.{colors.reset}"
327
+ )
328
+ print(f" {colors.dim}Data will appear after the first Claude interaction.{colors.reset}")
329
+ print()
330
+
331
+
332
+ def main() -> None:
333
+ """Main entry point for context-stats CLI."""
334
+ args = parse_args()
335
+
336
+ # Load config for token_detail setting
337
+ config = Config.load()
338
+
339
+ # Setup colors
340
+ color_enabled = not args.no_color and sys.stdout.isatty()
341
+ colors = ColorManager(enabled=color_enabled)
342
+
343
+ # Setup state file
344
+ state_file = StateFile(args.session_id)
345
+
346
+ # Find state file
347
+ file_path = state_file.find_latest_state_file()
348
+
349
+ # Handle case where no state file exists yet
350
+ if not file_path or not file_path.exists():
351
+ if args.no_watch:
352
+ # Single run mode - show friendly message and exit
353
+ if args.session_id:
354
+ show_waiting_message(colors, args.session_id)
355
+ else:
356
+ print(f"{colors.yellow}No session data found.{colors.reset}")
357
+ print(f"{colors.dim}Run Claude Code to generate token usage data.{colors.reset}")
358
+ sys.exit(0)
359
+ else:
360
+ # Watch mode - continue and wait for data
361
+ pass
362
+
363
+ # Setup renderer
364
+ renderer = GraphRenderer(
365
+ colors=colors,
366
+ token_detail=config.token_detail,
367
+ )
368
+
369
+ # Run
370
+ if args.no_watch:
371
+ success = render_once(state_file, args.type, renderer, colors)
372
+ if not success:
373
+ sys.exit(1)
374
+ else:
375
+ run_watch_mode(state_file, args.type, args.watch, renderer, colors)
376
+
377
+
378
+ if __name__ == "__main__":
379
+ main()
@@ -0,0 +1,172 @@
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 (
30
+ BLUE,
31
+ DIM,
32
+ GREEN,
33
+ RED,
34
+ RESET,
35
+ YELLOW,
36
+ )
37
+ from claude_statusline.core.config import Config
38
+ from claude_statusline.core.git import get_git_info
39
+ from claude_statusline.core.state import StateEntry, StateFile
40
+ from claude_statusline.formatters.time import get_current_timestamp
41
+ from claude_statusline.formatters.tokens import calculate_context_usage, format_tokens
42
+
43
+
44
+ def main() -> None:
45
+ """Main entry point for claude-statusline CLI."""
46
+ try:
47
+ data = json.load(sys.stdin)
48
+ except json.JSONDecodeError:
49
+ print("[Claude] ~")
50
+ return
51
+
52
+ # Extract data
53
+ cwd = data.get("workspace", {}).get("current_dir", "~")
54
+ project_dir = data.get("workspace", {}).get("project_dir", cwd)
55
+ model = data.get("model", {}).get("display_name", "Claude")
56
+ dir_name = cwd.rsplit("/", 1)[-1] if "/" in cwd else cwd or "~"
57
+
58
+ # Git info
59
+ git_info = get_git_info(project_dir)
60
+
61
+ # Read settings from config file
62
+ config = Config.load()
63
+
64
+ # Extract session_id once for reuse
65
+ session_id = data.get("session_id")
66
+
67
+ # Context window calculation
68
+ context_info = ""
69
+ ac_info = ""
70
+ delta_info = ""
71
+ session_info = ""
72
+
73
+ total_size = data.get("context_window", {}).get("context_window_size", 0)
74
+ current_usage = data.get("context_window", {}).get("current_usage")
75
+ total_input_tokens = data.get("context_window", {}).get("total_input_tokens", 0)
76
+ total_output_tokens = data.get("context_window", {}).get("total_output_tokens", 0)
77
+ cost_usd = data.get("cost", {}).get("total_cost_usd", 0)
78
+ lines_added = data.get("cost", {}).get("total_lines_added", 0)
79
+ lines_removed = data.get("cost", {}).get("total_lines_removed", 0)
80
+ model_id = data.get("model", {}).get("id", "")
81
+ workspace_project_dir = data.get("workspace", {}).get("project_dir", "")
82
+
83
+ if total_size > 0 and current_usage:
84
+ # Get tokens from current_usage (includes cache)
85
+ input_tokens = current_usage.get("input_tokens", 0)
86
+ cache_creation = current_usage.get("cache_creation_input_tokens", 0)
87
+ cache_read = current_usage.get("cache_read_input_tokens", 0)
88
+
89
+ # Total used from current request
90
+ used_tokens = input_tokens + cache_creation + cache_read
91
+
92
+ # Calculate context usage
93
+ free_tokens, free_pct, autocompact_buffer = calculate_context_usage(
94
+ used_tokens,
95
+ total_size,
96
+ config.autocompact,
97
+ )
98
+
99
+ if config.autocompact:
100
+ buffer_k = autocompact_buffer // 1000
101
+ ac_info = f" {DIM}[AC:{buffer_k}k]{RESET}"
102
+ else:
103
+ ac_info = f" {DIM}[AC:off]{RESET}"
104
+
105
+ # Format tokens based on token_detail setting
106
+ free_display = format_tokens(free_tokens, config.token_detail)
107
+
108
+ # Color based on free percentage
109
+ free_pct_int = int(free_pct)
110
+ if free_pct_int > 50:
111
+ ctx_color = GREEN
112
+ elif free_pct_int > 25:
113
+ ctx_color = YELLOW
114
+ else:
115
+ ctx_color = RED
116
+
117
+ context_info = f" | {ctx_color}{free_display} free ({free_pct:.1f}%){RESET}"
118
+
119
+ # Calculate and display token delta if enabled
120
+ if config.show_delta:
121
+ state_file = StateFile(session_id)
122
+ prev_entry = state_file.read_last_entry()
123
+
124
+ prev_tokens = prev_entry.current_used_tokens if prev_entry else 0
125
+ has_prev = prev_entry is not None
126
+
127
+ # Calculate delta
128
+ delta = used_tokens - prev_tokens
129
+
130
+ # Only show positive delta (and skip first run when no previous state)
131
+ if has_prev and delta > 0:
132
+ delta_display = format_tokens(delta, config.token_detail)
133
+ delta_info = f" {DIM}[+{delta_display}]{RESET}"
134
+
135
+ # Build current entry
136
+ cur_input_tokens = current_usage.get("input_tokens", 0)
137
+ cur_output_tokens = current_usage.get("output_tokens", 0)
138
+
139
+ entry = StateEntry(
140
+ timestamp=get_current_timestamp(),
141
+ total_input_tokens=total_input_tokens,
142
+ total_output_tokens=total_output_tokens,
143
+ current_input_tokens=cur_input_tokens,
144
+ current_output_tokens=cur_output_tokens,
145
+ cache_creation=cache_creation,
146
+ cache_read=cache_read,
147
+ cost_usd=cost_usd,
148
+ lines_added=lines_added,
149
+ lines_removed=lines_removed,
150
+ session_id=session_id or "",
151
+ model_id=model_id,
152
+ workspace_project_dir=workspace_project_dir,
153
+ context_window_size=total_size,
154
+ )
155
+
156
+ # Only append if context usage changed (avoid duplicates from multiple refreshes)
157
+ if not has_prev or used_tokens != prev_tokens:
158
+ state_file.append_entry(entry)
159
+
160
+ # Display session_id if enabled
161
+ if config.show_session and session_id:
162
+ session_info = f" {DIM}{session_id}{RESET}"
163
+
164
+ # Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [session_id]
165
+ print(
166
+ f"{DIM}[{model}]{RESET} {BLUE}{dir_name}{RESET}"
167
+ f"{git_info}{context_info}{delta_info}{ac_info}{session_info}"
168
+ )
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()
@@ -0,0 +1 @@
1
+ """Core functionality for claude-statusline."""
@@ -0,0 +1,55 @@
1
+ """ANSI color constants and utilities."""
2
+
3
+ # ANSI color codes
4
+ BLUE = "\033[0;34m"
5
+ MAGENTA = "\033[0;35m"
6
+ CYAN = "\033[0;36m"
7
+ GREEN = "\033[0;32m"
8
+ YELLOW = "\033[0;33m"
9
+ RED = "\033[0;31m"
10
+ BOLD = "\033[1m"
11
+ DIM = "\033[2m"
12
+ RESET = "\033[0m"
13
+
14
+
15
+ class ColorManager:
16
+ """Manage color output based on terminal capabilities."""
17
+
18
+ def __init__(self, enabled: bool = True) -> None:
19
+ self.enabled = enabled
20
+
21
+ @property
22
+ def blue(self) -> str:
23
+ return BLUE if self.enabled else ""
24
+
25
+ @property
26
+ def magenta(self) -> str:
27
+ return MAGENTA if self.enabled else ""
28
+
29
+ @property
30
+ def cyan(self) -> str:
31
+ return CYAN if self.enabled else ""
32
+
33
+ @property
34
+ def green(self) -> str:
35
+ return GREEN if self.enabled else ""
36
+
37
+ @property
38
+ def yellow(self) -> str:
39
+ return YELLOW if self.enabled else ""
40
+
41
+ @property
42
+ def red(self) -> str:
43
+ return RED if self.enabled else ""
44
+
45
+ @property
46
+ def bold(self) -> str:
47
+ return BOLD if self.enabled else ""
48
+
49
+ @property
50
+ def dim(self) -> str:
51
+ return DIM if self.enabled else ""
52
+
53
+ @property
54
+ def reset(self) -> str:
55
+ return RESET if self.enabled else ""
@@ -0,0 +1,98 @@
1
+ """Configuration management for statusline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class Config:
12
+ """Configuration settings for the statusline."""
13
+
14
+ autocompact: bool = True
15
+ token_detail: bool = True
16
+ show_delta: bool = True
17
+ show_session: bool = True
18
+ show_io_tokens: bool = True
19
+
20
+ _config_path: Path = field(default_factory=lambda: Path.home() / ".claude" / "statusline.conf")
21
+
22
+ @classmethod
23
+ def load(cls, config_path: str | Path | None = None) -> Config:
24
+ """Load configuration from file.
25
+
26
+ Args:
27
+ config_path: Path to config file. Defaults to ~/.claude/statusline.conf
28
+
29
+ Returns:
30
+ Config instance with loaded settings
31
+ """
32
+ config = cls()
33
+ if config_path:
34
+ config._config_path = Path(config_path).expanduser()
35
+
36
+ if not config._config_path.exists():
37
+ config._create_default()
38
+ return config
39
+
40
+ config._read_config()
41
+ return config
42
+
43
+ def _create_default(self) -> None:
44
+ """Create default config file if it doesn't exist."""
45
+ try:
46
+ self._config_path.parent.mkdir(parents=True, exist_ok=True)
47
+ self._config_path.write_text(
48
+ """# Autocompact setting - sync with Claude Code's /config
49
+ autocompact=true
50
+
51
+ # Token display format
52
+ token_detail=true
53
+
54
+ # Show token delta since last refresh (adds file I/O on every refresh)
55
+ # Disable if you don't need it to reduce overhead
56
+ show_delta=true
57
+
58
+ # Show session_id in status line
59
+ show_session=true
60
+ """
61
+ )
62
+ except OSError:
63
+ pass # Ignore errors creating config
64
+
65
+ def _read_config(self) -> None:
66
+ """Read settings from config file."""
67
+ try:
68
+ content = self._config_path.read_text()
69
+ for line in content.splitlines():
70
+ line = line.strip()
71
+ if line.startswith("#") or "=" not in line:
72
+ continue
73
+ key, value = line.split("=", 1)
74
+ key = key.strip()
75
+ value = value.strip().lower()
76
+
77
+ if key == "autocompact":
78
+ self.autocompact = value != "false"
79
+ elif key == "token_detail":
80
+ self.token_detail = value != "false"
81
+ elif key == "show_delta":
82
+ self.show_delta = value != "false"
83
+ elif key == "show_session":
84
+ self.show_session = value != "false"
85
+ elif key == "show_io_tokens":
86
+ self.show_io_tokens = value != "false"
87
+ except OSError:
88
+ pass # Use defaults on read error
89
+
90
+ def to_dict(self) -> dict[str, Any]:
91
+ """Convert config to dictionary."""
92
+ return {
93
+ "autocompact": self.autocompact,
94
+ "token_detail": self.token_detail,
95
+ "show_delta": self.show_delta,
96
+ "show_session": self.show_session,
97
+ "show_io_tokens": self.show_io_tokens,
98
+ }