kollabor 0.4.9__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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List, Dict, Any, Optional, Callable
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
# Platform check for keyboard shortcut display
|
|
11
|
+
IS_WINDOWS = sys.platform == "win32"
|
|
12
|
+
|
|
13
|
+
|
|
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
|
+
@dataclass
|
|
24
|
+
class BlockConfig:
|
|
25
|
+
"""Configuration for a single status block."""
|
|
26
|
+
|
|
27
|
+
width_fraction: float # 0.25, 0.33, 0.5, 0.67, 1.0
|
|
28
|
+
content_provider: Callable[[], List[str]] # Function that returns status content
|
|
29
|
+
title: str # Block title/label
|
|
30
|
+
priority: int = 0 # Block priority within view
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class StatusViewConfig:
|
|
35
|
+
"""Configuration for a complete status view."""
|
|
36
|
+
|
|
37
|
+
name: str # "Session Stats", "Performance", "My Plugin View"
|
|
38
|
+
plugin_source: str # Plugin that registered this view
|
|
39
|
+
priority: int # Display order priority
|
|
40
|
+
blocks: List[BlockConfig] # Block layout configuration
|
|
41
|
+
|
|
42
|
+
|
|
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
|
+
class StatusViewRegistry:
|
|
184
|
+
"""Registry for plugin-configurable status views with navigation."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, event_bus=None):
|
|
187
|
+
"""Initialize status view registry.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
event_bus: Event bus for firing status change events.
|
|
191
|
+
"""
|
|
192
|
+
self.views: List[StatusViewConfig] = []
|
|
193
|
+
self.current_index = 0
|
|
194
|
+
self.event_bus = event_bus
|
|
195
|
+
logger.info("StatusViewRegistry initialized")
|
|
196
|
+
|
|
197
|
+
def register_status_view(
|
|
198
|
+
self, plugin_name: str, config: StatusViewConfig
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Register a new status view from a plugin.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
plugin_name: Name of the plugin registering the view.
|
|
204
|
+
config: StatusViewConfig for the new view.
|
|
205
|
+
"""
|
|
206
|
+
# Add the view and sort by priority
|
|
207
|
+
self.views.append(config)
|
|
208
|
+
self.views.sort(key=lambda v: v.priority, reverse=True)
|
|
209
|
+
|
|
210
|
+
logger.info(
|
|
211
|
+
f"Registered status view '{config.name}' from plugin '{plugin_name}' with priority {config.priority}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def cycle_next(self) -> Optional[StatusViewConfig]:
|
|
215
|
+
"""Navigate to next status view.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
New current view config, or None if no views.
|
|
219
|
+
"""
|
|
220
|
+
if not self.views:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
self.current_index = (self.current_index + 1) % len(self.views)
|
|
224
|
+
current_view = self.views[self.current_index]
|
|
225
|
+
|
|
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
|
|
231
|
+
|
|
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}")
|
|
240
|
+
|
|
241
|
+
logger.debug(f"Cycled to next status view: '{current_view.name}'")
|
|
242
|
+
return current_view
|
|
243
|
+
|
|
244
|
+
def cycle_previous(self) -> Optional[StatusViewConfig]:
|
|
245
|
+
"""Navigate to previous status view.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
New current view config, or None if no views.
|
|
249
|
+
"""
|
|
250
|
+
if not self.views:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
self.current_index = (self.current_index - 1) % len(self.views)
|
|
254
|
+
current_view = self.views[self.current_index]
|
|
255
|
+
|
|
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}")
|
|
273
|
+
|
|
274
|
+
logger.debug(f"Cycled to previous status view: '{current_view.name}'")
|
|
275
|
+
return current_view
|
|
276
|
+
|
|
277
|
+
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
|
+
"""
|
|
283
|
+
if not self.views:
|
|
284
|
+
return None
|
|
285
|
+
return self.views[self.current_index]
|
|
286
|
+
|
|
287
|
+
def get_view_count(self) -> int:
|
|
288
|
+
"""Get total number of registered views."""
|
|
289
|
+
return len(self.views)
|
|
290
|
+
|
|
291
|
+
def get_view_names(self) -> List[str]:
|
|
292
|
+
"""Get names of all registered views."""
|
|
293
|
+
return [view.name for view in self.views]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class StatusRenderer:
|
|
297
|
+
"""Main status rendering system coordinating multiple areas."""
|
|
298
|
+
|
|
299
|
+
def __init__(
|
|
300
|
+
self,
|
|
301
|
+
terminal_width: int = 80,
|
|
302
|
+
status_registry: Optional[StatusViewRegistry] = None,
|
|
303
|
+
):
|
|
304
|
+
"""Initialize status renderer.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
terminal_width: Terminal width for layout calculations.
|
|
308
|
+
status_registry: Optional status view registry for block-based rendering.
|
|
309
|
+
"""
|
|
310
|
+
self.terminal_width = terminal_width
|
|
311
|
+
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)
|
|
354
|
+
|
|
355
|
+
def set_terminal_width(self, width: int) -> None:
|
|
356
|
+
"""Update terminal width for layout calculations.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
width: New terminal width.
|
|
360
|
+
"""
|
|
361
|
+
self.terminal_width = width
|
|
362
|
+
|
|
363
|
+
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.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
colorizer_func: Optional function to apply colors to text.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
List of formatted status lines.
|
|
387
|
+
"""
|
|
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:
|
|
396
|
+
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
|
|
609
|
+
|
|
610
|
+
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
|
+
"""
|
|
619
|
+
if not self.status_registry:
|
|
620
|
+
return []
|
|
621
|
+
|
|
622
|
+
current_view = self.status_registry.get_current_view()
|
|
623
|
+
if not current_view:
|
|
624
|
+
return []
|
|
625
|
+
|
|
626
|
+
# Get content from all blocks in the current view
|
|
627
|
+
block_contents = []
|
|
628
|
+
for block in current_view.blocks:
|
|
629
|
+
try:
|
|
630
|
+
content = block.content_provider()
|
|
631
|
+
if content:
|
|
632
|
+
block_contents.append(
|
|
633
|
+
{
|
|
634
|
+
"width_fraction": block.width_fraction,
|
|
635
|
+
"title": block.title,
|
|
636
|
+
"content": content,
|
|
637
|
+
"priority": block.priority,
|
|
638
|
+
}
|
|
639
|
+
)
|
|
640
|
+
except Exception as e:
|
|
641
|
+
logger.warning(
|
|
642
|
+
f"Failed to get content from block '{block.title}': {e}"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
if not block_contents:
|
|
646
|
+
return []
|
|
647
|
+
|
|
648
|
+
# Sort blocks by priority
|
|
649
|
+
block_contents.sort(key=lambda b: b["priority"], reverse=True)
|
|
650
|
+
|
|
651
|
+
# Calculate block layout
|
|
652
|
+
lines = self._calculate_and_render_blocks(block_contents, colorizer_func)
|
|
653
|
+
|
|
654
|
+
# Add cycling hint if multiple views are available
|
|
655
|
+
view_count = self.status_registry.get_view_count()
|
|
656
|
+
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
|
|
662
|
+
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"
|
|
664
|
+
lines.append(hint)
|
|
665
|
+
|
|
666
|
+
return lines
|
|
667
|
+
|
|
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
|
+
def _render_single_row_blocks(
|
|
697
|
+
self, block_contents: List[Dict], colorizer_func=None
|
|
698
|
+
) -> 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
|
+
"""
|
|
708
|
+
lines = []
|
|
709
|
+
|
|
710
|
+
# Calculate actual column widths
|
|
711
|
+
total_spacing = (
|
|
712
|
+
(len(block_contents) - 1) * self.spacing
|
|
713
|
+
if len(block_contents) > 1
|
|
714
|
+
else 0
|
|
715
|
+
)
|
|
716
|
+
available_width = self.terminal_width - total_spacing
|
|
717
|
+
|
|
718
|
+
column_widths = []
|
|
719
|
+
for block in block_contents:
|
|
720
|
+
width = int(available_width * block["width_fraction"])
|
|
721
|
+
column_widths.append(max(10, width)) # Minimum width of 10
|
|
722
|
+
|
|
723
|
+
# Find maximum lines across all blocks
|
|
724
|
+
max_lines = (
|
|
725
|
+
max(len(block["content"]) for block in block_contents)
|
|
726
|
+
if block_contents
|
|
727
|
+
else 0
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Create each row
|
|
731
|
+
for line_idx in range(max_lines):
|
|
732
|
+
columns = []
|
|
733
|
+
|
|
734
|
+
for i, block in enumerate(block_contents):
|
|
735
|
+
if line_idx < len(block["content"]):
|
|
736
|
+
text = block["content"][line_idx]
|
|
737
|
+
|
|
738
|
+
# Apply colorizer
|
|
739
|
+
if colorizer_func:
|
|
740
|
+
text = colorizer_func(text)
|
|
741
|
+
|
|
742
|
+
# Truncate if too long
|
|
743
|
+
visible_text = self._strip_ansi(text)
|
|
744
|
+
max_width = column_widths[i]
|
|
745
|
+
|
|
746
|
+
if len(visible_text) > max_width:
|
|
747
|
+
if max_width > 3:
|
|
748
|
+
text = (
|
|
749
|
+
self._truncate_with_ansi(text, max_width - 3) + "..."
|
|
750
|
+
)
|
|
751
|
+
else:
|
|
752
|
+
text = "..."
|
|
753
|
+
|
|
754
|
+
columns.append(text)
|
|
755
|
+
else:
|
|
756
|
+
columns.append("") # Empty column
|
|
757
|
+
|
|
758
|
+
# Join columns with smart spacing (no separator needed)
|
|
759
|
+
formatted_line = ""
|
|
760
|
+
for i, col in enumerate(columns):
|
|
761
|
+
formatted_line += col
|
|
762
|
+
|
|
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
|
|
768
|
+
visible_length = len(self._strip_ansi(col))
|
|
769
|
+
padding = max(0, column_widths[i] - visible_length)
|
|
770
|
+
formatted_line += " " * padding
|
|
771
|
+
# Add clean inter-column spacing
|
|
772
|
+
formatted_line += " " * self.spacing
|
|
773
|
+
|
|
774
|
+
if formatted_line.strip():
|
|
775
|
+
lines.append(formatted_line.rstrip())
|
|
776
|
+
|
|
777
|
+
return lines
|
|
778
|
+
|
|
779
|
+
def _render_multi_row_blocks(
|
|
780
|
+
self, block_contents: List[Dict], colorizer_func=None
|
|
781
|
+
) -> 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
|
+
"""
|
|
791
|
+
lines = []
|
|
792
|
+
|
|
793
|
+
# For now, render each block on its own line(s)
|
|
794
|
+
# This is a simple fallback - can be enhanced later
|
|
795
|
+
for block in block_contents:
|
|
796
|
+
for content_line in block["content"]:
|
|
797
|
+
if colorizer_func:
|
|
798
|
+
content_line = colorizer_func(content_line)
|
|
799
|
+
|
|
800
|
+
# Truncate if too long
|
|
801
|
+
visible_text = self._strip_ansi(content_line)
|
|
802
|
+
if len(visible_text) > self.terminal_width - 3:
|
|
803
|
+
content_line = (
|
|
804
|
+
self._truncate_with_ansi(
|
|
805
|
+
content_line, self.terminal_width - 6
|
|
806
|
+
)
|
|
807
|
+
+ "..."
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
lines.append(content_line)
|
|
811
|
+
|
|
812
|
+
return lines
|
|
813
|
+
|
|
814
|
+
def get_status_summary(self) -> Dict[str, Any]:
|
|
815
|
+
"""Get summary of status rendering state.
|
|
816
|
+
|
|
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
|