omnimancer-cli 0.1.0__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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive Input Handler for Omnimancer Agent Approval System.
|
|
3
|
+
|
|
4
|
+
This module provides keyboard input handling, navigation controls, and
|
|
5
|
+
interactive features for the approval dialog system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
import termios
|
|
12
|
+
import tty
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InputMode(Enum):
|
|
21
|
+
"""Input modes for the interactive handler."""
|
|
22
|
+
|
|
23
|
+
NORMAL = "normal"
|
|
24
|
+
NAVIGATION = "navigation"
|
|
25
|
+
SEARCH = "search"
|
|
26
|
+
HELP = "help"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class KeyAction(Enum):
|
|
30
|
+
"""Available key actions."""
|
|
31
|
+
|
|
32
|
+
APPROVE = "approve"
|
|
33
|
+
DENY = "deny"
|
|
34
|
+
QUIT = "quit"
|
|
35
|
+
HELP = "help"
|
|
36
|
+
NAVIGATE_UP = "navigate_up"
|
|
37
|
+
NAVIGATE_DOWN = "navigate_down"
|
|
38
|
+
NAVIGATE_LEFT = "navigate_left"
|
|
39
|
+
NAVIGATE_RIGHT = "navigate_right"
|
|
40
|
+
PAGE_UP = "page_up"
|
|
41
|
+
PAGE_DOWN = "page_down"
|
|
42
|
+
HOME = "home"
|
|
43
|
+
END = "end"
|
|
44
|
+
TOGGLE_DIFF = "toggle_diff"
|
|
45
|
+
TOGGLE_DETAILS = "toggle_details"
|
|
46
|
+
ZOOM_IN = "zoom_in"
|
|
47
|
+
ZOOM_OUT = "zoom_out"
|
|
48
|
+
SEARCH = "search"
|
|
49
|
+
COPY = "copy"
|
|
50
|
+
EXPORT = "export"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class KeyBinding:
|
|
55
|
+
"""Represents a key binding configuration."""
|
|
56
|
+
|
|
57
|
+
key: str
|
|
58
|
+
action: KeyAction
|
|
59
|
+
description: str
|
|
60
|
+
mode: InputMode = InputMode.NORMAL
|
|
61
|
+
requires_modifier: bool = False
|
|
62
|
+
modifier: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class InputState:
|
|
67
|
+
"""Current state of input handling."""
|
|
68
|
+
|
|
69
|
+
mode: InputMode = InputMode.NORMAL
|
|
70
|
+
current_section: str = "diff"
|
|
71
|
+
scroll_position: int = 0
|
|
72
|
+
zoom_level: float = 1.0
|
|
73
|
+
search_query: str = ""
|
|
74
|
+
help_visible: bool = False
|
|
75
|
+
diff_expanded: bool = False
|
|
76
|
+
details_expanded: bool = False
|
|
77
|
+
last_input: Optional[str] = None
|
|
78
|
+
input_history: List[str] = field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class InteractiveInputHandler:
|
|
82
|
+
"""
|
|
83
|
+
Handles keyboard input and interactive controls for approval dialogs.
|
|
84
|
+
|
|
85
|
+
Provides comprehensive keyboard shortcuts, navigation controls,
|
|
86
|
+
and interactive features for the approval interface.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, timeout_seconds: Optional[int] = None):
|
|
90
|
+
"""
|
|
91
|
+
Initialize interactive input handler.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
timeout_seconds: Optional timeout for input operations
|
|
95
|
+
"""
|
|
96
|
+
self.timeout_seconds = timeout_seconds
|
|
97
|
+
self.state = InputState()
|
|
98
|
+
|
|
99
|
+
# Default key bindings
|
|
100
|
+
self.key_bindings: Dict[str, KeyBinding] = {
|
|
101
|
+
# Approval actions
|
|
102
|
+
"y": KeyBinding("y", KeyAction.APPROVE, "Approve operation"),
|
|
103
|
+
"Y": KeyBinding("Y", KeyAction.APPROVE, "Approve operation"),
|
|
104
|
+
"n": KeyBinding("n", KeyAction.DENY, "Deny operation"),
|
|
105
|
+
"N": KeyBinding("N", KeyAction.DENY, "Deny operation"),
|
|
106
|
+
"q": KeyBinding("q", KeyAction.QUIT, "Quit/Cancel"),
|
|
107
|
+
"Q": KeyBinding("Q", KeyAction.QUIT, "Quit/Cancel"),
|
|
108
|
+
# Navigation
|
|
109
|
+
"k": KeyBinding("k", KeyAction.NAVIGATE_UP, "Navigate up"),
|
|
110
|
+
"j": KeyBinding("j", KeyAction.NAVIGATE_DOWN, "Navigate down"),
|
|
111
|
+
"h": KeyBinding("h", KeyAction.NAVIGATE_LEFT, "Navigate left"),
|
|
112
|
+
"l": KeyBinding("l", KeyAction.NAVIGATE_RIGHT, "Navigate right"),
|
|
113
|
+
"\x1b[A": KeyBinding("\x1b[A", KeyAction.NAVIGATE_UP, "Up arrow"),
|
|
114
|
+
"\x1b[B": KeyBinding("\x1b[B", KeyAction.NAVIGATE_DOWN, "Down arrow"),
|
|
115
|
+
"\x1b[C": KeyBinding("\x1b[C", KeyAction.NAVIGATE_RIGHT, "Right arrow"),
|
|
116
|
+
"\x1b[D": KeyBinding("\x1b[D", KeyAction.NAVIGATE_LEFT, "Left arrow"),
|
|
117
|
+
# Page navigation
|
|
118
|
+
"\x1b[5~": KeyBinding("\x1b[5~", KeyAction.PAGE_UP, "Page up"),
|
|
119
|
+
"\x1b[6~": KeyBinding("\x1b[6~", KeyAction.PAGE_DOWN, "Page down"),
|
|
120
|
+
"\x1b[H": KeyBinding("\x1b[H", KeyAction.HOME, "Home"),
|
|
121
|
+
"\x1b[F": KeyBinding("\x1b[F", KeyAction.END, "End"),
|
|
122
|
+
# Display controls
|
|
123
|
+
"d": KeyBinding("d", KeyAction.TOGGLE_DIFF, "Toggle diff view"),
|
|
124
|
+
"D": KeyBinding("D", KeyAction.TOGGLE_DETAILS, "Toggle details view"),
|
|
125
|
+
"+": KeyBinding("+", KeyAction.ZOOM_IN, "Zoom in"),
|
|
126
|
+
"=": KeyBinding("=", KeyAction.ZOOM_IN, "Zoom in"),
|
|
127
|
+
"-": KeyBinding("-", KeyAction.ZOOM_OUT, "Zoom out"),
|
|
128
|
+
"_": KeyBinding("_", KeyAction.ZOOM_OUT, "Zoom out"),
|
|
129
|
+
# Utility functions
|
|
130
|
+
"?": KeyBinding("?", KeyAction.HELP, "Show help"),
|
|
131
|
+
"/": KeyBinding("/", KeyAction.SEARCH, "Search"),
|
|
132
|
+
"c": KeyBinding("c", KeyAction.COPY, "Copy content"),
|
|
133
|
+
"e": KeyBinding("e", KeyAction.EXPORT, "Export details"),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Action handlers
|
|
137
|
+
self.action_handlers: Dict[KeyAction, Callable] = {
|
|
138
|
+
KeyAction.APPROVE: self._handle_approve,
|
|
139
|
+
KeyAction.DENY: self._handle_deny,
|
|
140
|
+
KeyAction.QUIT: self._handle_quit,
|
|
141
|
+
KeyAction.HELP: self._handle_help,
|
|
142
|
+
KeyAction.NAVIGATE_UP: self._handle_navigate_up,
|
|
143
|
+
KeyAction.NAVIGATE_DOWN: self._handle_navigate_down,
|
|
144
|
+
KeyAction.NAVIGATE_LEFT: self._handle_navigate_left,
|
|
145
|
+
KeyAction.NAVIGATE_RIGHT: self._handle_navigate_right,
|
|
146
|
+
KeyAction.PAGE_UP: self._handle_page_up,
|
|
147
|
+
KeyAction.PAGE_DOWN: self._handle_page_down,
|
|
148
|
+
KeyAction.HOME: self._handle_home,
|
|
149
|
+
KeyAction.END: self._handle_end,
|
|
150
|
+
KeyAction.TOGGLE_DIFF: self._handle_toggle_diff,
|
|
151
|
+
KeyAction.TOGGLE_DETAILS: self._handle_toggle_details,
|
|
152
|
+
KeyAction.ZOOM_IN: self._handle_zoom_in,
|
|
153
|
+
KeyAction.ZOOM_OUT: self._handle_zoom_out,
|
|
154
|
+
KeyAction.SEARCH: self._handle_search,
|
|
155
|
+
KeyAction.COPY: self._handle_copy,
|
|
156
|
+
KeyAction.EXPORT: self._handle_export,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Callbacks for external integration
|
|
160
|
+
self.approval_callback: Optional[Callable] = None
|
|
161
|
+
self.denial_callback: Optional[Callable] = None
|
|
162
|
+
self.quit_callback: Optional[Callable] = None
|
|
163
|
+
self.display_update_callback: Optional[Callable] = None
|
|
164
|
+
|
|
165
|
+
# Terminal state management
|
|
166
|
+
self.original_terminal_settings: Optional[List] = None
|
|
167
|
+
self.terminal_setup: bool = False
|
|
168
|
+
|
|
169
|
+
def set_callbacks(
|
|
170
|
+
self,
|
|
171
|
+
approval_callback: Optional[Callable] = None,
|
|
172
|
+
denial_callback: Optional[Callable] = None,
|
|
173
|
+
quit_callback: Optional[Callable] = None,
|
|
174
|
+
display_update_callback: Optional[Callable] = None,
|
|
175
|
+
):
|
|
176
|
+
"""Set callback functions for various actions."""
|
|
177
|
+
if approval_callback:
|
|
178
|
+
self.approval_callback = approval_callback
|
|
179
|
+
if denial_callback:
|
|
180
|
+
self.denial_callback = denial_callback
|
|
181
|
+
if quit_callback:
|
|
182
|
+
self.quit_callback = quit_callback
|
|
183
|
+
if display_update_callback:
|
|
184
|
+
self.display_update_callback = display_update_callback
|
|
185
|
+
|
|
186
|
+
def add_key_binding(
|
|
187
|
+
self,
|
|
188
|
+
key: str,
|
|
189
|
+
action: KeyAction,
|
|
190
|
+
description: str,
|
|
191
|
+
mode: InputMode = InputMode.NORMAL,
|
|
192
|
+
):
|
|
193
|
+
"""Add or modify a key binding."""
|
|
194
|
+
self.key_bindings[key] = KeyBinding(key, action, description, mode)
|
|
195
|
+
|
|
196
|
+
def remove_key_binding(self, key: str):
|
|
197
|
+
"""Remove a key binding."""
|
|
198
|
+
if key in self.key_bindings:
|
|
199
|
+
del self.key_bindings[key]
|
|
200
|
+
|
|
201
|
+
def get_key_bindings_by_mode(self, mode: InputMode) -> Dict[str, KeyBinding]:
|
|
202
|
+
"""Get key bindings for a specific mode."""
|
|
203
|
+
return {k: v for k, v in self.key_bindings.items() if v.mode == mode}
|
|
204
|
+
|
|
205
|
+
async def setup_terminal(self):
|
|
206
|
+
"""Setup terminal for raw input mode."""
|
|
207
|
+
if self.terminal_setup:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
# Save original terminal settings
|
|
212
|
+
self.original_terminal_settings = termios.tcgetattr(sys.stdin)
|
|
213
|
+
|
|
214
|
+
# Set terminal to raw mode
|
|
215
|
+
tty.setraw(sys.stdin.fileno())
|
|
216
|
+
self.terminal_setup = True
|
|
217
|
+
|
|
218
|
+
except (termios.error, OSError) as e:
|
|
219
|
+
logger.warning(f"Could not setup terminal for raw input: {e}")
|
|
220
|
+
# Fall back to line-based input if raw mode fails
|
|
221
|
+
|
|
222
|
+
async def restore_terminal(self):
|
|
223
|
+
"""Restore original terminal settings."""
|
|
224
|
+
if not self.terminal_setup or not self.original_terminal_settings:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
termios.tcsetattr(
|
|
229
|
+
sys.stdin, termios.TCSADRAIN, self.original_terminal_settings
|
|
230
|
+
)
|
|
231
|
+
self.terminal_setup = False
|
|
232
|
+
self.original_terminal_settings = None
|
|
233
|
+
except (termios.error, OSError) as e:
|
|
234
|
+
logger.warning(f"Could not restore terminal settings: {e}")
|
|
235
|
+
|
|
236
|
+
async def read_key(self) -> Optional[str]:
|
|
237
|
+
"""
|
|
238
|
+
Read a single key from input.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
The key pressed, or None if timeout/error
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
if not self.terminal_setup:
|
|
245
|
+
# Fallback to line input
|
|
246
|
+
if self.timeout_seconds:
|
|
247
|
+
try:
|
|
248
|
+
line = await asyncio.wait_for(
|
|
249
|
+
asyncio.get_event_loop().run_in_executor(None, input),
|
|
250
|
+
timeout=self.timeout_seconds,
|
|
251
|
+
)
|
|
252
|
+
return line.strip().lower()
|
|
253
|
+
except asyncio.TimeoutError:
|
|
254
|
+
return None
|
|
255
|
+
else:
|
|
256
|
+
return input().strip().lower()
|
|
257
|
+
|
|
258
|
+
# Raw character input
|
|
259
|
+
def _read_char():
|
|
260
|
+
char = sys.stdin.read(1)
|
|
261
|
+
# Handle escape sequences
|
|
262
|
+
if char == "\x1b":
|
|
263
|
+
char += sys.stdin.read(2) # Read next 2 chars for arrow keys
|
|
264
|
+
if char.endswith("["):
|
|
265
|
+
char += sys.stdin.read(1) # Read the final character
|
|
266
|
+
return char
|
|
267
|
+
|
|
268
|
+
if self.timeout_seconds:
|
|
269
|
+
try:
|
|
270
|
+
key = await asyncio.wait_for(
|
|
271
|
+
asyncio.get_event_loop().run_in_executor(None, _read_char),
|
|
272
|
+
timeout=self.timeout_seconds,
|
|
273
|
+
)
|
|
274
|
+
return key
|
|
275
|
+
except asyncio.TimeoutError:
|
|
276
|
+
return None
|
|
277
|
+
else:
|
|
278
|
+
return await asyncio.get_event_loop().run_in_executor(None, _read_char)
|
|
279
|
+
|
|
280
|
+
except (KeyboardInterrupt, EOFError):
|
|
281
|
+
return "q" # Treat as quit
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.error(f"Error reading key input: {e}")
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
async def handle_input_loop(self) -> Optional[KeyAction]:
|
|
287
|
+
"""
|
|
288
|
+
Main input handling loop.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
The final action that should terminate the loop
|
|
292
|
+
"""
|
|
293
|
+
await self.setup_terminal()
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
while True:
|
|
297
|
+
key = await self.read_key()
|
|
298
|
+
|
|
299
|
+
if key is None:
|
|
300
|
+
# Timeout or error - return quit
|
|
301
|
+
return KeyAction.QUIT
|
|
302
|
+
|
|
303
|
+
# Handle the key
|
|
304
|
+
action = await self.handle_key(key)
|
|
305
|
+
|
|
306
|
+
# Check if this action should terminate the loop
|
|
307
|
+
if action in [
|
|
308
|
+
KeyAction.APPROVE,
|
|
309
|
+
KeyAction.DENY,
|
|
310
|
+
KeyAction.QUIT,
|
|
311
|
+
]:
|
|
312
|
+
return action
|
|
313
|
+
|
|
314
|
+
# Update display if callback is available
|
|
315
|
+
if self.display_update_callback:
|
|
316
|
+
await self.display_update_callback(self.state)
|
|
317
|
+
|
|
318
|
+
finally:
|
|
319
|
+
await self.restore_terminal()
|
|
320
|
+
|
|
321
|
+
async def handle_key(self, key: str) -> Optional[KeyAction]:
|
|
322
|
+
"""
|
|
323
|
+
Handle a single key press.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
key: The key that was pressed
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
The action that was triggered, if any
|
|
330
|
+
"""
|
|
331
|
+
# Record input history
|
|
332
|
+
self.state.last_input = key
|
|
333
|
+
self.state.input_history.append(key)
|
|
334
|
+
|
|
335
|
+
# Keep history limited
|
|
336
|
+
if len(self.state.input_history) > 100:
|
|
337
|
+
self.state.input_history = self.state.input_history[-100:]
|
|
338
|
+
|
|
339
|
+
# Look up key binding
|
|
340
|
+
binding = self.key_bindings.get(key)
|
|
341
|
+
|
|
342
|
+
if not binding:
|
|
343
|
+
# Unknown key - ignore or show help
|
|
344
|
+
if key not in ["\r", "\n", " "]: # Ignore common non-action keys
|
|
345
|
+
logger.debug(f"Unknown key pressed: {repr(key)}")
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
# Check if binding is valid for current mode
|
|
349
|
+
if binding.mode != InputMode.NORMAL and binding.mode != self.state.mode:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
# Get the action handler
|
|
353
|
+
handler = self.action_handlers.get(binding.action)
|
|
354
|
+
|
|
355
|
+
if not handler:
|
|
356
|
+
logger.warning(f"No handler for action: {binding.action}")
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
# Execute the handler
|
|
360
|
+
try:
|
|
361
|
+
await handler()
|
|
362
|
+
return binding.action
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"Error executing action {binding.action}: {e}")
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
# Action handlers
|
|
368
|
+
async def _handle_approve(self) -> bool:
|
|
369
|
+
"""Handle approval action."""
|
|
370
|
+
if self.approval_callback:
|
|
371
|
+
return await self.approval_callback()
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
async def _handle_deny(self) -> bool:
|
|
375
|
+
"""Handle denial action."""
|
|
376
|
+
if self.denial_callback:
|
|
377
|
+
return await self.denial_callback()
|
|
378
|
+
return True
|
|
379
|
+
|
|
380
|
+
async def _handle_quit(self) -> bool:
|
|
381
|
+
"""Handle quit action."""
|
|
382
|
+
if self.quit_callback:
|
|
383
|
+
return await self.quit_callback()
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
async def _handle_help(self) -> bool:
|
|
387
|
+
"""Handle help display toggle."""
|
|
388
|
+
self.state.help_visible = not self.state.help_visible
|
|
389
|
+
self.state.mode = (
|
|
390
|
+
InputMode.HELP if self.state.help_visible else InputMode.NORMAL
|
|
391
|
+
)
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
async def _handle_navigate_up(self) -> bool:
|
|
395
|
+
"""Handle upward navigation."""
|
|
396
|
+
if self.state.scroll_position > 0:
|
|
397
|
+
self.state.scroll_position -= 1
|
|
398
|
+
return True
|
|
399
|
+
|
|
400
|
+
async def _handle_navigate_down(self) -> bool:
|
|
401
|
+
"""Handle downward navigation."""
|
|
402
|
+
self.state.scroll_position += 1
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
async def _handle_navigate_left(self) -> bool:
|
|
406
|
+
"""Handle leftward navigation."""
|
|
407
|
+
sections = ["diff", "details", "operation", "risk"]
|
|
408
|
+
try:
|
|
409
|
+
current_idx = sections.index(self.state.current_section)
|
|
410
|
+
if current_idx > 0:
|
|
411
|
+
self.state.current_section = sections[current_idx - 1]
|
|
412
|
+
except ValueError:
|
|
413
|
+
self.state.current_section = sections[0]
|
|
414
|
+
return True
|
|
415
|
+
|
|
416
|
+
async def _handle_navigate_right(self) -> bool:
|
|
417
|
+
"""Handle rightward navigation."""
|
|
418
|
+
sections = ["diff", "details", "operation", "risk"]
|
|
419
|
+
try:
|
|
420
|
+
current_idx = sections.index(self.state.current_section)
|
|
421
|
+
if current_idx < len(sections) - 1:
|
|
422
|
+
self.state.current_section = sections[current_idx + 1]
|
|
423
|
+
except ValueError:
|
|
424
|
+
self.state.current_section = sections[0]
|
|
425
|
+
return True
|
|
426
|
+
|
|
427
|
+
async def _handle_page_up(self) -> bool:
|
|
428
|
+
"""Handle page up navigation."""
|
|
429
|
+
self.state.scroll_position = max(0, self.state.scroll_position - 10)
|
|
430
|
+
return True
|
|
431
|
+
|
|
432
|
+
async def _handle_page_down(self) -> bool:
|
|
433
|
+
"""Handle page down navigation."""
|
|
434
|
+
self.state.scroll_position += 10
|
|
435
|
+
return True
|
|
436
|
+
|
|
437
|
+
async def _handle_home(self) -> bool:
|
|
438
|
+
"""Handle home navigation."""
|
|
439
|
+
self.state.scroll_position = 0
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
async def _handle_end(self) -> bool:
|
|
443
|
+
"""Handle end navigation."""
|
|
444
|
+
# Would need access to content length to implement properly
|
|
445
|
+
self.state.scroll_position = 1000 # Large number as approximation
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
async def _handle_toggle_diff(self) -> bool:
|
|
449
|
+
"""Handle diff view toggle."""
|
|
450
|
+
self.state.diff_expanded = not self.state.diff_expanded
|
|
451
|
+
return True
|
|
452
|
+
|
|
453
|
+
async def _handle_toggle_details(self) -> bool:
|
|
454
|
+
"""Handle details view toggle."""
|
|
455
|
+
self.state.details_expanded = not self.state.details_expanded
|
|
456
|
+
return True
|
|
457
|
+
|
|
458
|
+
async def _handle_zoom_in(self) -> bool:
|
|
459
|
+
"""Handle zoom in."""
|
|
460
|
+
self.state.zoom_level = min(2.0, self.state.zoom_level + 0.1)
|
|
461
|
+
return True
|
|
462
|
+
|
|
463
|
+
async def _handle_zoom_out(self) -> bool:
|
|
464
|
+
"""Handle zoom out."""
|
|
465
|
+
self.state.zoom_level = max(0.5, self.state.zoom_level - 0.1)
|
|
466
|
+
return True
|
|
467
|
+
|
|
468
|
+
async def _handle_search(self) -> bool:
|
|
469
|
+
"""Handle search mode activation."""
|
|
470
|
+
self.state.mode = InputMode.SEARCH
|
|
471
|
+
# Would implement search input handling here
|
|
472
|
+
return True
|
|
473
|
+
|
|
474
|
+
async def _handle_copy(self) -> bool:
|
|
475
|
+
"""Handle copy to clipboard."""
|
|
476
|
+
# Would implement clipboard functionality here
|
|
477
|
+
logger.info("Copy functionality not yet implemented")
|
|
478
|
+
return True
|
|
479
|
+
|
|
480
|
+
async def _handle_export(self) -> bool:
|
|
481
|
+
"""Handle export functionality."""
|
|
482
|
+
# Would implement export functionality here
|
|
483
|
+
logger.info("Export functionality not yet implemented")
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
def get_help_text(self) -> str:
|
|
487
|
+
"""Generate help text for current mode."""
|
|
488
|
+
mode_bindings = self.get_key_bindings_by_mode(self.state.mode)
|
|
489
|
+
|
|
490
|
+
help_lines = [
|
|
491
|
+
f"=== Help - {self.state.mode.value.title()} Mode ===",
|
|
492
|
+
"",
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
# Group bindings by category
|
|
496
|
+
categories = {
|
|
497
|
+
"Approval Actions": [
|
|
498
|
+
KeyAction.APPROVE,
|
|
499
|
+
KeyAction.DENY,
|
|
500
|
+
KeyAction.QUIT,
|
|
501
|
+
],
|
|
502
|
+
"Navigation": [
|
|
503
|
+
KeyAction.NAVIGATE_UP,
|
|
504
|
+
KeyAction.NAVIGATE_DOWN,
|
|
505
|
+
KeyAction.NAVIGATE_LEFT,
|
|
506
|
+
KeyAction.NAVIGATE_RIGHT,
|
|
507
|
+
KeyAction.PAGE_UP,
|
|
508
|
+
KeyAction.PAGE_DOWN,
|
|
509
|
+
KeyAction.HOME,
|
|
510
|
+
KeyAction.END,
|
|
511
|
+
],
|
|
512
|
+
"Display Control": [
|
|
513
|
+
KeyAction.TOGGLE_DIFF,
|
|
514
|
+
KeyAction.TOGGLE_DETAILS,
|
|
515
|
+
KeyAction.ZOOM_IN,
|
|
516
|
+
KeyAction.ZOOM_OUT,
|
|
517
|
+
],
|
|
518
|
+
"Utility": [
|
|
519
|
+
KeyAction.HELP,
|
|
520
|
+
KeyAction.SEARCH,
|
|
521
|
+
KeyAction.COPY,
|
|
522
|
+
KeyAction.EXPORT,
|
|
523
|
+
],
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for category, actions in categories.items():
|
|
527
|
+
help_lines.append(f"{category}:")
|
|
528
|
+
for key, binding in mode_bindings.items():
|
|
529
|
+
if binding.action in actions:
|
|
530
|
+
help_lines.append(f" {key:<8} - {binding.description}")
|
|
531
|
+
help_lines.append("")
|
|
532
|
+
|
|
533
|
+
help_lines.extend(
|
|
534
|
+
["Press '?' again to close help", "Press 'q' to quit the dialog"]
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
return "\n".join(help_lines)
|
|
538
|
+
|
|
539
|
+
def get_status_info(self) -> Dict[str, Any]:
|
|
540
|
+
"""Get current input handler status information."""
|
|
541
|
+
return {
|
|
542
|
+
"mode": self.state.mode.value,
|
|
543
|
+
"current_section": self.state.current_section,
|
|
544
|
+
"scroll_position": self.state.scroll_position,
|
|
545
|
+
"zoom_level": self.state.zoom_level,
|
|
546
|
+
"help_visible": self.state.help_visible,
|
|
547
|
+
"diff_expanded": self.state.diff_expanded,
|
|
548
|
+
"details_expanded": self.state.details_expanded,
|
|
549
|
+
"last_input": self.state.last_input,
|
|
550
|
+
"input_count": len(self.state.input_history),
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# Utility functions
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def create_input_handler(
|
|
558
|
+
timeout_seconds: Optional[int] = None,
|
|
559
|
+
) -> InteractiveInputHandler:
|
|
560
|
+
"""Create an input handler with default configuration."""
|
|
561
|
+
return InteractiveInputHandler(timeout_seconds=timeout_seconds)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def create_custom_input_handler(
|
|
565
|
+
key_bindings: Optional[Dict[str, KeyBinding]] = None,
|
|
566
|
+
timeout_seconds: Optional[int] = None,
|
|
567
|
+
) -> InteractiveInputHandler:
|
|
568
|
+
"""Create an input handler with custom key bindings."""
|
|
569
|
+
handler = InteractiveInputHandler(timeout_seconds=timeout_seconds)
|
|
570
|
+
|
|
571
|
+
if key_bindings:
|
|
572
|
+
# Clear default bindings and add custom ones
|
|
573
|
+
handler.key_bindings.clear()
|
|
574
|
+
handler.key_bindings.update(key_bindings)
|
|
575
|
+
|
|
576
|
+
return handler
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# Predefined key binding sets
|
|
580
|
+
|
|
581
|
+
MINIMAL_KEY_BINDINGS = {
|
|
582
|
+
"y": KeyBinding("y", KeyAction.APPROVE, "Approve"),
|
|
583
|
+
"n": KeyBinding("n", KeyAction.DENY, "Deny"),
|
|
584
|
+
"q": KeyBinding("q", KeyAction.QUIT, "Quit"),
|
|
585
|
+
"?": KeyBinding("?", KeyAction.HELP, "Help"),
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
VIM_STYLE_KEY_BINDINGS = {
|
|
589
|
+
"y": KeyBinding("y", KeyAction.APPROVE, "Approve (yes)"),
|
|
590
|
+
"n": KeyBinding("n", KeyAction.DENY, "Deny (no)"),
|
|
591
|
+
"q": KeyBinding("q", KeyAction.QUIT, "Quit"),
|
|
592
|
+
"j": KeyBinding("j", KeyAction.NAVIGATE_DOWN, "Down"),
|
|
593
|
+
"k": KeyBinding("k", KeyAction.NAVIGATE_UP, "Up"),
|
|
594
|
+
"h": KeyBinding("h", KeyAction.NAVIGATE_LEFT, "Left"),
|
|
595
|
+
"l": KeyBinding("l", KeyAction.NAVIGATE_RIGHT, "Right"),
|
|
596
|
+
"g": KeyBinding("g", KeyAction.HOME, "Go to top"),
|
|
597
|
+
"G": KeyBinding("G", KeyAction.END, "Go to bottom"),
|
|
598
|
+
"d": KeyBinding("d", KeyAction.TOGGLE_DIFF, "Toggle diff"),
|
|
599
|
+
"/": KeyBinding("/", KeyAction.SEARCH, "Search"),
|
|
600
|
+
"?": KeyBinding("?", KeyAction.HELP, "Help"),
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
ARROW_KEY_BINDINGS = {
|
|
604
|
+
"y": KeyBinding("y", KeyAction.APPROVE, "Approve"),
|
|
605
|
+
"n": KeyBinding("n", KeyAction.DENY, "Deny"),
|
|
606
|
+
"q": KeyBinding("q", KeyAction.QUIT, "Quit"),
|
|
607
|
+
"\x1b[A": KeyBinding("\x1b[A", KeyAction.NAVIGATE_UP, "Up arrow"),
|
|
608
|
+
"\x1b[B": KeyBinding("\x1b[B", KeyAction.NAVIGATE_DOWN, "Down arrow"),
|
|
609
|
+
"\x1b[C": KeyBinding("\x1b[C", KeyAction.NAVIGATE_RIGHT, "Right arrow"),
|
|
610
|
+
"\x1b[D": KeyBinding("\x1b[D", KeyAction.NAVIGATE_LEFT, "Left arrow"),
|
|
611
|
+
"\x1b[5~": KeyBinding("\x1b[5~", KeyAction.PAGE_UP, "Page Up"),
|
|
612
|
+
"\x1b[6~": KeyBinding("\x1b[6~", KeyAction.PAGE_DOWN, "Page Down"),
|
|
613
|
+
"?": KeyBinding("?", KeyAction.HELP, "Help"),
|
|
614
|
+
}
|