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

1# type: ignore 

2""" 

3Statusline configuration loader for Claude Code 

4 

5 

6Loads and manages statusline configuration from .moai/config/statusline-config.yaml 

7""" 

8 

9import logging 

10from dataclasses import dataclass 

11from pathlib import Path 

12from typing import Any, Dict, Optional 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17@dataclass 

18class CacheConfig: 

19 """Cache TTL configuration""" 

20 

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 

29 

30 

31@dataclass 

32class ColorConfig: 

33 """Color configuration""" 

34 

35 enabled: bool = True 

36 theme: str = "auto" # auto | light | dark | high-contrast 

37 palette: Dict[str, str] = None 

38 

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 } 

56 

57 

58@dataclass 

59class DisplayConfig: 

60 """Information display settings - Custom ordered status bar""" 

61 

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 

73 

74 

75@dataclass 

76class FormatConfig: 

77 """Format configuration""" 

78 

79 max_branch_length: int = 20 

80 truncate_with: str = "..." 

81 separator: str = " | " 

82 

83 # Icon configuration for better visual recognition 

84 icons: Dict[str, str] = None 

85 

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 } 

105 

106 

107@dataclass 

108class ErrorHandlingConfig: 

109 """Error handling configuration""" 

110 

111 graceful_degradation: bool = True 

112 log_level: str = "warning" # warning | error 

113 fallback_text: str = "" 

114 

115 

116class StatuslineConfig: 

117 """ 

118 Singleton configuration loader for statusline 

119 

120 Loads configuration from .moai/config/statusline-config.yaml 

121 Falls back to default values if file not found or parsing fails 

122 """ 

123 

124 _instance: Optional["StatuslineConfig"] = None 

125 _config: Dict[str, Any] = {} 

126 

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 

132 

133 def _load_config(self) -> None: 

134 """Load configuration from YAML file""" 

135 config_path = self._find_config_file() 

136 

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() 

147 

148 @staticmethod 

149 def _find_config_file() -> Optional[Path]: 

150 """ 

151 Find statusline config file starting from current directory up to project root 

152 

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 ] 

162 

163 for path in locations: 

164 if path.exists(): 

165 return path 

166 

167 return None 

168 

169 @staticmethod 

170 def _parse_yaml(path: Path) -> Dict[str, Any]: 

171 """ 

172 Parse YAML file 

173 

174 Args: 

175 path: Path to YAML file 

176 

177 Returns: 

178 Parsed configuration dictionary 

179 """ 

180 try: 

181 import yaml 

182 

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) 

189 

190 @staticmethod 

191 def _parse_json_fallback(path: Path) -> Dict[str, Any]: 

192 """ 

193 Parse YAML as JSON fallback (limited support) 

194 

195 Args: 

196 path: Path to file 

197 

198 Returns: 

199 Parsed configuration dictionary 

200 """ 

201 import json 

202 

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 {} 

209 

210 @staticmethod 

211 def _get_default_config() -> Dict[str, Any]: 

212 """ 

213 Get default configuration 

214 

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 } 

289 

290 def get(self, key: str, default: Any = None) -> Any: 

291 """ 

292 Get configuration value by dot-notation key 

293 

294 Args: 

295 key: Configuration key (e.g., "statusline.mode", "statusline.cache.git_ttl_seconds") 

296 default: Default value if key not found 

297 

298 Returns: 

299 Configuration value or default 

300 """ 

301 keys = key.split(".") 

302 value = self._config 

303 

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 

311 

312 return value if value is not None else default 

313 

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 ) 

324 

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 ) 

333 

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 ) 

347 

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 ) 

356 

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 )