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
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2Statusline renderer for Claude Code status display
4"""
6# type: ignore
8from dataclasses import dataclass
9from typing import List
11from .config import StatuslineConfig # type: ignore[attr-defined]
14@dataclass
15class StatuslineData:
16 """Status line data structure containing all necessary information"""
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 = ""
32class StatuslineRenderer:
33 """Renders status information in various modes (compact, extended, minimal)"""
35 # Constraints for each mode
36 _MODE_CONSTRAINTS = {
37 "compact": 80,
38 "extended": 120,
39 "minimal": 40,
40 }
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()
48 def render(self, data: StatuslineData, mode: str = "compact") -> str:
49 """
50 Render statusline with given data in specified mode
52 Args:
53 data: StatuslineData instance with all required fields
54 mode: Display mode - "compact" (80 chars), "extended" (120 chars), "minimal" (40 chars)
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)
65 return render_method(data)
67 def _render_compact(self, data: StatuslineData) -> str:
68 """
69 Render compact mode: [MODEL] [DURATION] | [DIR] | [VERSION] | [BRANCH] | [GIT] | [TASK]
70 Constraint: <= 80 characters
72 Args:
73 data: StatuslineData instance
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)
82 # Adjust if too long
83 if len(result) > max_length:
84 result = self._fit_to_constraint(data, max_length)
86 return result
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
93 Args:
94 data: StatuslineData instance
96 Returns:
97 List of parts to be joined
98 """
99 parts = []
101 # Add model if display enabled (most important - cloud service context)
102 if self._display_config.model:
103 parts.append(f"🤖 {data.model}")
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}")
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}")
116 # Add output style if not empty
117 if data.output_style:
118 parts.append(f"💬 {data.output_style}")
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}")
124 # Add Git info (development context)
125 if self._display_config.branch:
126 parts.append(f"🔀 {data.branch}")
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)
132 return parts
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
139 Args:
140 data: StatuslineData instance
141 max_length: Maximum allowed length
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}"
150 parts = [f"🤖 {data.model}"]
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}")
157 parts.append(f"🗿 {version_str}")
159 # Add output style if not empty
160 if data.output_style:
161 parts.append(f"💬 {data.output_style}")
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}")
167 # Add Git info
168 parts.append(f"🔀 {truncated_branch}")
170 # Only add active_task if it's not empty
171 if data.active_task.strip():
172 parts.append(data.active_task)
174 result = self._format_config.separator.join(parts)
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}"]
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}")
187 parts.append(f"🗿 {version_str}")
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)
198 # If still too long, remove output_style and active_task
199 if len(result) > max_length:
200 parts = [f"🤖 {data.model}"]
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}")
208 parts.append(f"🗿 {version_str}")
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)
215 # Final fallback to minimal if still too long
216 if len(result) > max_length:
217 result = self._render_minimal(data)
219 return result
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
227 Args:
228 data: StatuslineData instance
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}"
236 parts = []
238 # Add model if display enabled
239 if self._display_config.model:
240 parts.append(f"🤖 {data.model}")
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}")
247 # Add MoAI version if display enabled
248 if self._display_config.version:
249 parts.append(f"🗿 {version_str}")
251 # Add output style if not empty
252 if data.output_style:
253 parts.append(f"💬 {data.output_style}")
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}")
259 # Add Git info (development context)
260 if self._display_config.branch:
261 parts.append(f"🔀 {branch}")
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)
267 result = self._format_config.separator.join(parts)
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}"]
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}")
280 parts.append(f"🗿 {version_str}")
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}")
286 # Add output style if not empty
287 if data.output_style:
288 parts.append(f"💬 {data.output_style}")
290 parts.append(f"🔀 {branch}")
292 if data.active_task.strip():
293 parts.append(data.active_task)
294 result = self._format_config.separator.join(parts)
296 return result
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
304 Args:
305 data: StatuslineData instance
307 Returns:
308 Formatted statusline string (max 40 chars)
309 """
310 parts = []
312 # Add model if display enabled
313 if self._display_config.model:
314 parts.append(f"🤖 {data.model}")
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}")
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}")
331 result = self._format_config.separator.join(parts)
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}"
343 return result
345 @staticmethod
346 def _truncate_branch(branch: str, max_length: int = 30) -> str:
347 """
348 Truncate branch name intelligently, preserving SPEC ID if present
350 Args:
351 branch: Branch name to truncate
352 max_length: Maximum allowed length
354 Returns:
355 Truncated branch name
356 """
357 if len(branch) <= max_length:
358 return branch
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
370 # Simple truncation with ellipsis for very long names
371 return f"{branch[:max_length-1]}…" if len(branch) > max_length else branch
373 @staticmethod
374 def _truncate_version(version: str) -> str:
375 """
376 Truncate version string for minimal display by removing 'v' prefix
378 Args:
379 version: Version string (e.g., "v0.20.1" or "0.20.1")
381 Returns:
382 Truncated version string
383 """
384 if version.startswith("v"):
385 return version[1:]
386 return version