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
core/io/layout.py
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
"""Layout management system for terminal rendering."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import deque
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import List, Optional, Dict, Any, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LayoutMode(Enum):
|
|
11
|
+
"""Layout rendering modes."""
|
|
12
|
+
|
|
13
|
+
HORIZONTAL = "horizontal"
|
|
14
|
+
VERTICAL = "vertical"
|
|
15
|
+
STACKED = "stacked"
|
|
16
|
+
ADAPTIVE = "adaptive"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AreaAlignment(Enum):
|
|
20
|
+
"""Alignment options for layout areas."""
|
|
21
|
+
|
|
22
|
+
LEFT = "left"
|
|
23
|
+
CENTER = "center"
|
|
24
|
+
RIGHT = "right"
|
|
25
|
+
JUSTIFY = "justify"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ScreenRegion:
|
|
30
|
+
"""Represents a region of the screen."""
|
|
31
|
+
|
|
32
|
+
x: int
|
|
33
|
+
y: int
|
|
34
|
+
width: int
|
|
35
|
+
height: int
|
|
36
|
+
|
|
37
|
+
def contains_point(self, x: int, y: int) -> bool:
|
|
38
|
+
"""Check if point is within this region."""
|
|
39
|
+
return (
|
|
40
|
+
self.x <= x < self.x + self.width and self.y <= y < self.y + self.height
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def intersects(self, other: "ScreenRegion") -> bool:
|
|
44
|
+
"""Check if this region intersects with another."""
|
|
45
|
+
return not (
|
|
46
|
+
self.x + self.width <= other.x
|
|
47
|
+
or other.x + other.width <= self.x
|
|
48
|
+
or self.y + self.height <= other.y
|
|
49
|
+
or other.y + other.height <= self.y
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class LayoutArea:
|
|
55
|
+
"""Represents a layout area with content and configuration."""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
content: List[str] = field(default_factory=list)
|
|
59
|
+
region: Optional[ScreenRegion] = None
|
|
60
|
+
alignment: AreaAlignment = AreaAlignment.LEFT
|
|
61
|
+
visible: bool = True
|
|
62
|
+
priority: int = 0
|
|
63
|
+
min_width: int = 10
|
|
64
|
+
min_height: int = 1
|
|
65
|
+
max_width: Optional[int] = None
|
|
66
|
+
max_height: Optional[int] = None
|
|
67
|
+
padding: int = 0
|
|
68
|
+
|
|
69
|
+
def get_content_width(self) -> int:
|
|
70
|
+
"""Get the maximum width of content in this area."""
|
|
71
|
+
if not self.content:
|
|
72
|
+
return 0
|
|
73
|
+
# Account for ANSI codes when measuring width
|
|
74
|
+
return max(len(self._strip_ansi(line)) for line in self.content)
|
|
75
|
+
|
|
76
|
+
def get_content_height(self) -> int:
|
|
77
|
+
"""Get the height of content in this area."""
|
|
78
|
+
return len(self.content)
|
|
79
|
+
|
|
80
|
+
def _strip_ansi(self, text: str) -> str:
|
|
81
|
+
"""Remove ANSI escape codes from text for width calculation."""
|
|
82
|
+
return re.sub(r"\033\[[0-9;]*m", "", text)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ThinkingAnimationManager:
|
|
86
|
+
"""Manages thinking animation state and display."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, spinner_frames: List[str] = None):
|
|
89
|
+
"""Initialize thinking animation manager.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
spinner_frames: Custom spinner frames (uses default if None).
|
|
93
|
+
"""
|
|
94
|
+
self.spinner_frames = spinner_frames or [
|
|
95
|
+
"⠋",
|
|
96
|
+
"⠙",
|
|
97
|
+
"⠹",
|
|
98
|
+
"⠸",
|
|
99
|
+
"⠼",
|
|
100
|
+
"⠴",
|
|
101
|
+
"⠦",
|
|
102
|
+
"⠧",
|
|
103
|
+
]
|
|
104
|
+
self.current_frame = 0
|
|
105
|
+
self.is_active = False
|
|
106
|
+
self.start_time = None
|
|
107
|
+
self.messages = deque(maxlen=2)
|
|
108
|
+
|
|
109
|
+
def start_thinking(self, message: str = "") -> None:
|
|
110
|
+
"""Start thinking animation with optional message.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
message: Thinking message to display.
|
|
114
|
+
"""
|
|
115
|
+
import time
|
|
116
|
+
|
|
117
|
+
self.is_active = True
|
|
118
|
+
if message:
|
|
119
|
+
# Clear previous messages and set the new one
|
|
120
|
+
self.messages.clear()
|
|
121
|
+
self.messages.append(message)
|
|
122
|
+
if not self.start_time:
|
|
123
|
+
self.start_time = time.time()
|
|
124
|
+
|
|
125
|
+
def stop_thinking(self) -> Optional[str]:
|
|
126
|
+
"""Stop thinking animation and return completion message.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Completion message if thinking was active.
|
|
130
|
+
"""
|
|
131
|
+
import time
|
|
132
|
+
|
|
133
|
+
if not self.is_active:
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
self.is_active = False
|
|
137
|
+
completion_msg = None
|
|
138
|
+
|
|
139
|
+
if self.start_time:
|
|
140
|
+
duration = time.time() - self.start_time
|
|
141
|
+
completion_msg = f"Thought for {duration:.1f} seconds"
|
|
142
|
+
self.start_time = None
|
|
143
|
+
|
|
144
|
+
self.messages.clear()
|
|
145
|
+
return completion_msg
|
|
146
|
+
|
|
147
|
+
def get_next_frame(self) -> str:
|
|
148
|
+
"""Get next spinner frame for animation.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Current spinner frame character.
|
|
152
|
+
"""
|
|
153
|
+
if not self.is_active:
|
|
154
|
+
return ""
|
|
155
|
+
|
|
156
|
+
frame = self.spinner_frames[self.current_frame]
|
|
157
|
+
self.current_frame = (self.current_frame + 1) % len(self.spinner_frames)
|
|
158
|
+
return frame
|
|
159
|
+
|
|
160
|
+
def get_display_lines(self, apply_effect_func) -> List[str]:
|
|
161
|
+
"""Get formatted display lines for thinking animation.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
apply_effect_func: Function to apply visual effects to text.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
List of formatted display lines.
|
|
168
|
+
"""
|
|
169
|
+
if not self.is_active or not self.messages:
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
lines = []
|
|
173
|
+
spinner = self.get_next_frame()
|
|
174
|
+
|
|
175
|
+
for i, msg in enumerate(self.messages):
|
|
176
|
+
if i == len(self.messages) - 1:
|
|
177
|
+
# Main thinking line with spinner
|
|
178
|
+
formatted_text = apply_effect_func(f"{spinner} Thinking: {msg}")
|
|
179
|
+
lines.append(formatted_text)
|
|
180
|
+
else:
|
|
181
|
+
# Secondary thinking line
|
|
182
|
+
formatted_text = apply_effect_func(f" {msg}")
|
|
183
|
+
lines.append(formatted_text)
|
|
184
|
+
|
|
185
|
+
return lines
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class LayoutManager:
|
|
189
|
+
"""Manages terminal layout with multiple areas and adaptive sizing."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, terminal_width: int = 80, terminal_height: int = 24):
|
|
192
|
+
"""Initialize layout manager.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
terminal_width: Terminal width in characters.
|
|
196
|
+
terminal_height: Terminal height in characters.
|
|
197
|
+
"""
|
|
198
|
+
self.terminal_width = terminal_width
|
|
199
|
+
self.terminal_height = terminal_height
|
|
200
|
+
|
|
201
|
+
# Layout areas
|
|
202
|
+
self._areas: Dict[str, LayoutArea] = {}
|
|
203
|
+
|
|
204
|
+
# Layout state
|
|
205
|
+
self._dirty = True
|
|
206
|
+
self._last_render_lines = 0
|
|
207
|
+
|
|
208
|
+
# Initialize standard areas
|
|
209
|
+
self._initialize_standard_areas()
|
|
210
|
+
|
|
211
|
+
def _initialize_standard_areas(self) -> None:
|
|
212
|
+
"""Initialize standard layout areas (status, input, thinking)."""
|
|
213
|
+
self._areas["status_a"] = LayoutArea("status_a", priority=10)
|
|
214
|
+
self._areas["status_b"] = LayoutArea("status_b", priority=10)
|
|
215
|
+
self._areas["status_c"] = LayoutArea("status_c", priority=10)
|
|
216
|
+
self._areas["input"] = LayoutArea("input", priority=20)
|
|
217
|
+
self._areas["thinking"] = LayoutArea("thinking", priority=30)
|
|
218
|
+
|
|
219
|
+
def set_terminal_size(self, width: int, height: int) -> None:
|
|
220
|
+
"""Update terminal dimensions.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
width: New terminal width.
|
|
224
|
+
height: New terminal height.
|
|
225
|
+
"""
|
|
226
|
+
if self.terminal_width != width or self.terminal_height != height:
|
|
227
|
+
self.terminal_width = width
|
|
228
|
+
self.terminal_height = height
|
|
229
|
+
self._dirty = True
|
|
230
|
+
|
|
231
|
+
def add_area(self, name: str, area: LayoutArea) -> None:
|
|
232
|
+
"""Add a layout area.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
name: Area name.
|
|
236
|
+
area: LayoutArea instance.
|
|
237
|
+
"""
|
|
238
|
+
self._areas[name] = area
|
|
239
|
+
self._dirty = True
|
|
240
|
+
|
|
241
|
+
def get_area(self, name: str) -> Optional[LayoutArea]:
|
|
242
|
+
"""Get a layout area by name.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
name: Area name.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
LayoutArea instance or None if not found.
|
|
249
|
+
"""
|
|
250
|
+
return self._areas.get(name)
|
|
251
|
+
|
|
252
|
+
def update_area_content(self, name: str, content: List[str]) -> None:
|
|
253
|
+
"""Update content for a specific area.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
name: Area name.
|
|
257
|
+
content: New content lines.
|
|
258
|
+
"""
|
|
259
|
+
area = self._areas.get(name)
|
|
260
|
+
if area:
|
|
261
|
+
area.content = content.copy()
|
|
262
|
+
self._dirty = True
|
|
263
|
+
|
|
264
|
+
def set_area_visibility(self, name: str, visible: bool) -> None:
|
|
265
|
+
"""Set visibility for a specific area.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
name: Area name.
|
|
269
|
+
visible: Whether area should be visible.
|
|
270
|
+
"""
|
|
271
|
+
area = self._areas.get(name)
|
|
272
|
+
if area and area.visible != visible:
|
|
273
|
+
area.visible = visible
|
|
274
|
+
self._dirty = True
|
|
275
|
+
|
|
276
|
+
def calculate_layout(
|
|
277
|
+
self, mode: LayoutMode = LayoutMode.ADAPTIVE
|
|
278
|
+
) -> Dict[str, ScreenRegion]:
|
|
279
|
+
"""Calculate layout regions for all visible areas.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
mode: Layout mode to use.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Dictionary mapping area names to screen regions.
|
|
286
|
+
"""
|
|
287
|
+
visible_areas = {
|
|
288
|
+
name: area
|
|
289
|
+
for name, area in self._areas.items()
|
|
290
|
+
if area.visible and area.content
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if not visible_areas:
|
|
294
|
+
return {}
|
|
295
|
+
|
|
296
|
+
if mode == LayoutMode.ADAPTIVE:
|
|
297
|
+
return self._calculate_adaptive_layout(visible_areas)
|
|
298
|
+
elif mode == LayoutMode.HORIZONTAL:
|
|
299
|
+
return self._calculate_horizontal_layout(visible_areas)
|
|
300
|
+
elif mode == LayoutMode.VERTICAL:
|
|
301
|
+
return self._calculate_vertical_layout(visible_areas)
|
|
302
|
+
else:
|
|
303
|
+
return self._calculate_stacked_layout(visible_areas)
|
|
304
|
+
|
|
305
|
+
def _calculate_adaptive_layout(
|
|
306
|
+
self, areas: Dict[str, LayoutArea]
|
|
307
|
+
) -> Dict[str, ScreenRegion]:
|
|
308
|
+
"""Calculate adaptive layout based on terminal size and content.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
areas: Dictionary of visible areas.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Dictionary mapping area names to screen regions.
|
|
315
|
+
"""
|
|
316
|
+
regions = {}
|
|
317
|
+
current_y = 0
|
|
318
|
+
|
|
319
|
+
# Sort areas by priority (higher priority first)
|
|
320
|
+
_ = sorted(areas.items(), key=lambda x: x[1].priority, reverse=True)
|
|
321
|
+
|
|
322
|
+
# Handle thinking area first (if present)
|
|
323
|
+
if "thinking" in areas and areas["thinking"].content:
|
|
324
|
+
thinking_height = areas["thinking"].get_content_height()
|
|
325
|
+
regions["thinking"] = ScreenRegion(
|
|
326
|
+
0, current_y, self.terminal_width, thinking_height
|
|
327
|
+
)
|
|
328
|
+
current_y += thinking_height + 1 # Add spacing
|
|
329
|
+
|
|
330
|
+
# Handle input area
|
|
331
|
+
if "input" in areas and areas["input"].content:
|
|
332
|
+
input_height = areas["input"].get_content_height()
|
|
333
|
+
regions["input"] = ScreenRegion(
|
|
334
|
+
0, current_y, self.terminal_width, input_height
|
|
335
|
+
)
|
|
336
|
+
current_y += input_height + 1
|
|
337
|
+
|
|
338
|
+
# Handle status areas
|
|
339
|
+
status_areas = {
|
|
340
|
+
name: area
|
|
341
|
+
for name, area in areas.items()
|
|
342
|
+
if name.startswith("status_") and area.content
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if status_areas:
|
|
346
|
+
status_regions = self._layout_status_areas(status_areas, current_y)
|
|
347
|
+
regions.update(status_regions)
|
|
348
|
+
|
|
349
|
+
return regions
|
|
350
|
+
|
|
351
|
+
def _calculate_horizontal_layout(
|
|
352
|
+
self, areas: Dict[str, LayoutArea]
|
|
353
|
+
) -> Dict[str, ScreenRegion]:
|
|
354
|
+
"""Calculate horizontal layout (side-by-side areas).
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
areas: Dictionary of visible areas.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Dictionary mapping area names to screen regions.
|
|
361
|
+
"""
|
|
362
|
+
regions = {}
|
|
363
|
+
area_count = len(areas)
|
|
364
|
+
|
|
365
|
+
if area_count == 0:
|
|
366
|
+
return regions
|
|
367
|
+
|
|
368
|
+
area_width = max(1, (self.terminal_width - (area_count - 1)) // area_count)
|
|
369
|
+
current_x = 0
|
|
370
|
+
|
|
371
|
+
for i, (name, area) in enumerate(areas.items()):
|
|
372
|
+
# Last area gets remaining width
|
|
373
|
+
if i == area_count - 1:
|
|
374
|
+
width = self.terminal_width - current_x
|
|
375
|
+
else:
|
|
376
|
+
width = area_width
|
|
377
|
+
|
|
378
|
+
regions[name] = ScreenRegion(
|
|
379
|
+
current_x, 0, width, area.get_content_height()
|
|
380
|
+
)
|
|
381
|
+
current_x += width + 1 # Add spacing
|
|
382
|
+
|
|
383
|
+
return regions
|
|
384
|
+
|
|
385
|
+
def _calculate_vertical_layout(
|
|
386
|
+
self, areas: Dict[str, LayoutArea]
|
|
387
|
+
) -> Dict[str, ScreenRegion]:
|
|
388
|
+
"""Calculate vertical layout (stacked areas).
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
areas: Dictionary of visible areas.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Dictionary mapping area names to screen regions.
|
|
395
|
+
"""
|
|
396
|
+
regions = {}
|
|
397
|
+
current_y = 0
|
|
398
|
+
|
|
399
|
+
for name, area in areas.items():
|
|
400
|
+
height = area.get_content_height()
|
|
401
|
+
regions[name] = ScreenRegion(0, current_y, self.terminal_width, height)
|
|
402
|
+
current_y += height + 1 # Add spacing
|
|
403
|
+
|
|
404
|
+
return regions
|
|
405
|
+
|
|
406
|
+
def _calculate_stacked_layout(
|
|
407
|
+
self, areas: Dict[str, LayoutArea]
|
|
408
|
+
) -> Dict[str, ScreenRegion]:
|
|
409
|
+
"""Calculate stacked layout (overlapping areas).
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
areas: Dictionary of visible areas.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Dictionary mapping area names to screen regions.
|
|
416
|
+
"""
|
|
417
|
+
regions = {}
|
|
418
|
+
|
|
419
|
+
# Simple stacked layout - each area takes full width
|
|
420
|
+
for name, area in areas.items():
|
|
421
|
+
height = area.get_content_height()
|
|
422
|
+
regions[name] = ScreenRegion(0, 0, self.terminal_width, height)
|
|
423
|
+
|
|
424
|
+
return regions
|
|
425
|
+
|
|
426
|
+
def _layout_status_areas(
|
|
427
|
+
self, status_areas: Dict[str, LayoutArea], start_y: int
|
|
428
|
+
) -> Dict[str, ScreenRegion]:
|
|
429
|
+
"""Layout status areas with adaptive column management.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
status_areas: Dictionary of status areas.
|
|
433
|
+
start_y: Starting Y position.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Dictionary mapping status area names to screen regions.
|
|
437
|
+
"""
|
|
438
|
+
regions = {}
|
|
439
|
+
|
|
440
|
+
if not status_areas:
|
|
441
|
+
return regions
|
|
442
|
+
|
|
443
|
+
# Determine layout based on terminal width
|
|
444
|
+
if self.terminal_width >= 80:
|
|
445
|
+
# Three-column layout for wide terminals
|
|
446
|
+
column_width = (self.terminal_width - 6) // 3 # 6 for spacing
|
|
447
|
+
|
|
448
|
+
# Get areas A, B, C in order
|
|
449
|
+
area_names = ["status_a", "status_b", "status_c"]
|
|
450
|
+
areas_with_content = [
|
|
451
|
+
(name, status_areas.get(name))
|
|
452
|
+
for name in area_names
|
|
453
|
+
if name in status_areas
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
max_height = (
|
|
457
|
+
max(area.get_content_height() for _, area in areas_with_content)
|
|
458
|
+
if areas_with_content
|
|
459
|
+
else 1
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
for i, (name, area) in enumerate(areas_with_content):
|
|
463
|
+
x_pos = i * (column_width + 2) # 2 for spacing
|
|
464
|
+
regions[name] = ScreenRegion(
|
|
465
|
+
x_pos, start_y, column_width, max_height
|
|
466
|
+
)
|
|
467
|
+
else:
|
|
468
|
+
# Vertical layout for narrow terminals
|
|
469
|
+
current_y = start_y
|
|
470
|
+
for name, area in status_areas.items():
|
|
471
|
+
height = area.get_content_height()
|
|
472
|
+
regions[name] = ScreenRegion(
|
|
473
|
+
0, current_y, self.terminal_width, height
|
|
474
|
+
)
|
|
475
|
+
current_y += height + 1
|
|
476
|
+
|
|
477
|
+
return regions
|
|
478
|
+
|
|
479
|
+
def render_areas(self, regions: Dict[str, ScreenRegion]) -> List[str]:
|
|
480
|
+
"""Render all areas into display lines.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
regions: Dictionary mapping area names to screen regions.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
List of formatted display lines.
|
|
487
|
+
"""
|
|
488
|
+
lines = []
|
|
489
|
+
max_y = (
|
|
490
|
+
max(region.y + region.height for region in regions.values())
|
|
491
|
+
if regions
|
|
492
|
+
else 0
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
for y in range(max_y):
|
|
496
|
+
line_parts = {}
|
|
497
|
+
|
|
498
|
+
# Collect content for this line from all areas
|
|
499
|
+
for name, region in regions.items():
|
|
500
|
+
if region.y <= y < region.y + region.height:
|
|
501
|
+
area = self._areas[name]
|
|
502
|
+
content_index = y - region.y
|
|
503
|
+
|
|
504
|
+
if content_index < len(area.content):
|
|
505
|
+
content = area.content[content_index]
|
|
506
|
+
line_parts[region.x] = (
|
|
507
|
+
region,
|
|
508
|
+
content,
|
|
509
|
+
area.alignment,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Build the line
|
|
513
|
+
if line_parts:
|
|
514
|
+
line = self._build_line_from_parts(line_parts)
|
|
515
|
+
lines.append(line)
|
|
516
|
+
else:
|
|
517
|
+
lines.append("")
|
|
518
|
+
|
|
519
|
+
self._last_render_lines = len(lines)
|
|
520
|
+
self._dirty = False
|
|
521
|
+
return lines
|
|
522
|
+
|
|
523
|
+
def _build_line_from_parts(
|
|
524
|
+
self, line_parts: Dict[int, Tuple[ScreenRegion, str, AreaAlignment]]
|
|
525
|
+
) -> str:
|
|
526
|
+
"""Build a single display line from area parts.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
line_parts: Dictionary mapping x-position to (region, content, alignment).
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
Formatted line string.
|
|
533
|
+
"""
|
|
534
|
+
if not line_parts:
|
|
535
|
+
return ""
|
|
536
|
+
|
|
537
|
+
# Sort by x position
|
|
538
|
+
sorted_parts = sorted(line_parts.items())
|
|
539
|
+
line_chars = [" "] * self.terminal_width
|
|
540
|
+
|
|
541
|
+
for x_pos, (region, content, alignment) in sorted_parts:
|
|
542
|
+
# Remove ANSI codes for width calculation
|
|
543
|
+
visible_content = re.sub(r"\033\[[0-9;]*m", "", content)
|
|
544
|
+
|
|
545
|
+
# Apply alignment within the region
|
|
546
|
+
if alignment == AreaAlignment.CENTER:
|
|
547
|
+
padding = max(0, (region.width - len(visible_content)) // 2)
|
|
548
|
+
start_x = region.x + padding
|
|
549
|
+
elif alignment == AreaAlignment.RIGHT:
|
|
550
|
+
padding = max(0, region.width - len(visible_content))
|
|
551
|
+
start_x = region.x + padding
|
|
552
|
+
else: # LEFT or JUSTIFY
|
|
553
|
+
start_x = region.x
|
|
554
|
+
|
|
555
|
+
# Place content in line, handling ANSI codes
|
|
556
|
+
content_chars = list(content)
|
|
557
|
+
for i, char in enumerate(content_chars):
|
|
558
|
+
pos = start_x + i
|
|
559
|
+
if 0 <= pos < self.terminal_width and pos < region.x + region.width:
|
|
560
|
+
line_chars[pos] = char
|
|
561
|
+
|
|
562
|
+
return "".join(line_chars).rstrip()
|
|
563
|
+
|
|
564
|
+
def get_render_info(self) -> Dict[str, Any]:
|
|
565
|
+
"""Get layout rendering information for debugging.
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
Dictionary with layout information.
|
|
569
|
+
"""
|
|
570
|
+
return {
|
|
571
|
+
"terminal_size": (self.terminal_width, self.terminal_height),
|
|
572
|
+
"areas_count": len(self._areas),
|
|
573
|
+
"visible_areas": [
|
|
574
|
+
name for name, area in self._areas.items() if area.visible
|
|
575
|
+
],
|
|
576
|
+
"dirty": self._dirty,
|
|
577
|
+
"last_render_lines": self._last_render_lines,
|
|
578
|
+
"area_stats": {
|
|
579
|
+
name: {
|
|
580
|
+
"content_lines": len(area.content),
|
|
581
|
+
"content_width": area.get_content_width(),
|
|
582
|
+
"visible": area.visible,
|
|
583
|
+
"priority": area.priority,
|
|
584
|
+
}
|
|
585
|
+
for name, area in self._areas.items()
|
|
586
|
+
},
|
|
587
|
+
}
|