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/input_handler.py
CHANGED
|
@@ -1,41 +1,52 @@
|
|
|
1
|
-
"""Input handling system for Kollabor CLI.
|
|
1
|
+
"""Input handling system for Kollabor CLI.
|
|
2
|
+
|
|
3
|
+
This is a facade that coordinates between refactored input components:
|
|
4
|
+
- InputLoopManager: Main loop, platform I/O, paste detection
|
|
5
|
+
- KeyPressHandler: Key processing, Enter/Escape handling
|
|
6
|
+
- CommandModeHandler: Slash commands, menus
|
|
7
|
+
- ModalController: All modal types
|
|
8
|
+
- HookRegistrar: Event hook registration
|
|
9
|
+
- DisplayController: Display updates, pause/resume
|
|
10
|
+
- PasteProcessor: Paste detection, placeholders
|
|
11
|
+
- StatusModalRenderer: Status modal line generation
|
|
12
|
+
"""
|
|
2
13
|
|
|
3
|
-
import asyncio
|
|
4
14
|
import logging
|
|
5
|
-
import
|
|
6
|
-
import time
|
|
7
|
-
from typing import Dict, Any, List
|
|
15
|
+
from typing import Dict, Any, Optional
|
|
8
16
|
|
|
9
|
-
# Platform-specific imports for input handling
|
|
10
|
-
IS_WINDOWS = sys.platform == "win32"
|
|
11
|
-
|
|
12
|
-
if IS_WINDOWS:
|
|
13
|
-
import msvcrt
|
|
14
|
-
else:
|
|
15
|
-
import select
|
|
16
|
-
|
|
17
|
-
from ..events import EventType
|
|
18
17
|
from ..events.models import CommandMode
|
|
19
18
|
from ..commands.parser import SlashCommandParser
|
|
20
19
|
from ..commands.registry import SlashCommandRegistry
|
|
21
20
|
from ..commands.executor import SlashCommandExecutor
|
|
22
21
|
from ..commands.menu_renderer import CommandMenuRenderer
|
|
23
|
-
from .key_parser import KeyParser
|
|
22
|
+
from .key_parser import KeyParser
|
|
24
23
|
from .buffer_manager import BufferManager
|
|
25
|
-
from .input_errors import InputErrorHandler
|
|
24
|
+
from .input_errors import InputErrorHandler
|
|
25
|
+
|
|
26
|
+
# Refactored components
|
|
27
|
+
from .input.input_loop_manager import InputLoopManager
|
|
28
|
+
from .input.key_press_handler import KeyPressHandler
|
|
29
|
+
from .input.command_mode_handler import CommandModeHandler
|
|
30
|
+
from .input.modal_controller import ModalController
|
|
31
|
+
from .input.hook_registrar import HookRegistrar
|
|
32
|
+
from .input.display_controller import DisplayController
|
|
33
|
+
from .input.paste_processor import PasteProcessor
|
|
34
|
+
from .input.status_modal_renderer import StatusModalRenderer
|
|
26
35
|
|
|
27
36
|
logger = logging.getLogger(__name__)
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
class InputHandler:
|
|
31
|
-
"""
|
|
40
|
+
"""Facade for the input handling system.
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
This class coordinates between modular components that handle:
|
|
34
43
|
- Extended key sequence support (arrow keys, function keys)
|
|
35
44
|
- Robust buffer management with validation
|
|
36
45
|
- Advanced error handling and recovery
|
|
37
46
|
- Command history navigation
|
|
38
47
|
- Cursor positioning and editing
|
|
48
|
+
- Slash command menus
|
|
49
|
+
- Modal interactions
|
|
39
50
|
"""
|
|
40
51
|
|
|
41
52
|
def __init__(self, event_bus, renderer, config) -> None:
|
|
@@ -50,53 +61,25 @@ class InputHandler:
|
|
|
50
61
|
self.renderer = renderer
|
|
51
62
|
self.config = config
|
|
52
63
|
self.running = False
|
|
53
|
-
self.rendering_paused =
|
|
54
|
-
False # Flag to pause rendering during special effects
|
|
55
|
-
)
|
|
64
|
+
self.rendering_paused = False
|
|
56
65
|
|
|
57
66
|
# Load configurable parameters
|
|
58
|
-
|
|
59
|
-
self.error_delay = config.get("input.error_delay", 0.1)
|
|
60
|
-
buffer_limit = config.get(
|
|
61
|
-
"input.input_buffer_limit", 100000
|
|
62
|
-
) # 100KB limit - wide open!
|
|
67
|
+
buffer_limit = config.get("input.input_buffer_limit", 100000)
|
|
63
68
|
history_limit = config.get("input.history_limit", 100)
|
|
64
69
|
|
|
65
|
-
#
|
|
66
|
-
# 1. PRIMARY (ALWAYS ON): Large chunk detection (>10 chars)
|
|
67
|
-
# creates "[Pasted #N ...]" placeholders
|
|
68
|
-
# - Located in _input_loop() around line 181
|
|
69
|
-
# - Triggers automatically when terminal sends big chunks
|
|
70
|
-
# - This is what users see when they paste
|
|
71
|
-
# 2. SECONDARY (DISABLED): Character-by-character timing fallback
|
|
72
|
-
# - Located in _process_character() around line 265
|
|
73
|
-
# - Alternative detection method for edge cases
|
|
74
|
-
# - Currently disabled via this flag
|
|
75
|
-
self.paste_detection_enabled = False # Only disables SECONDARY system
|
|
76
|
-
|
|
77
|
-
# Initialize components
|
|
70
|
+
# Initialize core components
|
|
78
71
|
self.key_parser = KeyParser()
|
|
79
72
|
self.buffer_manager = BufferManager(buffer_limit, history_limit)
|
|
80
73
|
|
|
81
74
|
# Initialize slash command system
|
|
82
|
-
self.
|
|
75
|
+
self._command_mode_local = CommandMode.NORMAL # Local fallback before components created
|
|
83
76
|
self.slash_parser = SlashCommandParser()
|
|
84
77
|
self.command_registry = SlashCommandRegistry()
|
|
85
78
|
self.command_executor = SlashCommandExecutor(self.command_registry)
|
|
86
79
|
self.command_menu_renderer = CommandMenuRenderer(self.renderer)
|
|
87
80
|
self.command_menu_active = False
|
|
88
|
-
self.selected_command_index = 0
|
|
89
81
|
|
|
90
|
-
# Initialize
|
|
91
|
-
self.modal_renderer = None # Will be initialized when needed
|
|
92
|
-
|
|
93
|
-
# Initialize live modal state (for tmux, logs, etc.)
|
|
94
|
-
self.live_modal_renderer = None # LiveModalRenderer instance when active
|
|
95
|
-
self.live_modal_content_generator = None # Content generator function
|
|
96
|
-
self.live_modal_input_callback = None # Input callback for passthrough
|
|
97
|
-
|
|
98
|
-
# Initialize status modal state
|
|
99
|
-
self.current_status_modal_config = None
|
|
82
|
+
# Initialize error handler
|
|
100
83
|
self.error_handler = InputErrorHandler(
|
|
101
84
|
{
|
|
102
85
|
"error_threshold": config.get("input.error_threshold", 10),
|
|
@@ -105,758 +88,185 @@ class InputHandler:
|
|
|
105
88
|
}
|
|
106
89
|
)
|
|
107
90
|
|
|
108
|
-
#
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
111
|
-
|
|
112
|
-
# Simple paste detection state
|
|
113
|
-
self._paste_buffer = []
|
|
114
|
-
self._last_char_time = 0
|
|
115
|
-
# GENIUS PASTE SYSTEM - immediate synchronous storage
|
|
116
|
-
self._paste_bucket = {} # {paste_id: actual_content}
|
|
117
|
-
self._paste_counter = 0 # Counter for paste numbering
|
|
118
|
-
self._current_paste_id = None # Currently building paste ID
|
|
119
|
-
self._last_paste_time = 0 # Last chunk timestamp
|
|
120
|
-
|
|
121
|
-
logger.info("Input handler initialized with enhanced capabilities")
|
|
91
|
+
# Create refactored components
|
|
92
|
+
self._create_components(event_bus, renderer, config)
|
|
93
|
+
self._wire_component_callbacks()
|
|
122
94
|
|
|
123
|
-
|
|
124
|
-
"""Start the input handling loop."""
|
|
125
|
-
self.running = True
|
|
126
|
-
self.renderer.enter_raw_mode()
|
|
95
|
+
logger.info("Input handler initialized with modular components")
|
|
127
96
|
|
|
128
|
-
|
|
97
|
+
def _create_components(self, event_bus, renderer, config) -> None:
|
|
98
|
+
"""Create all refactored components.
|
|
129
99
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
logger.info("About to register COMMAND_MENU_RENDER hook")
|
|
144
|
-
await self._register_command_menu_render_hook()
|
|
100
|
+
Args:
|
|
101
|
+
event_bus: Event bus for emitting events.
|
|
102
|
+
renderer: Terminal renderer.
|
|
103
|
+
config: Configuration manager.
|
|
104
|
+
"""
|
|
105
|
+
# Phase 1: Foundation components
|
|
106
|
+
self._status_modal_renderer = StatusModalRenderer(renderer)
|
|
107
|
+
self._display_controller = DisplayController(
|
|
108
|
+
renderer, self.buffer_manager, self.error_handler
|
|
109
|
+
)
|
|
110
|
+
self._paste_processor = PasteProcessor(
|
|
111
|
+
self.buffer_manager, self._display_controller.update_display
|
|
112
|
+
)
|
|
145
113
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
114
|
+
# Phase 2: Core processing
|
|
115
|
+
self._input_loop_manager = InputLoopManager(
|
|
116
|
+
renderer, self.key_parser, self.error_handler,
|
|
117
|
+
self._paste_processor, config
|
|
118
|
+
)
|
|
149
119
|
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
120
|
+
# Phase 3: Command/Modal components
|
|
121
|
+
self._key_press_handler = KeyPressHandler(
|
|
122
|
+
self.buffer_manager, self.key_parser, event_bus,
|
|
123
|
+
self.error_handler, self._display_controller,
|
|
124
|
+
self._paste_processor, renderer
|
|
125
|
+
)
|
|
153
126
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
127
|
+
self._command_mode_handler = CommandModeHandler(
|
|
128
|
+
self.buffer_manager, renderer, event_bus,
|
|
129
|
+
self.command_registry, self.command_executor,
|
|
130
|
+
self.command_menu_renderer, self.slash_parser,
|
|
131
|
+
self.error_handler
|
|
132
|
+
)
|
|
157
133
|
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
await self._register_status_modal_render_hook()
|
|
134
|
+
# Connect KeyPressHandler to CommandModeHandler for state sync
|
|
135
|
+
self._key_press_handler.command_mode_handler = self._command_mode_handler
|
|
161
136
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
137
|
+
self._modal_controller = ModalController(
|
|
138
|
+
renderer, event_bus, config, self._status_modal_renderer,
|
|
139
|
+
self._display_controller.update_display,
|
|
140
|
+
self._command_mode_handler.exit_command_mode,
|
|
141
|
+
self._sync_command_mode # Callback to sync command_mode changes
|
|
142
|
+
)
|
|
165
143
|
|
|
166
|
-
|
|
144
|
+
# Phase 4: Hook system
|
|
145
|
+
self._hook_registrar = HookRegistrar(
|
|
146
|
+
event_bus,
|
|
147
|
+
self._handle_command_menu_render,
|
|
148
|
+
self._modal_controller._handle_modal_trigger,
|
|
149
|
+
self._modal_controller._handle_status_modal_trigger,
|
|
150
|
+
self._modal_controller._handle_live_modal_trigger,
|
|
151
|
+
self._modal_controller._handle_status_modal_render,
|
|
152
|
+
self._handle_command_output_display,
|
|
153
|
+
self._handle_pause_rendering,
|
|
154
|
+
self._handle_resume_rendering,
|
|
155
|
+
self._modal_controller._handle_modal_hide
|
|
156
|
+
)
|
|
167
157
|
|
|
168
|
-
|
|
169
|
-
|
|
158
|
+
def _wire_component_callbacks(self) -> None:
|
|
159
|
+
"""Wire all component callbacks after construction."""
|
|
160
|
+
# CommandModeHandler callbacks
|
|
161
|
+
self._command_mode_handler.set_update_display_callback(
|
|
162
|
+
self._display_controller.update_display
|
|
163
|
+
)
|
|
164
|
+
self._command_mode_handler.set_exit_modal_callback(
|
|
165
|
+
self._modal_controller._exit_modal_mode
|
|
166
|
+
)
|
|
167
|
+
self._command_mode_handler.set_modal_callbacks(
|
|
168
|
+
self._modal_controller._handle_modal_keypress,
|
|
169
|
+
self._modal_controller._handle_status_modal_keypress,
|
|
170
|
+
self._modal_controller._handle_live_modal_keypress
|
|
171
|
+
)
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
# KeyPressHandler callbacks
|
|
174
|
+
self._key_press_handler.set_callbacks(
|
|
175
|
+
enter_command_mode=self._command_mode_handler.enter_command_mode,
|
|
176
|
+
handle_command_mode_keypress=self._command_mode_handler.handle_command_mode_keypress,
|
|
177
|
+
handle_status_view_previous=self._command_mode_handler.handle_status_view_previous,
|
|
178
|
+
handle_status_view_next=self._command_mode_handler.handle_status_view_next,
|
|
179
|
+
expand_paste_placeholders=self._paste_processor.expand_paste_placeholders
|
|
180
|
+
)
|
|
174
181
|
|
|
175
|
-
#
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# Platform-specific input checking
|
|
186
|
-
has_input = await self._check_input_available()
|
|
187
|
-
|
|
188
|
-
if has_input:
|
|
189
|
-
# Read input data
|
|
190
|
-
chunk = await self._read_input_chunk()
|
|
191
|
-
|
|
192
|
-
if not chunk:
|
|
193
|
-
await asyncio.sleep(self.polling_delay)
|
|
194
|
-
continue
|
|
195
|
-
|
|
196
|
-
# Check if this is an escape sequence (arrow keys, etc.)
|
|
197
|
-
def is_escape_sequence(text: str) -> bool:
|
|
198
|
-
"""Check if input is an escape sequence
|
|
199
|
-
that should bypass paste detection."""
|
|
200
|
-
if not text:
|
|
201
|
-
return False
|
|
202
|
-
# Common escape sequences start with ESC (\x1b)
|
|
203
|
-
if text.startswith("\x1b"):
|
|
204
|
-
return True
|
|
205
|
-
return False
|
|
206
|
-
|
|
207
|
-
# PRIMARY PASTE DETECTION:
|
|
208
|
-
# Large chunk detection (ALWAYS ACTIVE)
|
|
209
|
-
# When user pastes, terminal sends all chars
|
|
210
|
-
# in one/few chunks
|
|
211
|
-
# This creates "[Pasted #N X lines, Y chars]" placeholders
|
|
212
|
-
if len(chunk) > 10 and not is_escape_sequence(chunk):
|
|
213
|
-
|
|
214
|
-
import time
|
|
215
|
-
|
|
216
|
-
current_time = time.time()
|
|
217
|
-
|
|
218
|
-
# Check if this continues the current paste (within 100ms)
|
|
219
|
-
if (
|
|
220
|
-
self._current_paste_id
|
|
221
|
-
and self._last_paste_time > 0
|
|
222
|
-
and (current_time - self._last_paste_time) < 0.1
|
|
223
|
-
):
|
|
224
|
-
|
|
225
|
-
# Merge with existing paste
|
|
226
|
-
self._paste_bucket[self._current_paste_id] += chunk
|
|
227
|
-
self._last_paste_time = current_time
|
|
228
|
-
|
|
229
|
-
# Update the placeholder to show new size
|
|
230
|
-
await self._update_paste_placeholder()
|
|
231
|
-
else:
|
|
232
|
-
# New paste - store immediately
|
|
233
|
-
self._paste_counter += 1
|
|
234
|
-
self._current_paste_id = f"PASTE_{self._paste_counter}"
|
|
235
|
-
self._paste_bucket[self._current_paste_id] = chunk
|
|
236
|
-
self._last_paste_time = current_time
|
|
237
|
-
|
|
238
|
-
# Create placeholder immediately
|
|
239
|
-
await self._create_paste_placeholder(
|
|
240
|
-
self._current_paste_id
|
|
241
|
-
)
|
|
242
|
-
elif is_escape_sequence(chunk):
|
|
243
|
-
# Escape sequence - process character by character
|
|
244
|
-
# to allow key parser to handle it
|
|
245
|
-
logger.debug(
|
|
246
|
-
f"Processing escape sequence "
|
|
247
|
-
f"character-by-character: {repr(chunk)}"
|
|
248
|
-
)
|
|
249
|
-
for char in chunk:
|
|
250
|
-
await self._process_character(char)
|
|
251
|
-
else:
|
|
252
|
-
# Normal input (single or multi-character)
|
|
253
|
-
# process each character individually
|
|
254
|
-
logger.info(
|
|
255
|
-
f"🔤 Processing normal input "
|
|
256
|
-
f"character-by-character: {repr(chunk)}"
|
|
257
|
-
)
|
|
258
|
-
# await self._process_character(chunk)
|
|
259
|
-
for char in chunk:
|
|
260
|
-
await self._process_character(char)
|
|
261
|
-
else:
|
|
262
|
-
# No input available - check for standalone ESC key
|
|
263
|
-
esc_key = self.key_parser.check_for_standalone_escape()
|
|
264
|
-
if esc_key:
|
|
265
|
-
logger.info("DETECTED STANDALONE ESC KEY!")
|
|
266
|
-
# CRITICAL FIX: Route escape to correct handler based on mode
|
|
267
|
-
if self.command_mode == CommandMode.MODAL:
|
|
268
|
-
await self._handle_command_mode_keypress(esc_key)
|
|
269
|
-
elif self.command_mode == CommandMode.LIVE_MODAL:
|
|
270
|
-
await self._handle_live_modal_keypress(esc_key)
|
|
271
|
-
else:
|
|
272
|
-
await self._handle_key_press(esc_key)
|
|
273
|
-
|
|
274
|
-
await asyncio.sleep(self.polling_delay)
|
|
275
|
-
|
|
276
|
-
except KeyboardInterrupt:
|
|
277
|
-
logger.info("Ctrl+C received")
|
|
278
|
-
raise
|
|
279
|
-
except OSError as e:
|
|
280
|
-
await self.error_handler.handle_error(
|
|
281
|
-
ErrorType.IO_ERROR,
|
|
282
|
-
f"I/O error in input loop: {e}",
|
|
283
|
-
ErrorSeverity.HIGH,
|
|
284
|
-
{"buffer_manager": self.buffer_manager},
|
|
285
|
-
)
|
|
286
|
-
await asyncio.sleep(self.error_delay)
|
|
287
|
-
except Exception as e:
|
|
288
|
-
await self.error_handler.handle_error(
|
|
289
|
-
ErrorType.SYSTEM_ERROR,
|
|
290
|
-
f"Unexpected error in input loop: {e}",
|
|
291
|
-
ErrorSeverity.MEDIUM,
|
|
292
|
-
{"buffer_manager": self.buffer_manager},
|
|
293
|
-
)
|
|
294
|
-
await asyncio.sleep(self.error_delay)
|
|
182
|
+
# InputLoopManager callbacks
|
|
183
|
+
self._input_loop_manager.set_callbacks(
|
|
184
|
+
process_character=self._key_press_handler.process_character,
|
|
185
|
+
handle_key_press=self._key_press_handler._handle_key_press,
|
|
186
|
+
handle_command_mode_keypress=self._command_mode_handler.handle_command_mode_keypress,
|
|
187
|
+
handle_live_modal_keypress=self._modal_controller._handle_live_modal_keypress,
|
|
188
|
+
register_hooks=self._hook_registrar.register_all_hooks,
|
|
189
|
+
get_command_mode=lambda: self._command_mode_handler.command_mode
|
|
190
|
+
)
|
|
191
|
+
self._input_loop_manager.set_buffer_manager(self.buffer_manager)
|
|
295
192
|
|
|
296
|
-
|
|
297
|
-
"""Check if input is available (cross-platform).
|
|
193
|
+
# ==================== COMMAND MODE PROPERTY ====================
|
|
298
194
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
"""
|
|
302
|
-
if
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
else:
|
|
306
|
-
# Unix: Use select with timeout
|
|
307
|
-
return bool(select.select([sys.stdin], [], [], self.polling_delay)[0])
|
|
195
|
+
@property
|
|
196
|
+
def command_mode(self) -> CommandMode:
|
|
197
|
+
"""Get current command mode from handler."""
|
|
198
|
+
if hasattr(self, '_command_mode_handler') and self._command_mode_handler:
|
|
199
|
+
return self._command_mode_handler.command_mode
|
|
200
|
+
return self._command_mode_local
|
|
308
201
|
|
|
309
|
-
|
|
310
|
-
|
|
202
|
+
@command_mode.setter
|
|
203
|
+
def command_mode(self, value: CommandMode) -> None:
|
|
204
|
+
"""Set command mode on handler."""
|
|
205
|
+
if hasattr(self, '_command_mode_handler') and self._command_mode_handler:
|
|
206
|
+
self._command_mode_handler.command_mode = value
|
|
207
|
+
self._command_mode_local = value
|
|
311
208
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
"""
|
|
315
|
-
import os
|
|
316
|
-
|
|
317
|
-
if IS_WINDOWS:
|
|
318
|
-
# Windows: Read all available characters using msvcrt
|
|
319
|
-
chunk = b""
|
|
320
|
-
while msvcrt.kbhit():
|
|
321
|
-
char = msvcrt.getch()
|
|
322
|
-
char_code = char[0] if isinstance(char, bytes) else ord(char)
|
|
323
|
-
|
|
324
|
-
# Handle Windows extended keys (arrow keys, function keys, etc.)
|
|
325
|
-
# Extended keys are prefixed with 0x00 or 0xE0 (224)
|
|
326
|
-
if char_code in (0, 224):
|
|
327
|
-
# Read the actual key code
|
|
328
|
-
ext_char = msvcrt.getch()
|
|
329
|
-
ext_code = ext_char[0] if isinstance(ext_char, bytes) else ord(ext_char)
|
|
330
|
-
# Map Windows extended key codes to ANSI escape sequences
|
|
331
|
-
win_key_map = {
|
|
332
|
-
72: b"\x1b[A", # ArrowUp
|
|
333
|
-
80: b"\x1b[B", # ArrowDown
|
|
334
|
-
75: b"\x1b[D", # ArrowLeft
|
|
335
|
-
77: b"\x1b[C", # ArrowRight
|
|
336
|
-
71: b"\x1b[H", # Home
|
|
337
|
-
79: b"\x1b[F", # End
|
|
338
|
-
73: b"\x1b[5~", # PageUp
|
|
339
|
-
81: b"\x1b[6~", # PageDown
|
|
340
|
-
82: b"\x1b[2~", # Insert
|
|
341
|
-
83: b"\x1b[3~", # Delete
|
|
342
|
-
59: b"\x1bOP", # F1
|
|
343
|
-
60: b"\x1bOQ", # F2
|
|
344
|
-
61: b"\x1bOR", # F3
|
|
345
|
-
62: b"\x1bOS", # F4
|
|
346
|
-
63: b"\x1b[15~", # F5
|
|
347
|
-
64: b"\x1b[17~", # F6
|
|
348
|
-
65: b"\x1b[18~", # F7
|
|
349
|
-
66: b"\x1b[19~", # F8
|
|
350
|
-
67: b"\x1b[20~", # F9
|
|
351
|
-
68: b"\x1b[21~", # F10
|
|
352
|
-
133: b"\x1b[23~", # F11
|
|
353
|
-
134: b"\x1b[24~", # F12
|
|
354
|
-
}
|
|
355
|
-
if ext_code in win_key_map:
|
|
356
|
-
chunk += win_key_map[ext_code]
|
|
357
|
-
else:
|
|
358
|
-
logger.debug(f"Unknown Windows extended key: {ext_code}")
|
|
359
|
-
else:
|
|
360
|
-
chunk += char
|
|
361
|
-
|
|
362
|
-
# Small delay to allow for more input
|
|
363
|
-
await asyncio.sleep(0.001)
|
|
364
|
-
# Check if there's more data immediately available
|
|
365
|
-
if not msvcrt.kbhit():
|
|
366
|
-
break
|
|
367
|
-
return chunk.decode("utf-8", errors="ignore") if chunk else ""
|
|
368
|
-
else:
|
|
369
|
-
# Unix: Read ALL available data using os.read
|
|
370
|
-
chunk = b""
|
|
371
|
-
while True:
|
|
372
|
-
try:
|
|
373
|
-
# Read in 8KB chunks
|
|
374
|
-
more_data = os.read(0, 8192)
|
|
375
|
-
if not more_data:
|
|
376
|
-
break
|
|
377
|
-
chunk += more_data
|
|
378
|
-
# Check if more data is immediately available
|
|
379
|
-
if not select.select([sys.stdin], [], [], 0.001)[0]:
|
|
380
|
-
break # No more data waiting
|
|
381
|
-
except OSError:
|
|
382
|
-
break # No more data available
|
|
383
|
-
return chunk.decode("utf-8", errors="ignore") if chunk else ""
|
|
384
|
-
|
|
385
|
-
async def _process_character(self, char: str) -> None:
|
|
386
|
-
"""Process a single character input.
|
|
209
|
+
def _sync_command_mode(self, value: CommandMode) -> None:
|
|
210
|
+
"""Sync command_mode from ModalController to CommandModeHandler.
|
|
387
211
|
|
|
388
|
-
|
|
389
|
-
char: Character received from terminal.
|
|
212
|
+
Called by ModalController when it changes command_mode.
|
|
390
213
|
"""
|
|
391
|
-
|
|
392
|
-
|
|
214
|
+
if hasattr(self, '_command_mode_handler') and self._command_mode_handler:
|
|
215
|
+
self._command_mode_handler.command_mode = value
|
|
216
|
+
self._command_mode_local = value
|
|
393
217
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
):
|
|
401
|
-
await self._enter_command_mode()
|
|
402
|
-
return
|
|
403
|
-
|
|
404
|
-
# SECONDARY PASTE DETECTION:
|
|
405
|
-
# Character-by-character timing (DISABLED)
|
|
406
|
-
# This is a fallback system - primary chunk detection
|
|
407
|
-
# above handles most cases
|
|
408
|
-
if self.paste_detection_enabled:
|
|
409
|
-
# Currently False - secondary system disabled
|
|
410
|
-
paste_handled = await self._simple_paste_detection(
|
|
411
|
-
char, current_time
|
|
412
|
-
)
|
|
413
|
-
if paste_handled:
|
|
414
|
-
# Character consumed by paste detection,
|
|
415
|
-
# skip normal processing
|
|
416
|
-
return
|
|
417
|
-
|
|
418
|
-
# Parse character into structured key press
|
|
419
|
-
# (this handles escape sequences)
|
|
420
|
-
key_press = self.key_parser.parse_char(char)
|
|
421
|
-
if not key_press:
|
|
422
|
-
# For modal mode, add timeout-based
|
|
423
|
-
# standalone escape detection
|
|
424
|
-
if self.command_mode == CommandMode.MODAL:
|
|
425
|
-
# Schedule delayed check for standalone escape
|
|
426
|
-
# (100ms delay)
|
|
427
|
-
async def delayed_escape_check():
|
|
428
|
-
await asyncio.sleep(0.1)
|
|
429
|
-
standalone_escape = (
|
|
430
|
-
self.key_parser.check_for_standalone_escape()
|
|
431
|
-
)
|
|
432
|
-
if standalone_escape:
|
|
433
|
-
await self._handle_command_mode_keypress(
|
|
434
|
-
standalone_escape
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
asyncio.create_task(delayed_escape_check())
|
|
438
|
-
# Incomplete escape sequence - wait for more characters
|
|
439
|
-
return
|
|
440
|
-
|
|
441
|
-
# Check for slash command mode handling AFTER parsing
|
|
442
|
-
# (so arrow keys work)
|
|
443
|
-
if self.command_mode != CommandMode.NORMAL:
|
|
444
|
-
logger.info(
|
|
445
|
-
f"🎯 Processing key '{key_press.name}' "
|
|
446
|
-
f"in command mode: {self.command_mode}"
|
|
447
|
-
)
|
|
448
|
-
handled = await self._handle_command_mode_keypress(key_press)
|
|
449
|
-
if handled:
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
# Emit key press event for plugins
|
|
453
|
-
key_result = await self.event_bus.emit_with_hooks(
|
|
454
|
-
EventType.KEY_PRESS,
|
|
455
|
-
{
|
|
456
|
-
"key": key_press.name,
|
|
457
|
-
"char_code": key_press.code,
|
|
458
|
-
"key_type": key_press.type.value,
|
|
459
|
-
"modifiers": key_press.modifiers,
|
|
460
|
-
},
|
|
461
|
-
"input",
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
# Check if any plugin handled this key
|
|
465
|
-
prevent_default = self._check_prevent_default(key_result)
|
|
466
|
-
|
|
467
|
-
# Process key if not prevented by plugins
|
|
468
|
-
if not prevent_default:
|
|
469
|
-
await self._handle_key_press(key_press)
|
|
470
|
-
|
|
471
|
-
# Update renderer
|
|
472
|
-
await self._update_display()
|
|
218
|
+
@property
|
|
219
|
+
def current_status_modal_config(self):
|
|
220
|
+
"""Get current status modal config from ModalController."""
|
|
221
|
+
if hasattr(self, '_modal_controller') and self._modal_controller:
|
|
222
|
+
return self._modal_controller.current_status_modal_config
|
|
223
|
+
return None
|
|
473
224
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
{"char": repr(char), "buffer_manager": self.buffer_manager},
|
|
480
|
-
)
|
|
225
|
+
@current_status_modal_config.setter
|
|
226
|
+
def current_status_modal_config(self, value):
|
|
227
|
+
"""Set current status modal config on ModalController."""
|
|
228
|
+
if hasattr(self, '_modal_controller') and self._modal_controller:
|
|
229
|
+
self._modal_controller.current_status_modal_config = value
|
|
481
230
|
|
|
482
|
-
|
|
483
|
-
"""Check if plugins want to prevent default key handling.
|
|
231
|
+
# ==================== LIFECYCLE METHODS ====================
|
|
484
232
|
|
|
485
|
-
|
|
486
|
-
|
|
233
|
+
async def start(self) -> None:
|
|
234
|
+
"""Start the input handling loop.
|
|
487
235
|
|
|
488
|
-
|
|
489
|
-
|
|
236
|
+
Delegates to InputLoopManager which handles:
|
|
237
|
+
- Entering raw mode
|
|
238
|
+
- Registering hooks via callback
|
|
239
|
+
- Running the main input loop
|
|
490
240
|
"""
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if isinstance(hook_result, dict) and hook_result.get(
|
|
494
|
-
"prevent_default"
|
|
495
|
-
):
|
|
496
|
-
return True
|
|
497
|
-
return False
|
|
241
|
+
self.running = True
|
|
242
|
+
await self._input_loop_manager.start()
|
|
498
243
|
|
|
499
|
-
async def
|
|
500
|
-
"""
|
|
244
|
+
async def stop(self) -> None:
|
|
245
|
+
"""Stop the input handling loop with cleanup.
|
|
501
246
|
|
|
502
|
-
|
|
503
|
-
|
|
247
|
+
Delegates to InputLoopManager which handles:
|
|
248
|
+
- Stopping the loop
|
|
249
|
+
- Cleanup
|
|
250
|
+
- Exiting raw mode
|
|
504
251
|
"""
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
# Log all key presses for debugging
|
|
508
|
-
logger.info(
|
|
509
|
-
f"🔍 Key press: name='{key_press.name}', "
|
|
510
|
-
f"char='{key_press.char}', code={key_press.code}, "
|
|
511
|
-
f"type={key_press.type}, "
|
|
512
|
-
f"modifiers={getattr(key_press, 'modifiers', None)}"
|
|
513
|
-
)
|
|
514
|
-
|
|
515
|
-
# CRITICAL FIX: Modal input isolation
|
|
516
|
-
# capture ALL input when in modal mode
|
|
517
|
-
if self.command_mode == CommandMode.MODAL:
|
|
518
|
-
logger.info(
|
|
519
|
-
f"🎯 Modal mode active - routing ALL input "
|
|
520
|
-
f"to modal handler: {key_press.name}"
|
|
521
|
-
)
|
|
522
|
-
await self._handle_command_mode_keypress(key_press)
|
|
523
|
-
return
|
|
524
|
-
|
|
525
|
-
# Handle control keys
|
|
526
|
-
if self.key_parser.is_control_key(key_press, "Ctrl+C"):
|
|
527
|
-
logger.info("Ctrl+C received")
|
|
528
|
-
raise KeyboardInterrupt
|
|
529
|
-
|
|
530
|
-
elif self.key_parser.is_control_key(key_press, "Enter"):
|
|
531
|
-
await self._handle_enter()
|
|
532
|
-
|
|
533
|
-
elif self.key_parser.is_control_key(key_press, "Backspace"):
|
|
534
|
-
self.buffer_manager.delete_char()
|
|
535
|
-
|
|
536
|
-
elif key_press.name == "Escape":
|
|
537
|
-
await self._handle_escape()
|
|
538
|
-
|
|
539
|
-
elif key_press.name == "Delete":
|
|
540
|
-
self.buffer_manager.delete_forward()
|
|
541
|
-
|
|
542
|
-
# Handle arrow keys for cursor movement and history
|
|
543
|
-
elif key_press.name == "ArrowLeft":
|
|
544
|
-
moved = self.buffer_manager.move_cursor("left")
|
|
545
|
-
if moved:
|
|
546
|
-
logger.debug(
|
|
547
|
-
f"Arrow Left: cursor moved to position {self.buffer_manager.cursor_position}"
|
|
548
|
-
)
|
|
549
|
-
await self._update_display(force_render=True)
|
|
550
|
-
|
|
551
|
-
elif key_press.name == "ArrowRight":
|
|
552
|
-
moved = self.buffer_manager.move_cursor("right")
|
|
553
|
-
if moved:
|
|
554
|
-
logger.debug(
|
|
555
|
-
f"Arrow Right: cursor moved to position {self.buffer_manager.cursor_position}"
|
|
556
|
-
)
|
|
557
|
-
await self._update_display(force_render=True)
|
|
558
|
-
|
|
559
|
-
elif key_press.name == "ArrowUp":
|
|
560
|
-
self.buffer_manager.navigate_history("up")
|
|
561
|
-
await self._update_display(force_render=True)
|
|
562
|
-
|
|
563
|
-
elif key_press.name == "ArrowDown":
|
|
564
|
-
self.buffer_manager.navigate_history("down")
|
|
565
|
-
await self._update_display(force_render=True)
|
|
566
|
-
|
|
567
|
-
# Handle Home/End keys
|
|
568
|
-
elif key_press.name == "Home":
|
|
569
|
-
self.buffer_manager.move_to_start()
|
|
570
|
-
await self._update_display(force_render=True)
|
|
571
|
-
|
|
572
|
-
elif key_press.name == "End":
|
|
573
|
-
self.buffer_manager.move_to_end()
|
|
574
|
-
await self._update_display(force_render=True)
|
|
575
|
-
|
|
576
|
-
# Handle Option/Alt+comma/period keys for status view navigation
|
|
577
|
-
# macOS sends ≤/≥, Windows/Linux send Alt modifier or ESC+key
|
|
578
|
-
elif (key_press.char == "≤" or
|
|
579
|
-
key_press.name == "Alt+," or
|
|
580
|
-
(key_press.char == "," and key_press.modifiers.get("alt"))):
|
|
581
|
-
logger.info(
|
|
582
|
-
"🔑 Alt+Comma detected - switching to previous status view"
|
|
583
|
-
)
|
|
584
|
-
await self._handle_status_view_previous()
|
|
585
|
-
|
|
586
|
-
elif (key_press.char == "≥" or
|
|
587
|
-
key_press.name == "Alt+." or
|
|
588
|
-
(key_press.char == "." and key_press.modifiers.get("alt"))):
|
|
589
|
-
logger.info(
|
|
590
|
-
"🔑 Alt+Period detected - switching to next status view"
|
|
591
|
-
)
|
|
592
|
-
await self._handle_status_view_next()
|
|
593
|
-
|
|
594
|
-
# Handle Cmd key combinations (mapped to Ctrl sequences on macOS)
|
|
595
|
-
elif self.key_parser.is_control_key(key_press, "Ctrl+A"):
|
|
596
|
-
logger.info("🔑 Ctrl+A (Cmd+Left) - moving cursor to start")
|
|
597
|
-
self.buffer_manager.move_to_start()
|
|
598
|
-
await self._update_display(force_render=True)
|
|
599
|
-
|
|
600
|
-
elif self.key_parser.is_control_key(key_press, "Ctrl+E"):
|
|
601
|
-
logger.info("🔑 Ctrl+E (Cmd+Right) - moving cursor to end")
|
|
602
|
-
self.buffer_manager.move_to_end()
|
|
603
|
-
await self._update_display(force_render=True)
|
|
604
|
-
|
|
605
|
-
elif self.key_parser.is_control_key(key_press, "Ctrl+U"):
|
|
606
|
-
logger.info("🔑 Ctrl+U (Cmd+Backspace) - clearing line")
|
|
607
|
-
self.buffer_manager.clear()
|
|
608
|
-
await self._update_display(force_render=True)
|
|
609
|
-
|
|
610
|
-
# Handle printable characters
|
|
611
|
-
elif self.key_parser.is_printable_char(key_press):
|
|
612
|
-
# Normal character processing
|
|
613
|
-
success = self.buffer_manager.insert_char(key_press.char)
|
|
614
|
-
if not success:
|
|
615
|
-
await self.error_handler.handle_error(
|
|
616
|
-
ErrorType.BUFFER_ERROR,
|
|
617
|
-
"Failed to insert character - buffer limit reached",
|
|
618
|
-
ErrorSeverity.LOW,
|
|
619
|
-
{
|
|
620
|
-
"char": key_press.char,
|
|
621
|
-
"buffer_manager": self.buffer_manager,
|
|
622
|
-
},
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
# Handle other special keys (F1-F12, etc.)
|
|
626
|
-
elif key_press.type == KeyTypeEnum.EXTENDED:
|
|
627
|
-
logger.debug(f"Extended key pressed: {key_press.name}")
|
|
628
|
-
# Could emit special events for function keys, etc.
|
|
629
|
-
|
|
630
|
-
except Exception as e:
|
|
631
|
-
await self.error_handler.handle_error(
|
|
632
|
-
ErrorType.EVENT_ERROR,
|
|
633
|
-
f"Error handling key press: {e}",
|
|
634
|
-
ErrorSeverity.MEDIUM,
|
|
635
|
-
{
|
|
636
|
-
"key_press": key_press,
|
|
637
|
-
"buffer_manager": self.buffer_manager,
|
|
638
|
-
},
|
|
639
|
-
)
|
|
640
|
-
|
|
641
|
-
async def _update_display(self, force_render: bool = False) -> None:
|
|
642
|
-
"""Update the terminal display with current buffer state."""
|
|
643
|
-
try:
|
|
644
|
-
# Skip rendering if paused (during special effects like Matrix)
|
|
645
|
-
if self.rendering_paused and not force_render:
|
|
646
|
-
return
|
|
647
|
-
|
|
648
|
-
buffer_content, cursor_pos = self.buffer_manager.get_display_info()
|
|
649
|
-
|
|
650
|
-
# Update renderer with buffer content and cursor position
|
|
651
|
-
self.renderer.input_buffer = buffer_content
|
|
652
|
-
self.renderer.cursor_position = cursor_pos
|
|
653
|
-
|
|
654
|
-
# Force immediate rendering if requested (needed for paste operations)
|
|
655
|
-
if force_render:
|
|
656
|
-
try:
|
|
657
|
-
if hasattr(
|
|
658
|
-
self.renderer, "render_active_area"
|
|
659
|
-
) and asyncio.iscoroutinefunction(
|
|
660
|
-
self.renderer.render_active_area
|
|
661
|
-
):
|
|
662
|
-
await self.renderer.render_active_area()
|
|
663
|
-
elif hasattr(
|
|
664
|
-
self.renderer, "render_input"
|
|
665
|
-
) and asyncio.iscoroutinefunction(self.renderer.render_input):
|
|
666
|
-
await self.renderer.render_input()
|
|
667
|
-
elif hasattr(self.renderer, "render_active_area"):
|
|
668
|
-
self.renderer.render_active_area()
|
|
669
|
-
elif hasattr(self.renderer, "render_input"):
|
|
670
|
-
self.renderer.render_input()
|
|
671
|
-
except Exception as e:
|
|
672
|
-
logger.debug(f"Force render failed: {e}")
|
|
673
|
-
# Continue without forced render
|
|
674
|
-
|
|
675
|
-
# Only update cursor if position changed
|
|
676
|
-
if cursor_pos != self._last_cursor_pos:
|
|
677
|
-
# Could implement cursor positioning in renderer
|
|
678
|
-
self._last_cursor_pos = cursor_pos
|
|
252
|
+
self.running = False
|
|
253
|
+
await self._input_loop_manager.stop()
|
|
679
254
|
|
|
680
|
-
|
|
681
|
-
await self.error_handler.handle_error(
|
|
682
|
-
ErrorType.SYSTEM_ERROR,
|
|
683
|
-
f"Error updating display: {e}",
|
|
684
|
-
ErrorSeverity.LOW,
|
|
685
|
-
{"buffer_manager": self.buffer_manager},
|
|
686
|
-
)
|
|
255
|
+
# ==================== RENDERING CONTROL ====================
|
|
687
256
|
|
|
688
257
|
def pause_rendering(self):
|
|
689
258
|
"""Pause all UI rendering for special effects."""
|
|
690
259
|
self.rendering_paused = True
|
|
260
|
+
self._display_controller.pause_rendering()
|
|
691
261
|
logger.debug("Input rendering paused")
|
|
692
262
|
|
|
693
263
|
def resume_rendering(self):
|
|
694
264
|
"""Resume normal UI rendering."""
|
|
695
265
|
self.rendering_paused = False
|
|
266
|
+
self._display_controller.resume_rendering()
|
|
696
267
|
logger.debug("Input rendering resumed")
|
|
697
268
|
|
|
698
|
-
|
|
699
|
-
"""Handle Enter key press with enhanced validation."""
|
|
700
|
-
try:
|
|
701
|
-
if self.buffer_manager.is_empty:
|
|
702
|
-
return
|
|
703
|
-
|
|
704
|
-
# Validate input before processing
|
|
705
|
-
validation_errors = self.buffer_manager.validate_content()
|
|
706
|
-
if validation_errors:
|
|
707
|
-
for error in validation_errors:
|
|
708
|
-
logger.warning(f"Input validation warning: {error}")
|
|
709
|
-
|
|
710
|
-
# Get message and clear buffer
|
|
711
|
-
message = self.buffer_manager.get_content_and_clear()
|
|
712
|
-
|
|
713
|
-
# Check if this is a slash command - handle immediately without paste expansion
|
|
714
|
-
if message.strip().startswith('/'):
|
|
715
|
-
logger.info(f"🎯 Detected slash command, bypassing paste expansion: '{message}'")
|
|
716
|
-
expanded_message = message
|
|
717
|
-
else:
|
|
718
|
-
# GENIUS PASTE BUCKET: Immediate expansion - no waiting needed!
|
|
719
|
-
logger.debug(f"GENIUS SUBMIT: Original message: '{message}'")
|
|
720
|
-
logger.debug(
|
|
721
|
-
f"GENIUS SUBMIT: Paste bucket contains: {list(self._paste_bucket.keys())}"
|
|
722
|
-
)
|
|
723
|
-
|
|
724
|
-
expanded_message = self._expand_paste_placeholders(message)
|
|
725
|
-
logger.debug(
|
|
726
|
-
f"GENIUS SUBMIT: Final expanded: '{expanded_message[:100]}...' ({len(expanded_message)} chars)"
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
# Add to history (with expanded content)
|
|
730
|
-
self.buffer_manager.add_to_history(expanded_message)
|
|
731
|
-
|
|
732
|
-
# Update renderer
|
|
733
|
-
self.renderer.input_buffer = ""
|
|
734
|
-
self.renderer.clear_active_area()
|
|
735
|
-
|
|
736
|
-
# Emit user input event (with expanded content!)
|
|
737
|
-
await self.event_bus.emit_with_hooks(
|
|
738
|
-
EventType.USER_INPUT,
|
|
739
|
-
{
|
|
740
|
-
"message": expanded_message,
|
|
741
|
-
"validation_errors": validation_errors,
|
|
742
|
-
},
|
|
743
|
-
"user",
|
|
744
|
-
)
|
|
745
|
-
|
|
746
|
-
logger.debug(
|
|
747
|
-
f"Processed user input: {message[:100]}..."
|
|
748
|
-
if len(message) > 100
|
|
749
|
-
else f"Processed user input: {message}"
|
|
750
|
-
)
|
|
751
|
-
|
|
752
|
-
except Exception as e:
|
|
753
|
-
await self.error_handler.handle_error(
|
|
754
|
-
ErrorType.EVENT_ERROR,
|
|
755
|
-
f"Error handling Enter key: {e}",
|
|
756
|
-
ErrorSeverity.HIGH,
|
|
757
|
-
{"buffer_manager": self.buffer_manager},
|
|
758
|
-
)
|
|
759
|
-
|
|
760
|
-
async def _handle_escape(self) -> None:
|
|
761
|
-
"""Handle Escape key press for request cancellation."""
|
|
762
|
-
try:
|
|
763
|
-
logger.info("_handle_escape called - emitting CANCEL_REQUEST event")
|
|
764
|
-
|
|
765
|
-
# Emit cancellation event
|
|
766
|
-
result = await self.event_bus.emit_with_hooks(
|
|
767
|
-
EventType.CANCEL_REQUEST,
|
|
768
|
-
{"reason": "user_escape", "source": "input_handler"},
|
|
769
|
-
"input",
|
|
770
|
-
)
|
|
771
|
-
|
|
772
|
-
logger.info(
|
|
773
|
-
f"ESC key pressed - cancellation request sent, result: {result}"
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
except Exception as e:
|
|
777
|
-
await self.error_handler.handle_error(
|
|
778
|
-
ErrorType.EVENT_ERROR,
|
|
779
|
-
f"Error handling Escape key: {e}",
|
|
780
|
-
ErrorSeverity.MEDIUM,
|
|
781
|
-
{"buffer_manager": self.buffer_manager},
|
|
782
|
-
)
|
|
783
|
-
|
|
784
|
-
async def _handle_status_view_previous(self) -> None:
|
|
785
|
-
"""Handle comma key press for previous status view."""
|
|
786
|
-
try:
|
|
787
|
-
logger.info("Attempting to switch to previous status view")
|
|
788
|
-
# Check if renderer has a status registry
|
|
789
|
-
if (
|
|
790
|
-
hasattr(self.renderer, "status_renderer")
|
|
791
|
-
and self.renderer.status_renderer
|
|
792
|
-
):
|
|
793
|
-
status_renderer = self.renderer.status_renderer
|
|
794
|
-
logger.info(
|
|
795
|
-
f"[OK] Found status_renderer: {type(status_renderer).__name__}"
|
|
796
|
-
)
|
|
797
|
-
if (
|
|
798
|
-
hasattr(status_renderer, "status_registry")
|
|
799
|
-
and status_renderer.status_registry
|
|
800
|
-
):
|
|
801
|
-
registry = status_renderer.status_registry
|
|
802
|
-
logger.info(
|
|
803
|
-
f"[OK] Found status_registry with {len(registry.views)} views"
|
|
804
|
-
)
|
|
805
|
-
if hasattr(registry, "cycle_previous"):
|
|
806
|
-
previous_view = registry.cycle_previous()
|
|
807
|
-
if previous_view:
|
|
808
|
-
logger.info(
|
|
809
|
-
f"[OK] Switched to previous status view: '{previous_view.name}'"
|
|
810
|
-
)
|
|
811
|
-
else:
|
|
812
|
-
logger.info("No status views available to cycle to")
|
|
813
|
-
else:
|
|
814
|
-
logger.info("cycle_previous method not found in registry")
|
|
815
|
-
else:
|
|
816
|
-
logger.info("No status registry available for view cycling")
|
|
817
|
-
else:
|
|
818
|
-
logger.info("No status renderer available for view cycling")
|
|
819
|
-
|
|
820
|
-
except Exception as e:
|
|
821
|
-
await self.error_handler.handle_error(
|
|
822
|
-
ErrorType.EVENT_ERROR,
|
|
823
|
-
f"Error handling status view previous: {e}",
|
|
824
|
-
ErrorSeverity.LOW,
|
|
825
|
-
{"key": "Ctrl+ArrowLeft"},
|
|
826
|
-
)
|
|
827
|
-
|
|
828
|
-
async def _handle_status_view_next(self) -> None:
|
|
829
|
-
"""Handle Ctrl+Right arrow key press for next status view."""
|
|
830
|
-
try:
|
|
831
|
-
# Check if renderer has a status registry
|
|
832
|
-
if (
|
|
833
|
-
hasattr(self.renderer, "status_renderer")
|
|
834
|
-
and self.renderer.status_renderer
|
|
835
|
-
):
|
|
836
|
-
status_renderer = self.renderer.status_renderer
|
|
837
|
-
if (
|
|
838
|
-
hasattr(status_renderer, "status_registry")
|
|
839
|
-
and status_renderer.status_registry
|
|
840
|
-
):
|
|
841
|
-
next_view = status_renderer.status_registry.cycle_next()
|
|
842
|
-
if next_view:
|
|
843
|
-
logger.debug(
|
|
844
|
-
f"Switched to next status view: '{next_view.name}'"
|
|
845
|
-
)
|
|
846
|
-
else:
|
|
847
|
-
logger.debug("No status views available to cycle to")
|
|
848
|
-
else:
|
|
849
|
-
logger.debug("No status registry available for view cycling")
|
|
850
|
-
else:
|
|
851
|
-
logger.debug("No status renderer available for view cycling")
|
|
852
|
-
|
|
853
|
-
except Exception as e:
|
|
854
|
-
await self.error_handler.handle_error(
|
|
855
|
-
ErrorType.EVENT_ERROR,
|
|
856
|
-
f"Error handling status view next: {e}",
|
|
857
|
-
ErrorSeverity.LOW,
|
|
858
|
-
{"key": "Ctrl+ArrowRight"},
|
|
859
|
-
)
|
|
269
|
+
# ==================== STATUS ====================
|
|
860
270
|
|
|
861
271
|
def get_status(self) -> Dict[str, Any]:
|
|
862
272
|
"""Get current input handler status for debugging.
|
|
@@ -877,226 +287,16 @@ class InputHandler:
|
|
|
877
287
|
},
|
|
878
288
|
}
|
|
879
289
|
|
|
880
|
-
|
|
881
|
-
"""Perform cleanup operations."""
|
|
882
|
-
try:
|
|
883
|
-
# Clear old errors
|
|
884
|
-
cleared_errors = self.error_handler.clear_old_errors()
|
|
885
|
-
if cleared_errors > 0:
|
|
886
|
-
logger.info(f"Cleaned up {cleared_errors} old errors")
|
|
887
|
-
|
|
888
|
-
# Reset parser state
|
|
889
|
-
self.key_parser._reset_escape_state()
|
|
890
|
-
|
|
891
|
-
logger.debug("Input handler cleanup completed")
|
|
892
|
-
|
|
893
|
-
except Exception as e:
|
|
894
|
-
logger.error(f"Error during cleanup: {e}")
|
|
895
|
-
|
|
896
|
-
def _expand_paste_placeholders(self, message: str) -> str:
|
|
897
|
-
"""Expand paste placeholders with actual content from paste bucket.
|
|
898
|
-
|
|
899
|
-
Your brilliant idea: Replace [⚡ Pasted #N ...] with actual pasted content!
|
|
900
|
-
"""
|
|
901
|
-
logger.debug(f"PASTE DEBUG: Expanding message: '{message}'")
|
|
902
|
-
logger.debug(
|
|
903
|
-
f"PASTE DEBUG: Paste bucket contains: {list(self._paste_bucket.keys())}"
|
|
904
|
-
)
|
|
905
|
-
|
|
906
|
-
expanded = message
|
|
907
|
-
|
|
908
|
-
# Find and replace each paste placeholder
|
|
909
|
-
import re
|
|
910
|
-
|
|
911
|
-
for paste_id, content in self._paste_bucket.items():
|
|
912
|
-
# Extract paste number from paste_id (PASTE_1 -> 1)
|
|
913
|
-
paste_num = paste_id.split("_")[1]
|
|
914
|
-
|
|
915
|
-
# Pattern to match: [Pasted #N X lines, Y chars]
|
|
916
|
-
pattern = rf"\[Pasted #{paste_num} \d+ lines?, \d+ chars\]"
|
|
917
|
-
|
|
918
|
-
logger.debug(f"PASTE DEBUG: Looking for pattern: {pattern}")
|
|
919
|
-
logger.debug(
|
|
920
|
-
f"PASTE DEBUG: Will replace with content: '{content[:50]}...'"
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
# Replace with actual content
|
|
924
|
-
matches = re.findall(pattern, expanded)
|
|
925
|
-
logger.debug(f"PASTE DEBUG: Found {len(matches)} matches")
|
|
926
|
-
|
|
927
|
-
expanded = re.sub(pattern, content, expanded)
|
|
928
|
-
|
|
929
|
-
logger.debug(f"PASTE DEBUG: Final expanded message: '{expanded[:100]}...'")
|
|
930
|
-
logger.info(
|
|
931
|
-
f"Paste expansion: {len(self._paste_bucket)} placeholders expanded"
|
|
932
|
-
)
|
|
933
|
-
|
|
934
|
-
# Clear paste bucket after expansion (one-time use)
|
|
935
|
-
self._paste_bucket.clear()
|
|
936
|
-
|
|
937
|
-
return expanded
|
|
938
|
-
|
|
939
|
-
async def _create_paste_placeholder(self, paste_id: str) -> None:
|
|
940
|
-
"""Create placeholder for paste - GENIUS IMMEDIATE VERSION."""
|
|
941
|
-
content = self._paste_bucket[paste_id]
|
|
942
|
-
|
|
943
|
-
# Create elegant placeholder for user to see
|
|
944
|
-
line_count = content.count("\n") + 1 if "\n" in content else 1
|
|
945
|
-
char_count = len(content)
|
|
946
|
-
paste_num = paste_id.split("_")[1] # Extract number from PASTE_1
|
|
947
|
-
placeholder = f"[Pasted #{paste_num} {line_count} lines, {char_count} chars]"
|
|
948
|
-
|
|
949
|
-
# Insert placeholder into buffer (what user sees)
|
|
950
|
-
for char in placeholder:
|
|
951
|
-
self.buffer_manager.insert_char(char)
|
|
952
|
-
|
|
953
|
-
logger.info(
|
|
954
|
-
f"GENIUS: Created placeholder for {char_count} chars as {paste_id}"
|
|
955
|
-
)
|
|
956
|
-
|
|
957
|
-
# Update display once at the end
|
|
958
|
-
await self._update_display(force_render=True)
|
|
959
|
-
|
|
960
|
-
async def _update_paste_placeholder(self) -> None:
|
|
961
|
-
"""Update existing placeholder when paste grows - GENIUS VERSION."""
|
|
962
|
-
# For now, just log - updating existing placeholder is complex
|
|
963
|
-
# The merge approach usually works fast enough that this isn't needed
|
|
964
|
-
content = self._paste_bucket[self._current_paste_id]
|
|
965
|
-
logger.info(
|
|
966
|
-
f"GENIUS: Updated {self._current_paste_id} to {len(content)} chars"
|
|
967
|
-
)
|
|
968
|
-
|
|
969
|
-
async def _simple_paste_detection(self, char: str, current_time: float) -> bool:
|
|
970
|
-
"""Simple, reliable paste detection using timing only.
|
|
971
|
-
|
|
972
|
-
Returns:
|
|
973
|
-
True if character was consumed by paste detection, False otherwise.
|
|
974
|
-
"""
|
|
975
|
-
# Check cooldown to prevent overlapping paste detections
|
|
976
|
-
if self._paste_cooldown > 0 and (current_time - self._paste_cooldown) < 1.0:
|
|
977
|
-
# Still in cooldown period, skip paste detection
|
|
978
|
-
self._last_char_time = current_time
|
|
979
|
-
return False
|
|
980
|
-
|
|
981
|
-
# Check if we have a pending paste buffer that timed out
|
|
982
|
-
if self._paste_buffer and self._last_char_time > 0:
|
|
983
|
-
gap_ms = (current_time - self._last_char_time) * 1000
|
|
984
|
-
|
|
985
|
-
if gap_ms > self._paste_timeout_ms:
|
|
986
|
-
# Buffer timed out, process it
|
|
987
|
-
if len(self._paste_buffer) >= self.paste_min_chars:
|
|
988
|
-
self._process_simple_paste_sync()
|
|
989
|
-
self._paste_cooldown = current_time # Set cooldown
|
|
990
|
-
else:
|
|
991
|
-
# Too few chars, process them as individual keystrokes
|
|
992
|
-
self._flush_paste_buffer_as_keystrokes_sync()
|
|
993
|
-
self._paste_buffer = []
|
|
994
|
-
|
|
995
|
-
# Now handle the current character
|
|
996
|
-
if self._last_char_time > 0:
|
|
997
|
-
gap_ms = (current_time - self._last_char_time) * 1000
|
|
998
|
-
|
|
999
|
-
# If character arrived quickly, start/continue paste buffer
|
|
1000
|
-
if gap_ms < self.paste_threshold_ms:
|
|
1001
|
-
self._paste_buffer.append(char)
|
|
1002
|
-
self._last_char_time = current_time
|
|
1003
|
-
return True # Character consumed by paste buffer
|
|
1004
|
-
|
|
1005
|
-
# Character not part of paste, process normally
|
|
1006
|
-
self._last_char_time = current_time
|
|
1007
|
-
return False
|
|
1008
|
-
|
|
1009
|
-
def _flush_paste_buffer_as_keystrokes_sync(self) -> None:
|
|
1010
|
-
"""Process paste buffer contents as individual keystrokes (sync version)."""
|
|
1011
|
-
logger.debug(
|
|
1012
|
-
f"Flushing {len(self._paste_buffer)} chars as individual keystrokes"
|
|
1013
|
-
)
|
|
1014
|
-
|
|
1015
|
-
# Just add characters to buffer without async processing
|
|
1016
|
-
for char in self._paste_buffer:
|
|
1017
|
-
if char.isprintable() or char in [" ", "\t"]:
|
|
1018
|
-
self.buffer_manager.insert_char(char)
|
|
1019
|
-
|
|
1020
|
-
def _process_simple_paste_sync(self) -> None:
|
|
1021
|
-
"""Process detected paste content (sync version with inline indicator)."""
|
|
1022
|
-
if not self._paste_buffer:
|
|
1023
|
-
return
|
|
1024
|
-
|
|
1025
|
-
# Get the content and clean any terminal markers
|
|
1026
|
-
content = "".join(self._paste_buffer)
|
|
1027
|
-
|
|
1028
|
-
# Clean bracketed paste markers if present
|
|
1029
|
-
if content.startswith("[200~"):
|
|
1030
|
-
content = content[5:]
|
|
1031
|
-
if content.endswith("01~"):
|
|
1032
|
-
content = content[:-3]
|
|
1033
|
-
elif content.endswith("[201~"):
|
|
1034
|
-
content = content[:-6]
|
|
1035
|
-
|
|
1036
|
-
# Count lines
|
|
1037
|
-
line_count = content.count("\n") + 1
|
|
1038
|
-
char_count = len(content)
|
|
1039
|
-
|
|
1040
|
-
# Increment paste counter
|
|
1041
|
-
self._paste_counter += 1
|
|
1042
|
-
|
|
1043
|
-
# Create inline paste indicator exactly as user requested
|
|
1044
|
-
indicator = f"[⚡ Pasted #{self._paste_counter} {line_count} lines]"
|
|
1045
|
-
|
|
1046
|
-
# Insert the indicator into the buffer at current position
|
|
1047
|
-
try:
|
|
1048
|
-
for char in indicator:
|
|
1049
|
-
self.buffer_manager.insert_char(char)
|
|
1050
|
-
logger.info(
|
|
1051
|
-
f"Paste #{self._paste_counter}: {char_count} chars, {line_count} lines"
|
|
1052
|
-
)
|
|
1053
|
-
except Exception as e:
|
|
1054
|
-
logger.error(f"Paste processing error: {e}")
|
|
1055
|
-
|
|
1056
|
-
# Clear paste buffer
|
|
1057
|
-
self._paste_buffer = []
|
|
1058
|
-
|
|
1059
|
-
async def _flush_paste_buffer_as_keystrokes(self) -> None:
|
|
1060
|
-
"""Process paste buffer contents as individual keystrokes."""
|
|
1061
|
-
self._flush_paste_buffer_as_keystrokes_sync()
|
|
1062
|
-
|
|
1063
|
-
async def _process_simple_paste(self) -> None:
|
|
1064
|
-
"""Process detected paste content."""
|
|
1065
|
-
self._process_simple_paste_sync()
|
|
1066
|
-
await self._update_display(force_render=True)
|
|
1067
|
-
|
|
1068
|
-
# ==================== COMMAND MENU RENDER HOOK ====================
|
|
1069
|
-
|
|
1070
|
-
async def _register_command_menu_render_hook(self) -> None:
|
|
1071
|
-
"""Register hook to provide command menu content for COMMAND_MENU_RENDER events."""
|
|
1072
|
-
try:
|
|
1073
|
-
if self.event_bus:
|
|
1074
|
-
from ..events.models import Hook, HookPriority
|
|
1075
|
-
|
|
1076
|
-
hook = Hook(
|
|
1077
|
-
name="command_menu_render",
|
|
1078
|
-
plugin_name="input_handler",
|
|
1079
|
-
event_type=EventType.COMMAND_MENU_RENDER,
|
|
1080
|
-
priority=HookPriority.DISPLAY.value,
|
|
1081
|
-
callback=self._handle_command_menu_render,
|
|
1082
|
-
)
|
|
1083
|
-
success = await self.event_bus.register_hook(hook)
|
|
1084
|
-
if success:
|
|
1085
|
-
logger.info(
|
|
1086
|
-
"Successfully registered COMMAND_MENU_RENDER hook for command menu display"
|
|
1087
|
-
)
|
|
1088
|
-
else:
|
|
1089
|
-
logger.error("Failed to register COMMAND_MENU_RENDER hook")
|
|
1090
|
-
except Exception as e:
|
|
1091
|
-
logger.error(f"Failed to register COMMAND_MENU_RENDER hook: {e}")
|
|
290
|
+
# ==================== HOOK HANDLERS (kept in facade) ====================
|
|
1092
291
|
|
|
1093
292
|
async def _handle_command_menu_render(
|
|
1094
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
293
|
+
self, event_data: Dict[str, Any], context: Optional[str] = None
|
|
1095
294
|
) -> Dict[str, Any]:
|
|
1096
295
|
"""Handle COMMAND_MENU_RENDER events to provide command menu content.
|
|
1097
296
|
|
|
1098
297
|
Args:
|
|
1099
298
|
event_data: Event data containing render request info.
|
|
299
|
+
context: Hook execution context.
|
|
1100
300
|
|
|
1101
301
|
Returns:
|
|
1102
302
|
Dictionary with menu_lines if command mode is active.
|
|
@@ -1104,277 +304,41 @@ class InputHandler:
|
|
|
1104
304
|
try:
|
|
1105
305
|
# Only provide command menu if we're in menu popup mode
|
|
1106
306
|
if (
|
|
1107
|
-
self.command_mode == CommandMode.MENU_POPUP
|
|
1108
|
-
and self.command_menu_active
|
|
307
|
+
self._command_mode_handler.command_mode == CommandMode.MENU_POPUP
|
|
308
|
+
and self._command_mode_handler.command_menu_active
|
|
1109
309
|
and hasattr(self.command_menu_renderer, "current_menu_lines")
|
|
1110
310
|
and self.command_menu_renderer.current_menu_lines
|
|
1111
311
|
):
|
|
1112
|
-
|
|
1113
312
|
return {"menu_lines": self.command_menu_renderer.current_menu_lines}
|
|
1114
313
|
|
|
1115
|
-
# No command menu to display
|
|
1116
314
|
return {}
|
|
1117
315
|
|
|
1118
316
|
except Exception as e:
|
|
1119
317
|
logger.error(f"Error in COMMAND_MENU_RENDER handler: {e}")
|
|
1120
318
|
return {}
|
|
1121
319
|
|
|
1122
|
-
async def _register_modal_trigger_hook(self) -> None:
|
|
1123
|
-
"""Register hook to handle modal trigger events."""
|
|
1124
|
-
try:
|
|
1125
|
-
if self.event_bus:
|
|
1126
|
-
from ..events.models import Hook, HookPriority, EventType
|
|
1127
|
-
|
|
1128
|
-
hook = Hook(
|
|
1129
|
-
name="modal_trigger",
|
|
1130
|
-
plugin_name="input_handler",
|
|
1131
|
-
event_type=EventType.MODAL_TRIGGER,
|
|
1132
|
-
priority=HookPriority.DISPLAY.value,
|
|
1133
|
-
callback=self._handle_modal_trigger,
|
|
1134
|
-
)
|
|
1135
|
-
success = await self.event_bus.register_hook(hook)
|
|
1136
|
-
if success:
|
|
1137
|
-
logger.info("Successfully registered MODAL_TRIGGER hook")
|
|
1138
|
-
else:
|
|
1139
|
-
logger.error("Failed to register MODAL_TRIGGER hook")
|
|
1140
|
-
except Exception as e:
|
|
1141
|
-
logger.error(f"Failed to register MODAL_TRIGGER hook: {e}")
|
|
1142
|
-
|
|
1143
|
-
async def _register_status_modal_trigger_hook(self) -> None:
|
|
1144
|
-
"""Register hook to handle status modal trigger events."""
|
|
1145
|
-
try:
|
|
1146
|
-
if self.event_bus:
|
|
1147
|
-
from ..events.models import Hook, HookPriority, EventType
|
|
1148
|
-
|
|
1149
|
-
hook = Hook(
|
|
1150
|
-
name="status_modal_trigger",
|
|
1151
|
-
plugin_name="input_handler",
|
|
1152
|
-
event_type=EventType.STATUS_MODAL_TRIGGER,
|
|
1153
|
-
priority=HookPriority.DISPLAY.value,
|
|
1154
|
-
callback=self._handle_status_modal_trigger,
|
|
1155
|
-
)
|
|
1156
|
-
success = await self.event_bus.register_hook(hook)
|
|
1157
|
-
if success:
|
|
1158
|
-
logger.info("Successfully registered STATUS_MODAL_TRIGGER hook")
|
|
1159
|
-
else:
|
|
1160
|
-
logger.error("Failed to register STATUS_MODAL_TRIGGER hook")
|
|
1161
|
-
except Exception as e:
|
|
1162
|
-
logger.error(f"Failed to register STATUS_MODAL_TRIGGER hook: {e}")
|
|
1163
|
-
|
|
1164
|
-
async def _register_live_modal_trigger_hook(self) -> None:
|
|
1165
|
-
"""Register hook to handle live modal trigger events."""
|
|
1166
|
-
try:
|
|
1167
|
-
if self.event_bus:
|
|
1168
|
-
from ..events.models import Hook, HookPriority, EventType
|
|
1169
|
-
|
|
1170
|
-
hook = Hook(
|
|
1171
|
-
name="live_modal_trigger",
|
|
1172
|
-
plugin_name="input_handler",
|
|
1173
|
-
event_type=EventType.LIVE_MODAL_TRIGGER,
|
|
1174
|
-
priority=HookPriority.DISPLAY.value,
|
|
1175
|
-
callback=self._handle_live_modal_trigger,
|
|
1176
|
-
)
|
|
1177
|
-
success = await self.event_bus.register_hook(hook)
|
|
1178
|
-
if success:
|
|
1179
|
-
logger.info("Successfully registered LIVE_MODAL_TRIGGER hook")
|
|
1180
|
-
else:
|
|
1181
|
-
logger.error("Failed to register LIVE_MODAL_TRIGGER hook")
|
|
1182
|
-
except Exception as e:
|
|
1183
|
-
logger.error(f"Failed to register LIVE_MODAL_TRIGGER hook: {e}")
|
|
1184
|
-
|
|
1185
|
-
async def _handle_live_modal_trigger(
|
|
1186
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
1187
|
-
) -> Dict[str, Any]:
|
|
1188
|
-
"""Handle live modal trigger events to show live modals.
|
|
1189
|
-
|
|
1190
|
-
Args:
|
|
1191
|
-
event_data: Event data containing content_generator, config, input_callback.
|
|
1192
|
-
context: Hook execution context.
|
|
1193
|
-
|
|
1194
|
-
Returns:
|
|
1195
|
-
Dictionary with live modal result.
|
|
1196
|
-
"""
|
|
1197
|
-
try:
|
|
1198
|
-
content_generator = event_data.get("content_generator")
|
|
1199
|
-
config = event_data.get("config")
|
|
1200
|
-
input_callback = event_data.get("input_callback")
|
|
1201
|
-
|
|
1202
|
-
if content_generator:
|
|
1203
|
-
logger.info(f"Live modal trigger received: {config.title if config else 'untitled'}")
|
|
1204
|
-
# Enter live modal mode (this will block until modal closes)
|
|
1205
|
-
result = await self.enter_live_modal_mode(
|
|
1206
|
-
content_generator,
|
|
1207
|
-
config,
|
|
1208
|
-
input_callback
|
|
1209
|
-
)
|
|
1210
|
-
return {"success": True, "live_modal_activated": True, "result": result}
|
|
1211
|
-
else:
|
|
1212
|
-
logger.warning("Live modal trigger received without content_generator")
|
|
1213
|
-
return {"success": False, "error": "Missing content_generator"}
|
|
1214
|
-
except Exception as e:
|
|
1215
|
-
logger.error(f"Error handling live modal trigger: {e}")
|
|
1216
|
-
return {"success": False, "error": str(e)}
|
|
1217
|
-
|
|
1218
|
-
async def _register_status_modal_render_hook(self) -> None:
|
|
1219
|
-
"""Register hook to handle status modal render events."""
|
|
1220
|
-
try:
|
|
1221
|
-
if self.event_bus:
|
|
1222
|
-
from ..events.models import Hook, HookPriority, EventType
|
|
1223
|
-
|
|
1224
|
-
hook = Hook(
|
|
1225
|
-
name="status_modal_render",
|
|
1226
|
-
plugin_name="input_handler",
|
|
1227
|
-
event_type=EventType.STATUS_MODAL_RENDER,
|
|
1228
|
-
priority=HookPriority.DISPLAY.value,
|
|
1229
|
-
callback=self._handle_status_modal_render,
|
|
1230
|
-
)
|
|
1231
|
-
success = await self.event_bus.register_hook(hook)
|
|
1232
|
-
if success:
|
|
1233
|
-
logger.info("Successfully registered STATUS_MODAL_RENDER hook")
|
|
1234
|
-
else:
|
|
1235
|
-
logger.error("Failed to register STATUS_MODAL_RENDER hook")
|
|
1236
|
-
except Exception as e:
|
|
1237
|
-
logger.error(f"Failed to register STATUS_MODAL_RENDER hook: {e}")
|
|
1238
|
-
|
|
1239
|
-
async def _register_command_output_display_hook(self) -> None:
|
|
1240
|
-
"""Register hook to handle command output display events."""
|
|
1241
|
-
try:
|
|
1242
|
-
if self.event_bus:
|
|
1243
|
-
from ..events.models import Hook, HookPriority, EventType
|
|
1244
|
-
|
|
1245
|
-
hook = Hook(
|
|
1246
|
-
name="command_output_display",
|
|
1247
|
-
plugin_name="input_handler",
|
|
1248
|
-
event_type=EventType.COMMAND_OUTPUT_DISPLAY,
|
|
1249
|
-
priority=HookPriority.DISPLAY.value,
|
|
1250
|
-
callback=self._handle_command_output_display,
|
|
1251
|
-
)
|
|
1252
|
-
success = await self.event_bus.register_hook(hook)
|
|
1253
|
-
if success:
|
|
1254
|
-
logger.info(
|
|
1255
|
-
"Successfully registered COMMAND_OUTPUT_DISPLAY hook"
|
|
1256
|
-
)
|
|
1257
|
-
else:
|
|
1258
|
-
logger.error("Failed to register COMMAND_OUTPUT_DISPLAY hook")
|
|
1259
|
-
except Exception as e:
|
|
1260
|
-
logger.error(f"Failed to register COMMAND_OUTPUT_DISPLAY hook: {e}")
|
|
1261
|
-
|
|
1262
|
-
# Register pause/resume rendering hooks
|
|
1263
|
-
await self._register_pause_rendering_hook()
|
|
1264
|
-
await self._register_resume_rendering_hook()
|
|
1265
|
-
|
|
1266
|
-
# Register modal hide hook for Matrix effect cleanup
|
|
1267
|
-
await self._register_modal_hide_hook()
|
|
1268
|
-
|
|
1269
|
-
async def _register_pause_rendering_hook(self) -> None:
|
|
1270
|
-
"""Register hook for pause rendering events."""
|
|
1271
|
-
try:
|
|
1272
|
-
if self.event_bus:
|
|
1273
|
-
from ..events.models import Hook, HookPriority, EventType
|
|
1274
|
-
|
|
1275
|
-
hook = Hook(
|
|
1276
|
-
name="pause_rendering",
|
|
1277
|
-
plugin_name="input_handler",
|
|
1278
|
-
event_type=EventType.PAUSE_RENDERING,
|
|
1279
|
-
priority=HookPriority.DISPLAY.value,
|
|
1280
|
-
callback=self._handle_pause_rendering,
|
|
1281
|
-
)
|
|
1282
|
-
success = await self.event_bus.register_hook(hook)
|
|
1283
|
-
if success:
|
|
1284
|
-
logger.info("Successfully registered PAUSE_RENDERING hook")
|
|
1285
|
-
else:
|
|
1286
|
-
logger.error("Failed to register PAUSE_RENDERING hook")
|
|
1287
|
-
except Exception as e:
|
|
1288
|
-
logger.error(f"Error registering PAUSE_RENDERING hook: {e}")
|
|
1289
|
-
|
|
1290
|
-
async def _register_resume_rendering_hook(self) -> None:
|
|
1291
|
-
"""Register hook for resume rendering events."""
|
|
1292
|
-
try:
|
|
1293
|
-
if self.event_bus:
|
|
1294
|
-
from ..events.models import Hook, HookPriority, EventType
|
|
1295
|
-
|
|
1296
|
-
hook = Hook(
|
|
1297
|
-
name="resume_rendering",
|
|
1298
|
-
plugin_name="input_handler",
|
|
1299
|
-
event_type=EventType.RESUME_RENDERING,
|
|
1300
|
-
priority=HookPriority.DISPLAY.value,
|
|
1301
|
-
callback=self._handle_resume_rendering,
|
|
1302
|
-
)
|
|
1303
|
-
success = await self.event_bus.register_hook(hook)
|
|
1304
|
-
if success:
|
|
1305
|
-
logger.info("Successfully registered RESUME_RENDERING hook")
|
|
1306
|
-
else:
|
|
1307
|
-
logger.error("Failed to register RESUME_RENDERING hook")
|
|
1308
|
-
except Exception as e:
|
|
1309
|
-
logger.error(f"Error registering RESUME_RENDERING hook: {e}")
|
|
1310
|
-
|
|
1311
320
|
async def _handle_pause_rendering(
|
|
1312
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
321
|
+
self, event_data: Dict[str, Any], context: Optional[str] = None
|
|
1313
322
|
) -> Dict[str, Any]:
|
|
1314
323
|
"""Handle pause rendering event."""
|
|
1315
|
-
logger.info("
|
|
324
|
+
logger.info("PAUSE_RENDERING event received - pausing input rendering")
|
|
1316
325
|
self.rendering_paused = True
|
|
326
|
+
self._display_controller.pause_rendering()
|
|
1317
327
|
return {"status": "paused"}
|
|
1318
328
|
|
|
1319
329
|
async def _handle_resume_rendering(
|
|
1320
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
330
|
+
self, event_data: Dict[str, Any], context: Optional[str] = None
|
|
1321
331
|
) -> Dict[str, Any]:
|
|
1322
332
|
"""Handle resume rendering event."""
|
|
1323
|
-
logger.info("
|
|
333
|
+
logger.info("RESUME_RENDERING event received - resuming input rendering")
|
|
1324
334
|
self.rendering_paused = False
|
|
335
|
+
self._display_controller.resume_rendering()
|
|
1325
336
|
# Force a refresh when resuming
|
|
1326
|
-
await self.
|
|
337
|
+
await self._display_controller.update_display(force_render=True)
|
|
1327
338
|
return {"status": "resumed"}
|
|
1328
339
|
|
|
1329
|
-
async def _register_modal_hide_hook(self) -> None:
|
|
1330
|
-
"""Register hook for modal hide events."""
|
|
1331
|
-
try:
|
|
1332
|
-
if self.event_bus:
|
|
1333
|
-
from ..events.models import Hook, HookPriority, EventType
|
|
1334
|
-
|
|
1335
|
-
hook = Hook(
|
|
1336
|
-
name="modal_hide",
|
|
1337
|
-
plugin_name="input_handler",
|
|
1338
|
-
event_type=EventType.MODAL_HIDE,
|
|
1339
|
-
priority=HookPriority.DISPLAY.value,
|
|
1340
|
-
callback=self._handle_modal_hide,
|
|
1341
|
-
)
|
|
1342
|
-
success = await self.event_bus.register_hook(hook)
|
|
1343
|
-
if success:
|
|
1344
|
-
logger.info("Successfully registered MODAL_HIDE hook")
|
|
1345
|
-
else:
|
|
1346
|
-
logger.error("Failed to register MODAL_HIDE hook")
|
|
1347
|
-
except Exception as e:
|
|
1348
|
-
logger.error(f"Error registering MODAL_HIDE hook: {e}")
|
|
1349
|
-
|
|
1350
|
-
async def _handle_modal_hide(
|
|
1351
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
1352
|
-
) -> Dict[str, Any]:
|
|
1353
|
-
"""Handle modal hide event to exit modal mode."""
|
|
1354
|
-
logger.info("🔄 MODAL_HIDE event received - exiting modal mode")
|
|
1355
|
-
try:
|
|
1356
|
-
from ..events.models import CommandMode
|
|
1357
|
-
|
|
1358
|
-
# CRITICAL FIX: Clear input area before restoring (like config modal does)
|
|
1359
|
-
self.renderer.clear_active_area()
|
|
1360
|
-
self.renderer.writing_messages = False
|
|
1361
|
-
|
|
1362
|
-
self.command_mode = CommandMode.NORMAL
|
|
1363
|
-
# CRITICAL FIX: Clear fullscreen session flag when exiting modal
|
|
1364
|
-
if hasattr(self, "_fullscreen_session_active"):
|
|
1365
|
-
self._fullscreen_session_active = False
|
|
1366
|
-
logger.info("🔄 Fullscreen session marked as inactive")
|
|
1367
|
-
logger.info("🔄 Command mode reset to NORMAL after modal hide")
|
|
1368
|
-
|
|
1369
|
-
# Force refresh of display when exiting modal mode
|
|
1370
|
-
await self._update_display(force_render=True)
|
|
1371
|
-
return {"success": True, "modal_deactivated": True}
|
|
1372
|
-
except Exception as e:
|
|
1373
|
-
logger.error(f"Error handling modal hide: {e}")
|
|
1374
|
-
return {"success": False, "error": str(e)}
|
|
1375
|
-
|
|
1376
340
|
async def _handle_command_output_display(
|
|
1377
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
341
|
+
self, event_data: Dict[str, Any], context: Optional[str] = None
|
|
1378
342
|
) -> Dict[str, Any]:
|
|
1379
343
|
"""Handle command output display events.
|
|
1380
344
|
|
|
@@ -1388,35 +352,30 @@ class InputHandler:
|
|
|
1388
352
|
try:
|
|
1389
353
|
message = event_data.get("message", "")
|
|
1390
354
|
display_type = event_data.get("display_type", "info")
|
|
1391
|
-
_ = event_data.get("success", True)
|
|
1392
355
|
|
|
1393
356
|
if message:
|
|
1394
357
|
# Format message based on display type
|
|
1395
358
|
if display_type == "error":
|
|
1396
|
-
formatted_message = f"
|
|
359
|
+
formatted_message = f"[x] {message}"
|
|
1397
360
|
elif display_type == "warning":
|
|
1398
|
-
formatted_message = f"
|
|
361
|
+
formatted_message = f"[!] {message}"
|
|
1399
362
|
elif display_type == "success":
|
|
1400
|
-
formatted_message = f"
|
|
1401
|
-
else:
|
|
1402
|
-
formatted_message = f"
|
|
363
|
+
formatted_message = f"[ok] {message}"
|
|
364
|
+
else:
|
|
365
|
+
formatted_message = f"[i] {message}"
|
|
1403
366
|
|
|
1404
|
-
# FIXED: Remove writing_messages management here - message_coordinator handles it
|
|
1405
|
-
# The message_coordinator.display_message_sequence() properly manages the flag
|
|
1406
|
-
|
|
1407
367
|
# Clear the active input area first
|
|
1408
368
|
self.renderer.clear_active_area()
|
|
1409
369
|
|
|
1410
|
-
#
|
|
1411
|
-
# This routes through message_coordinator which handles writing_messages flag properly
|
|
370
|
+
# Use write_hook_message to display command output
|
|
1412
371
|
self.renderer.write_hook_message(
|
|
1413
372
|
formatted_message,
|
|
1414
373
|
display_type=display_type,
|
|
1415
374
|
source="command",
|
|
1416
375
|
)
|
|
1417
376
|
|
|
1418
|
-
# Force a display update
|
|
1419
|
-
await self.
|
|
377
|
+
# Force a display update
|
|
378
|
+
await self._display_controller.update_display(force_render=True)
|
|
1420
379
|
|
|
1421
380
|
logger.info(f"Command output displayed: {display_type}")
|
|
1422
381
|
|
|
@@ -1430,1226 +389,26 @@ class InputHandler:
|
|
|
1430
389
|
logger.error(f"Error handling command output display: {e}")
|
|
1431
390
|
return {"success": False, "error": str(e)}
|
|
1432
391
|
|
|
1433
|
-
|
|
1434
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
1435
|
-
) -> Dict[str, Any]:
|
|
1436
|
-
"""Handle modal trigger events to show modals.
|
|
1437
|
-
|
|
1438
|
-
Args:
|
|
1439
|
-
event_data: Event data containing modal configuration.
|
|
1440
|
-
|
|
1441
|
-
Returns:
|
|
1442
|
-
Dictionary with modal result.
|
|
1443
|
-
"""
|
|
1444
|
-
try:
|
|
1445
|
-
# Check if this is a Matrix effect trigger
|
|
1446
|
-
if event_data.get("matrix_effect"):
|
|
1447
|
-
logger.info(
|
|
1448
|
-
"🎯 Matrix effect modal trigger received - setting modal mode for complete terminal control"
|
|
1449
|
-
)
|
|
1450
|
-
# Set modal mode directly for Matrix effect (no UI config needed)
|
|
1451
|
-
from ..events.models import CommandMode
|
|
1452
|
-
|
|
1453
|
-
self.command_mode = CommandMode.MODAL
|
|
1454
|
-
logger.info("🎯 Command mode set to MODAL for Matrix effect")
|
|
1455
|
-
return {
|
|
1456
|
-
"success": True,
|
|
1457
|
-
"modal_activated": True,
|
|
1458
|
-
"matrix_mode": True,
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
# Check if this is a full-screen plugin trigger
|
|
1462
|
-
if event_data.get("fullscreen_plugin"):
|
|
1463
|
-
plugin_name = event_data.get("plugin_name", "unknown")
|
|
1464
|
-
logger.info(
|
|
1465
|
-
f"🎯 Full-screen plugin modal trigger received: {plugin_name}"
|
|
1466
|
-
)
|
|
1467
|
-
|
|
1468
|
-
# CRITICAL FIX: Clear input area before fullscreen mode (like config modal does)
|
|
1469
|
-
self.renderer.writing_messages = True
|
|
1470
|
-
self.renderer.clear_active_area()
|
|
1471
|
-
|
|
1472
|
-
# Set modal mode for full-screen plugin (no UI config needed)
|
|
1473
|
-
from ..events.models import CommandMode
|
|
1474
|
-
|
|
1475
|
-
self.command_mode = CommandMode.MODAL
|
|
1476
|
-
# CRITICAL FIX: Mark fullscreen session as active for input routing
|
|
1477
|
-
self._fullscreen_session_active = True
|
|
1478
|
-
logger.info(
|
|
1479
|
-
f"🎯 Command mode set to MODAL for full-screen plugin: {plugin_name}"
|
|
1480
|
-
)
|
|
1481
|
-
logger.info(
|
|
1482
|
-
"🎯 Fullscreen session marked as active for input routing"
|
|
1483
|
-
)
|
|
1484
|
-
return {
|
|
1485
|
-
"success": True,
|
|
1486
|
-
"modal_activated": True,
|
|
1487
|
-
"fullscreen_plugin": True,
|
|
1488
|
-
"plugin_name": plugin_name,
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
# Standard modal with UI config
|
|
1492
|
-
ui_config = event_data.get("ui_config")
|
|
1493
|
-
if ui_config:
|
|
1494
|
-
logger.info(f"🎯 Modal trigger received: {ui_config.title}")
|
|
1495
|
-
await self._enter_modal_mode(ui_config)
|
|
1496
|
-
return {"success": True, "modal_activated": True}
|
|
1497
|
-
else:
|
|
1498
|
-
logger.warning("Modal trigger received without ui_config")
|
|
1499
|
-
return {"success": False, "error": "Missing ui_config"}
|
|
1500
|
-
|
|
1501
|
-
except Exception as e:
|
|
1502
|
-
logger.error(f"Error handling modal trigger: {e}")
|
|
1503
|
-
return {"success": False, "error": str(e)}
|
|
1504
|
-
|
|
1505
|
-
# ==================== SLASH COMMAND SYSTEM ====================
|
|
1506
|
-
|
|
1507
|
-
async def _enter_command_mode(self) -> None:
|
|
1508
|
-
"""Enter slash command mode and show command menu."""
|
|
1509
|
-
try:
|
|
1510
|
-
logger.info("🎯 Entering slash command mode")
|
|
1511
|
-
self.command_mode = CommandMode.MENU_POPUP
|
|
1512
|
-
self.command_menu_active = True
|
|
1513
|
-
|
|
1514
|
-
# Reset selection to first command
|
|
1515
|
-
self.selected_command_index = 0
|
|
1516
|
-
|
|
1517
|
-
# Add the '/' character to buffer for visual feedback
|
|
1518
|
-
self.buffer_manager.insert_char("/")
|
|
1519
|
-
|
|
1520
|
-
# Show command menu via renderer
|
|
1521
|
-
available_commands = self._get_available_commands()
|
|
1522
|
-
self.command_menu_renderer.show_command_menu(available_commands, "")
|
|
1523
|
-
|
|
1524
|
-
# Emit command menu show event
|
|
1525
|
-
await self.event_bus.emit_with_hooks(
|
|
1526
|
-
EventType.COMMAND_MENU_SHOW,
|
|
1527
|
-
{"available_commands": available_commands, "filter_text": ""},
|
|
1528
|
-
"commands",
|
|
1529
|
-
)
|
|
1530
|
-
|
|
1531
|
-
# Update display to show command mode
|
|
1532
|
-
await self._update_display(force_render=True)
|
|
1533
|
-
|
|
1534
|
-
logger.info("Command menu activated")
|
|
1535
|
-
|
|
1536
|
-
except Exception as e:
|
|
1537
|
-
logger.error(f"Error entering command mode: {e}")
|
|
1538
|
-
await self._exit_command_mode()
|
|
1539
|
-
|
|
1540
|
-
async def _exit_command_mode(self) -> None:
|
|
1541
|
-
"""Exit command mode and restore normal input."""
|
|
1542
|
-
try:
|
|
1543
|
-
import traceback
|
|
1544
|
-
|
|
1545
|
-
logger.info("🚪 Exiting slash command mode")
|
|
1546
|
-
logger.info(
|
|
1547
|
-
f"🚪 Exit called from: {traceback.format_stack()[-2].strip()}"
|
|
1548
|
-
)
|
|
1549
|
-
|
|
1550
|
-
# Hide command menu via renderer
|
|
1551
|
-
self.command_menu_renderer.hide_menu()
|
|
1552
|
-
|
|
1553
|
-
# Emit command menu hide event
|
|
1554
|
-
if self.command_menu_active:
|
|
1555
|
-
await self.event_bus.emit_with_hooks(
|
|
1556
|
-
EventType.COMMAND_MENU_HIDE,
|
|
1557
|
-
{"reason": "manual_exit"},
|
|
1558
|
-
"commands",
|
|
1559
|
-
)
|
|
1560
|
-
|
|
1561
|
-
self.command_mode = CommandMode.NORMAL
|
|
1562
|
-
self.command_menu_active = False
|
|
1563
|
-
|
|
1564
|
-
# Clear command buffer (remove the '/' and any partial command)
|
|
1565
|
-
self.buffer_manager.clear()
|
|
1566
|
-
|
|
1567
|
-
# Update display
|
|
1568
|
-
await self._update_display(force_render=True)
|
|
1569
|
-
|
|
1570
|
-
logger.info("Returned to normal input mode")
|
|
1571
|
-
|
|
1572
|
-
except Exception as e:
|
|
1573
|
-
logger.error(f"Error exiting command mode: {e}")
|
|
1574
|
-
|
|
1575
|
-
async def _handle_command_mode_keypress(self, key_press: KeyPress) -> bool:
|
|
1576
|
-
"""Handle KeyPress while in command mode (supports arrow keys).
|
|
1577
|
-
|
|
1578
|
-
Args:
|
|
1579
|
-
key_press: Parsed key press to process.
|
|
1580
|
-
|
|
1581
|
-
Returns:
|
|
1582
|
-
True if key was handled, False to fall through to normal processing.
|
|
1583
|
-
"""
|
|
1584
|
-
try:
|
|
1585
|
-
if self.command_mode == CommandMode.MENU_POPUP:
|
|
1586
|
-
return await self._handle_menu_popup_keypress(key_press)
|
|
1587
|
-
elif self.command_mode == CommandMode.STATUS_TAKEOVER:
|
|
1588
|
-
return await self._handle_status_takeover_keypress(key_press)
|
|
1589
|
-
elif self.command_mode == CommandMode.MODAL:
|
|
1590
|
-
return await self._handle_modal_keypress(key_press)
|
|
1591
|
-
elif self.command_mode == CommandMode.STATUS_MODAL:
|
|
1592
|
-
return await self._handle_status_modal_keypress(key_press)
|
|
1593
|
-
elif self.command_mode == CommandMode.LIVE_MODAL:
|
|
1594
|
-
return await self._handle_live_modal_keypress(key_press)
|
|
1595
|
-
else:
|
|
1596
|
-
# Unknown command mode, exit to normal
|
|
1597
|
-
await self._exit_command_mode()
|
|
1598
|
-
return False
|
|
1599
|
-
|
|
1600
|
-
except Exception as e:
|
|
1601
|
-
logger.error(f"Error handling command mode keypress: {e}")
|
|
1602
|
-
await self._exit_command_mode()
|
|
1603
|
-
return False
|
|
1604
|
-
|
|
1605
|
-
async def _handle_command_mode_input(self, char: str) -> bool:
|
|
1606
|
-
"""Handle input while in command mode.
|
|
1607
|
-
|
|
1608
|
-
Args:
|
|
1609
|
-
char: Character input to process.
|
|
1610
|
-
|
|
1611
|
-
Returns:
|
|
1612
|
-
True if input was handled, False to fall through to normal processing.
|
|
1613
|
-
"""
|
|
1614
|
-
try:
|
|
1615
|
-
if self.command_mode == CommandMode.MENU_POPUP:
|
|
1616
|
-
return await self._handle_menu_popup_input(char)
|
|
1617
|
-
elif self.command_mode == CommandMode.STATUS_TAKEOVER:
|
|
1618
|
-
return await self._handle_status_takeover_input(char)
|
|
1619
|
-
elif self.command_mode == CommandMode.STATUS_MODAL:
|
|
1620
|
-
return await self._handle_status_modal_input(char)
|
|
1621
|
-
elif self.command_mode == CommandMode.LIVE_MODAL:
|
|
1622
|
-
return await self._handle_live_modal_input(char)
|
|
1623
|
-
else:
|
|
1624
|
-
# Unknown command mode, exit to normal
|
|
1625
|
-
await self._exit_command_mode()
|
|
1626
|
-
return False
|
|
1627
|
-
|
|
1628
|
-
except Exception as e:
|
|
1629
|
-
logger.error(f"Error handling command mode input: {e}")
|
|
1630
|
-
await self._exit_command_mode()
|
|
1631
|
-
return False
|
|
1632
|
-
|
|
1633
|
-
async def _handle_menu_popup_input(self, char: str) -> bool:
|
|
1634
|
-
"""Handle input during menu popup mode.
|
|
1635
|
-
|
|
1636
|
-
Args:
|
|
1637
|
-
char: Character input to process.
|
|
1638
|
-
|
|
1639
|
-
Returns:
|
|
1640
|
-
True if input was handled.
|
|
1641
|
-
"""
|
|
1642
|
-
# Handle special keys first
|
|
1643
|
-
if ord(char) == 27: # Escape key
|
|
1644
|
-
await self._exit_command_mode()
|
|
1645
|
-
return True
|
|
1646
|
-
elif ord(char) == 13: # Enter key
|
|
1647
|
-
await self._execute_selected_command()
|
|
1648
|
-
return True
|
|
1649
|
-
elif ord(char) == 8 or ord(char) == 127: # Backspace or Delete
|
|
1650
|
-
# If buffer only has '/', exit command mode
|
|
1651
|
-
if len(self.buffer_manager.content) <= 1:
|
|
1652
|
-
await self._exit_command_mode()
|
|
1653
|
-
return True
|
|
1654
|
-
else:
|
|
1655
|
-
# Remove character and update command filter
|
|
1656
|
-
self.buffer_manager.delete_char()
|
|
1657
|
-
await self._update_command_filter()
|
|
1658
|
-
return True
|
|
1659
|
-
|
|
1660
|
-
# Handle printable characters (add to command filter)
|
|
1661
|
-
if char.isprintable():
|
|
1662
|
-
self.buffer_manager.insert_char(char)
|
|
1663
|
-
await self._update_command_filter()
|
|
1664
|
-
return True
|
|
1665
|
-
|
|
1666
|
-
# Let other keys fall through for now
|
|
1667
|
-
return False
|
|
1668
|
-
|
|
1669
|
-
async def _handle_menu_popup_keypress(self, key_press: KeyPress) -> bool:
|
|
1670
|
-
"""Handle KeyPress during menu popup mode with arrow key navigation.
|
|
1671
|
-
|
|
1672
|
-
Args:
|
|
1673
|
-
key_press: Parsed key press to process.
|
|
1674
|
-
|
|
1675
|
-
Returns:
|
|
1676
|
-
True if key was handled.
|
|
1677
|
-
"""
|
|
1678
|
-
try:
|
|
1679
|
-
# Handle arrow key navigation
|
|
1680
|
-
if key_press.name == "ArrowUp":
|
|
1681
|
-
await self._navigate_menu("up")
|
|
1682
|
-
return True
|
|
1683
|
-
elif key_press.name == "ArrowDown":
|
|
1684
|
-
await self._navigate_menu("down")
|
|
1685
|
-
return True
|
|
1686
|
-
elif key_press.name == "Enter":
|
|
1687
|
-
await self._execute_selected_command()
|
|
1688
|
-
return True
|
|
1689
|
-
elif key_press.name == "Escape":
|
|
1690
|
-
await self._exit_command_mode()
|
|
1691
|
-
return True
|
|
1692
|
-
|
|
1693
|
-
# Handle printable characters (for filtering)
|
|
1694
|
-
elif key_press.char and key_press.char.isprintable():
|
|
1695
|
-
self.buffer_manager.insert_char(key_press.char)
|
|
1696
|
-
await self._update_command_filter()
|
|
1697
|
-
return True
|
|
1698
|
-
|
|
1699
|
-
# Handle backspace/delete
|
|
1700
|
-
elif key_press.name in ["Backspace", "Delete"]:
|
|
1701
|
-
# If buffer only has '/', exit command mode
|
|
1702
|
-
if len(self.buffer_manager.content) <= 1:
|
|
1703
|
-
await self._exit_command_mode()
|
|
1704
|
-
return True
|
|
1705
|
-
else:
|
|
1706
|
-
# Remove character and update command filter
|
|
1707
|
-
self.buffer_manager.delete_char()
|
|
1708
|
-
await self._update_command_filter()
|
|
1709
|
-
return True
|
|
1710
|
-
|
|
1711
|
-
# Other keys not handled
|
|
1712
|
-
return False
|
|
1713
|
-
|
|
1714
|
-
except Exception as e:
|
|
1715
|
-
logger.error(f"Error handling menu popup keypress: {e}")
|
|
1716
|
-
await self._exit_command_mode()
|
|
1717
|
-
return False
|
|
1718
|
-
|
|
1719
|
-
async def _handle_status_takeover_input(self, char: str) -> bool:
|
|
1720
|
-
"""Handle input during status area takeover mode.
|
|
1721
|
-
|
|
1722
|
-
Args:
|
|
1723
|
-
char: Character input to process.
|
|
1724
|
-
|
|
1725
|
-
Returns:
|
|
1726
|
-
True if input was handled.
|
|
1727
|
-
"""
|
|
1728
|
-
# For now, just handle Escape to exit
|
|
1729
|
-
if ord(char) == 27: # Escape key
|
|
1730
|
-
await self._exit_command_mode()
|
|
1731
|
-
return True
|
|
1732
|
-
|
|
1733
|
-
# TODO: Implement status area navigation
|
|
1734
|
-
return True
|
|
1735
|
-
|
|
1736
|
-
async def _handle_status_takeover_keypress(self, key_press: KeyPress) -> bool:
|
|
1737
|
-
"""Handle KeyPress during status area takeover mode.
|
|
1738
|
-
|
|
1739
|
-
Args:
|
|
1740
|
-
key_press: Parsed key press to process.
|
|
1741
|
-
|
|
1742
|
-
Returns:
|
|
1743
|
-
True if key was handled.
|
|
1744
|
-
"""
|
|
1745
|
-
# For now, just handle Escape to exit
|
|
1746
|
-
if key_press.name == "Escape":
|
|
1747
|
-
await self._exit_command_mode()
|
|
1748
|
-
return True
|
|
1749
|
-
|
|
1750
|
-
# TODO: Implement status area navigation
|
|
1751
|
-
return True
|
|
1752
|
-
|
|
1753
|
-
async def _handle_modal_keypress(self, key_press: KeyPress) -> bool:
|
|
1754
|
-
"""Handle KeyPress during modal mode.
|
|
1755
|
-
|
|
1756
|
-
Args:
|
|
1757
|
-
key_press: Parsed key press to process.
|
|
1758
|
-
|
|
1759
|
-
Returns:
|
|
1760
|
-
True if key was handled.
|
|
1761
|
-
"""
|
|
1762
|
-
try:
|
|
1763
|
-
# CRITICAL FIX: Check if this is a fullscreen plugin session first
|
|
1764
|
-
if (
|
|
1765
|
-
hasattr(self, "_fullscreen_session_active")
|
|
1766
|
-
and self._fullscreen_session_active
|
|
1767
|
-
):
|
|
1768
|
-
# SIMPLE SOLUTION: Check for exit keys directly
|
|
1769
|
-
if key_press.char in ["q", "\x1b"] or key_press.name == "Escape":
|
|
1770
|
-
# Exit fullscreen mode immediately
|
|
1771
|
-
self._fullscreen_session_active = False
|
|
1772
|
-
from ..events.models import CommandMode
|
|
1773
|
-
|
|
1774
|
-
self.command_mode = CommandMode.NORMAL
|
|
1775
|
-
await self._update_display(force_render=True)
|
|
1776
|
-
return True
|
|
1777
|
-
|
|
1778
|
-
# Route input to fullscreen session through event bus
|
|
1779
|
-
from ..events.models import EventType
|
|
1780
|
-
|
|
1781
|
-
await self.event_bus.emit_with_hooks(
|
|
1782
|
-
EventType.FULLSCREEN_INPUT,
|
|
1783
|
-
{"key_press": key_press, "source": "input_handler"},
|
|
1784
|
-
"input_handler",
|
|
1785
|
-
)
|
|
1786
|
-
return True
|
|
1787
|
-
|
|
1788
|
-
# Initialize modal renderer if needed
|
|
1789
|
-
if not self.modal_renderer:
|
|
1790
|
-
logger.warning(
|
|
1791
|
-
"Modal keypress received but no modal renderer active"
|
|
1792
|
-
)
|
|
1793
|
-
await self._exit_modal_mode()
|
|
1794
|
-
return True
|
|
1795
|
-
|
|
1796
|
-
# Handle save confirmation if active
|
|
1797
|
-
if self._pending_save_confirm:
|
|
1798
|
-
handled = await self._handle_save_confirmation(key_press)
|
|
1799
|
-
if handled:
|
|
1800
|
-
return True
|
|
1801
|
-
|
|
1802
|
-
# Handle navigation and widget interaction
|
|
1803
|
-
logger.info(f"🔍 Modal processing key: {key_press.name}")
|
|
1804
|
-
|
|
1805
|
-
nav_handled = self.modal_renderer._handle_widget_navigation(key_press)
|
|
1806
|
-
logger.info(f"🎯 Widget navigation handled: {nav_handled}")
|
|
1807
|
-
if nav_handled:
|
|
1808
|
-
# Re-render modal with updated focus
|
|
1809
|
-
await self._refresh_modal_display()
|
|
1810
|
-
return True
|
|
1811
|
-
|
|
1812
|
-
input_handled = self.modal_renderer._handle_widget_input(key_press)
|
|
1813
|
-
logger.info(f"🎯 Widget input handled: {input_handled}")
|
|
1814
|
-
if input_handled:
|
|
1815
|
-
# Re-render modal with updated widget state
|
|
1816
|
-
await self._refresh_modal_display()
|
|
1817
|
-
return True
|
|
1818
|
-
|
|
1819
|
-
if key_press.name == "Escape":
|
|
1820
|
-
logger.info("🚪 Processing Escape key for modal exit")
|
|
1821
|
-
# Check for unsaved changes
|
|
1822
|
-
if self.modal_renderer and self._has_pending_modal_changes():
|
|
1823
|
-
self._pending_save_confirm = True
|
|
1824
|
-
await self._show_save_confirmation()
|
|
1825
|
-
return True
|
|
1826
|
-
await self._exit_modal_mode()
|
|
1827
|
-
return True
|
|
1828
|
-
elif key_press.name == "Ctrl+S":
|
|
1829
|
-
logger.info("💾 Processing Ctrl+S for modal save")
|
|
1830
|
-
await self._save_and_exit_modal()
|
|
1831
|
-
return True
|
|
1832
|
-
elif key_press.name == "Enter":
|
|
1833
|
-
logger.info(
|
|
1834
|
-
"🔴 ENTER KEY HIJACKED - This should not happen if widget handled it!"
|
|
1835
|
-
)
|
|
1836
|
-
# Try to save modal changes and exit
|
|
1837
|
-
await self._save_and_exit_modal()
|
|
1838
|
-
return True
|
|
1839
|
-
|
|
1840
|
-
return True
|
|
1841
|
-
except Exception as e:
|
|
1842
|
-
logger.error(f"Error handling modal keypress: {e}")
|
|
1843
|
-
await self._exit_modal_mode()
|
|
1844
|
-
return False
|
|
1845
|
-
|
|
1846
|
-
async def _enter_modal_mode(self, ui_config):
|
|
1847
|
-
"""Enter modal mode and show modal renderer.
|
|
1848
|
-
|
|
1849
|
-
Args:
|
|
1850
|
-
ui_config: Modal configuration.
|
|
1851
|
-
"""
|
|
1852
|
-
try:
|
|
1853
|
-
# Import modal renderer here to avoid circular imports
|
|
1854
|
-
from ..ui.modal_renderer import ModalRenderer
|
|
1855
|
-
|
|
1856
|
-
# Create modal renderer instance with proper config service
|
|
1857
|
-
self.modal_renderer = ModalRenderer(
|
|
1858
|
-
terminal_renderer=self.renderer,
|
|
1859
|
-
visual_effects=getattr(self.renderer, "visual_effects", None),
|
|
1860
|
-
config_service=self.config, # Use config as config service
|
|
1861
|
-
)
|
|
1862
|
-
|
|
1863
|
-
# CRITICAL FIX: Clear input area before modal to prevent duplication
|
|
1864
|
-
self.renderer.writing_messages = True
|
|
1865
|
-
self.renderer.clear_active_area()
|
|
1866
|
-
|
|
1867
|
-
# Set modal mode FIRST
|
|
1868
|
-
self.command_mode = CommandMode.MODAL
|
|
1869
|
-
logger.info(f"🎯 Command mode set to: {self.command_mode}")
|
|
1870
|
-
|
|
1871
|
-
# Show the modal with alternate buffer
|
|
1872
|
-
logger.info(
|
|
1873
|
-
"🔧 DIRECT: About to call show_modal - this should trigger alternate buffer"
|
|
1874
|
-
)
|
|
1875
|
-
await self.modal_renderer.show_modal(ui_config)
|
|
1876
|
-
|
|
1877
|
-
# Reset writing flag (modal will handle its own rendering from here)
|
|
1878
|
-
self.renderer.writing_messages = False
|
|
1879
|
-
|
|
1880
|
-
logger.info("🎯 Entered modal mode with persistent input loop")
|
|
1881
|
-
|
|
1882
|
-
except Exception as e:
|
|
1883
|
-
logger.error(f"Error entering modal mode: {e}")
|
|
1884
|
-
self.command_mode = CommandMode.NORMAL
|
|
1885
|
-
|
|
1886
|
-
async def _refresh_modal_display(self):
|
|
1887
|
-
"""Refresh modal display after widget interactions."""
|
|
1888
|
-
try:
|
|
1889
|
-
if self.modal_renderer and hasattr(
|
|
1890
|
-
self.modal_renderer, "current_ui_config"
|
|
1891
|
-
):
|
|
1892
|
-
|
|
1893
|
-
# CRITICAL FIX: Force complete display clearing to prevent duplication
|
|
1894
|
-
# Clear active area completely before refresh
|
|
1895
|
-
self.renderer.clear_active_area()
|
|
1896
|
-
|
|
1897
|
-
# Clear any message buffers that might accumulate content
|
|
1898
|
-
if hasattr(self.renderer, "message_renderer"):
|
|
1899
|
-
if hasattr(self.renderer.message_renderer, "buffer"):
|
|
1900
|
-
self.renderer.message_renderer.buffer.clear_buffer()
|
|
1901
|
-
# Also clear any accumulated messages in the renderer
|
|
1902
|
-
if hasattr(self.renderer.message_renderer, "clear_messages"):
|
|
1903
|
-
self.renderer.message_renderer.clear_messages()
|
|
1904
|
-
|
|
1905
|
-
# Re-render the modal with current widget states (preserve widgets!)
|
|
1906
|
-
modal_lines = self.modal_renderer._render_modal_box(
|
|
1907
|
-
self.modal_renderer.current_ui_config,
|
|
1908
|
-
preserve_widgets=True,
|
|
1909
|
-
)
|
|
1910
|
-
# FIXED: Use state_manager.render_modal_content() instead of _render_modal_lines()
|
|
1911
|
-
# to avoid re-calling prepare_modal_display() which causes buffer switching
|
|
1912
|
-
if self.modal_renderer.state_manager:
|
|
1913
|
-
self.modal_renderer.state_manager.render_modal_content(
|
|
1914
|
-
modal_lines
|
|
1915
|
-
)
|
|
1916
|
-
else:
|
|
1917
|
-
# Fallback to old method if state_manager not available
|
|
1918
|
-
await self.modal_renderer._render_modal_lines(modal_lines)
|
|
1919
|
-
else:
|
|
1920
|
-
pass
|
|
1921
|
-
except Exception as e:
|
|
1922
|
-
logger.error(f"Error refreshing modal display: {e}")
|
|
1923
|
-
|
|
1924
|
-
def _has_pending_modal_changes(self) -> bool:
|
|
1925
|
-
"""Check if there are unsaved changes in modal widgets."""
|
|
1926
|
-
if not self.modal_renderer or not self.modal_renderer.widgets:
|
|
1927
|
-
return False
|
|
1928
|
-
for widget in self.modal_renderer.widgets:
|
|
1929
|
-
if hasattr(widget, '_pending_value') and widget._pending_value is not None:
|
|
1930
|
-
# Check if pending value differs from current config value
|
|
1931
|
-
current = widget.get_value() if hasattr(widget, 'get_value') else None
|
|
1932
|
-
if widget._pending_value != current:
|
|
1933
|
-
return True
|
|
1934
|
-
return False
|
|
1935
|
-
|
|
1936
|
-
async def _show_save_confirmation(self):
|
|
1937
|
-
"""Show save confirmation prompt in modal."""
|
|
1938
|
-
# Update modal footer to show confirmation prompt
|
|
1939
|
-
if self.modal_renderer:
|
|
1940
|
-
self.modal_renderer._save_confirm_active = True
|
|
1941
|
-
await self._refresh_modal_display()
|
|
1942
|
-
|
|
1943
|
-
async def _handle_save_confirmation(self, key_press) -> bool:
|
|
1944
|
-
"""Handle y/n input for save confirmation."""
|
|
1945
|
-
if key_press.char and key_press.char.lower() == 'y':
|
|
1946
|
-
logger.info("💾 User confirmed save")
|
|
1947
|
-
self._pending_save_confirm = False
|
|
1948
|
-
if self.modal_renderer:
|
|
1949
|
-
self.modal_renderer._save_confirm_active = False
|
|
1950
|
-
await self._save_and_exit_modal()
|
|
1951
|
-
return True
|
|
1952
|
-
elif key_press.char and key_press.char.lower() == 'n':
|
|
1953
|
-
logger.info("🚫 User declined save")
|
|
1954
|
-
self._pending_save_confirm = False
|
|
1955
|
-
if self.modal_renderer:
|
|
1956
|
-
self.modal_renderer._save_confirm_active = False
|
|
1957
|
-
await self._exit_modal_mode()
|
|
1958
|
-
return True
|
|
1959
|
-
elif key_press.name == "Escape":
|
|
1960
|
-
# Cancel confirmation, stay in modal
|
|
1961
|
-
logger.info("↩️ User cancelled confirmation")
|
|
1962
|
-
self._pending_save_confirm = False
|
|
1963
|
-
if self.modal_renderer:
|
|
1964
|
-
self.modal_renderer._save_confirm_active = False
|
|
1965
|
-
await self._refresh_modal_display()
|
|
1966
|
-
return True
|
|
1967
|
-
return False
|
|
1968
|
-
|
|
1969
|
-
async def _save_and_exit_modal(self):
|
|
1970
|
-
"""Save modal changes and exit modal mode."""
|
|
1971
|
-
try:
|
|
1972
|
-
if self.modal_renderer and hasattr(
|
|
1973
|
-
self.modal_renderer, "action_handler"
|
|
1974
|
-
):
|
|
1975
|
-
# Get widget values and save them using proper action handler interface
|
|
1976
|
-
result = await self.modal_renderer.action_handler.handle_action(
|
|
1977
|
-
"save", self.modal_renderer.widgets
|
|
1978
|
-
)
|
|
1979
|
-
if result.get("success"):
|
|
1980
|
-
pass
|
|
1981
|
-
else:
|
|
1982
|
-
logger.warning(
|
|
1983
|
-
f"Failed to save modal changes: {result.get('message', 'Unknown error')}"
|
|
1984
|
-
)
|
|
1985
|
-
|
|
1986
|
-
await self._exit_modal_mode()
|
|
1987
|
-
except Exception as e:
|
|
1988
|
-
logger.error(f"Error saving and exiting modal: {e}")
|
|
1989
|
-
await self._exit_modal_mode()
|
|
1990
|
-
|
|
1991
|
-
async def _exit_modal_mode(self):
|
|
1992
|
-
"""Exit modal mode using existing patterns."""
|
|
1993
|
-
try:
|
|
1994
|
-
|
|
1995
|
-
# CRITICAL FIX: Complete terminal state restoration
|
|
1996
|
-
# Clear active area to remove modal artifacts
|
|
1997
|
-
self.renderer.clear_active_area()
|
|
1998
|
-
|
|
1999
|
-
# Clear any buffered modal content that might persist
|
|
2000
|
-
if hasattr(self.renderer, "message_renderer"):
|
|
2001
|
-
if hasattr(self.renderer.message_renderer, "buffer"):
|
|
2002
|
-
self.renderer.message_renderer.buffer.clear_buffer()
|
|
2003
|
-
|
|
2004
|
-
# CRITICAL FIX: Properly close modal with alternate buffer restoration
|
|
2005
|
-
if self.modal_renderer:
|
|
2006
|
-
# FIRST: Close modal and restore terminal state (alternate buffer)
|
|
2007
|
-
_ = self.modal_renderer.close_modal()
|
|
2008
|
-
|
|
2009
|
-
# THEN: Reset modal renderer widgets
|
|
2010
|
-
self.modal_renderer.widgets = []
|
|
2011
|
-
self.modal_renderer.focused_widget_index = 0
|
|
2012
|
-
self.modal_renderer = None
|
|
2013
|
-
|
|
2014
|
-
# Return to normal mode
|
|
2015
|
-
self.command_mode = CommandMode.NORMAL
|
|
2016
|
-
|
|
2017
|
-
# Complete display restoration with force refresh
|
|
2018
|
-
self.renderer.clear_active_area()
|
|
2019
|
-
await self._update_display(force_render=True)
|
|
2020
|
-
|
|
2021
|
-
# Ensure cursor is properly positioned
|
|
2022
|
-
# Note: cursor management handled by terminal_state
|
|
2023
|
-
|
|
2024
|
-
except Exception as e:
|
|
2025
|
-
logger.error(f"Error exiting modal mode: {e}")
|
|
2026
|
-
self.command_mode = CommandMode.NORMAL
|
|
2027
|
-
self.modal_renderer = None
|
|
2028
|
-
# Emergency cleanup
|
|
2029
|
-
self.renderer.clear_active_area()
|
|
2030
|
-
|
|
2031
|
-
async def _navigate_menu(self, direction: str) -> None:
|
|
2032
|
-
"""Navigate the command menu up or down.
|
|
2033
|
-
|
|
2034
|
-
Args:
|
|
2035
|
-
direction: "up" or "down"
|
|
2036
|
-
"""
|
|
2037
|
-
try:
|
|
2038
|
-
# Get current filtered commands
|
|
2039
|
-
current_input = self.buffer_manager.content
|
|
2040
|
-
filter_text = (
|
|
2041
|
-
current_input[1:] if current_input.startswith("/") else current_input
|
|
2042
|
-
)
|
|
2043
|
-
filtered_commands = self._filter_commands(filter_text)
|
|
2044
|
-
|
|
2045
|
-
if not filtered_commands:
|
|
2046
|
-
return
|
|
2047
|
-
|
|
2048
|
-
# Update selection index
|
|
2049
|
-
if direction == "up":
|
|
2050
|
-
self.selected_command_index = max(0, self.selected_command_index - 1)
|
|
2051
|
-
elif direction == "down":
|
|
2052
|
-
self.selected_command_index = min(
|
|
2053
|
-
len(filtered_commands) - 1, self.selected_command_index + 1
|
|
2054
|
-
)
|
|
2055
|
-
|
|
2056
|
-
# Update menu renderer with new selection (don't reset selection during navigation)
|
|
2057
|
-
self.command_menu_renderer.set_selected_index(
|
|
2058
|
-
self.selected_command_index
|
|
2059
|
-
)
|
|
2060
|
-
self.command_menu_renderer.filter_commands(
|
|
2061
|
-
filtered_commands, filter_text, reset_selection=False
|
|
2062
|
-
)
|
|
2063
|
-
|
|
2064
|
-
# Note: No need to call _update_display - filter_commands already renders the menu
|
|
2065
|
-
|
|
2066
|
-
except Exception as e:
|
|
2067
|
-
logger.error(f"Error navigating menu: {e}")
|
|
2068
|
-
|
|
2069
|
-
async def _update_command_filter(self) -> None:
|
|
2070
|
-
"""Update command menu based on current buffer content."""
|
|
2071
|
-
try:
|
|
2072
|
-
# Get current input (minus the leading '/')
|
|
2073
|
-
current_input = self.buffer_manager.content
|
|
2074
|
-
filter_text = (
|
|
2075
|
-
current_input[1:] if current_input.startswith("/") else current_input
|
|
2076
|
-
)
|
|
2077
|
-
|
|
2078
|
-
# Update menu renderer with filtered commands
|
|
2079
|
-
filtered_commands = self._filter_commands(filter_text)
|
|
2080
|
-
|
|
2081
|
-
# Reset selection when filtering
|
|
2082
|
-
self.selected_command_index = 0
|
|
2083
|
-
self.command_menu_renderer.set_selected_index(
|
|
2084
|
-
self.selected_command_index
|
|
2085
|
-
)
|
|
2086
|
-
self.command_menu_renderer.filter_commands(
|
|
2087
|
-
filtered_commands, filter_text
|
|
2088
|
-
)
|
|
2089
|
-
|
|
2090
|
-
# Emit filter update event
|
|
2091
|
-
await self.event_bus.emit_with_hooks(
|
|
2092
|
-
EventType.COMMAND_MENU_FILTER,
|
|
2093
|
-
{
|
|
2094
|
-
"filter_text": filter_text,
|
|
2095
|
-
"available_commands": self._get_available_commands(),
|
|
2096
|
-
"filtered_commands": filtered_commands,
|
|
2097
|
-
},
|
|
2098
|
-
"commands",
|
|
2099
|
-
)
|
|
2100
|
-
|
|
2101
|
-
# Update display
|
|
2102
|
-
await self._update_display(force_render=True)
|
|
2103
|
-
|
|
2104
|
-
except Exception as e:
|
|
2105
|
-
logger.error(f"Error updating command filter: {e}")
|
|
2106
|
-
|
|
2107
|
-
async def _execute_selected_command(self) -> None:
|
|
2108
|
-
"""Execute the currently selected command."""
|
|
2109
|
-
try:
|
|
2110
|
-
# Get the full command string from buffer (includes arguments)
|
|
2111
|
-
command_string = self.buffer_manager.content
|
|
2112
|
-
|
|
2113
|
-
# If buffer is empty or just '/', use the selected command from menu
|
|
2114
|
-
if not command_string or command_string == "/":
|
|
2115
|
-
selected_command = self.command_menu_renderer.get_selected_command()
|
|
2116
|
-
if not selected_command:
|
|
2117
|
-
logger.warning("No command selected")
|
|
2118
|
-
await self._exit_command_mode()
|
|
2119
|
-
return
|
|
2120
|
-
command_string = f"/{selected_command['name']}"
|
|
2121
|
-
|
|
2122
|
-
# Parse the command
|
|
2123
|
-
command = self.slash_parser.parse_command(command_string)
|
|
2124
|
-
if command:
|
|
2125
|
-
logger.info(f"🚀 Executing selected command: {command.name}")
|
|
2126
|
-
|
|
2127
|
-
# Exit command mode first
|
|
2128
|
-
await self._exit_command_mode()
|
|
2129
|
-
|
|
2130
|
-
# Execute the command
|
|
2131
|
-
result = await self.command_executor.execute_command(
|
|
2132
|
-
command, self.event_bus
|
|
2133
|
-
)
|
|
2134
|
-
|
|
2135
|
-
# Handle the result
|
|
2136
|
-
if result.success:
|
|
2137
|
-
logger.info(f"✅ Command {command.name} completed successfully")
|
|
2138
|
-
|
|
2139
|
-
# Modal display is handled by event bus trigger, not here
|
|
2140
|
-
if result.message:
|
|
2141
|
-
# Display success message in status area
|
|
2142
|
-
logger.info(f"Command result: {result.message}")
|
|
2143
|
-
# TODO: Display in status area
|
|
2144
|
-
else:
|
|
2145
|
-
logger.warning(
|
|
2146
|
-
f"❌ Command {command.name} failed: {result.message}"
|
|
2147
|
-
)
|
|
2148
|
-
# TODO: Display error message in status area
|
|
2149
|
-
else:
|
|
2150
|
-
logger.warning("Failed to parse selected command")
|
|
2151
|
-
await self._exit_command_mode()
|
|
2152
|
-
|
|
2153
|
-
except Exception as e:
|
|
2154
|
-
logger.error(f"Error executing command: {e}")
|
|
2155
|
-
await self._exit_command_mode()
|
|
2156
|
-
|
|
2157
|
-
def _get_available_commands(self) -> List[Dict[str, Any]]:
|
|
2158
|
-
"""Get list of available commands for menu display.
|
|
2159
|
-
|
|
2160
|
-
Returns:
|
|
2161
|
-
List of command dictionaries for menu rendering.
|
|
2162
|
-
"""
|
|
2163
|
-
commands = []
|
|
2164
|
-
command_defs = self.command_registry.list_commands()
|
|
2165
|
-
|
|
2166
|
-
for cmd_def in command_defs:
|
|
2167
|
-
commands.append(
|
|
2168
|
-
{
|
|
2169
|
-
"name": cmd_def.name,
|
|
2170
|
-
"description": cmd_def.description,
|
|
2171
|
-
"aliases": cmd_def.aliases,
|
|
2172
|
-
"category": cmd_def.category.value,
|
|
2173
|
-
"plugin": cmd_def.plugin_name,
|
|
2174
|
-
"icon": cmd_def.icon,
|
|
2175
|
-
}
|
|
2176
|
-
)
|
|
2177
|
-
|
|
2178
|
-
return commands
|
|
2179
|
-
|
|
2180
|
-
def _filter_commands(self, filter_text: str) -> List[Dict[str, Any]]:
|
|
2181
|
-
"""Filter commands based on input text.
|
|
2182
|
-
|
|
2183
|
-
Args:
|
|
2184
|
-
filter_text: Text to filter commands by.
|
|
2185
|
-
|
|
2186
|
-
Returns:
|
|
2187
|
-
List of filtered command dictionaries.
|
|
2188
|
-
"""
|
|
2189
|
-
if not filter_text:
|
|
2190
|
-
return self._get_available_commands()
|
|
2191
|
-
|
|
2192
|
-
# Use registry search functionality
|
|
2193
|
-
matching_defs = self.command_registry.search_commands(filter_text)
|
|
2194
|
-
|
|
2195
|
-
filtered_commands = []
|
|
2196
|
-
for cmd_def in matching_defs:
|
|
2197
|
-
filtered_commands.append(
|
|
2198
|
-
{
|
|
2199
|
-
"name": cmd_def.name,
|
|
2200
|
-
"description": cmd_def.description,
|
|
2201
|
-
"aliases": cmd_def.aliases,
|
|
2202
|
-
"category": cmd_def.category.value,
|
|
2203
|
-
"plugin": cmd_def.plugin_name,
|
|
2204
|
-
"icon": cmd_def.icon,
|
|
2205
|
-
}
|
|
2206
|
-
)
|
|
2207
|
-
|
|
2208
|
-
return filtered_commands
|
|
2209
|
-
|
|
2210
|
-
# ==================== STATUS MODAL METHODS ====================
|
|
392
|
+
# ==================== DELEGATING METHODS (for backward compatibility) ====================
|
|
2211
393
|
|
|
2212
394
|
async def _handle_status_modal_trigger(
|
|
2213
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
2214
|
-
) -> Dict[str, Any]:
|
|
2215
|
-
"""Handle status modal trigger events to show status modals.
|
|
2216
|
-
|
|
2217
|
-
Args:
|
|
2218
|
-
event_data: Event data containing modal configuration.
|
|
2219
|
-
context: Hook execution context.
|
|
2220
|
-
|
|
2221
|
-
Returns:
|
|
2222
|
-
Dictionary with status modal result.
|
|
2223
|
-
"""
|
|
2224
|
-
try:
|
|
2225
|
-
ui_config = event_data.get("ui_config")
|
|
2226
|
-
if ui_config:
|
|
2227
|
-
logger.info(f"Status modal trigger received: {ui_config.title}")
|
|
2228
|
-
logger.info(f"Status modal trigger UI config type: {ui_config.type}")
|
|
2229
|
-
await self._enter_status_modal_mode(ui_config)
|
|
2230
|
-
return {"success": True, "status_modal_activated": True}
|
|
2231
|
-
else:
|
|
2232
|
-
logger.warning("Status modal trigger received without ui_config")
|
|
2233
|
-
return {"success": False, "error": "Missing ui_config"}
|
|
2234
|
-
except Exception as e:
|
|
2235
|
-
logger.error(f"Error handling status modal trigger: {e}")
|
|
2236
|
-
return {"success": False, "error": str(e)}
|
|
2237
|
-
|
|
2238
|
-
async def _enter_status_modal_mode(self, ui_config):
|
|
2239
|
-
"""Enter status modal mode - modal confined to status area.
|
|
2240
|
-
|
|
2241
|
-
Args:
|
|
2242
|
-
ui_config: Status modal configuration.
|
|
2243
|
-
"""
|
|
2244
|
-
try:
|
|
2245
|
-
# Set status modal mode
|
|
2246
|
-
self.command_mode = CommandMode.STATUS_MODAL
|
|
2247
|
-
self.current_status_modal_config = ui_config
|
|
2248
|
-
logger.info(f"Entered status modal mode: {ui_config.title}")
|
|
2249
|
-
|
|
2250
|
-
# Unlike full modals, status modals don't take over the screen
|
|
2251
|
-
# They just appear in the status area via the renderer
|
|
2252
|
-
await self._update_display(force_render=True)
|
|
2253
|
-
|
|
2254
|
-
except Exception as e:
|
|
2255
|
-
logger.error(f"Error entering status modal mode: {e}")
|
|
2256
|
-
await self._exit_command_mode()
|
|
2257
|
-
|
|
2258
|
-
async def _handle_status_modal_keypress(self, key_press: KeyPress) -> bool:
|
|
2259
|
-
"""Handle keypress during status modal mode.
|
|
2260
|
-
|
|
2261
|
-
Args:
|
|
2262
|
-
key_press: Parsed key press to process.
|
|
2263
|
-
|
|
2264
|
-
Returns:
|
|
2265
|
-
True if key was handled, False otherwise.
|
|
2266
|
-
"""
|
|
2267
|
-
try:
|
|
2268
|
-
logger.info(
|
|
2269
|
-
f"Status modal received key: name='{key_press.name}', char='{key_press.char}', code={key_press.code}"
|
|
2270
|
-
)
|
|
2271
|
-
|
|
2272
|
-
if key_press.name == "Escape":
|
|
2273
|
-
logger.info("Escape key detected, closing status modal")
|
|
2274
|
-
await self._exit_status_modal_mode()
|
|
2275
|
-
return True
|
|
2276
|
-
elif key_press.name == "Enter":
|
|
2277
|
-
logger.info("Enter key detected, closing status modal")
|
|
2278
|
-
await self._exit_status_modal_mode()
|
|
2279
|
-
return True
|
|
2280
|
-
elif key_press.char and ord(key_press.char) == 3: # Ctrl+C
|
|
2281
|
-
logger.info("Ctrl+C detected, closing status modal")
|
|
2282
|
-
await self._exit_status_modal_mode()
|
|
2283
|
-
return True
|
|
2284
|
-
else:
|
|
2285
|
-
logger.info(f"Unhandled key in status modal: {key_press.name}")
|
|
2286
|
-
return True
|
|
2287
|
-
|
|
2288
|
-
except Exception as e:
|
|
2289
|
-
logger.error(f"Error handling status modal keypress: {e}")
|
|
2290
|
-
await self._exit_status_modal_mode()
|
|
2291
|
-
return False
|
|
2292
|
-
|
|
2293
|
-
async def _handle_status_modal_input(self, char: str) -> bool:
|
|
2294
|
-
"""Handle input during status modal mode.
|
|
2295
|
-
|
|
2296
|
-
Args:
|
|
2297
|
-
char: Character input to process.
|
|
2298
|
-
|
|
2299
|
-
Returns:
|
|
2300
|
-
True if input was handled, False otherwise.
|
|
2301
|
-
"""
|
|
2302
|
-
try:
|
|
2303
|
-
# For now, ignore character input in status modals
|
|
2304
|
-
# Could add search/filter functionality later
|
|
2305
|
-
return True
|
|
2306
|
-
except Exception as e:
|
|
2307
|
-
logger.error(f"Error handling status modal input: {e}")
|
|
2308
|
-
await self._exit_status_modal_mode()
|
|
2309
|
-
return False
|
|
2310
|
-
|
|
2311
|
-
async def _exit_status_modal_mode(self):
|
|
2312
|
-
"""Exit status modal mode and return to normal input."""
|
|
2313
|
-
try:
|
|
2314
|
-
logger.info("Exiting status modal mode...")
|
|
2315
|
-
self.command_mode = CommandMode.NORMAL
|
|
2316
|
-
self.current_status_modal_config = None
|
|
2317
|
-
logger.info("Status modal mode exited successfully")
|
|
2318
|
-
|
|
2319
|
-
# Refresh display to remove the status modal
|
|
2320
|
-
await self._update_display(force_render=True)
|
|
2321
|
-
logger.info("Display updated after status modal exit")
|
|
2322
|
-
|
|
2323
|
-
except Exception as e:
|
|
2324
|
-
logger.error(f"Error exiting status modal mode: {e}")
|
|
2325
|
-
self.command_mode = CommandMode.NORMAL
|
|
2326
|
-
|
|
2327
|
-
# ==================== Live Modal Mode ====================
|
|
2328
|
-
# For live-updating content like tmux sessions, log tailing, etc.
|
|
2329
|
-
|
|
2330
|
-
async def enter_live_modal_mode(
|
|
2331
|
-
self,
|
|
2332
|
-
content_generator,
|
|
2333
|
-
config=None,
|
|
2334
|
-
input_callback=None
|
|
395
|
+
self, event_data: Dict[str, Any], context: Optional[str] = None
|
|
2335
396
|
) -> Dict[str, Any]:
|
|
2336
|
-
"""
|
|
2337
|
-
|
|
2338
|
-
This is non-blocking - it starts the modal and returns immediately.
|
|
2339
|
-
The input loop continues to process keys, routing them to the modal.
|
|
2340
|
-
Press Escape to exit the modal.
|
|
2341
|
-
|
|
2342
|
-
Args:
|
|
2343
|
-
content_generator: Function returning List[str] of current content.
|
|
2344
|
-
config: LiveModalConfig instance.
|
|
2345
|
-
input_callback: Optional callback for input passthrough.
|
|
2346
|
-
|
|
2347
|
-
Returns:
|
|
2348
|
-
Result dict indicating modal was started.
|
|
2349
|
-
"""
|
|
2350
|
-
try:
|
|
2351
|
-
from ..ui.live_modal_renderer import LiveModalRenderer, LiveModalConfig
|
|
2352
|
-
|
|
2353
|
-
# Store state
|
|
2354
|
-
self.command_mode = CommandMode.LIVE_MODAL
|
|
2355
|
-
self.live_modal_content_generator = content_generator
|
|
2356
|
-
self.live_modal_input_callback = input_callback
|
|
2357
|
-
|
|
2358
|
-
# Create and store the live modal renderer
|
|
2359
|
-
terminal_state = self.renderer.terminal_state
|
|
2360
|
-
self.live_modal_renderer = LiveModalRenderer(terminal_state)
|
|
2361
|
-
|
|
2362
|
-
# Use default config if none provided
|
|
2363
|
-
if config is None:
|
|
2364
|
-
config = LiveModalConfig()
|
|
2365
|
-
|
|
2366
|
-
logger.info(f"Entering live modal mode: {config.title}")
|
|
2367
|
-
|
|
2368
|
-
# Start the live modal (non-blocking)
|
|
2369
|
-
# The refresh loop runs as a background task
|
|
2370
|
-
# Input will be handled by _handle_live_modal_keypress
|
|
2371
|
-
success = self.live_modal_renderer.start_live_modal(
|
|
2372
|
-
content_generator,
|
|
2373
|
-
config,
|
|
2374
|
-
input_callback
|
|
2375
|
-
)
|
|
2376
|
-
|
|
2377
|
-
if success:
|
|
2378
|
-
return {"success": True, "modal_started": True}
|
|
2379
|
-
else:
|
|
2380
|
-
await self._exit_live_modal_mode()
|
|
2381
|
-
return {"success": False, "error": "Failed to start modal"}
|
|
2382
|
-
|
|
2383
|
-
except Exception as e:
|
|
2384
|
-
logger.error(f"Error entering live modal mode: {e}")
|
|
2385
|
-
await self._exit_live_modal_mode()
|
|
2386
|
-
return {"success": False, "error": str(e)}
|
|
2387
|
-
|
|
2388
|
-
async def _handle_live_modal_keypress(self, key_press: "KeyPress") -> bool:
|
|
2389
|
-
"""Handle keypress during live modal mode.
|
|
2390
|
-
|
|
2391
|
-
Args:
|
|
2392
|
-
key_press: Parsed key press to process.
|
|
2393
|
-
|
|
2394
|
-
Returns:
|
|
2395
|
-
True if key was handled.
|
|
2396
|
-
"""
|
|
2397
|
-
try:
|
|
2398
|
-
logger.info(
|
|
2399
|
-
f"LIVE_MODAL_KEY: name='{key_press.name}', char='{key_press.char}', code={key_press.code}"
|
|
2400
|
-
)
|
|
2401
|
-
|
|
2402
|
-
# Forward to live modal renderer
|
|
2403
|
-
if self.live_modal_renderer:
|
|
2404
|
-
should_close = await self.live_modal_renderer.handle_input(key_press)
|
|
2405
|
-
if should_close:
|
|
2406
|
-
await self._exit_live_modal_mode()
|
|
2407
|
-
return True
|
|
2408
|
-
|
|
2409
|
-
# Fallback: Escape always exits
|
|
2410
|
-
if key_press.name == "Escape":
|
|
2411
|
-
await self._exit_live_modal_mode()
|
|
2412
|
-
return True
|
|
2413
|
-
|
|
2414
|
-
return True
|
|
2415
|
-
|
|
2416
|
-
except Exception as e:
|
|
2417
|
-
logger.error(f"Error handling live modal keypress: {e}")
|
|
2418
|
-
await self._exit_live_modal_mode()
|
|
2419
|
-
return False
|
|
2420
|
-
|
|
2421
|
-
async def _handle_live_modal_input(self, char: str) -> bool:
|
|
2422
|
-
"""Handle character input during live modal mode.
|
|
2423
|
-
|
|
2424
|
-
Args:
|
|
2425
|
-
char: Character input to process.
|
|
2426
|
-
|
|
2427
|
-
Returns:
|
|
2428
|
-
True if input was handled.
|
|
2429
|
-
"""
|
|
2430
|
-
try:
|
|
2431
|
-
# Convert char to KeyPress for consistent handling
|
|
2432
|
-
from .key_parser import KeyPress
|
|
2433
|
-
key_press = KeyPress(char=char, name=None, code=ord(char) if char else 0)
|
|
2434
|
-
return await self._handle_live_modal_keypress(key_press)
|
|
2435
|
-
|
|
2436
|
-
except Exception as e:
|
|
2437
|
-
logger.error(f"Error handling live modal input: {e}")
|
|
2438
|
-
await self._exit_live_modal_mode()
|
|
2439
|
-
return False
|
|
2440
|
-
|
|
2441
|
-
async def _exit_live_modal_mode(self):
|
|
2442
|
-
"""Exit live modal mode and restore terminal."""
|
|
2443
|
-
try:
|
|
2444
|
-
logger.info("Exiting live modal mode...")
|
|
2445
|
-
|
|
2446
|
-
# Close the live modal renderer (restores from alt buffer)
|
|
2447
|
-
if self.live_modal_renderer:
|
|
2448
|
-
await self.live_modal_renderer.close_modal()
|
|
2449
|
-
|
|
2450
|
-
# Reset state
|
|
2451
|
-
self.command_mode = CommandMode.NORMAL
|
|
2452
|
-
self.live_modal_renderer = None
|
|
2453
|
-
self.live_modal_content_generator = None
|
|
2454
|
-
self.live_modal_input_callback = None
|
|
2455
|
-
|
|
2456
|
-
# Force display refresh with full redraw
|
|
2457
|
-
self.renderer.clear_active_area()
|
|
2458
|
-
await self._update_display(force_render=True)
|
|
2459
|
-
|
|
2460
|
-
logger.info("Live modal mode exited successfully")
|
|
2461
|
-
|
|
2462
|
-
except Exception as e:
|
|
2463
|
-
logger.error(f"Error exiting live modal mode: {e}")
|
|
2464
|
-
self.command_mode = CommandMode.NORMAL
|
|
2465
|
-
|
|
2466
|
-
async def _handle_status_modal_render(
|
|
2467
|
-
self, event_data: Dict[str, Any], context: str = None
|
|
2468
|
-
) -> Dict[str, Any]:
|
|
2469
|
-
"""Handle status modal render events to provide modal display lines.
|
|
2470
|
-
|
|
2471
|
-
Args:
|
|
2472
|
-
event_data: Event data containing render request.
|
|
2473
|
-
context: Hook execution context.
|
|
2474
|
-
|
|
2475
|
-
Returns:
|
|
2476
|
-
Dictionary with status modal lines if active.
|
|
2477
|
-
"""
|
|
2478
|
-
try:
|
|
2479
|
-
if (
|
|
2480
|
-
self.command_mode == CommandMode.STATUS_MODAL
|
|
2481
|
-
and self.current_status_modal_config
|
|
2482
|
-
):
|
|
2483
|
-
|
|
2484
|
-
# Generate status modal display lines
|
|
2485
|
-
modal_lines = self._generate_status_modal_lines(
|
|
2486
|
-
self.current_status_modal_config
|
|
2487
|
-
)
|
|
2488
|
-
|
|
2489
|
-
return {"success": True, "status_modal_lines": modal_lines}
|
|
2490
|
-
else:
|
|
2491
|
-
return {"success": True, "status_modal_lines": []}
|
|
2492
|
-
|
|
2493
|
-
except Exception as e:
|
|
2494
|
-
logger.error(f"Error handling status modal render: {e}")
|
|
2495
|
-
return {"success": False, "status_modal_lines": []}
|
|
2496
|
-
|
|
2497
|
-
def _generate_status_modal_lines(self, ui_config) -> List[str]:
|
|
2498
|
-
"""Generate formatted lines for status modal display using visual effects.
|
|
2499
|
-
|
|
2500
|
-
Args:
|
|
2501
|
-
ui_config: UI configuration for the status modal.
|
|
2502
|
-
|
|
2503
|
-
Returns:
|
|
2504
|
-
List of formatted lines for display.
|
|
2505
|
-
"""
|
|
2506
|
-
try:
|
|
2507
|
-
# Get dynamic terminal width
|
|
2508
|
-
terminal_width = getattr(self.renderer.terminal_state, "width", 80)
|
|
2509
|
-
# Reserve space for borders and padding (│ content │ = 4 chars total)
|
|
2510
|
-
content_width = terminal_width - 6 # Leave 6 for borders/padding
|
|
2511
|
-
max_line_length = content_width - 4 # Additional safety margin
|
|
2512
|
-
|
|
2513
|
-
content_lines = []
|
|
2514
|
-
|
|
2515
|
-
# Modal content based on config (no duplicate headers)
|
|
2516
|
-
modal_config = ui_config.modal_config or {}
|
|
2517
|
-
|
|
2518
|
-
if "sections" in modal_config:
|
|
2519
|
-
for section in modal_config["sections"]:
|
|
2520
|
-
# Skip section title since it's redundant with modal title
|
|
2521
|
-
# Display commands directly
|
|
2522
|
-
commands = section.get("commands", [])
|
|
2523
|
-
for cmd in commands:
|
|
2524
|
-
name = cmd.get("name", "")
|
|
2525
|
-
description = cmd.get("description", "")
|
|
2526
|
-
|
|
2527
|
-
# Format command line with better alignment, using dynamic width
|
|
2528
|
-
cmd_line = f"{name:<28} {description}"
|
|
2529
|
-
if len(cmd_line) > max_line_length:
|
|
2530
|
-
cmd_line = cmd_line[: max_line_length - 3] + "..."
|
|
2531
|
-
|
|
2532
|
-
content_lines.append(cmd_line)
|
|
2533
|
-
|
|
2534
|
-
# Add spacing before footer
|
|
2535
|
-
content_lines.append("")
|
|
2536
|
-
|
|
2537
|
-
# Modal footer with special styling marker
|
|
2538
|
-
footer = modal_config.get(
|
|
2539
|
-
"footer",
|
|
2540
|
-
"Press Esc to close • Use /help <command> for detailed help",
|
|
2541
|
-
)
|
|
2542
|
-
content_lines.append(f"__FOOTER__{footer}")
|
|
2543
|
-
|
|
2544
|
-
# Clean content lines for box rendering (no ANSI codes)
|
|
2545
|
-
clean_content = []
|
|
2546
|
-
for line in content_lines:
|
|
2547
|
-
if line.startswith("__FOOTER__"):
|
|
2548
|
-
footer_text = line.replace("__FOOTER__", "")
|
|
2549
|
-
clean_content.append(footer_text)
|
|
2550
|
-
else:
|
|
2551
|
-
clean_content.append(line)
|
|
2552
|
-
|
|
2553
|
-
# Use BoxRenderer from enhanced input plugin if available
|
|
2554
|
-
try:
|
|
2555
|
-
from ..plugins.enhanced_input.box_renderer import BoxRenderer
|
|
2556
|
-
from ..plugins.enhanced_input.box_styles import (
|
|
2557
|
-
BoxStyleRegistry,
|
|
2558
|
-
)
|
|
2559
|
-
from ..plugins.enhanced_input.color_engine import ColorEngine
|
|
2560
|
-
from ..plugins.enhanced_input.geometry import (
|
|
2561
|
-
GeometryCalculator,
|
|
2562
|
-
)
|
|
2563
|
-
from ..plugins.enhanced_input.text_processor import (
|
|
2564
|
-
TextProcessor,
|
|
2565
|
-
)
|
|
2566
|
-
|
|
2567
|
-
# Initialize components
|
|
2568
|
-
style_registry = BoxStyleRegistry()
|
|
2569
|
-
color_engine = ColorEngine()
|
|
2570
|
-
geometry = GeometryCalculator()
|
|
2571
|
-
text_processor = TextProcessor()
|
|
2572
|
-
box_renderer = BoxRenderer(
|
|
2573
|
-
style_registry, color_engine, geometry, text_processor
|
|
2574
|
-
)
|
|
2575
|
-
|
|
2576
|
-
# Render with clean rounded style first, using dynamic width
|
|
2577
|
-
bordered_lines = box_renderer.render_box(
|
|
2578
|
-
clean_content, content_width, "rounded"
|
|
2579
|
-
)
|
|
2580
|
-
|
|
2581
|
-
# Add title to top border
|
|
2582
|
-
title = ui_config.title or "Status Modal"
|
|
2583
|
-
if bordered_lines:
|
|
2584
|
-
_ = bordered_lines[0]
|
|
2585
|
-
# Create title border: ╭─ Title ─────...─╮
|
|
2586
|
-
title_text = f"─ {title} "
|
|
2587
|
-
remaining_width = max(
|
|
2588
|
-
0, content_width - 2 - len(title_text)
|
|
2589
|
-
) # content_width - 2 border chars - title length
|
|
2590
|
-
titled_border = f"╭{title_text}{'─' * remaining_width}╮"
|
|
2591
|
-
bordered_lines[0] = titled_border
|
|
2592
|
-
|
|
2593
|
-
# Apply styling to content lines after border rendering
|
|
2594
|
-
styled_lines = []
|
|
2595
|
-
for i, line in enumerate(bordered_lines):
|
|
2596
|
-
if i == 0 or i == len(bordered_lines) - 1:
|
|
2597
|
-
# Border lines - keep as is
|
|
2598
|
-
styled_lines.append(line)
|
|
2599
|
-
elif line.strip() and "│" in line:
|
|
2600
|
-
# Content lines with borders
|
|
2601
|
-
if any(
|
|
2602
|
-
footer in line for footer in ["Press Esc", "Use /help"]
|
|
2603
|
-
):
|
|
2604
|
-
# Footer line - apply cyan
|
|
2605
|
-
styled_line = line.replace("│", "│\033[2;36m", 1)
|
|
2606
|
-
styled_line = styled_line.replace("│", "\033[0m│", -1)
|
|
2607
|
-
styled_lines.append(styled_line)
|
|
2608
|
-
elif line.strip() != "│" + " " * 76 + "│": # Not empty line
|
|
2609
|
-
# Command line - apply dim
|
|
2610
|
-
styled_line = line.replace("│", "│\033[2m", 1)
|
|
2611
|
-
styled_line = styled_line.replace("│", "\033[0m│", -1)
|
|
2612
|
-
styled_lines.append(styled_line)
|
|
2613
|
-
else:
|
|
2614
|
-
# Empty line
|
|
2615
|
-
styled_lines.append(line)
|
|
2616
|
-
else:
|
|
2617
|
-
styled_lines.append(line)
|
|
2618
|
-
|
|
2619
|
-
return styled_lines
|
|
2620
|
-
|
|
2621
|
-
except ImportError:
|
|
2622
|
-
# Fallback to simple manual borders if enhanced input not available
|
|
2623
|
-
return self._create_simple_bordered_content(clean_content)
|
|
2624
|
-
|
|
2625
|
-
except Exception as e:
|
|
2626
|
-
logger.error(f"Error generating status modal lines: {e}")
|
|
2627
|
-
return [f"Error displaying status modal: {e}"]
|
|
2628
|
-
|
|
2629
|
-
def _create_simple_bordered_content(self, content_lines: List[str]) -> List[str]:
|
|
2630
|
-
"""Create simple bordered content as fallback.
|
|
2631
|
-
|
|
2632
|
-
Args:
|
|
2633
|
-
content_lines: Content lines to border.
|
|
2634
|
-
|
|
2635
|
-
Returns:
|
|
2636
|
-
Lines with simple borders.
|
|
2637
|
-
"""
|
|
2638
|
-
# Get dynamic terminal width
|
|
2639
|
-
terminal_width = getattr(self.renderer.terminal_state, "width", 80)
|
|
2640
|
-
# Reserve space for borders and padding
|
|
2641
|
-
width = terminal_width - 6 # Leave 6 for borders/padding
|
|
2642
|
-
lines = []
|
|
397
|
+
"""Delegate to ModalController."""
|
|
398
|
+
return await self._modal_controller._handle_status_modal_trigger(event_data, context)
|
|
2643
399
|
|
|
2644
|
-
|
|
2645
|
-
|
|
400
|
+
async def _enter_status_modal_mode(self, ui_config) -> None:
|
|
401
|
+
"""Delegate to ModalController."""
|
|
402
|
+
return await self._modal_controller._enter_status_modal_mode(ui_config)
|
|
2646
403
|
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
lines.append(f"│ {padded_line} │")
|
|
404
|
+
async def _handle_status_modal_keypress(self, key_press) -> bool:
|
|
405
|
+
"""Delegate to ModalController."""
|
|
406
|
+
return await self._modal_controller._handle_status_modal_keypress(key_press)
|
|
2651
407
|
|
|
2652
|
-
|
|
2653
|
-
|
|
408
|
+
async def _exit_status_modal_mode(self) -> None:
|
|
409
|
+
"""Delegate to ModalController."""
|
|
410
|
+
return await self._modal_controller._exit_status_modal_mode()
|
|
2654
411
|
|
|
2655
|
-
|
|
412
|
+
def _generate_status_modal_lines(self, ui_config) -> list:
|
|
413
|
+
"""Delegate to StatusModalRenderer."""
|
|
414
|
+
return self._status_modal_renderer.generate_status_modal_lines(ui_config)
|