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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
core/io/status_renderer.py
CHANGED
|
@@ -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}'
|
|
68
|
+
f"Registered status view '{config.name}' from plugin '{plugin_name}' "
|
|
69
|
+
f"with priority {config.priority}"
|
|
212
70
|
)
|
|
213
71
|
|
|
214
|
-
def
|
|
215
|
-
"""
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
107
|
+
logger.debug(f"Cycled to next status view: '{current_view.name}'")
|
|
108
|
+
return current_view
|
|
243
109
|
|
|
244
|
-
|
|
245
|
-
|
|
110
|
+
self.current_index = start_index
|
|
111
|
+
return self.views[self.current_index] if self.views else None
|
|
246
112
|
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
275
|
-
return
|
|
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
|
|
289
|
-
return
|
|
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
|
-
"""
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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))
|
|
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
|
-
#
|
|
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("")
|
|
318
|
+
columns.append("")
|
|
757
319
|
|
|
758
|
-
# Join columns with
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
815
|
-
"""
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
""
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|