Coverage for src / moai_adk / statusline / config.py: 52.85%
123 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1# type: ignore
2"""
3Statusline configuration loader for Claude Code
6Loads and manages statusline configuration from .moai/config/statusline-config.yaml
7"""
9import logging
10from dataclasses import dataclass
11from pathlib import Path
12from typing import Any, Dict, Optional
14logger = logging.getLogger(__name__)
17@dataclass
18class CacheConfig:
19 """Cache TTL configuration"""
21 git_ttl_seconds: int = 5
22 metrics_ttl_seconds: int = 10
23 alfred_ttl_seconds: int = 1
24 todo_ttl_seconds: int = 3
25 memory_ttl_seconds: int = 5
26 output_style_ttl_seconds: int = 60
27 version_ttl_seconds: int = 60
28 update_ttl_seconds: int = 300
31@dataclass
32class ColorConfig:
33 """Color configuration"""
35 enabled: bool = True
36 theme: str = "auto" # auto | light | dark | high-contrast
37 palette: Dict[str, str] = None
39 def __post_init__(self):
40 if self.palette is None:
41 self.palette = {
42 "model": "38;5;33", # Blue
43 "output_style": "38;5;219", # Pink/Magenta
44 "feature_branch": "38;5;226", # Yellow
45 "develop_branch": "38;5;51", # Cyan
46 "main_branch": "38;5;46", # Green
47 "staged": "38;5;46", # Green
48 "modified": "38;5;208", # Orange
49 "untracked": "38;5;196", # Red
50 "update_available": "38;5;208", # Orange
51 "memory_usage": "38;5;172", # Brown/Orange
52 "duration": "38;5;240", # Gray
53 "todo_count": "38;5;123", # Purple
54 "separator": "38;5;250", # Light Gray
55 }
58@dataclass
59class DisplayConfig:
60 """Information display settings - Custom ordered status bar"""
62 model: bool = True # 🤖 Model name (glm-4.6, claude-3.5-sonnet, etc.)
63 version: bool = True # 🗿 MoAI-ADK version (0.23.0, etc.)
64 output_style: bool = True # ✍️ Output style (Explanatory, Concise, etc.)
65 memory_usage: bool = True # 💾 Session memory usage
66 todo_count: bool = True # 📋 Active TODO items count
67 branch: bool = True # 🔀 Git branch
68 git_status: bool = True # ✅2 M1 ?10 Git changes status
69 duration: bool = True # ⏱️ Session elapsed time
70 directory: bool = True # 📁 Project name/directory
71 active_task: bool = True # 🎯 Alfred active task
72 update_indicator: bool = True # 🔄 Update notification
75@dataclass
76class FormatConfig:
77 """Format configuration"""
79 max_branch_length: int = 20
80 truncate_with: str = "..."
81 separator: str = " | "
83 # Icon configuration for better visual recognition
84 icons: Dict[str, str] = None
86 def __post_init__(self):
87 if self.icons is None:
88 self.icons = {
89 "git": "🔀", # Git branch icon (more intuitive than 📊)
90 "staged": "✅", # Staged files
91 "modified": "📝", # Modified files
92 "untracked": "❓", # Untracked files
93 "added": "➕", # Added files
94 "deleted": "➖", # Deleted files
95 "renamed": "🔄", # Renamed files
96 "conflict": "⚠️", # Conflict files
97 "model": "🤖", # AI model
98 "output_style": "✍️", # Writing style
99 "duration": "⏱️", # Time duration
100 "memory": "💾", # Memory usage
101 "todo": "📋", # TODO items
102 "update": "🔄", # Update available
103 "project": "📁", # Project directory
104 }
107@dataclass
108class ErrorHandlingConfig:
109 """Error handling configuration"""
111 graceful_degradation: bool = True
112 log_level: str = "warning" # warning | error
113 fallback_text: str = ""
116class StatuslineConfig:
117 """
118 Singleton configuration loader for statusline
120 Loads configuration from .moai/config/statusline-config.yaml
121 Falls back to default values if file not found or parsing fails
122 """
124 _instance: Optional["StatuslineConfig"] = None
125 _config: Dict[str, Any] = {}
127 def __new__(cls):
128 if cls._instance is None:
129 cls._instance = super().__new__(cls)
130 cls._instance._load_config()
131 return cls._instance
133 def _load_config(self) -> None:
134 """Load configuration from YAML file"""
135 config_path = self._find_config_file()
137 if config_path and config_path.exists():
138 try:
139 self._config = self._parse_yaml(config_path)
140 logger.debug(f"Loaded statusline config from {config_path}")
141 except Exception as e:
142 logger.warning(f"Failed to load config from {config_path}: {e}")
143 self._config = self._get_default_config()
144 else:
145 logger.debug("Statusline config file not found, using defaults")
146 self._config = self._get_default_config()
148 @staticmethod
149 def _find_config_file() -> Optional[Path]:
150 """
151 Find statusline config file starting from current directory up to project root
153 Returns:
154 Path to config file or None if not found
155 """
156 # Try common locations
157 locations = [
158 Path.cwd() / ".moai" / "config" / "statusline-config.yaml",
159 Path.cwd() / ".moai" / "config" / "statusline-config.yml",
160 Path.home() / ".moai" / "config" / "statusline-config.yaml",
161 ]
163 for path in locations:
164 if path.exists():
165 return path
167 return None
169 @staticmethod
170 def _parse_yaml(path: Path) -> Dict[str, Any]:
171 """
172 Parse YAML file
174 Args:
175 path: Path to YAML file
177 Returns:
178 Parsed configuration dictionary
179 """
180 try:
181 import yaml
183 with open(path, "r", encoding="utf-8") as f:
184 data = yaml.safe_load(f)
185 return data or {}
186 except ImportError:
187 logger.debug("PyYAML not available, attempting JSON fallback")
188 return StatuslineConfig._parse_json_fallback(path)
190 @staticmethod
191 def _parse_json_fallback(path: Path) -> Dict[str, Any]:
192 """
193 Parse YAML as JSON fallback (limited support)
195 Args:
196 path: Path to file
198 Returns:
199 Parsed configuration dictionary
200 """
201 import json
203 try:
204 with open(path, "r", encoding="utf-8") as f:
205 return json.load(f)
206 except Exception as e:
207 logger.debug(f"JSON fallback failed: {e}")
208 return {}
210 @staticmethod
211 def _get_default_config() -> Dict[str, Any]:
212 """
213 Get default configuration
215 Returns:
216 Default configuration dictionary
217 """
218 return {
219 "statusline": {
220 "enabled": True,
221 "mode": "extended",
222 "refresh_interval_ms": 300,
223 "colors": {
224 "enabled": True,
225 "theme": "auto",
226 "palette": {
227 "model": "38;5;33",
228 "feature_branch": "38;5;226",
229 "develop_branch": "38;5;51",
230 "main_branch": "38;5;46",
231 "staged": "38;5;46",
232 "modified": "38;5;208",
233 "untracked": "38;5;196",
234 "update_available": "38;5;208",
235 },
236 },
237 "cache": {
238 "git_ttl_seconds": 5,
239 "metrics_ttl_seconds": 10,
240 "alfred_ttl_seconds": 1,
241 "todo_ttl_seconds": 3,
242 "memory_ttl_seconds": 5,
243 "output_style_ttl_seconds": 60,
244 "version_ttl_seconds": 60,
245 "update_ttl_seconds": 300,
246 },
247 "display": {
248 "model": True, # 🤖 Model name (glm-4.6, claude-3.5-sonnet, etc.)
249 "version": True, # 🗿 MoAI-ADK version (0.23.0, etc.)
250 "output_style": True, # ✍️ Output style (Explanatory, Concise, etc.)
251 "memory_usage": True, # 💾 Session memory usage
252 "todo_count": True, # 📋 Active TODO items count
253 "branch": True, # 🔀 Git branch
254 "git_status": True, # ✅2 M1 ?10 Git changes status
255 "duration": True, # ⏱️ Session elapsed time
256 "directory": True, # 📁 Project name/directory
257 "active_task": True, # 🎯 Alfred active task
258 "update_indicator": True, # 🔄 Update notification
259 },
260 "error_handling": {
261 "graceful_degradation": True,
262 "log_level": "warning",
263 "fallback_text": "",
264 },
265 "format": {
266 "max_branch_length": 20,
267 "truncate_with": "...",
268 "separator": " | ",
269 "icons": {
270 "git": "🔀", # Git branch icon (more intuitive than 📊)
271 "staged": "✅", # Staged files
272 "modified": "📝", # Modified files
273 "untracked": "❓", # Untracked files
274 "added": "➕", # Added files
275 "deleted": "➖", # Deleted files
276 "renamed": "🔄", # Renamed files
277 "conflict": "⚠️", # Conflict files
278 "model": "🤖", # AI model
279 "output_style": "✍️", # Writing style
280 "duration": "⏱️", # Time duration
281 "memory": "💾", # Memory usage
282 "todo": "📋", # TODO items
283 "update": "🔄", # Update available
284 "project": "📁", # Project directory
285 },
286 },
287 }
288 }
290 def get(self, key: str, default: Any = None) -> Any:
291 """
292 Get configuration value by dot-notation key
294 Args:
295 key: Configuration key (e.g., "statusline.mode", "statusline.cache.git_ttl_seconds")
296 default: Default value if key not found
298 Returns:
299 Configuration value or default
300 """
301 keys = key.split(".")
302 value = self._config
304 for k in keys:
305 if isinstance(value, dict):
306 value = value.get(k)
307 if value is None:
308 return default
309 else:
310 return default
312 return value if value is not None else default
314 def get_cache_config(self) -> CacheConfig:
315 """Get cache configuration"""
316 cache_data = self.get("statusline.cache", {})
317 return CacheConfig(
318 git_ttl_seconds=cache_data.get("git_ttl_seconds", 5),
319 metrics_ttl_seconds=cache_data.get("metrics_ttl_seconds", 10),
320 alfred_ttl_seconds=cache_data.get("alfred_ttl_seconds", 1),
321 version_ttl_seconds=cache_data.get("version_ttl_seconds", 60),
322 update_ttl_seconds=cache_data.get("update_ttl_seconds", 300),
323 )
325 def get_color_config(self) -> ColorConfig:
326 """Get color configuration"""
327 color_data = self.get("statusline.colors", {})
328 return ColorConfig(
329 enabled=color_data.get("enabled", True),
330 theme=color_data.get("theme", "auto"),
331 palette=color_data.get("palette", {}),
332 )
334 def get_display_config(self) -> DisplayConfig:
335 """Get display configuration"""
336 display_data = self.get("statusline.display", {})
337 return DisplayConfig(
338 model=display_data.get("model", True),
339 duration=display_data.get("duration", True),
340 directory=display_data.get("directory", True),
341 version=display_data.get("version", True),
342 branch=display_data.get("branch", True),
343 git_status=display_data.get("git_status", True),
344 active_task=display_data.get("active_task", True),
345 update_indicator=display_data.get("update_indicator", True),
346 )
348 def get_format_config(self) -> FormatConfig:
349 """Get format configuration"""
350 format_data = self.get("statusline.format", {})
351 return FormatConfig(
352 max_branch_length=format_data.get("max_branch_length", 20),
353 truncate_with=format_data.get("truncate_with", "..."),
354 separator=format_data.get("separator", " | "),
355 )
357 def get_error_handling_config(self) -> ErrorHandlingConfig:
358 """Get error handling configuration"""
359 error_data = self.get("statusline.error_handling", {})
360 return ErrorHandlingConfig(
361 graceful_degradation=error_data.get("graceful_degradation", True),
362 log_level=error_data.get("log_level", "warning"),
363 fallback_text=error_data.get("fallback_text", ""),
364 )