kollabor 0.4.9__py3-none-any.whl → 0.4.15__py3-none-any.whl

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 (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -1,25 +1,23 @@
1
+ """Status rendering system for terminal applications.
2
+
3
+ This module provides block-based status rendering for terminal applications
4
+ with plugin-configurable views and navigation.
5
+ """
6
+
1
7
  import re
2
8
  import logging
3
9
  import sys
4
10
  from dataclasses import dataclass
5
- from enum import Enum
6
11
  from typing import List, Dict, Any, Optional, Callable
7
12
 
13
+ from .visual_effects import ColorPalette
14
+
8
15
  logger = logging.getLogger(__name__)
9
16
 
10
17
  # Platform check for keyboard shortcut display
11
18
  IS_WINDOWS = sys.platform == "win32"
12
19
 
13
20
 
14
- class StatusFormat(Enum):
15
- """Status area formatting styles."""
16
-
17
- COMPACT = "compact"
18
- DETAILED = "detailed"
19
- MINIMAL = "minimal"
20
- BRACKETED = "bracketed"
21
-
22
-
23
21
  @dataclass
24
22
  class BlockConfig:
25
23
  """Configuration for a single status block."""
@@ -40,146 +38,6 @@ class StatusViewConfig:
40
38
  blocks: List[BlockConfig] # Block layout configuration
41
39
 
42
40
 
43
- @dataclass
44
- class StatusMetric:
45
- """Represents a single status metric."""
46
-
47
- key: str
48
- value: Any
49
- format_type: str = (
50
- "default" # "number", "boolean", "time", "ratio", "percentage"
51
- )
52
- color_hint: Optional[str] = None
53
- priority: int = 0
54
-
55
- def format_value(self) -> str:
56
- """Format the value based on its type."""
57
- if self.format_type == "boolean":
58
- return "Yes" if self.value else "No"
59
- elif self.format_type == "time":
60
- if isinstance(self.value, (int, float)):
61
- return f"{self.value:.1f}s"
62
- return str(self.value)
63
- elif self.format_type == "ratio":
64
- if isinstance(self.value, tuple) and len(self.value) == 2:
65
- return f"{self.value[0]}/{self.value[1]}"
66
- return str(self.value)
67
- elif self.format_type == "percentage":
68
- if isinstance(self.value, (int, float)):
69
- return f"{self.value:.1f}%"
70
- return str(self.value)
71
- elif self.format_type == "number":
72
- if isinstance(self.value, int) and self.value >= 1000:
73
- # Add comma separators for large numbers
74
- return f"{self.value:,}"
75
- return str(self.value)
76
- else:
77
- return str(self.value)
78
-
79
- def get_display_text(self) -> str:
80
- """Get formatted display text for this metric."""
81
- formatted_value = self.format_value()
82
- return f"{self.key}: {formatted_value}"
83
-
84
-
85
- class StatusAreaManager:
86
- """Manages individual status areas and their content."""
87
-
88
- def __init__(self, area_name: str):
89
- """Initialize status area manager.
90
-
91
- Args:
92
- area_name: Name of the status area (A, B, C).
93
- """
94
- self.area_name = area_name
95
- self.metrics: Dict[str, StatusMetric] = {}
96
- self.custom_lines: List[str] = []
97
- self.format_style = StatusFormat.BRACKETED
98
-
99
- def add_metric(self, metric: StatusMetric) -> None:
100
- """Add or update a status metric.
101
-
102
- Args:
103
- metric: StatusMetric to add.
104
- """
105
- self.metrics[metric.key] = metric
106
-
107
- def update_metric(self, key: str, value: Any, **kwargs) -> None:
108
- """Update an existing metric or create a new one.
109
-
110
- Args:
111
- key: Metric key.
112
- value: New value.
113
- **kwargs: Additional metric properties.
114
- """
115
- if key in self.metrics:
116
- self.metrics[key].value = value
117
- for attr, val in kwargs.items():
118
- if hasattr(self.metrics[key], attr):
119
- setattr(self.metrics[key], attr, val)
120
- else:
121
- self.add_metric(StatusMetric(key, value, **kwargs))
122
-
123
- def remove_metric(self, key: str) -> None:
124
- """Remove a metric from the status area.
125
-
126
- Args:
127
- key: Metric key to remove.
128
- """
129
- self.metrics.pop(key, None)
130
-
131
- def add_custom_line(self, line: str) -> None:
132
- """Add a custom formatted line to the status area.
133
-
134
- Args:
135
- line: Custom line text.
136
- """
137
- self.custom_lines.append(line)
138
-
139
- def clear_custom_lines(self) -> None:
140
- """Clear all custom lines."""
141
- self.custom_lines.clear()
142
-
143
- def get_formatted_lines(self, colorizer_func=None) -> List[str]:
144
- """Get formatted lines for display.
145
-
146
- Args:
147
- colorizer_func: Optional function to apply colors to text.
148
-
149
- Returns:
150
- List of formatted status lines.
151
- """
152
- lines = []
153
-
154
- # Add metric lines (sorted by priority)
155
- sorted_metrics = sorted(
156
- self.metrics.values(), key=lambda m: m.priority, reverse=True
157
- )
158
- for metric in sorted_metrics:
159
- line = metric.get_display_text()
160
-
161
- # Apply color hints if specified
162
- if metric.color_hint and colorizer_func:
163
- line = colorizer_func(line)
164
- elif colorizer_func:
165
- line = colorizer_func(line)
166
-
167
- lines.append(line)
168
-
169
- # Add custom lines
170
- for line in self.custom_lines:
171
- if colorizer_func:
172
- line = colorizer_func(line)
173
- lines.append(line)
174
-
175
- return lines
176
-
177
- def clear(self) -> None:
178
- """Clear all metrics and custom lines."""
179
- self.metrics.clear()
180
- self.custom_lines.clear()
181
-
182
-
183
41
  class StatusViewRegistry:
184
42
  """Registry for plugin-configurable status views with navigation."""
185
43
 
@@ -203,98 +61,122 @@ class StatusViewRegistry:
203
61
  plugin_name: Name of the plugin registering the view.
204
62
  config: StatusViewConfig for the new view.
205
63
  """
206
- # Add the view and sort by priority
207
64
  self.views.append(config)
208
65
  self.views.sort(key=lambda v: v.priority, reverse=True)
209
66
 
210
67
  logger.info(
211
- f"Registered status view '{config.name}' from plugin '{plugin_name}' with priority {config.priority}"
68
+ f"Registered status view '{config.name}' from plugin '{plugin_name}' "
69
+ f"with priority {config.priority}"
212
70
  )
213
71
 
214
- def cycle_next(self) -> Optional[StatusViewConfig]:
215
- """Navigate to next status view.
72
+ def _view_has_content(self, view: StatusViewConfig) -> bool:
73
+ """Check if a view has any content to display."""
74
+ for block in view.blocks:
75
+ try:
76
+ content = block.content_provider()
77
+ if content:
78
+ return True
79
+ except Exception:
80
+ pass
81
+ return False
216
82
 
217
- Returns:
218
- New current view config, or None if no views.
219
- """
83
+ def cycle_next(self) -> Optional[StatusViewConfig]:
84
+ """Navigate to next status view (skips empty views)."""
220
85
  if not self.views:
221
86
  return None
222
87
 
223
- self.current_index = (self.current_index + 1) % len(self.views)
224
- current_view = self.views[self.current_index]
88
+ start_index = self.current_index
89
+ for _ in range(len(self.views)):
90
+ self.current_index = (self.current_index + 1) % len(self.views)
91
+ current_view = self.views[self.current_index]
225
92
 
226
- # Fire status view changed event
227
- if self.event_bus:
228
- try:
229
- # Import here to avoid circular imports
230
- from ..events.models import EventType, Event
93
+ if self._view_has_content(current_view):
94
+ if self.event_bus:
95
+ try:
96
+ from ..events.models import EventType, Event
231
97
 
232
- event = Event(
233
- type=EventType.STATUS_VIEW_CHANGED,
234
- data={"view_name": current_view.name, "direction": "next"},
235
- source="status_view_registry",
236
- )
237
- self.event_bus.fire_event(event)
238
- except Exception as e:
239
- logger.warning(f"Failed to fire STATUS_VIEW_CHANGED event: {e}")
98
+ event = Event(
99
+ type=EventType.STATUS_VIEW_CHANGED,
100
+ data={"view_name": current_view.name, "direction": "next"},
101
+ source="status_view_registry",
102
+ )
103
+ self.event_bus.fire_event(event)
104
+ except Exception as e:
105
+ logger.warning(f"Failed to fire STATUS_VIEW_CHANGED event: {e}")
240
106
 
241
- logger.debug(f"Cycled to next status view: '{current_view.name}'")
242
- return current_view
107
+ logger.debug(f"Cycled to next status view: '{current_view.name}'")
108
+ return current_view
243
109
 
244
- def cycle_previous(self) -> Optional[StatusViewConfig]:
245
- """Navigate to previous status view.
110
+ self.current_index = start_index
111
+ return self.views[self.current_index] if self.views else None
246
112
 
247
- Returns:
248
- New current view config, or None if no views.
249
- """
113
+ def cycle_previous(self) -> Optional[StatusViewConfig]:
114
+ """Navigate to previous status view (skips empty views)."""
250
115
  if not self.views:
251
116
  return None
252
117
 
253
- self.current_index = (self.current_index - 1) % len(self.views)
254
- current_view = self.views[self.current_index]
118
+ start_index = self.current_index
119
+ for _ in range(len(self.views)):
120
+ self.current_index = (self.current_index - 1) % len(self.views)
121
+ current_view = self.views[self.current_index]
122
+
123
+ if self._view_has_content(current_view):
124
+ if self.event_bus:
125
+ try:
126
+ from ..events.models import EventType, Event
127
+
128
+ event = Event(
129
+ type=EventType.STATUS_VIEW_CHANGED,
130
+ data={
131
+ "view_name": current_view.name,
132
+ "direction": "previous",
133
+ },
134
+ source="status_view_registry",
135
+ )
136
+ self.event_bus.fire_event(event)
137
+ except Exception as e:
138
+ logger.warning(f"Failed to fire STATUS_VIEW_CHANGED event: {e}")
255
139
 
256
- # Fire status view changed event
257
- if self.event_bus:
258
- try:
259
- # Import here to avoid circular imports
260
- from ..events.models import EventType, Event
261
-
262
- event = Event(
263
- type=EventType.STATUS_VIEW_CHANGED,
264
- data={
265
- "view_name": current_view.name,
266
- "direction": "previous",
267
- },
268
- source="status_view_registry",
269
- )
270
- self.event_bus.fire_event(event)
271
- except Exception as e:
272
- logger.warning(f"Failed to fire STATUS_VIEW_CHANGED event: {e}")
140
+ logger.debug(f"Cycled to previous status view: '{current_view.name}'")
141
+ return current_view
273
142
 
274
- logger.debug(f"Cycled to previous status view: '{current_view.name}'")
275
- return current_view
143
+ self.current_index = start_index
144
+ return self.views[self.current_index] if self.views else None
276
145
 
277
146
  def get_current_view(self) -> Optional[StatusViewConfig]:
278
- """Get the currently active status view.
279
-
280
- Returns:
281
- Current view config, or None if no views registered.
282
- """
147
+ """Get the currently active status view."""
283
148
  if not self.views:
284
149
  return None
285
150
  return self.views[self.current_index]
286
151
 
287
152
  def get_view_count(self) -> int:
288
- """Get total number of registered views."""
289
- return len(self.views)
153
+ """Get total number of views with content."""
154
+ return sum(1 for view in self.views if self._view_has_content(view))
155
+
156
+ def get_current_view_index(self) -> int:
157
+ """Get 1-indexed position of current view among views with content."""
158
+ if not self.views:
159
+ return 0
160
+ current_view = self.views[self.current_index]
161
+ index = 0
162
+ for view in self.views:
163
+ if self._view_has_content(view):
164
+ index += 1
165
+ if view is current_view:
166
+ return index
167
+ return index
290
168
 
291
169
  def get_view_names(self) -> List[str]:
292
170
  """Get names of all registered views."""
293
171
  return [view.name for view in self.views]
294
172
 
173
+ def get_active_view_names(self) -> List[str]:
174
+ """Get names of views with content."""
175
+ return [view.name for view in self.views if self._view_has_content(view)]
176
+
295
177
 
296
178
  class StatusRenderer:
297
- """Main status rendering system coordinating multiple areas."""
179
+ """Block-based status rendering system."""
298
180
 
299
181
  def __init__(
300
182
  self,
@@ -305,79 +187,18 @@ class StatusRenderer:
305
187
 
306
188
  Args:
307
189
  terminal_width: Terminal width for layout calculations.
308
- status_registry: Optional status view registry for block-based rendering.
190
+ status_registry: Status view registry for block-based rendering.
309
191
  """
310
192
  self.terminal_width = terminal_width
311
193
  self.status_registry = status_registry
312
-
313
- # Create status area managers (legacy compatibility)
314
- self.areas: Dict[str, StatusAreaManager] = {
315
- "A": StatusAreaManager("A"),
316
- "B": StatusAreaManager("B"),
317
- "C": StatusAreaManager("C"),
318
- }
319
-
320
- # Rendering configuration
321
- self.bracket_style = {
322
- "open": "",
323
- "close": "",
324
- "color": "",
325
- } # No brackets
326
- self.spacing = (
327
- 4 # Spacing between columns (increased for clarity without separator)
328
- )
329
- self.separator_style = "" # No separator - clean minimal aesthetic
330
-
331
- def get_area(self, area_name: str) -> Optional[StatusAreaManager]:
332
- """Get status area manager by name.
333
-
334
- Args:
335
- area_name: Area name (A, B, or C).
336
-
337
- Returns:
338
- StatusAreaManager instance or None.
339
- """
340
- return self.areas.get(area_name.upper())
341
-
342
- def update_area_content(self, area_name: str, content: List[str]) -> None:
343
- """Update area content with raw lines (backward compatibility).
344
-
345
- Args:
346
- area_name: Area name.
347
- content: List of content lines.
348
- """
349
- area = self.get_area(area_name)
350
- if area:
351
- area.clear()
352
- for line in content:
353
- area.add_custom_line(line)
194
+ self.spacing = 4 # Spacing between columns
354
195
 
355
196
  def set_terminal_width(self, width: int) -> None:
356
- """Update terminal width for layout calculations.
357
-
358
- Args:
359
- width: New terminal width.
360
- """
197
+ """Update terminal width for layout calculations."""
361
198
  self.terminal_width = width
362
199
 
363
200
  def render_horizontal_layout(self, colorizer_func=None) -> List[str]:
364
- """Render status areas in horizontal (column) layout.
365
-
366
- Args:
367
- colorizer_func: Optional function to apply colors to text.
368
-
369
- Returns:
370
- List of formatted status lines.
371
- """
372
- # Use block-based rendering if registry is available and has views
373
- if self.status_registry and self.status_registry.get_view_count() > 0:
374
- return self._render_block_layout(colorizer_func)
375
-
376
- # Fallback to legacy area-based rendering
377
- return self._render_legacy_layout(colorizer_func)
378
-
379
- def _render_legacy_layout(self, colorizer_func=None) -> List[str]:
380
- """Render legacy area-based layout for backwards compatibility.
201
+ """Render status views in horizontal layout.
381
202
 
382
203
  Args:
383
204
  colorizer_func: Optional function to apply colors to text.
@@ -385,237 +206,12 @@ class StatusRenderer:
385
206
  Returns:
386
207
  List of formatted status lines.
387
208
  """
388
- # Get content for all areas
389
- area_contents = {}
390
- for name, area in self.areas.items():
391
- content = area.get_formatted_lines(colorizer_func)
392
- if content:
393
- area_contents[name] = content
394
-
395
- if not area_contents:
209
+ if not self.status_registry:
396
210
  return []
397
-
398
- # Use three-column layout for wide terminals
399
- if self.terminal_width >= 80:
400
- return self._render_three_column_layout(area_contents, colorizer_func)
401
- else:
402
- return self._render_vertical_layout(area_contents, colorizer_func)
403
-
404
- def _render_three_column_layout(
405
- self, area_contents: Dict[str, List[str]], colorizer_func=None
406
- ) -> List[str]:
407
- """Render three-column layout for wide terminals.
408
-
409
- Args:
410
- area_contents: Dictionary of area contents.
411
- colorizer_func: Optional colorizer function.
412
-
413
- Returns:
414
- List of formatted lines.
415
- """
416
- lines = []
417
-
418
- # Improved column width calculation
419
- # Reserve space for brackets [text] and spacing between columns
420
- brackets_overhead = 4 # 2 brackets + 2 padding spaces per column
421
- total_spacing = (3 - 1) * self.spacing # spacing between 3 columns
422
- available_width = self.terminal_width - total_spacing
423
- column_width = max(15, (available_width - (3 * brackets_overhead)) // 3)
424
-
425
- # Get content for areas A, B, C in order
426
- area_names = ["A", "B", "C"]
427
- area_data = []
428
- for area_name in area_names:
429
- content = area_contents.get(area_name, [])
430
- area_data.append(content)
431
-
432
- # Find maximum lines across all areas
433
- max_lines = max(len(content) for content in area_data) if area_data else 0
434
-
435
- # Create each row with three columns
436
- for line_idx in range(max_lines):
437
- columns = []
438
-
439
- for content in area_data:
440
- if line_idx < len(content):
441
- text = content[line_idx]
442
-
443
- # Truncate if too long for column (account for brackets)
444
- visible_text = self._strip_ansi(text)
445
- max_text_width = column_width - 2 # Reserve space for brackets
446
-
447
- if len(visible_text) > max_text_width:
448
- # Smart truncation - preserve important parts
449
- if max_text_width > 3:
450
- truncated = self._truncate_with_ansi(
451
- text, max_text_width - 3
452
- )
453
- text = truncated + "..."
454
- else:
455
- text = "..."
456
-
457
- # Apply bracket formatting
458
- bracketed_text = self._apply_brackets(text)
459
- columns.append(bracketed_text)
460
- else:
461
- columns.append("") # Empty column
462
-
463
- # Join columns with improved spacing
464
- formatted_line = self._join_columns_improved(
465
- columns, column_width + brackets_overhead
466
- )
467
-
468
- # Only add line if it has content
469
- if formatted_line.strip():
470
- lines.append(formatted_line.rstrip())
471
-
472
- return lines
473
-
474
- def _render_vertical_layout(
475
- self, area_contents: Dict[str, List[str]], colorizer_func=None
476
- ) -> List[str]:
477
- """Render vertical layout for narrow terminals.
478
-
479
- Args:
480
- area_contents: Dictionary of area contents.
481
- colorizer_func: Optional colorizer function.
482
-
483
- Returns:
484
- List of formatted lines.
485
- """
486
- lines = []
487
-
488
- # Render each area vertically
489
- for area_name in ["A", "B", "C"]:
490
- content = area_contents.get(area_name, [])
491
- for line in content:
492
- if line.strip():
493
- bracketed_line = self._apply_brackets(line)
494
- lines.append(bracketed_line)
495
-
496
- return lines
497
-
498
- def _apply_brackets(self, text: str) -> str:
499
- """Apply bracket styling to text.
500
-
501
- Args:
502
- text: Text to apply brackets to.
503
-
504
- Returns:
505
- Text with brackets applied.
506
- """
507
- bracket_color = self.bracket_style["color"]
508
- reset = "\033[0m"
509
- open_bracket = self.bracket_style["open"]
510
- close_bracket = self.bracket_style["close"]
511
-
512
- return f"{bracket_color}{open_bracket}{reset}{text}{bracket_color}{close_bracket}{reset}"
513
-
514
- def _join_columns(self, columns: List[str], column_width: int) -> str:
515
- """Join columns with proper spacing and alignment (legacy method).
516
-
517
- Args:
518
- columns: List of column strings.
519
- column_width: Width of each column.
520
-
521
- Returns:
522
- Joined line string.
523
- """
524
- return self._join_columns_improved(columns, column_width)
525
-
526
- def _join_columns_improved(self, columns: List[str], column_width: int) -> str:
527
- """Join columns with improved spacing and alignment.
528
-
529
- Args:
530
- columns: List of column strings.
531
- column_width: Width of each column (including brackets).
532
-
533
- Returns:
534
- Joined line string.
535
- """
536
- formatted_line = ""
537
-
538
- for i, col in enumerate(columns):
539
- if col:
540
- # Add the column content
541
- formatted_line += col
542
-
543
- # Calculate padding needed
544
- visible_length = len(self._strip_ansi(col))
545
- padding = max(0, column_width - visible_length)
546
-
547
- # Add padding only if not the last column
548
- if i < len(columns) - 1:
549
- formatted_line += " " * padding
550
- # Add inter-column spacing
551
- formatted_line += " " * self.spacing
552
- else:
553
- # Empty column - add spacing if not last
554
- if i < len(columns) - 1:
555
- formatted_line += " " * column_width
556
- formatted_line += " " * self.spacing
557
-
558
- return formatted_line
559
-
560
- def _strip_ansi(self, text: str) -> str:
561
- """Remove ANSI escape codes from text.
562
-
563
- Args:
564
- text: Text with potential ANSI codes.
565
-
566
- Returns:
567
- Text with ANSI codes removed.
568
- """
569
- return re.sub(r"\033\[[0-9;]*m", "", text)
570
-
571
- def _truncate_with_ansi(self, text: str, max_length: int) -> str:
572
- """Truncate text while preserving ANSI codes.
573
-
574
- Args:
575
- text: Text to truncate.
576
- max_length: Maximum visible length.
577
-
578
- Returns:
579
- Truncated text with ANSI codes preserved.
580
- """
581
- result = ""
582
- visible_count = 0
583
- i = 0
584
-
585
- while i < len(text) and visible_count < max_length:
586
- # Check for ANSI escape sequence
587
- if (
588
- text[i : i + 1] == "\033"
589
- and i + 1 < len(text)
590
- and text[i + 1] == "["
591
- ):
592
- # Find end of ANSI sequence
593
- end = i + 2
594
- while end < len(text) and text[end] not in "mhlABCDEFGHJKSTfimpsuI":
595
- end += 1
596
- if end < len(text):
597
- end += 1
598
-
599
- # Add the entire ANSI sequence
600
- result += text[i:end]
601
- i = end
602
- else:
603
- # Regular character
604
- result += text[i]
605
- visible_count += 1
606
- i += 1
607
-
608
- return result
211
+ return self._render_block_layout(colorizer_func)
609
212
 
610
213
  def _render_block_layout(self, colorizer_func=None) -> List[str]:
611
- """Render flexible block-based layout using StatusViewRegistry.
612
-
613
- Args:
614
- colorizer_func: Optional function to apply colors to text.
615
-
616
- Returns:
617
- List of formatted status lines.
618
- """
214
+ """Render flexible block-based layout using StatusViewRegistry."""
619
215
  if not self.status_registry:
620
216
  return []
621
217
 
@@ -649,65 +245,33 @@ class StatusRenderer:
649
245
  block_contents.sort(key=lambda b: b["priority"], reverse=True)
650
246
 
651
247
  # Calculate block layout
652
- lines = self._calculate_and_render_blocks(block_contents, colorizer_func)
248
+ total_width = sum(block["width_fraction"] for block in block_contents)
249
+ if total_width <= 1.0:
250
+ lines = self._render_single_row_blocks(block_contents, colorizer_func)
251
+ else:
252
+ lines = self._render_multi_row_blocks(block_contents, colorizer_func)
653
253
 
654
- # Add cycling hint if multiple views are available
254
+ # Add cycling hint if multiple views with content are available
655
255
  view_count = self.status_registry.get_view_count()
656
256
  if view_count > 1:
657
- current_index = (
658
- self.status_registry.current_index + 1
659
- ) # 1-indexed for display
660
- # Use INFO_CYAN from Neon Minimal palette
661
- # Use platform-appropriate modifier key name
257
+ current_index = self.status_registry.get_current_view_index()
662
258
  mod_key = "Alt" if IS_WINDOWS else "Opt"
663
- hint = f"\033[38;2;6;182;212m({mod_key}+, / {mod_key}+. to cycle • View {current_index}/{view_count}: {current_view.name})\033[0m"
259
+ hint = (
260
+ f"{ColorPalette.INFO_CYAN}({mod_key}+Left/Right to cycle • "
261
+ f"View {current_index}/{view_count}: {current_view.name})"
262
+ f"{ColorPalette.RESET}"
263
+ )
664
264
  lines.append(hint)
665
265
 
666
266
  return lines
667
267
 
668
- def _calculate_and_render_blocks(
669
- self, block_contents: List[Dict], colorizer_func=None
670
- ) -> List[str]:
671
- """Calculate block layout and render status lines.
672
-
673
- Args:
674
- block_contents: List of block content dictionaries.
675
- colorizer_func: Optional colorizer function.
676
-
677
- Returns:
678
- List of formatted status lines.
679
- """
680
- if not block_contents:
681
- return []
682
-
683
- # For now, implement horizontal layout similar to the legacy system
684
- # This can be enhanced later for more complex layouts
685
-
686
- # Calculate how many blocks can fit horizontally
687
- total_width_needed = sum(block["width_fraction"] for block in block_contents)
688
-
689
- if total_width_needed <= 1.0:
690
- # All blocks fit in one row
691
- return self._render_single_row_blocks(block_contents, colorizer_func)
692
- else:
693
- # Need multiple rows or vertical layout
694
- return self._render_multi_row_blocks(block_contents, colorizer_func)
695
-
696
268
  def _render_single_row_blocks(
697
269
  self, block_contents: List[Dict], colorizer_func=None
698
270
  ) -> List[str]:
699
- """Render blocks in a single horizontal row.
700
-
701
- Args:
702
- block_contents: List of block content dictionaries.
703
- colorizer_func: Optional colorizer function.
704
-
705
- Returns:
706
- List of formatted status lines.
707
- """
271
+ """Render blocks in a single horizontal row."""
708
272
  lines = []
709
273
 
710
- # Calculate actual column widths
274
+ # Calculate column widths
711
275
  total_spacing = (
712
276
  (len(block_contents) - 1) * self.spacing
713
277
  if len(block_contents) > 1
@@ -718,7 +282,7 @@ class StatusRenderer:
718
282
  column_widths = []
719
283
  for block in block_contents:
720
284
  width = int(available_width * block["width_fraction"])
721
- column_widths.append(max(10, width)) # Minimum width of 10
285
+ column_widths.append(max(10, width))
722
286
 
723
287
  # Find maximum lines across all blocks
724
288
  max_lines = (
@@ -735,8 +299,8 @@ class StatusRenderer:
735
299
  if line_idx < len(block["content"]):
736
300
  text = block["content"][line_idx]
737
301
 
738
- # Apply colorizer
739
- if colorizer_func:
302
+ # Skip colorizer for pre-colored content
303
+ if colorizer_func and "\033[" not in text:
740
304
  text = colorizer_func(text)
741
305
 
742
306
  # Truncate if too long
@@ -745,30 +309,23 @@ class StatusRenderer:
745
309
 
746
310
  if len(visible_text) > max_width:
747
311
  if max_width > 3:
748
- text = (
749
- self._truncate_with_ansi(text, max_width - 3) + "..."
750
- )
312
+ text = self._truncate_with_ansi(text, max_width - 3) + "..."
751
313
  else:
752
314
  text = "..."
753
315
 
754
316
  columns.append(text)
755
317
  else:
756
- columns.append("") # Empty column
318
+ columns.append("")
757
319
 
758
- # Join columns with smart spacing (no separator needed)
320
+ # Join columns with spacing
759
321
  formatted_line = ""
760
322
  for i, col in enumerate(columns):
761
323
  formatted_line += col
762
324
 
763
- # Add spacing between columns (not after last)
764
- if i < len(columns) - 1 and any(
765
- columns[i + 1 :]
766
- ): # Only add spacing if there are more non-empty columns
767
- # Pad current column to its width
325
+ if i < len(columns) - 1 and any(columns[i + 1:]):
768
326
  visible_length = len(self._strip_ansi(col))
769
327
  padding = max(0, column_widths[i] - visible_length)
770
328
  formatted_line += " " * padding
771
- # Add clean inter-column spacing
772
329
  formatted_line += " " * self.spacing
773
330
 
774
331
  if formatted_line.strip():
@@ -779,31 +336,20 @@ class StatusRenderer:
779
336
  def _render_multi_row_blocks(
780
337
  self, block_contents: List[Dict], colorizer_func=None
781
338
  ) -> List[str]:
782
- """Render blocks that don't fit in a single row.
783
-
784
- Args:
785
- block_contents: List of block content dictionaries.
786
- colorizer_func: Optional colorizer function.
787
-
788
- Returns:
789
- List of formatted status lines.
790
- """
339
+ """Render blocks that don't fit in a single row."""
791
340
  lines = []
792
341
 
793
- # For now, render each block on its own line(s)
794
- # This is a simple fallback - can be enhanced later
795
342
  for block in block_contents:
796
343
  for content_line in block["content"]:
797
- if colorizer_func:
344
+ # Skip colorizer for pre-colored content
345
+ if colorizer_func and "\033[" not in content_line:
798
346
  content_line = colorizer_func(content_line)
799
347
 
800
348
  # Truncate if too long
801
349
  visible_text = self._strip_ansi(content_line)
802
350
  if len(visible_text) > self.terminal_width - 3:
803
351
  content_line = (
804
- self._truncate_with_ansi(
805
- content_line, self.terminal_width - 6
806
- )
352
+ self._truncate_with_ansi(content_line, self.terminal_width - 6)
807
353
  + "..."
808
354
  )
809
355
 
@@ -811,35 +357,29 @@ class StatusRenderer:
811
357
 
812
358
  return lines
813
359
 
814
- def get_status_summary(self) -> Dict[str, Any]:
815
- """Get summary of status rendering state.
360
+ def _strip_ansi(self, text: str) -> str:
361
+ """Remove ANSI escape codes from text."""
362
+ return re.sub(r"\033\[[0-9;]*m", "", text)
816
363
 
817
- Returns:
818
- Dictionary with status information.
819
- """
820
- summary = {
821
- "terminal_width": self.terminal_width,
822
- "areas": {
823
- name: {
824
- "metrics_count": len(area.metrics),
825
- "custom_lines_count": len(area.custom_lines),
826
- "total_lines": len(area.get_formatted_lines()),
827
- }
828
- for name, area in self.areas.items()
829
- },
830
- "bracket_style": self.bracket_style,
831
- "spacing": self.spacing,
832
- "separator_style": self.separator_style,
833
- }
834
-
835
- # Add status registry information if available
836
- if self.status_registry:
837
- current_view = self.status_registry.get_current_view()
838
- summary["status_registry"] = {
839
- "view_count": self.status_registry.get_view_count(),
840
- "view_names": self.status_registry.get_view_names(),
841
- "current_view": current_view.name if current_view else None,
842
- "current_blocks": (len(current_view.blocks) if current_view else 0),
843
- }
844
-
845
- return summary
364
+ def _truncate_with_ansi(self, text: str, max_length: int) -> str:
365
+ """Truncate text while preserving ANSI codes."""
366
+ result = ""
367
+ visible_count = 0
368
+ i = 0
369
+
370
+ while i < len(text) and visible_count < max_length:
371
+ if text[i:i + 1] == "\033" and i + 1 < len(text) and text[i + 1] == "[":
372
+ # Find end of ANSI sequence
373
+ end = i + 2
374
+ while end < len(text) and text[end] not in "mhlABCDEFGHJKSTfimpsuI":
375
+ end += 1
376
+ if end < len(text):
377
+ end += 1
378
+ result += text[i:end]
379
+ i = end
380
+ else:
381
+ result += text[i]
382
+ visible_count += 1
383
+ i += 1
384
+
385
+ return result