kollabor 0.4.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
"""Raw input processing system for Kollabor CLI - extracted from InputHandler."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import Optional, Dict, Any, Callable
|
|
8
|
+
|
|
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
|
+
from ..events.models import CommandMode
|
|
19
|
+
from .key_parser import KeyParser, KeyPress, KeyType as KeyTypeEnum
|
|
20
|
+
from .buffer_manager import BufferManager
|
|
21
|
+
from .input_errors import InputErrorHandler, ErrorType, ErrorSeverity
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RawInputProcessor:
|
|
27
|
+
"""Handles raw terminal input processing, key parsing, and buffer management.
|
|
28
|
+
|
|
29
|
+
This component is responsible for:
|
|
30
|
+
- Main input loop with select() polling
|
|
31
|
+
- Raw data reading and chunking
|
|
32
|
+
- Escape sequence detection
|
|
33
|
+
- Character parsing and key press handling
|
|
34
|
+
- Display updates and cursor management
|
|
35
|
+
- Paste detection integration
|
|
36
|
+
- Basic event processing (Enter, Escape)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
event_bus,
|
|
42
|
+
renderer,
|
|
43
|
+
config,
|
|
44
|
+
buffer_manager: BufferManager,
|
|
45
|
+
key_parser: KeyParser,
|
|
46
|
+
error_handler: InputErrorHandler,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize the raw input processor.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
event_bus: Event bus for emitting input events.
|
|
52
|
+
renderer: Terminal renderer for updating input display.
|
|
53
|
+
config: Configuration manager for input settings.
|
|
54
|
+
buffer_manager: Buffer manager for text operations.
|
|
55
|
+
key_parser: Key parser for handling escape sequences.
|
|
56
|
+
error_handler: Error handler for input errors.
|
|
57
|
+
"""
|
|
58
|
+
self.event_bus = event_bus
|
|
59
|
+
self.renderer = renderer
|
|
60
|
+
self.config = config
|
|
61
|
+
self.buffer_manager = buffer_manager
|
|
62
|
+
self.key_parser = key_parser
|
|
63
|
+
self.error_handler = error_handler
|
|
64
|
+
|
|
65
|
+
# Control flags
|
|
66
|
+
self.running = False
|
|
67
|
+
self.rendering_paused = (
|
|
68
|
+
False # Flag to pause rendering during special effects
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Load configurable parameters
|
|
72
|
+
self.polling_delay = config.get("input.polling_delay", 0.01)
|
|
73
|
+
self.error_delay = config.get("input.error_delay", 0.1)
|
|
74
|
+
|
|
75
|
+
# Paste detection configuration
|
|
76
|
+
self.paste_detection_enabled = False
|
|
77
|
+
|
|
78
|
+
# State tracking
|
|
79
|
+
self._last_cursor_pos = 0
|
|
80
|
+
|
|
81
|
+
# Simple paste detection state
|
|
82
|
+
self._paste_buffer = []
|
|
83
|
+
self._last_char_time = 0
|
|
84
|
+
# GENIUS PASTE SYSTEM - immediate synchronous storage
|
|
85
|
+
self._paste_bucket = {} # {paste_id: actual_content}
|
|
86
|
+
self._paste_counter = 0 # Counter for paste numbering
|
|
87
|
+
self._current_paste_id = None # Currently building paste ID
|
|
88
|
+
self._last_paste_time = 0 # Last chunk timestamp
|
|
89
|
+
|
|
90
|
+
# Callbacks for delegation back to InputHandler
|
|
91
|
+
self.on_command_mode_keypress: Optional[Callable] = None
|
|
92
|
+
self.on_prevent_default_check: Optional[Callable] = None
|
|
93
|
+
self.get_command_mode: Optional[Callable] = None
|
|
94
|
+
self.on_status_view_previous: Optional[Callable] = None
|
|
95
|
+
self.on_status_view_next: Optional[Callable] = None
|
|
96
|
+
|
|
97
|
+
logger.info("Raw input processor initialized")
|
|
98
|
+
|
|
99
|
+
def set_callbacks(
|
|
100
|
+
self,
|
|
101
|
+
on_command_mode_keypress: Callable,
|
|
102
|
+
on_prevent_default_check: Callable,
|
|
103
|
+
get_command_mode: Callable,
|
|
104
|
+
on_status_view_previous: Callable,
|
|
105
|
+
on_status_view_next: Callable,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Set callbacks for delegation back to InputHandler."""
|
|
108
|
+
self.on_command_mode_keypress = on_command_mode_keypress
|
|
109
|
+
self.on_prevent_default_check = on_prevent_default_check
|
|
110
|
+
self.get_command_mode = get_command_mode
|
|
111
|
+
self.on_status_view_previous = on_status_view_previous
|
|
112
|
+
self.on_status_view_next = on_status_view_next
|
|
113
|
+
|
|
114
|
+
async def start_input_loop(self) -> None:
|
|
115
|
+
"""Start the input processing loop."""
|
|
116
|
+
self.running = True
|
|
117
|
+
await self._input_loop()
|
|
118
|
+
|
|
119
|
+
def stop_input_loop(self) -> None:
|
|
120
|
+
"""Stop the input processing loop."""
|
|
121
|
+
self.running = False
|
|
122
|
+
|
|
123
|
+
def pause_rendering(self):
|
|
124
|
+
"""Pause all UI rendering for special effects."""
|
|
125
|
+
self.rendering_paused = True
|
|
126
|
+
logger.debug("Input rendering paused")
|
|
127
|
+
|
|
128
|
+
def resume_rendering(self):
|
|
129
|
+
"""Resume normal UI rendering."""
|
|
130
|
+
self.rendering_paused = False
|
|
131
|
+
logger.debug("Input rendering resumed")
|
|
132
|
+
|
|
133
|
+
async def _input_loop(self) -> None:
|
|
134
|
+
"""Main input processing loop with enhanced error handling."""
|
|
135
|
+
while self.running:
|
|
136
|
+
try:
|
|
137
|
+
# Platform-specific input checking
|
|
138
|
+
has_input = await self._check_input_available()
|
|
139
|
+
|
|
140
|
+
if has_input:
|
|
141
|
+
# Read input data
|
|
142
|
+
chunk = await self._read_input_chunk()
|
|
143
|
+
|
|
144
|
+
if not chunk:
|
|
145
|
+
await asyncio.sleep(self.polling_delay)
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Check if this is an escape sequence (arrow keys, etc.)
|
|
149
|
+
def is_escape_sequence(text: str) -> bool:
|
|
150
|
+
"""Check if input is an escape sequence that should bypass paste detection."""
|
|
151
|
+
if not text:
|
|
152
|
+
return False
|
|
153
|
+
# Common escape sequences start with ESC (\x1b)
|
|
154
|
+
if text.startswith("\x1b"):
|
|
155
|
+
return True
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# If we got multiple characters, check if it's an escape sequence first
|
|
159
|
+
if len(chunk) > 10 and not is_escape_sequence(chunk):
|
|
160
|
+
|
|
161
|
+
import time
|
|
162
|
+
|
|
163
|
+
current_time = time.time()
|
|
164
|
+
|
|
165
|
+
# Check if this continues the current paste (within 100ms)
|
|
166
|
+
if (
|
|
167
|
+
self._current_paste_id
|
|
168
|
+
and self._last_paste_time > 0
|
|
169
|
+
and (current_time - self._last_paste_time) < 0.1
|
|
170
|
+
):
|
|
171
|
+
|
|
172
|
+
# Merge with existing paste
|
|
173
|
+
self._paste_bucket[self._current_paste_id] += chunk
|
|
174
|
+
self._last_paste_time = current_time
|
|
175
|
+
|
|
176
|
+
# Update the placeholder to show new size
|
|
177
|
+
await self._update_paste_placeholder()
|
|
178
|
+
else:
|
|
179
|
+
# New paste - store immediately
|
|
180
|
+
self._paste_counter += 1
|
|
181
|
+
self._current_paste_id = f"PASTE_{self._paste_counter}"
|
|
182
|
+
self._paste_bucket[self._current_paste_id] = chunk
|
|
183
|
+
self._last_paste_time = current_time
|
|
184
|
+
|
|
185
|
+
# Create placeholder immediately
|
|
186
|
+
await self._create_paste_placeholder(
|
|
187
|
+
self._current_paste_id
|
|
188
|
+
)
|
|
189
|
+
elif is_escape_sequence(chunk):
|
|
190
|
+
# Escape sequence - process character by character to allow key parser to handle it
|
|
191
|
+
logger.debug(
|
|
192
|
+
f"Processing escape sequence character-by-character: {repr(chunk)}"
|
|
193
|
+
)
|
|
194
|
+
for char in chunk:
|
|
195
|
+
await self._process_character(char)
|
|
196
|
+
else:
|
|
197
|
+
# Normal input (single or multi-character) - process each character individually
|
|
198
|
+
logger.info(
|
|
199
|
+
f"🔤 Processing normal input character-by-character: {repr(chunk)}"
|
|
200
|
+
)
|
|
201
|
+
# await self._process_character(chunk)
|
|
202
|
+
for char in chunk:
|
|
203
|
+
await self._process_character(char)
|
|
204
|
+
else:
|
|
205
|
+
# No input available - check for standalone ESC key
|
|
206
|
+
esc_key = self.key_parser.check_for_standalone_escape()
|
|
207
|
+
if esc_key and self.on_command_mode_keypress:
|
|
208
|
+
logger.info("DETECTED STANDALONE ESC KEY!")
|
|
209
|
+
# CRITICAL FIX: Route escape to command mode handler if in modal mode
|
|
210
|
+
command_mode = (
|
|
211
|
+
self.get_command_mode()
|
|
212
|
+
if self.get_command_mode
|
|
213
|
+
else CommandMode.NORMAL
|
|
214
|
+
)
|
|
215
|
+
if command_mode == CommandMode.MODAL:
|
|
216
|
+
await self.on_command_mode_keypress(esc_key)
|
|
217
|
+
else:
|
|
218
|
+
await self._handle_key_press(esc_key)
|
|
219
|
+
|
|
220
|
+
await asyncio.sleep(self.polling_delay)
|
|
221
|
+
|
|
222
|
+
except KeyboardInterrupt:
|
|
223
|
+
logger.info("Ctrl+C received")
|
|
224
|
+
raise
|
|
225
|
+
except OSError as e:
|
|
226
|
+
await self.error_handler.handle_error(
|
|
227
|
+
ErrorType.IO_ERROR,
|
|
228
|
+
f"I/O error in input loop: {e}",
|
|
229
|
+
ErrorSeverity.HIGH,
|
|
230
|
+
{"buffer_manager": self.buffer_manager},
|
|
231
|
+
)
|
|
232
|
+
await asyncio.sleep(self.error_delay)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
await self.error_handler.handle_error(
|
|
235
|
+
ErrorType.SYSTEM_ERROR,
|
|
236
|
+
f"Unexpected error in input loop: {e}",
|
|
237
|
+
ErrorSeverity.MEDIUM,
|
|
238
|
+
{"buffer_manager": self.buffer_manager},
|
|
239
|
+
)
|
|
240
|
+
await asyncio.sleep(self.error_delay)
|
|
241
|
+
|
|
242
|
+
async def _check_input_available(self) -> bool:
|
|
243
|
+
"""Check if input is available (cross-platform).
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
True if input is available, False otherwise.
|
|
247
|
+
"""
|
|
248
|
+
if IS_WINDOWS:
|
|
249
|
+
# Windows: Use msvcrt.kbhit() to check for available input
|
|
250
|
+
return msvcrt.kbhit()
|
|
251
|
+
else:
|
|
252
|
+
# Unix: Use select with timeout
|
|
253
|
+
return bool(select.select([sys.stdin], [], [], self.polling_delay)[0])
|
|
254
|
+
|
|
255
|
+
async def _read_input_chunk(self) -> str:
|
|
256
|
+
"""Read available input data (cross-platform).
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Decoded input string, or empty string if no input.
|
|
260
|
+
"""
|
|
261
|
+
import os
|
|
262
|
+
|
|
263
|
+
if IS_WINDOWS:
|
|
264
|
+
# Windows: Read all available characters using msvcrt
|
|
265
|
+
chunk = b""
|
|
266
|
+
while msvcrt.kbhit():
|
|
267
|
+
char = msvcrt.getch()
|
|
268
|
+
char_code = char[0] if isinstance(char, bytes) else ord(char)
|
|
269
|
+
|
|
270
|
+
# Handle Windows extended keys (arrow keys, function keys, etc.)
|
|
271
|
+
# Extended keys are prefixed with 0x00 or 0xE0 (224)
|
|
272
|
+
if char_code in (0, 224):
|
|
273
|
+
# Read the actual key code
|
|
274
|
+
ext_char = msvcrt.getch()
|
|
275
|
+
ext_code = ext_char[0] if isinstance(ext_char, bytes) else ord(ext_char)
|
|
276
|
+
# Map Windows extended key codes to ANSI escape sequences
|
|
277
|
+
win_key_map = {
|
|
278
|
+
72: b"\x1b[A", # ArrowUp
|
|
279
|
+
80: b"\x1b[B", # ArrowDown
|
|
280
|
+
75: b"\x1b[D", # ArrowLeft
|
|
281
|
+
77: b"\x1b[C", # ArrowRight
|
|
282
|
+
71: b"\x1b[H", # Home
|
|
283
|
+
79: b"\x1b[F", # End
|
|
284
|
+
73: b"\x1b[5~", # PageUp
|
|
285
|
+
81: b"\x1b[6~", # PageDown
|
|
286
|
+
82: b"\x1b[2~", # Insert
|
|
287
|
+
83: b"\x1b[3~", # Delete
|
|
288
|
+
59: b"\x1bOP", # F1
|
|
289
|
+
60: b"\x1bOQ", # F2
|
|
290
|
+
61: b"\x1bOR", # F3
|
|
291
|
+
62: b"\x1bOS", # F4
|
|
292
|
+
63: b"\x1b[15~", # F5
|
|
293
|
+
64: b"\x1b[17~", # F6
|
|
294
|
+
65: b"\x1b[18~", # F7
|
|
295
|
+
66: b"\x1b[19~", # F8
|
|
296
|
+
67: b"\x1b[20~", # F9
|
|
297
|
+
68: b"\x1b[21~", # F10
|
|
298
|
+
133: b"\x1b[23~", # F11
|
|
299
|
+
134: b"\x1b[24~", # F12
|
|
300
|
+
}
|
|
301
|
+
if ext_code in win_key_map:
|
|
302
|
+
chunk += win_key_map[ext_code]
|
|
303
|
+
else:
|
|
304
|
+
logger.debug(f"Unknown Windows extended key: {ext_code}")
|
|
305
|
+
else:
|
|
306
|
+
chunk += char
|
|
307
|
+
|
|
308
|
+
# Small delay to allow for more input
|
|
309
|
+
await asyncio.sleep(0.001)
|
|
310
|
+
# Check if there's more data immediately available
|
|
311
|
+
if not msvcrt.kbhit():
|
|
312
|
+
break
|
|
313
|
+
return chunk.decode("utf-8", errors="ignore") if chunk else ""
|
|
314
|
+
else:
|
|
315
|
+
# Unix: Read ALL available data using os.read
|
|
316
|
+
chunk = b""
|
|
317
|
+
while True:
|
|
318
|
+
try:
|
|
319
|
+
# Read in 8KB chunks
|
|
320
|
+
more_data = os.read(0, 8192)
|
|
321
|
+
if not more_data:
|
|
322
|
+
break
|
|
323
|
+
chunk += more_data
|
|
324
|
+
# Check if more data is available (longer timeout for escape sequences)
|
|
325
|
+
if not select.select([sys.stdin], [], [], 0.02)[0]:
|
|
326
|
+
break # No more data waiting
|
|
327
|
+
except OSError:
|
|
328
|
+
break # No more data available
|
|
329
|
+
return chunk.decode("utf-8", errors="ignore") if chunk else ""
|
|
330
|
+
|
|
331
|
+
async def _process_character(self, char: str) -> None:
|
|
332
|
+
"""Process a single character input.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
char: Character received from terminal.
|
|
336
|
+
"""
|
|
337
|
+
try:
|
|
338
|
+
current_time = time.time()
|
|
339
|
+
|
|
340
|
+
# NOTE: Slash command detection moved to key press level for proper buffer handling
|
|
341
|
+
|
|
342
|
+
# Simple paste detection - skip normal processing if character is part of paste
|
|
343
|
+
if self.paste_detection_enabled:
|
|
344
|
+
paste_handled = await self._simple_paste_detection(
|
|
345
|
+
char, current_time
|
|
346
|
+
)
|
|
347
|
+
if paste_handled:
|
|
348
|
+
return # Character consumed by paste detection, skip normal processing
|
|
349
|
+
|
|
350
|
+
# Parse character into structured key press (this handles escape sequences)
|
|
351
|
+
key_press = self.key_parser.parse_char(char)
|
|
352
|
+
if not key_press:
|
|
353
|
+
# For modal mode, add timeout-based standalone escape detection
|
|
354
|
+
command_mode = (
|
|
355
|
+
self.get_command_mode()
|
|
356
|
+
if self.get_command_mode
|
|
357
|
+
else CommandMode.NORMAL
|
|
358
|
+
)
|
|
359
|
+
if (
|
|
360
|
+
command_mode == CommandMode.MODAL
|
|
361
|
+
and self.on_command_mode_keypress
|
|
362
|
+
):
|
|
363
|
+
# Schedule a delayed check for standalone escape (100ms delay)
|
|
364
|
+
async def delayed_escape_check():
|
|
365
|
+
await asyncio.sleep(0.1)
|
|
366
|
+
standalone_escape = (
|
|
367
|
+
self.key_parser.check_for_standalone_escape()
|
|
368
|
+
)
|
|
369
|
+
if standalone_escape:
|
|
370
|
+
await self.on_command_mode_keypress(standalone_escape)
|
|
371
|
+
|
|
372
|
+
asyncio.create_task(delayed_escape_check())
|
|
373
|
+
return # Incomplete escape sequence - wait for more characters
|
|
374
|
+
|
|
375
|
+
# Check for slash command mode handling AFTER parsing (so arrow keys work)
|
|
376
|
+
command_mode = (
|
|
377
|
+
self.get_command_mode()
|
|
378
|
+
if self.get_command_mode
|
|
379
|
+
else CommandMode.NORMAL
|
|
380
|
+
)
|
|
381
|
+
if command_mode != CommandMode.NORMAL and self.on_command_mode_keypress:
|
|
382
|
+
logger.info(
|
|
383
|
+
f"🎯 Processing key '{key_press.name}' in command mode: {command_mode}"
|
|
384
|
+
)
|
|
385
|
+
handled = await self.on_command_mode_keypress(key_press)
|
|
386
|
+
if handled:
|
|
387
|
+
# CRITICAL FIX: Update display after command mode processing
|
|
388
|
+
await self._update_display()
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
# Emit key press event for plugins
|
|
392
|
+
key_result = await self.event_bus.emit_with_hooks(
|
|
393
|
+
EventType.KEY_PRESS,
|
|
394
|
+
{
|
|
395
|
+
"key": key_press.name,
|
|
396
|
+
"char_code": key_press.code,
|
|
397
|
+
"key_type": key_press.type.value,
|
|
398
|
+
"modifiers": key_press.modifiers,
|
|
399
|
+
},
|
|
400
|
+
"input",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Check if any plugin handled this key
|
|
404
|
+
prevent_default = self._check_prevent_default(key_result)
|
|
405
|
+
|
|
406
|
+
# Process key if not prevented by plugins
|
|
407
|
+
if not prevent_default:
|
|
408
|
+
await self._handle_key_press(key_press)
|
|
409
|
+
|
|
410
|
+
# Update renderer
|
|
411
|
+
await self._update_display()
|
|
412
|
+
|
|
413
|
+
except Exception as e:
|
|
414
|
+
await self.error_handler.handle_error(
|
|
415
|
+
ErrorType.PARSING_ERROR,
|
|
416
|
+
f"Error processing character: {e}",
|
|
417
|
+
ErrorSeverity.MEDIUM,
|
|
418
|
+
{"char": repr(char), "buffer_manager": self.buffer_manager},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def _check_prevent_default(self, key_result: Dict[str, Any]) -> bool:
|
|
422
|
+
"""Check if plugins want to prevent default key handling.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
key_result: Result from key press event.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
True if default handling should be prevented.
|
|
429
|
+
"""
|
|
430
|
+
if self.on_prevent_default_check:
|
|
431
|
+
return self.on_prevent_default_check(key_result)
|
|
432
|
+
|
|
433
|
+
# Fallback implementation
|
|
434
|
+
if "main" in key_result:
|
|
435
|
+
for hook_result in key_result["main"].values():
|
|
436
|
+
if isinstance(hook_result, dict) and hook_result.get(
|
|
437
|
+
"prevent_default"
|
|
438
|
+
):
|
|
439
|
+
return True
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
async def _handle_key_press(self, key_press: KeyPress) -> None:
|
|
443
|
+
"""Handle a parsed key press.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
key_press: Parsed key press event.
|
|
447
|
+
"""
|
|
448
|
+
# Process key press
|
|
449
|
+
try:
|
|
450
|
+
# Log all key presses for debugging
|
|
451
|
+
logger.info(
|
|
452
|
+
f"🔍 Key press: name='{key_press.name}', "
|
|
453
|
+
f"char='{key_press.char}', code={key_press.code}, "
|
|
454
|
+
f"type={key_press.type}, "
|
|
455
|
+
f"modifiers={getattr(key_press, 'modifiers', None)}"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# CRITICAL FIX: Command mode input routing - handle ALL command modes
|
|
459
|
+
command_mode = (
|
|
460
|
+
self.get_command_mode()
|
|
461
|
+
if self.get_command_mode
|
|
462
|
+
else CommandMode.NORMAL
|
|
463
|
+
)
|
|
464
|
+
if command_mode != CommandMode.NORMAL and self.on_command_mode_keypress:
|
|
465
|
+
logger.info(
|
|
466
|
+
f"🎯 Command mode active ({command_mode}) - routing input to command handler: {key_press.name}"
|
|
467
|
+
)
|
|
468
|
+
handled = await self.on_command_mode_keypress(key_press)
|
|
469
|
+
if handled:
|
|
470
|
+
# CRITICAL FIX: Update display after command mode processing
|
|
471
|
+
await self._update_display()
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# LEGACY: Modal input isolation - kept for backward compatibility
|
|
475
|
+
if command_mode == CommandMode.MODAL and self.on_command_mode_keypress:
|
|
476
|
+
logger.info(
|
|
477
|
+
f"🎯 Modal mode active - routing ALL input to modal handler: {key_press.name}"
|
|
478
|
+
)
|
|
479
|
+
await self.on_command_mode_keypress(key_press)
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
# Handle control keys
|
|
483
|
+
if self.key_parser.is_control_key(key_press, "Ctrl+C"):
|
|
484
|
+
logger.info("Ctrl+C received")
|
|
485
|
+
raise KeyboardInterrupt
|
|
486
|
+
|
|
487
|
+
elif self.key_parser.is_control_key(key_press, "Enter"):
|
|
488
|
+
await self._handle_enter()
|
|
489
|
+
|
|
490
|
+
elif self.key_parser.is_control_key(key_press, "Backspace"):
|
|
491
|
+
self.buffer_manager.delete_char()
|
|
492
|
+
|
|
493
|
+
elif key_press.name == "Escape":
|
|
494
|
+
await self._handle_escape()
|
|
495
|
+
|
|
496
|
+
elif key_press.name == "Delete":
|
|
497
|
+
self.buffer_manager.delete_forward()
|
|
498
|
+
|
|
499
|
+
# Handle arrow keys for cursor movement and history
|
|
500
|
+
elif key_press.name == "ArrowLeft":
|
|
501
|
+
moved = self.buffer_manager.move_cursor("left")
|
|
502
|
+
if moved:
|
|
503
|
+
logger.debug(
|
|
504
|
+
f"Arrow Left: cursor moved to position {self.buffer_manager.cursor_position}"
|
|
505
|
+
)
|
|
506
|
+
await self._update_display(force_render=True)
|
|
507
|
+
|
|
508
|
+
elif key_press.name == "ArrowRight":
|
|
509
|
+
moved = self.buffer_manager.move_cursor("right")
|
|
510
|
+
if moved:
|
|
511
|
+
logger.debug(
|
|
512
|
+
f"Arrow Right: cursor moved to position {self.buffer_manager.cursor_position}"
|
|
513
|
+
)
|
|
514
|
+
await self._update_display(force_render=True)
|
|
515
|
+
|
|
516
|
+
elif key_press.name == "ArrowUp":
|
|
517
|
+
self.buffer_manager.navigate_history("up")
|
|
518
|
+
await self._update_display(force_render=True)
|
|
519
|
+
|
|
520
|
+
elif key_press.name == "ArrowDown":
|
|
521
|
+
self.buffer_manager.navigate_history("down")
|
|
522
|
+
await self._update_display(force_render=True)
|
|
523
|
+
|
|
524
|
+
# Handle Home/End keys
|
|
525
|
+
elif key_press.name == "Home":
|
|
526
|
+
self.buffer_manager.move_to_start()
|
|
527
|
+
await self._update_display(force_render=True)
|
|
528
|
+
|
|
529
|
+
elif key_press.name == "End":
|
|
530
|
+
self.buffer_manager.move_to_end()
|
|
531
|
+
await self._update_display(force_render=True)
|
|
532
|
+
|
|
533
|
+
# Handle Option+comma/period keys for status view navigation
|
|
534
|
+
elif key_press.char == "≤": # Option+comma
|
|
535
|
+
logger.info(
|
|
536
|
+
"🔑 Option+Comma (≤) detected - switching to previous status view"
|
|
537
|
+
)
|
|
538
|
+
if self.on_status_view_previous:
|
|
539
|
+
await self.on_status_view_previous()
|
|
540
|
+
|
|
541
|
+
elif key_press.char == "≥": # Option+period
|
|
542
|
+
logger.info(
|
|
543
|
+
"🔑 Option+Period (≥) detected - switching to next status view"
|
|
544
|
+
)
|
|
545
|
+
if self.on_status_view_next:
|
|
546
|
+
await self.on_status_view_next()
|
|
547
|
+
|
|
548
|
+
# Handle Cmd key combinations (mapped to Ctrl sequences on macOS)
|
|
549
|
+
elif self.key_parser.is_control_key(key_press, "Ctrl+A"):
|
|
550
|
+
logger.info("🔑 Ctrl+A (Cmd+Left) - moving cursor to start")
|
|
551
|
+
self.buffer_manager.move_to_start()
|
|
552
|
+
await self._update_display(force_render=True)
|
|
553
|
+
|
|
554
|
+
elif self.key_parser.is_control_key(key_press, "Ctrl+E"):
|
|
555
|
+
logger.info("🔑 Ctrl+E (Cmd+Right) - moving cursor to end")
|
|
556
|
+
self.buffer_manager.move_to_end()
|
|
557
|
+
await self._update_display(force_render=True)
|
|
558
|
+
|
|
559
|
+
elif self.key_parser.is_control_key(key_press, "Ctrl+U"):
|
|
560
|
+
logger.info("🔑 Ctrl+U (Cmd+Backspace) - clearing line")
|
|
561
|
+
self.buffer_manager.clear()
|
|
562
|
+
await self._update_display(force_render=True)
|
|
563
|
+
|
|
564
|
+
# Handle printable characters
|
|
565
|
+
elif self.key_parser.is_printable_char(key_press):
|
|
566
|
+
# Normal character processing
|
|
567
|
+
success = self.buffer_manager.insert_char(key_press.char)
|
|
568
|
+
if not success:
|
|
569
|
+
await self.error_handler.handle_error(
|
|
570
|
+
ErrorType.BUFFER_ERROR,
|
|
571
|
+
"Failed to insert character - buffer limit reached",
|
|
572
|
+
ErrorSeverity.LOW,
|
|
573
|
+
{
|
|
574
|
+
"char": key_press.char,
|
|
575
|
+
"buffer_manager": self.buffer_manager,
|
|
576
|
+
},
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
# Check for slash command initiation AFTER character is in buffer
|
|
580
|
+
command_mode = (
|
|
581
|
+
self.get_command_mode()
|
|
582
|
+
if self.get_command_mode
|
|
583
|
+
else CommandMode.NORMAL
|
|
584
|
+
)
|
|
585
|
+
if (
|
|
586
|
+
key_press.char == "/"
|
|
587
|
+
and len(self.buffer_manager.content) == 1
|
|
588
|
+
and command_mode == CommandMode.NORMAL
|
|
589
|
+
):
|
|
590
|
+
# Slash was just inserted and buffer only contains slash - enter command mode
|
|
591
|
+
if self.on_command_mode_keypress:
|
|
592
|
+
# Create a proper key press for slash
|
|
593
|
+
await self.on_command_mode_keypress(key_press)
|
|
594
|
+
# CRITICAL FIX: Update display after slash command initiation
|
|
595
|
+
await self._update_display()
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
# Handle other special keys (F1-F12, etc.)
|
|
599
|
+
elif key_press.type == KeyTypeEnum.EXTENDED:
|
|
600
|
+
logger.debug(f"Extended key pressed: {key_press.name}")
|
|
601
|
+
# Could emit special events for function keys, etc.
|
|
602
|
+
|
|
603
|
+
except Exception as e:
|
|
604
|
+
await self.error_handler.handle_error(
|
|
605
|
+
ErrorType.EVENT_ERROR,
|
|
606
|
+
f"Error handling key press: {e}",
|
|
607
|
+
ErrorSeverity.MEDIUM,
|
|
608
|
+
{
|
|
609
|
+
"key_press": key_press,
|
|
610
|
+
"buffer_manager": self.buffer_manager,
|
|
611
|
+
},
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
async def _update_display(self, force_render: bool = False) -> None:
|
|
615
|
+
"""Update the terminal display with current buffer state."""
|
|
616
|
+
try:
|
|
617
|
+
# Skip rendering if paused (during special effects like Matrix)
|
|
618
|
+
if self.rendering_paused and not force_render:
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
buffer_content, cursor_pos = self.buffer_manager.get_display_info()
|
|
622
|
+
|
|
623
|
+
# Update renderer with buffer content and cursor position
|
|
624
|
+
self.renderer.input_buffer = buffer_content
|
|
625
|
+
self.renderer.cursor_position = cursor_pos
|
|
626
|
+
|
|
627
|
+
# Force immediate rendering if requested (needed for paste operations)
|
|
628
|
+
if force_render:
|
|
629
|
+
try:
|
|
630
|
+
if hasattr(
|
|
631
|
+
self.renderer, "render_active_area"
|
|
632
|
+
) and asyncio.iscoroutinefunction(
|
|
633
|
+
self.renderer.render_active_area
|
|
634
|
+
):
|
|
635
|
+
await self.renderer.render_active_area()
|
|
636
|
+
elif hasattr(
|
|
637
|
+
self.renderer, "render_input"
|
|
638
|
+
) and asyncio.iscoroutinefunction(self.renderer.render_input):
|
|
639
|
+
await self.renderer.render_input()
|
|
640
|
+
elif hasattr(self.renderer, "render_active_area"):
|
|
641
|
+
self.renderer.render_active_area()
|
|
642
|
+
elif hasattr(self.renderer, "render_input"):
|
|
643
|
+
self.renderer.render_input()
|
|
644
|
+
except Exception as e:
|
|
645
|
+
logger.debug(f"Force render failed: {e}")
|
|
646
|
+
# Continue without forced render
|
|
647
|
+
|
|
648
|
+
# Only update cursor if position changed
|
|
649
|
+
if cursor_pos != self._last_cursor_pos:
|
|
650
|
+
# Could implement cursor positioning in renderer
|
|
651
|
+
self._last_cursor_pos = cursor_pos
|
|
652
|
+
|
|
653
|
+
except Exception as e:
|
|
654
|
+
await self.error_handler.handle_error(
|
|
655
|
+
ErrorType.SYSTEM_ERROR,
|
|
656
|
+
f"Error updating display: {e}",
|
|
657
|
+
ErrorSeverity.LOW,
|
|
658
|
+
{"buffer_manager": self.buffer_manager},
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
async def _handle_enter(self) -> None:
|
|
662
|
+
"""Handle Enter key press with enhanced validation."""
|
|
663
|
+
try:
|
|
664
|
+
if self.buffer_manager.is_empty:
|
|
665
|
+
return
|
|
666
|
+
|
|
667
|
+
# Validate input before processing
|
|
668
|
+
validation_errors = self.buffer_manager.validate_content()
|
|
669
|
+
if validation_errors:
|
|
670
|
+
for error in validation_errors:
|
|
671
|
+
logger.warning(f"Input validation warning: {error}")
|
|
672
|
+
|
|
673
|
+
# Get message and clear buffer
|
|
674
|
+
message = self.buffer_manager.get_content_and_clear()
|
|
675
|
+
|
|
676
|
+
# GENIUS PASTE BUCKET: Immediate expansion - no waiting needed!
|
|
677
|
+
logger.debug(f"GENIUS SUBMIT: Original message: '{message}'")
|
|
678
|
+
logger.debug(
|
|
679
|
+
f"GENIUS SUBMIT: Paste bucket contains: {list(self._paste_bucket.keys())}"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
expanded_message = self._expand_paste_placeholders(message)
|
|
683
|
+
logger.debug(
|
|
684
|
+
f"GENIUS SUBMIT: Final expanded: '{expanded_message[:100]}...' ({len(expanded_message)} chars)"
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
# Add to history (with expanded content)
|
|
688
|
+
self.buffer_manager.add_to_history(expanded_message)
|
|
689
|
+
|
|
690
|
+
# Update renderer
|
|
691
|
+
self.renderer.input_buffer = ""
|
|
692
|
+
self.renderer.clear_active_area()
|
|
693
|
+
|
|
694
|
+
# Emit user input event (with expanded content!)
|
|
695
|
+
await self.event_bus.emit_with_hooks(
|
|
696
|
+
EventType.USER_INPUT,
|
|
697
|
+
{
|
|
698
|
+
"message": expanded_message,
|
|
699
|
+
"validation_errors": validation_errors,
|
|
700
|
+
},
|
|
701
|
+
"user",
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
logger.debug(
|
|
705
|
+
f"Processed user input: {message[:100]}..."
|
|
706
|
+
if len(message) > 100
|
|
707
|
+
else f"Processed user input: {message}"
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
except Exception as e:
|
|
711
|
+
await self.error_handler.handle_error(
|
|
712
|
+
ErrorType.EVENT_ERROR,
|
|
713
|
+
f"Error handling Enter key: {e}",
|
|
714
|
+
ErrorSeverity.HIGH,
|
|
715
|
+
{"buffer_manager": self.buffer_manager},
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
async def _handle_escape(self) -> None:
|
|
719
|
+
"""Handle Escape key press for request cancellation."""
|
|
720
|
+
try:
|
|
721
|
+
logger.info("_handle_escape called - emitting CANCEL_REQUEST event")
|
|
722
|
+
|
|
723
|
+
# Emit cancellation event
|
|
724
|
+
result = await self.event_bus.emit_with_hooks(
|
|
725
|
+
EventType.CANCEL_REQUEST,
|
|
726
|
+
{"reason": "user_escape", "source": "input_handler"},
|
|
727
|
+
"input",
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
logger.info(
|
|
731
|
+
f"ESC key pressed - cancellation request sent, result: {result}"
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
except Exception as e:
|
|
735
|
+
await self.error_handler.handle_error(
|
|
736
|
+
ErrorType.EVENT_ERROR,
|
|
737
|
+
f"Error handling Escape key: {e}",
|
|
738
|
+
ErrorSeverity.MEDIUM,
|
|
739
|
+
{"buffer_manager": self.buffer_manager},
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
def _expand_paste_placeholders(self, message: str) -> str:
|
|
743
|
+
"""Expand paste placeholders with actual content from paste bucket.
|
|
744
|
+
|
|
745
|
+
Your brilliant idea: Replace [⚡ Pasted #N ...] with actual pasted content!
|
|
746
|
+
"""
|
|
747
|
+
logger.debug(f"PASTE DEBUG: Expanding message: '{message}'")
|
|
748
|
+
logger.debug(
|
|
749
|
+
f"PASTE DEBUG: Paste bucket contains: {list(self._paste_bucket.keys())}"
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
expanded = message
|
|
753
|
+
|
|
754
|
+
# Find and replace each paste placeholder
|
|
755
|
+
import re
|
|
756
|
+
|
|
757
|
+
for paste_id, content in self._paste_bucket.items():
|
|
758
|
+
# Extract paste number from paste_id (PASTE_1 -> 1)
|
|
759
|
+
paste_num = paste_id.split("_")[1]
|
|
760
|
+
|
|
761
|
+
# Pattern to match: [Pasted #N X lines, Y chars]
|
|
762
|
+
pattern = rf"\[Pasted #{paste_num} \d+ lines?, \d+ chars\]"
|
|
763
|
+
|
|
764
|
+
logger.debug(f"PASTE DEBUG: Looking for pattern: {pattern}")
|
|
765
|
+
logger.debug(
|
|
766
|
+
f"PASTE DEBUG: Will replace with content: '{content[:50]}...'"
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Replace with actual content
|
|
770
|
+
matches = re.findall(pattern, expanded)
|
|
771
|
+
logger.debug(f"PASTE DEBUG: Found {len(matches)} matches")
|
|
772
|
+
|
|
773
|
+
expanded = re.sub(pattern, content, expanded)
|
|
774
|
+
|
|
775
|
+
logger.debug(f"PASTE DEBUG: Final expanded message: '{expanded[:100]}...'")
|
|
776
|
+
logger.info(
|
|
777
|
+
f"Paste expansion: {len(self._paste_bucket)} placeholders expanded"
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Clear paste bucket after expansion (one-time use)
|
|
781
|
+
self._paste_bucket.clear()
|
|
782
|
+
|
|
783
|
+
return expanded
|
|
784
|
+
|
|
785
|
+
async def _create_paste_placeholder(self, paste_id: str) -> None:
|
|
786
|
+
"""Create placeholder for paste - GENIUS IMMEDIATE VERSION."""
|
|
787
|
+
content = self._paste_bucket[paste_id]
|
|
788
|
+
|
|
789
|
+
# Create elegant placeholder for user to see
|
|
790
|
+
line_count = content.count("\n") + 1 if "\n" in content else 1
|
|
791
|
+
char_count = len(content)
|
|
792
|
+
paste_num = paste_id.split("_")[1] # Extract number from PASTE_1
|
|
793
|
+
placeholder = f"[Pasted #{paste_num} {line_count} lines, {char_count} chars]"
|
|
794
|
+
|
|
795
|
+
# Insert placeholder into buffer (what user sees)
|
|
796
|
+
for char in placeholder:
|
|
797
|
+
self.buffer_manager.insert_char(char)
|
|
798
|
+
|
|
799
|
+
logger.info(
|
|
800
|
+
f"GENIUS: Created placeholder for {char_count} chars as {paste_id}"
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
# Update display once at the end
|
|
804
|
+
await self._update_display(force_render=True)
|
|
805
|
+
|
|
806
|
+
async def _update_paste_placeholder(self) -> None:
|
|
807
|
+
"""Update existing placeholder when paste grows - GENIUS VERSION."""
|
|
808
|
+
# For now, just log - updating existing placeholder is complex
|
|
809
|
+
# The merge approach usually works fast enough that this isn't needed
|
|
810
|
+
content = self._paste_bucket[self._current_paste_id]
|
|
811
|
+
logger.info(
|
|
812
|
+
f"GENIUS: Updated {self._current_paste_id} to {len(content)} chars"
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
async def _simple_paste_detection(self, char: str, current_time: float) -> bool:
|
|
816
|
+
"""Simple, reliable paste detection using timing only.
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
True if character was consumed by paste detection, False otherwise.
|
|
820
|
+
"""
|
|
821
|
+
# Check cooldown to prevent overlapping paste detections
|
|
822
|
+
if (
|
|
823
|
+
hasattr(self, "_paste_cooldown")
|
|
824
|
+
and self._paste_cooldown > 0
|
|
825
|
+
and (current_time - self._paste_cooldown) < 1.0
|
|
826
|
+
):
|
|
827
|
+
# Still in cooldown period, skip paste detection
|
|
828
|
+
self._last_char_time = current_time
|
|
829
|
+
return False
|
|
830
|
+
|
|
831
|
+
# Check if we have a pending paste buffer that timed out
|
|
832
|
+
if self._paste_buffer and self._last_char_time > 0:
|
|
833
|
+
paste_timeout_ms = getattr(self, "_paste_timeout_ms", 50)
|
|
834
|
+
gap_ms = (current_time - self._last_char_time) * 1000
|
|
835
|
+
|
|
836
|
+
if gap_ms > paste_timeout_ms:
|
|
837
|
+
# Buffer timed out, process it
|
|
838
|
+
paste_min_chars = getattr(self, "paste_min_chars", 5)
|
|
839
|
+
if len(self._paste_buffer) >= paste_min_chars:
|
|
840
|
+
self._process_simple_paste_sync()
|
|
841
|
+
if not hasattr(self, "_paste_cooldown"):
|
|
842
|
+
self._paste_cooldown = 0
|
|
843
|
+
self._paste_cooldown = current_time # Set cooldown
|
|
844
|
+
else:
|
|
845
|
+
# Too few chars, process them as individual keystrokes
|
|
846
|
+
self._flush_paste_buffer_as_keystrokes_sync()
|
|
847
|
+
self._paste_buffer = []
|
|
848
|
+
|
|
849
|
+
# Now handle the current character
|
|
850
|
+
if self._last_char_time > 0:
|
|
851
|
+
paste_threshold_ms = getattr(self, "paste_threshold_ms", 20)
|
|
852
|
+
gap_ms = (current_time - self._last_char_time) * 1000
|
|
853
|
+
|
|
854
|
+
# If character arrived quickly, start/continue paste buffer
|
|
855
|
+
if gap_ms < paste_threshold_ms:
|
|
856
|
+
self._paste_buffer.append(char)
|
|
857
|
+
self._last_char_time = current_time
|
|
858
|
+
return True # Character consumed by paste buffer
|
|
859
|
+
|
|
860
|
+
# Character not part of paste, process normally
|
|
861
|
+
self._last_char_time = current_time
|
|
862
|
+
return False
|
|
863
|
+
|
|
864
|
+
def _flush_paste_buffer_as_keystrokes_sync(self) -> None:
|
|
865
|
+
"""Process paste buffer contents as individual keystrokes (sync version)."""
|
|
866
|
+
logger.debug(
|
|
867
|
+
f"Flushing {len(self._paste_buffer)} chars as individual keystrokes"
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# Just add characters to buffer without async processing
|
|
871
|
+
for char in self._paste_buffer:
|
|
872
|
+
if char.isprintable() or char in [" ", "\t"]:
|
|
873
|
+
self.buffer_manager.insert_char(char)
|
|
874
|
+
|
|
875
|
+
def _process_simple_paste_sync(self) -> None:
|
|
876
|
+
"""Process detected paste content (sync version with inline indicator)."""
|
|
877
|
+
if not self._paste_buffer:
|
|
878
|
+
return
|
|
879
|
+
|
|
880
|
+
# Get the content and clean any terminal markers
|
|
881
|
+
content = "".join(self._paste_buffer)
|
|
882
|
+
|
|
883
|
+
# Clean bracketed paste markers if present
|
|
884
|
+
if content.startswith("[200~"):
|
|
885
|
+
content = content[5:]
|
|
886
|
+
if content.endswith("01~"):
|
|
887
|
+
content = content[:-3]
|
|
888
|
+
elif content.endswith("[201~"):
|
|
889
|
+
content = content[:-6]
|
|
890
|
+
|
|
891
|
+
# Count lines
|
|
892
|
+
line_count = content.count("\n") + 1
|
|
893
|
+
char_count = len(content)
|
|
894
|
+
|
|
895
|
+
# Increment paste counter
|
|
896
|
+
self._paste_counter += 1
|
|
897
|
+
|
|
898
|
+
# Create inline paste indicator exactly as user requested
|
|
899
|
+
indicator = f"[⚡ Pasted #{self._paste_counter} {line_count} lines]"
|
|
900
|
+
|
|
901
|
+
# Insert the indicator into the buffer at current position
|
|
902
|
+
try:
|
|
903
|
+
for char in indicator:
|
|
904
|
+
self.buffer_manager.insert_char(char)
|
|
905
|
+
logger.info(
|
|
906
|
+
f"Paste #{self._paste_counter}: {char_count} chars, {line_count} lines"
|
|
907
|
+
)
|
|
908
|
+
except Exception as e:
|
|
909
|
+
logger.error(f"Paste processing error: {e}")
|
|
910
|
+
|
|
911
|
+
# Clear paste buffer
|
|
912
|
+
self._paste_buffer = []
|
|
913
|
+
|
|
914
|
+
def get_status(self) -> Dict[str, Any]:
|
|
915
|
+
"""Get current raw input processor status for debugging.
|
|
916
|
+
|
|
917
|
+
Returns:
|
|
918
|
+
Dictionary containing status information.
|
|
919
|
+
"""
|
|
920
|
+
return {
|
|
921
|
+
"running": self.running,
|
|
922
|
+
"rendering_paused": self.rendering_paused,
|
|
923
|
+
"paste_detection_enabled": self.paste_detection_enabled,
|
|
924
|
+
"paste_bucket_size": len(self._paste_bucket),
|
|
925
|
+
"paste_counter": self._paste_counter,
|
|
926
|
+
"parser_state": {
|
|
927
|
+
"in_escape_sequence": self.key_parser._in_escape_sequence,
|
|
928
|
+
"escape_buffer": self.key_parser._escape_buffer,
|
|
929
|
+
},
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async def cleanup(self) -> None:
|
|
933
|
+
"""Perform cleanup operations."""
|
|
934
|
+
try:
|
|
935
|
+
# Reset parser state
|
|
936
|
+
self.key_parser._reset_escape_state()
|
|
937
|
+
|
|
938
|
+
# Clear paste state
|
|
939
|
+
self._paste_buffer = []
|
|
940
|
+
self._paste_bucket.clear()
|
|
941
|
+
self._current_paste_id = None
|
|
942
|
+
|
|
943
|
+
logger.debug("Raw input processor cleanup completed")
|
|
944
|
+
|
|
945
|
+
except Exception as e:
|
|
946
|
+
logger.error(f"Error during raw input processor cleanup: {e}")
|