Coverage for src / moai_adk / statusline / renderer.py: 13.75%

160 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-20 20:52 +0900

1""" 

2Statusline renderer for Claude Code status display 

3 

4""" 

5 

6# type: ignore 

7 

8from dataclasses import dataclass 

9from typing import List 

10 

11from .config import StatuslineConfig # type: ignore[attr-defined] 

12 

13 

14@dataclass 

15class StatuslineData: 

16 """Status line data structure containing all necessary information""" 

17 

18 model: str 

19 version: str 

20 memory_usage: str 

21 branch: str 

22 git_status: str 

23 duration: str 

24 directory: str 

25 active_task: str 

26 claude_version: str = "" # Claude Code version (e.g., "2.0.46") 

27 output_style: str = "" # Output style name (e.g., "R2-D2", "Yoda") 

28 update_available: bool = False 

29 latest_version: str = "" 

30 

31 

32class StatuslineRenderer: 

33 """Renders status information in various modes (compact, extended, minimal)""" 

34 

35 # Constraints for each mode 

36 _MODE_CONSTRAINTS = { 

37 "compact": 80, 

38 "extended": 120, 

39 "minimal": 40, 

40 } 

41 

42 def __init__(self): 

43 """Initialize renderer with configuration""" 

44 self._config = StatuslineConfig() 

45 self._format_config = self._config.get_format_config() 

46 self._display_config = self._config.get_display_config() 

47 

48 def render(self, data: StatuslineData, mode: str = "compact") -> str: 

49 """ 

50 Render statusline with given data in specified mode 

51 

52 Args: 

53 data: StatuslineData instance with all required fields 

54 mode: Display mode - "compact" (80 chars), "extended" (120 chars), "minimal" (40 chars) 

55 

56 Returns: 

57 Formatted statusline string 

58 """ 

59 render_method = { 

60 "compact": self._render_compact, 

61 "extended": self._render_extended, 

62 "minimal": self._render_minimal, 

63 }.get(mode, self._render_compact) 

64 

65 return render_method(data) 

66 

67 def _render_compact(self, data: StatuslineData) -> str: 

68 """ 

69 Render compact mode: [MODEL] [DURATION] | [DIR] | [VERSION] | [BRANCH] | [GIT] | [TASK] 

70 Constraint: <= 80 characters 

71 

72 Args: 

73 data: StatuslineData instance 

74 

75 Returns: 

76 Formatted statusline string (max 80 chars) 

77 """ 

78 max_length = self._MODE_CONSTRAINTS["compact"] 

79 parts = self._build_compact_parts(data) 

80 result = self._format_config.separator.join(parts) 

81 

82 # Adjust if too long 

83 if len(result) > max_length: 

84 result = self._fit_to_constraint(data, max_length) 

85 

86 return result 

87 

88 def _build_compact_parts(self, data: StatuslineData) -> List[str]: 

89 """ 

90 Build parts list for compact mode with labeled sections 

91 New Format: 🤖 Model | 🔅 Claude Code Version | 🗿 MoAI Version | 💬 Style | 📊 Changes | 🔀 Branch 

92 

93 Args: 

94 data: StatuslineData instance 

95 

96 Returns: 

97 List of parts to be joined 

98 """ 

99 parts = [] 

100 

101 # Add model if display enabled (most important - cloud service context) 

102 if self._display_config.model: 

103 parts.append(f"🤖 {data.model}") 

104 

105 # Add Claude Code version if available 

106 if data.claude_version: 

107 claude_ver_str = data.claude_version if data.claude_version.startswith("v") else f"v{data.claude_version}" 

108 parts.append(f"🔅 {claude_ver_str}") 

109 

110 # Add MoAI version if display enabled (system status) 

111 if self._display_config.version: 

112 # Add 'v' prefix if not already present 

113 version_str = data.version if data.version.startswith("v") else f"v{data.version}" 

114 parts.append(f"🗿 {version_str}") 

115 

116 # Add output style if not empty 

117 if data.output_style: 

118 parts.append(f"💬 {data.output_style}") 

119 

120 # Add git status if display enabled and status not empty 

121 if self._display_config.git_status and data.git_status: 

122 parts.append(f"📊 {data.git_status}") 

123 

124 # Add Git info (development context) 

125 if self._display_config.branch: 

126 parts.append(f"🔀 {data.branch}") 

127 

128 # Add active_task if display enabled and not empty 

129 if self._display_config.active_task and data.active_task.strip(): 

130 parts.append(data.active_task) 

131 

132 return parts 

133 

134 def _fit_to_constraint(self, data: StatuslineData, max_length: int) -> str: 

135 """ 

136 Fit statusline to character constraint by truncating 

137 New Format: 🤖 Model | 🔅 Claude Code Version | 🗿 MoAI Version | 💬 Style | 📊 Changes | 🔀 Branch 

138 

139 Args: 

140 data: StatuslineData instance 

141 max_length: Maximum allowed length 

142 

143 Returns: 

144 Truncated statusline string 

145 """ 

146 # Try with truncated branch first 

147 truncated_branch = self._truncate_branch(data.branch, max_length=30) 

148 version_str = data.version if data.version.startswith("v") else f"v{data.version}" 

149 

150 parts = [f"🤖 {data.model}"] 

151 

152 # Add Claude Code version if available 

153 if data.claude_version: 

154 claude_ver_str = data.claude_version if data.claude_version.startswith("v") else f"v{data.claude_version}" 

155 parts.append(f"🔅 {claude_ver_str}") 

156 

157 parts.append(f"🗿 {version_str}") 

158 

159 # Add output style if not empty 

160 if data.output_style: 

161 parts.append(f"💬 {data.output_style}") 

162 

163 # Add git status if display enabled and status not empty 

164 if self._display_config.git_status and data.git_status: 

165 parts.append(f"📊 {data.git_status}") 

166 

167 # Add Git info 

168 parts.append(f"🔀 {truncated_branch}") 

169 

170 # Only add active_task if it's not empty 

171 if data.active_task.strip(): 

172 parts.append(data.active_task) 

173 

174 result = self._format_config.separator.join(parts) 

175 

176 # If still too long, try more aggressive branch truncation 

177 if len(result) > max_length: 

178 truncated_branch = self._truncate_branch(data.branch, max_length=12) 

179 parts = [f"🤖 {data.model}"] 

180 

181 if data.claude_version: 

182 claude_ver_str = ( 

183 data.claude_version if data.claude_version.startswith("v") else f"v{data.claude_version}" 

184 ) 

185 parts.append(f"🔅 {claude_ver_str}") 

186 

187 parts.append(f"🗿 {version_str}") 

188 

189 if data.git_status: 

190 parts.append(f"📊 {data.git_status}") 

191 if data.output_style: 

192 parts.append(f"💬 {data.output_style}") 

193 parts.append(f"🔀 {truncated_branch}") 

194 if data.active_task.strip(): 

195 parts.append(data.active_task) 

196 result = self._format_config.separator.join(parts) 

197 

198 # If still too long, remove output_style and active_task 

199 if len(result) > max_length: 

200 parts = [f"🤖 {data.model}"] 

201 

202 if data.claude_version: 

203 claude_ver_str = ( 

204 data.claude_version if data.claude_version.startswith("v") else f"v{data.claude_version}" 

205 ) 

206 parts.append(f"🔅 {claude_ver_str}") 

207 

208 parts.append(f"🗿 {version_str}") 

209 

210 if data.git_status: 

211 parts.append(f"📊 {data.git_status}") 

212 parts.append(f"🔀 {truncated_branch}") 

213 result = self._format_config.separator.join(parts) 

214 

215 # Final fallback to minimal if still too long 

216 if len(result) > max_length: 

217 result = self._render_minimal(data) 

218 

219 return result 

220 

221 def _render_extended(self, data: StatuslineData) -> str: 

222 """ 

223 Render extended mode: Full path and detailed info with labels 

224 Constraint: <= 120 characters 

225 New Format: 🤖 Model | 🔅 Claude Code Version | 🗿 MoAI Version | 💬 Style | 📊 Changes | 🔀 Branch 

226 

227 Args: 

228 data: StatuslineData instance 

229 

230 Returns: 

231 Formatted statusline string (max 120 chars) 

232 """ 

233 branch = self._truncate_branch(data.branch, max_length=30) 

234 version_str = data.version if data.version.startswith("v") else f"v{data.version}" 

235 

236 parts = [] 

237 

238 # Add model if display enabled 

239 if self._display_config.model: 

240 parts.append(f"🤖 {data.model}") 

241 

242 # Add Claude Code version if available 

243 if data.claude_version: 

244 claude_ver_str = data.claude_version if data.claude_version.startswith("v") else f"v{data.claude_version}" 

245 parts.append(f"🔅 {claude_ver_str}") 

246 

247 # Add MoAI version if display enabled 

248 if self._display_config.version: 

249 parts.append(f"🗿 {version_str}") 

250 

251 # Add output style if not empty 

252 if data.output_style: 

253 parts.append(f"💬 {data.output_style}") 

254 

255 # Add git status if display enabled and status not empty 

256 if self._display_config.git_status and data.git_status: 

257 parts.append(f"📊 {data.git_status}") 

258 

259 # Add Git info (development context) 

260 if self._display_config.branch: 

261 parts.append(f"🔀 {branch}") 

262 

263 # Add active_task if display enabled and not empty 

264 if self._display_config.active_task and data.active_task.strip(): 

265 parts.append(data.active_task) 

266 

267 result = self._format_config.separator.join(parts) 

268 

269 # If exceeds limit, try truncating branch 

270 if len(result) > 120: 

271 branch = self._truncate_branch(data.branch, max_length=30) 

272 parts = [f"🤖 {data.model}"] 

273 

274 if data.claude_version: 

275 claude_ver_str = ( 

276 data.claude_version if data.claude_version.startswith("v") else f"v{data.claude_version}" 

277 ) 

278 parts.append(f"🔅 {claude_ver_str}") 

279 

280 parts.append(f"🗿 {version_str}") 

281 

282 # Add git status if display enabled and not empty 

283 if self._display_config.git_status and data.git_status: 

284 parts.append(f"📊 {data.git_status}") 

285 

286 # Add output style if not empty 

287 if data.output_style: 

288 parts.append(f"💬 {data.output_style}") 

289 

290 parts.append(f"🔀 {branch}") 

291 

292 if data.active_task.strip(): 

293 parts.append(data.active_task) 

294 result = self._format_config.separator.join(parts) 

295 

296 return result 

297 

298 def _render_minimal(self, data: StatuslineData) -> str: 

299 """ 

300 Render minimal mode: Extreme space constraint with minimal labels 

301 Constraint: <= 40 characters 

302 New Format: 🤖 Model | 🔅 Claude Code Ver | 🗿 MoAI Ver | Changes 

303 

304 Args: 

305 data: StatuslineData instance 

306 

307 Returns: 

308 Formatted statusline string (max 40 chars) 

309 """ 

310 parts = [] 

311 

312 # Add model if display enabled 

313 if self._display_config.model: 

314 parts.append(f"🤖 {data.model}") 

315 

316 # Add Claude Code version if available (truncated for minimal) 

317 if data.claude_version: 

318 claude_ver_str = data.claude_version if data.claude_version.startswith("v") else f"v{data.claude_version}" 

319 # For minimal mode, just show major.minor (e.g., "v2.0" from "v2.0.46") 

320 if len(claude_ver_str.split('.')) > 2: 

321 claude_ver_str = '.'.join(claude_ver_str.split('.')[:2]) 

322 parts.append(f"🔅 {claude_ver_str}") 

323 

324 # Add MoAI version if display enabled 

325 if self._display_config.version: 

326 truncated_ver = self._truncate_version(data.version) 

327 # Add 'v' prefix if not already present 

328 version_str = truncated_ver if truncated_ver.startswith("v") else f"v{truncated_ver}" 

329 parts.append(f"🗿 {version_str}") 

330 

331 result = self._format_config.separator.join(parts) 

332 

333 # Add git_status if it fits (use abbreviated format for minimal) 

334 # and if display is enabled and status not empty 

335 if self._display_config.git_status and data.git_status: 

336 status_label = f"Chg: {data.git_status}" 

337 if ( 

338 len(result) + len(status_label) + len(self._format_config.separator) 

339 <= 40 

340 ): 

341 result += f"{self._format_config.separator}{status_label}" 

342 

343 return result 

344 

345 @staticmethod 

346 def _truncate_branch(branch: str, max_length: int = 30) -> str: 

347 """ 

348 Truncate branch name intelligently, preserving SPEC ID if present 

349 

350 Args: 

351 branch: Branch name to truncate 

352 max_length: Maximum allowed length 

353 

354 Returns: 

355 Truncated branch name 

356 """ 

357 if len(branch) <= max_length: 

358 return branch 

359 

360 # Try to preserve SPEC ID in feature branches 

361 if "SPEC" in branch: 

362 parts = branch.split("-") 

363 for i, part in enumerate(parts): 

364 if "SPEC" in part and i + 1 < len(parts): 

365 # Found SPEC ID, include it 

366 spec_truncated = "-".join(parts[: i + 2]) 

367 if len(spec_truncated) <= max_length: 

368 return spec_truncated 

369 

370 # Simple truncation with ellipsis for very long names 

371 return f"{branch[:max_length-1]}" if len(branch) > max_length else branch 

372 

373 @staticmethod 

374 def _truncate_version(version: str) -> str: 

375 """ 

376 Truncate version string for minimal display by removing 'v' prefix 

377 

378 Args: 

379 version: Version string (e.g., "v0.20.1" or "0.20.1") 

380 

381 Returns: 

382 Truncated version string 

383 """ 

384 if version.startswith("v"): 

385 return version[1:] 

386 return version