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.
Files changed (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. 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
+ ]