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,443 @@
|
|
|
1
|
+
"""Modal state management for proper terminal state isolation.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive terminal state management for modals,
|
|
4
|
+
ensuring complete isolation from the conversation system and proper
|
|
5
|
+
restoration of terminal state when modals are closed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
from ..io.terminal_state import TerminalState
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModalDisplayMode(Enum):
|
|
20
|
+
"""Modal display modes for different rendering strategies."""
|
|
21
|
+
OVERLAY = "overlay" # Modal overlays existing content
|
|
22
|
+
FULLSCREEN = "fullscreen" # Modal takes full screen
|
|
23
|
+
INLINE = "inline" # Modal appears inline (not recommended)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TerminalSnapshot:
|
|
28
|
+
"""Complete snapshot of terminal state before modal display."""
|
|
29
|
+
cursor_position: Tuple[int, int] = (0, 0)
|
|
30
|
+
cursor_visible: bool = True
|
|
31
|
+
terminal_size: Tuple[int, int] = (80, 24)
|
|
32
|
+
screen_buffer: List[str] = field(default_factory=list)
|
|
33
|
+
raw_mode_active: bool = False
|
|
34
|
+
saved_termios: Any = None
|
|
35
|
+
|
|
36
|
+
def __post_init__(self):
|
|
37
|
+
"""Post-initialization validation."""
|
|
38
|
+
if not isinstance(self.screen_buffer, list):
|
|
39
|
+
self.screen_buffer = []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ModalLayout:
|
|
44
|
+
"""Modal layout configuration for positioning and sizing."""
|
|
45
|
+
width: int = 80
|
|
46
|
+
height: int = 20
|
|
47
|
+
start_row: int = 5
|
|
48
|
+
start_col: int = 10
|
|
49
|
+
center_horizontal: bool = True
|
|
50
|
+
center_vertical: bool = True
|
|
51
|
+
padding: int = 2
|
|
52
|
+
border_style: str = "box" # "box", "simple", "none"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ModalStateManager:
|
|
56
|
+
"""Manages terminal state isolation for modal displays.
|
|
57
|
+
|
|
58
|
+
This class provides complete terminal state management for modals,
|
|
59
|
+
ensuring that modal display and interaction never interferes with
|
|
60
|
+
the underlying conversation or terminal state.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, terminal_state: TerminalState):
|
|
64
|
+
"""Initialize modal state manager.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
terminal_state: TerminalState instance for terminal control.
|
|
68
|
+
"""
|
|
69
|
+
self.terminal_state = terminal_state
|
|
70
|
+
self.modal_active = False
|
|
71
|
+
self.display_mode = ModalDisplayMode.OVERLAY
|
|
72
|
+
self.saved_snapshot: Optional[TerminalSnapshot] = None
|
|
73
|
+
self.current_layout: Optional[ModalLayout] = None
|
|
74
|
+
self.modal_content_cache: List[str] = []
|
|
75
|
+
|
|
76
|
+
def _strip_ansi(self, text: str) -> str:
|
|
77
|
+
"""Remove ANSI escape codes from text.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
text: Text with potential ANSI codes.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Text with ANSI codes removed.
|
|
84
|
+
"""
|
|
85
|
+
return re.sub(r'\033\[[0-9;]*m', '', text)
|
|
86
|
+
|
|
87
|
+
def prepare_modal_display(self, layout: ModalLayout,
|
|
88
|
+
display_mode: ModalDisplayMode = ModalDisplayMode.OVERLAY) -> bool:
|
|
89
|
+
"""Prepare terminal for modal display by saving current state.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
layout: Modal layout configuration.
|
|
93
|
+
display_mode: How modal should be displayed.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if preparation was successful.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
if self.modal_active:
|
|
100
|
+
logger.warning("Modal already active, closing previous modal first")
|
|
101
|
+
self.restore_terminal_state()
|
|
102
|
+
|
|
103
|
+
# Save current terminal state
|
|
104
|
+
self.saved_snapshot = self._capture_terminal_snapshot()
|
|
105
|
+
if not self.saved_snapshot:
|
|
106
|
+
logger.error("Failed to capture terminal snapshot")
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
# Store modal configuration
|
|
110
|
+
self.current_layout = layout
|
|
111
|
+
self.display_mode = display_mode
|
|
112
|
+
|
|
113
|
+
# Calculate final layout positions
|
|
114
|
+
self._calculate_modal_position(layout)
|
|
115
|
+
|
|
116
|
+
# Prepare terminal for modal rendering
|
|
117
|
+
self._prepare_modal_area()
|
|
118
|
+
|
|
119
|
+
self.modal_active = True
|
|
120
|
+
logger.info(f"Modal display prepared in {display_mode.value} mode")
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"Failed to prepare modal display: {e}")
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def render_modal_content(self, content_lines: List[str]) -> bool:
|
|
128
|
+
"""Render modal content using isolated terminal output.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
content_lines: Modal content lines to display.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if rendering was successful.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
if not self.modal_active or not self.current_layout:
|
|
138
|
+
logger.error("Modal not active or layout not configured")
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
# Cache content for refresh operations
|
|
142
|
+
self.modal_content_cache = content_lines.copy()
|
|
143
|
+
|
|
144
|
+
# Clear previous modal content
|
|
145
|
+
self._clear_modal_content_area()
|
|
146
|
+
|
|
147
|
+
# Render new content using direct terminal output
|
|
148
|
+
success = self._render_content_direct(content_lines)
|
|
149
|
+
|
|
150
|
+
if success:
|
|
151
|
+
logger.debug(f"Modal content rendered: {len(content_lines)} lines")
|
|
152
|
+
else:
|
|
153
|
+
logger.error("Failed to render modal content")
|
|
154
|
+
|
|
155
|
+
return success
|
|
156
|
+
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Failed to render modal content: {e}")
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
def refresh_modal_display(self) -> bool:
|
|
162
|
+
"""Refresh modal display without state changes.
|
|
163
|
+
|
|
164
|
+
This method re-renders the current modal content without
|
|
165
|
+
affecting any terminal state or conversation buffers.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if refresh was successful.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
if not self.modal_active:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# Re-render cached content
|
|
175
|
+
return self.render_modal_content(self.modal_content_cache)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Failed to refresh modal display: {e}")
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def restore_terminal_state(self) -> bool:
|
|
182
|
+
"""Restore terminal state to pre-modal condition.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
True if restoration was successful.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
if not self.modal_active:
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
# Clear modal content area
|
|
192
|
+
self._clear_modal_content_area()
|
|
193
|
+
|
|
194
|
+
# Restore terminal state from snapshot
|
|
195
|
+
if self.saved_snapshot:
|
|
196
|
+
self._restore_from_snapshot(self.saved_snapshot)
|
|
197
|
+
|
|
198
|
+
# Reset modal state
|
|
199
|
+
self._reset_modal_state()
|
|
200
|
+
|
|
201
|
+
logger.info("Terminal state restored after modal")
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"Failed to restore terminal state: {e}")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
def update_modal_layout(self, new_layout: ModalLayout) -> bool:
|
|
209
|
+
"""Update modal layout and re-render.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
new_layout: New layout configuration.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if layout update was successful.
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
if not self.modal_active:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
# Clear current modal area
|
|
222
|
+
self._clear_modal_content_area()
|
|
223
|
+
|
|
224
|
+
# Update layout
|
|
225
|
+
self.current_layout = new_layout
|
|
226
|
+
self._calculate_modal_position(new_layout)
|
|
227
|
+
|
|
228
|
+
# Re-render with new layout
|
|
229
|
+
return self.render_modal_content(self.modal_content_cache)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Failed to update modal layout: {e}")
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def _capture_terminal_snapshot(self) -> Optional[TerminalSnapshot]:
|
|
236
|
+
"""Capture complete terminal state snapshot.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
TerminalSnapshot or None if capture failed.
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
|
|
243
|
+
# Get current terminal dimensions
|
|
244
|
+
width, height = self.terminal_state.get_size()
|
|
245
|
+
|
|
246
|
+
# Check cursor hidden state
|
|
247
|
+
cursor_hidden = getattr(self.terminal_state, '_cursor_hidden', False)
|
|
248
|
+
|
|
249
|
+
# Check current mode
|
|
250
|
+
current_mode = getattr(self.terminal_state, 'current_mode', None)
|
|
251
|
+
|
|
252
|
+
mode_value = current_mode.value if current_mode and hasattr(current_mode, 'value') else 'unknown'
|
|
253
|
+
|
|
254
|
+
# Check original termios
|
|
255
|
+
original_termios = getattr(self.terminal_state, 'original_termios', None)
|
|
256
|
+
|
|
257
|
+
# SWITCH TO ALTERNATE SCREEN BUFFER (automatically saves entire screen)
|
|
258
|
+
alternate_buffer_success = self.terminal_state.write_raw("\033[?1049h")
|
|
259
|
+
|
|
260
|
+
# Create minimal snapshot (alternate buffer handles everything)
|
|
261
|
+
snapshot = TerminalSnapshot(
|
|
262
|
+
cursor_position=(0, 0), # Not needed - alternate buffer preserves this
|
|
263
|
+
cursor_visible=not cursor_hidden,
|
|
264
|
+
terminal_size=(width, height),
|
|
265
|
+
screen_buffer=[], # Not needed - alternate buffer handles screen content
|
|
266
|
+
raw_mode_active=mode_value == "raw",
|
|
267
|
+
saved_termios=original_termios
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return snapshot
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error(f"Failed to capture terminal snapshot: {e}")
|
|
274
|
+
import traceback
|
|
275
|
+
logger.error(f"Full traceback: {traceback.format_exc()}")
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
def _restore_from_snapshot(self, snapshot: TerminalSnapshot) -> bool:
|
|
279
|
+
"""Restore terminal state from snapshot.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
snapshot: TerminalSnapshot to restore from.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
True if restoration was successful.
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
# SWITCH BACK FROM ALTERNATE SCREEN BUFFER (automatically restores entire screen)
|
|
289
|
+
restore_buffer_success = self.terminal_state.write_raw("\033[?1049l")
|
|
290
|
+
|
|
291
|
+
# Restore cursor visibility (alternate buffer preserves position automatically)
|
|
292
|
+
if snapshot.cursor_visible:
|
|
293
|
+
self.terminal_state.show_cursor()
|
|
294
|
+
else:
|
|
295
|
+
self.terminal_state.hide_cursor()
|
|
296
|
+
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.error(f"Failed to restore from snapshot: {e}")
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
def _calculate_modal_position(self, layout: ModalLayout) -> None:
|
|
304
|
+
"""Calculate modal position based on layout configuration.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
layout: ModalLayout configuration.
|
|
308
|
+
"""
|
|
309
|
+
width, height = self.terminal_state.get_size()
|
|
310
|
+
|
|
311
|
+
if layout.center_horizontal:
|
|
312
|
+
layout.start_col = max(0, (width - layout.width) // 2)
|
|
313
|
+
|
|
314
|
+
if layout.center_vertical:
|
|
315
|
+
layout.start_row = max(0, (height - layout.height) // 2)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _prepare_modal_area(self) -> bool:
|
|
319
|
+
"""Prepare terminal area for modal rendering.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
True if preparation was successful.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
# Hide cursor for clean modal display
|
|
326
|
+
self.terminal_state.hide_cursor()
|
|
327
|
+
|
|
328
|
+
# FIXED: Always clear alternate buffer for clean modal display
|
|
329
|
+
# Clear entire alternate buffer and position cursor at top-left
|
|
330
|
+
self.terminal_state.write_raw("\033[2J\033[H")
|
|
331
|
+
|
|
332
|
+
# Additional preparation based on display mode
|
|
333
|
+
if self.display_mode == ModalDisplayMode.FULLSCREEN:
|
|
334
|
+
# Already cleared above, but add any fullscreen-specific setup here
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
logger.debug("Terminal area prepared for modal")
|
|
338
|
+
return True
|
|
339
|
+
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f"Failed to prepare modal area: {e}")
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
def _render_content_direct(self, content_lines: List[str]) -> bool:
|
|
345
|
+
"""Render content using direct terminal output.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
content_lines: Content lines to render.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
True if rendering was successful.
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
if not self.current_layout:
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
layout = self.current_layout
|
|
358
|
+
|
|
359
|
+
# Render each line at calculated position
|
|
360
|
+
for i, line in enumerate(content_lines):
|
|
361
|
+
if i >= layout.height:
|
|
362
|
+
break # Don't exceed modal height
|
|
363
|
+
|
|
364
|
+
# Calculate position for this line
|
|
365
|
+
row = layout.start_row + i + 1 # 1-based positioning
|
|
366
|
+
col = layout.start_col + 1 # 1-based positioning
|
|
367
|
+
|
|
368
|
+
# Position cursor and write line
|
|
369
|
+
self.terminal_state.write_raw(f"\033[{row};{col}H")
|
|
370
|
+
|
|
371
|
+
# Truncate line if too wide
|
|
372
|
+
# Don't truncate here - lines should already be properly sized by modal renderer
|
|
373
|
+
# Just write the line as-is
|
|
374
|
+
|
|
375
|
+
self.terminal_state.write_raw(line)
|
|
376
|
+
|
|
377
|
+
logger.debug(f"Modal content rendered: {len(content_lines)} lines")
|
|
378
|
+
return True
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f"Failed to render content directly: {e}")
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
def _clear_modal_content_area(self) -> bool:
|
|
385
|
+
"""Clear the modal content area.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
True if area was cleared successfully.
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
if not self.current_layout:
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
layout = self.current_layout
|
|
395
|
+
|
|
396
|
+
# Clear each line of the modal area with spaces (overwrite modal content)
|
|
397
|
+
for i in range(layout.height):
|
|
398
|
+
row = layout.start_row + i + 1 # 1-based row positioning
|
|
399
|
+
col = layout.start_col + 1 # 1-based col positioning
|
|
400
|
+
|
|
401
|
+
# Position cursor at start of this modal line
|
|
402
|
+
self.terminal_state.write_raw(f"\033[{row};{col}H")
|
|
403
|
+
|
|
404
|
+
# Write spaces to overwrite modal content for this line
|
|
405
|
+
spaces = " " * layout.width
|
|
406
|
+
self.terminal_state.write_raw(spaces)
|
|
407
|
+
|
|
408
|
+
logger.debug("Modal content area cleared")
|
|
409
|
+
return True
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.error(f"Failed to clear modal content area: {e}")
|
|
413
|
+
return False
|
|
414
|
+
|
|
415
|
+
def _reset_modal_state(self) -> None:
|
|
416
|
+
"""Reset modal state variables."""
|
|
417
|
+
self.modal_active = False
|
|
418
|
+
self.saved_snapshot = None
|
|
419
|
+
self.current_layout = None
|
|
420
|
+
self.modal_content_cache = []
|
|
421
|
+
self.display_mode = ModalDisplayMode.OVERLAY
|
|
422
|
+
|
|
423
|
+
def get_modal_state_info(self) -> Dict[str, Any]:
|
|
424
|
+
"""Get current modal state information.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Dictionary with modal state details.
|
|
428
|
+
"""
|
|
429
|
+
return {
|
|
430
|
+
"modal_active": self.modal_active,
|
|
431
|
+
"display_mode": self.display_mode.value if self.display_mode else None,
|
|
432
|
+
"has_saved_snapshot": self.saved_snapshot is not None,
|
|
433
|
+
"content_lines_cached": len(self.modal_content_cache),
|
|
434
|
+
"current_layout": {
|
|
435
|
+
"width": self.current_layout.width if self.current_layout else 0,
|
|
436
|
+
"height": self.current_layout.height if self.current_layout else 0,
|
|
437
|
+
"position": (
|
|
438
|
+
self.current_layout.start_row if self.current_layout else 0,
|
|
439
|
+
self.current_layout.start_col if self.current_layout else 0
|
|
440
|
+
)
|
|
441
|
+
} if self.current_layout else None,
|
|
442
|
+
"terminal_size": self.terminal_state.get_size()
|
|
443
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Widget integration methods for modal_renderer.py.
|
|
2
|
+
|
|
3
|
+
This module contains the complete widget integration logic ready to be
|
|
4
|
+
merged into modal_renderer.py when Phase 1 signals completion.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Dict, Any, Optional
|
|
8
|
+
from .widgets import BaseWidget, CheckboxWidget, DropdownWidget, TextInputWidget, SliderWidget
|
|
9
|
+
from ..io.key_parser import KeyPress
|
|
10
|
+
from ..io.visual_effects import ColorPalette
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WidgetIntegrationMixin:
|
|
14
|
+
"""Mixin class containing widget integration methods for ModalRenderer."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize widget management."""
|
|
18
|
+
self.widgets: List[BaseWidget] = []
|
|
19
|
+
self.focused_widget_index = 0
|
|
20
|
+
|
|
21
|
+
def _create_widgets(self, modal_config: dict) -> List[BaseWidget]:
|
|
22
|
+
"""Create widgets from modal configuration.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
modal_config: Modal configuration dictionary.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of instantiated widgets.
|
|
29
|
+
"""
|
|
30
|
+
widgets = []
|
|
31
|
+
for section in modal_config.get("sections", []):
|
|
32
|
+
for widget_config in section.get("widgets", []):
|
|
33
|
+
widget = self._create_widget(widget_config)
|
|
34
|
+
widgets.append(widget)
|
|
35
|
+
return widgets
|
|
36
|
+
|
|
37
|
+
def _create_widget(self, config: dict) -> BaseWidget:
|
|
38
|
+
"""Create a single widget from configuration.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
config: Widget configuration dictionary.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Instantiated widget.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If widget type is unknown.
|
|
48
|
+
"""
|
|
49
|
+
widget_type = config["type"]
|
|
50
|
+
|
|
51
|
+
# Use config_path directly if provided, otherwise construct from config_path + key
|
|
52
|
+
if "config_path" in config:
|
|
53
|
+
config_path = config["config_path"]
|
|
54
|
+
else:
|
|
55
|
+
config_path = f"{config.get('config_path', 'core.ui')}.{config['key']}"
|
|
56
|
+
|
|
57
|
+
if widget_type == "checkbox":
|
|
58
|
+
return CheckboxWidget(config, config_path, self.config_service)
|
|
59
|
+
elif widget_type == "dropdown":
|
|
60
|
+
return DropdownWidget(config, config_path, self.config_service)
|
|
61
|
+
elif widget_type == "text_input":
|
|
62
|
+
return TextInputWidget(config, config_path, self.config_service)
|
|
63
|
+
elif widget_type == "slider":
|
|
64
|
+
return SliderWidget(config, config_path, self.config_service)
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError(f"Unknown widget type: {widget_type}")
|
|
67
|
+
|
|
68
|
+
def _render_modal_content_with_widgets(self, modal_config: dict, width: int) -> List[str]:
|
|
69
|
+
"""Render modal content with interactive widgets.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
modal_config: Modal configuration dict.
|
|
73
|
+
width: Modal width.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of content lines with rendered widgets.
|
|
77
|
+
"""
|
|
78
|
+
lines = []
|
|
79
|
+
border_color = ColorPalette.DIM_CYAN
|
|
80
|
+
|
|
81
|
+
# Create widgets if not already created
|
|
82
|
+
if not self.widgets:
|
|
83
|
+
self.widgets = self._create_widgets(modal_config)
|
|
84
|
+
if self.widgets:
|
|
85
|
+
self.widgets[0].set_focus(True)
|
|
86
|
+
|
|
87
|
+
# Add empty line
|
|
88
|
+
lines.append(f"{border_color}│{' ' * (width-2)}│{ColorPalette.RESET}")
|
|
89
|
+
|
|
90
|
+
# Render sections with widgets
|
|
91
|
+
widget_index = 0
|
|
92
|
+
sections = modal_config.get("sections", [])
|
|
93
|
+
|
|
94
|
+
for section in sections:
|
|
95
|
+
section_title = section.get("title", "Section")
|
|
96
|
+
|
|
97
|
+
# Section title
|
|
98
|
+
title_text = f" {section_title}"
|
|
99
|
+
title_line = f"│{title_text.ljust(width-2)}│"
|
|
100
|
+
lines.append(f"{border_color}{title_line}{ColorPalette.RESET}")
|
|
101
|
+
|
|
102
|
+
# Empty line after title
|
|
103
|
+
lines.append(f"{border_color}│{' ' * (width-2)}│{ColorPalette.RESET}")
|
|
104
|
+
|
|
105
|
+
# Render widgets in this section
|
|
106
|
+
section_widgets = section.get("widgets", [])
|
|
107
|
+
for widget_config in section_widgets:
|
|
108
|
+
if widget_index < len(self.widgets):
|
|
109
|
+
widget = self.widgets[widget_index]
|
|
110
|
+
widget_lines = widget.render()
|
|
111
|
+
|
|
112
|
+
# Add each widget line with proper padding
|
|
113
|
+
for widget_line in widget_lines:
|
|
114
|
+
# Remove any existing padding and reformat for modal
|
|
115
|
+
clean_line = widget_line.strip()
|
|
116
|
+
if clean_line.startswith(" "): # Remove widget's default padding
|
|
117
|
+
clean_line = clean_line[2:]
|
|
118
|
+
|
|
119
|
+
# Add modal padding and border
|
|
120
|
+
padded_line = f" {clean_line}"
|
|
121
|
+
modal_line = f"│{padded_line.ljust(width-2)}│"
|
|
122
|
+
lines.append(f"{border_color}{modal_line}{ColorPalette.RESET}")
|
|
123
|
+
|
|
124
|
+
widget_index += 1
|
|
125
|
+
|
|
126
|
+
# Empty line after section
|
|
127
|
+
lines.append(f"{border_color}│{' ' * (width-2)}│{ColorPalette.RESET}")
|
|
128
|
+
|
|
129
|
+
return lines
|
|
130
|
+
|
|
131
|
+
def _handle_widget_navigation(self, key_press: KeyPress) -> bool:
|
|
132
|
+
"""Handle widget focus navigation.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
key_press: Key press event.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if navigation was handled.
|
|
139
|
+
"""
|
|
140
|
+
if not self.widgets:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
if key_press.name == "Tab" or key_press.name == "ArrowDown":
|
|
144
|
+
# Move to next widget
|
|
145
|
+
self.widgets[self.focused_widget_index].set_focus(False)
|
|
146
|
+
self.focused_widget_index = (self.focused_widget_index + 1) % len(self.widgets)
|
|
147
|
+
self.widgets[self.focused_widget_index].set_focus(True)
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
elif key_press.name == "ArrowUp":
|
|
151
|
+
# Move to previous widget
|
|
152
|
+
self.widgets[self.focused_widget_index].set_focus(False)
|
|
153
|
+
self.focused_widget_index = (self.focused_widget_index - 1) % len(self.widgets)
|
|
154
|
+
self.widgets[self.focused_widget_index].set_focus(True)
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def _handle_widget_input(self, key_press: KeyPress) -> bool:
|
|
160
|
+
"""Route input to focused widget.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
key_press: Key press event.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if input was handled by a widget.
|
|
167
|
+
"""
|
|
168
|
+
if not self.widgets or self.focused_widget_index >= len(self.widgets):
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
focused_widget = self.widgets[self.focused_widget_index]
|
|
172
|
+
return focused_widget.handle_input(key_press)
|
|
173
|
+
|
|
174
|
+
def _get_widget_values(self) -> Dict[str, Any]:
|
|
175
|
+
"""Get all widget values for saving.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Dictionary mapping config paths to values.
|
|
179
|
+
"""
|
|
180
|
+
values = {}
|
|
181
|
+
for widget in self.widgets:
|
|
182
|
+
if widget.has_pending_changes():
|
|
183
|
+
values[widget.config_path] = widget.get_pending_value()
|
|
184
|
+
return values
|
|
185
|
+
|
|
186
|
+
def _reset_widget_focus(self):
|
|
187
|
+
"""Reset widget focus to first widget."""
|
|
188
|
+
if self.widgets:
|
|
189
|
+
for widget in self.widgets:
|
|
190
|
+
widget.set_focus(False)
|
|
191
|
+
self.focused_widget_index = 0
|
|
192
|
+
self.widgets[0].set_focus(True)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# INTEGRATION INSTRUCTIONS FOR PHASE 1 COMPLETION:
|
|
196
|
+
"""
|
|
197
|
+
TO INTEGRATE WIDGETS INTO modal_renderer.py:
|
|
198
|
+
|
|
199
|
+
1. Add import at top:
|
|
200
|
+
from .widget_integration import WidgetIntegrationMixin
|
|
201
|
+
|
|
202
|
+
2. Make ModalRenderer inherit from mixin:
|
|
203
|
+
class ModalRenderer(WidgetIntegrationMixin):
|
|
204
|
+
|
|
205
|
+
3. Update __init__ method:
|
|
206
|
+
def __init__(self, terminal_renderer, visual_effects):
|
|
207
|
+
super().__init__() # Initialize widget management
|
|
208
|
+
self.terminal_renderer = terminal_renderer
|
|
209
|
+
self.visual_effects = visual_effects
|
|
210
|
+
self.gradient_renderer = GradientRenderer()
|
|
211
|
+
|
|
212
|
+
4. Replace _render_modal_content with:
|
|
213
|
+
def _render_modal_content(self, modal_config: dict, width: int) -> List[str]:
|
|
214
|
+
return self._render_modal_content_with_widgets(modal_config, width)
|
|
215
|
+
|
|
216
|
+
5. Update _handle_modal_input to include widget handling:
|
|
217
|
+
async def _handle_modal_input(self, ui_config: UIConfig) -> Dict[str, Any]:
|
|
218
|
+
# Add widget input handling here
|
|
219
|
+
# Navigation: self._handle_widget_navigation(key_press)
|
|
220
|
+
# Widget input: self._handle_widget_input(key_press)
|
|
221
|
+
# Save values: self._get_widget_values()
|
|
222
|
+
"""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Widget system for modal UI components.
|
|
2
|
+
|
|
3
|
+
This package provides interactive widgets for use in modal dialogs:
|
|
4
|
+
- BaseWidget: Foundation class for all widgets
|
|
5
|
+
- CheckboxWidget: Boolean toggle with ✓ symbol
|
|
6
|
+
- DropdownWidget: Option selection with ▼ indicator
|
|
7
|
+
- TextInputWidget: Text entry with cursor ▌
|
|
8
|
+
- SliderWidget: Numeric slider with █░ visual bar
|
|
9
|
+
|
|
10
|
+
All widgets integrate with the ColorPalette system and configuration management.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .base_widget import BaseWidget
|
|
14
|
+
from .checkbox import CheckboxWidget
|
|
15
|
+
from .dropdown import DropdownWidget
|
|
16
|
+
from .text_input import TextInputWidget
|
|
17
|
+
from .slider import SliderWidget
|
|
18
|
+
from .label import LabelWidget
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BaseWidget",
|
|
22
|
+
"CheckboxWidget",
|
|
23
|
+
"DropdownWidget",
|
|
24
|
+
"TextInputWidget",
|
|
25
|
+
"SliderWidget",
|
|
26
|
+
"LabelWidget"
|
|
27
|
+
]
|