repl-toolkit 1.0.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.
Potentially problematic release.
This version of repl-toolkit might be problematic. Click here for more details.
- repl_toolkit/__init__.py +58 -0
- repl_toolkit/actions/__init__.py +25 -0
- repl_toolkit/actions/action.py +217 -0
- repl_toolkit/actions/registry.py +538 -0
- repl_toolkit/actions/shell.py +62 -0
- repl_toolkit/async_repl.py +367 -0
- repl_toolkit/headless_repl.py +247 -0
- repl_toolkit/ptypes.py +119 -0
- repl_toolkit/tests/__init__.py +5 -0
- repl_toolkit/tests/test_actions.py +453 -0
- repl_toolkit/tests/test_async_repl.py +376 -0
- repl_toolkit/tests/test_headless.py +682 -0
- repl_toolkit/tests/test_types.py +173 -0
- repl_toolkit-1.0.0.dist-info/METADATA +641 -0
- repl_toolkit-1.0.0.dist-info/RECORD +18 -0
- repl_toolkit-1.0.0.dist-info/WHEEL +5 -0
- repl_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- repl_toolkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action registry for managing commands and keyboard shortcuts.
|
|
3
|
+
|
|
4
|
+
The registry serves as the central hub for registering, organizing, and
|
|
5
|
+
executing actions that can be triggered through multiple interaction methods.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from .shell import shell_command
|
|
13
|
+
|
|
14
|
+
from ..ptypes import ActionHandler, AsyncBackend
|
|
15
|
+
from .action import Action, ActionContext, ActionError, ActionValidationError, ActionExecutionError
|
|
16
|
+
|
|
17
|
+
class ActionRegistry(ActionHandler):
|
|
18
|
+
"""
|
|
19
|
+
Registry for managing both command and keyboard shortcut actions.
|
|
20
|
+
|
|
21
|
+
The registry provides a single point of control for all user-triggered
|
|
22
|
+
actions, whether they originate from typed commands or keyboard shortcuts.
|
|
23
|
+
It handles action registration, validation, execution, and help generation.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
registry = ActionRegistry()
|
|
27
|
+
|
|
28
|
+
# Register an action command and shortcut
|
|
29
|
+
registry.register_action(Action(
|
|
30
|
+
name="show_help",
|
|
31
|
+
description="Show help information",
|
|
32
|
+
category="General",
|
|
33
|
+
handler=help_handler,
|
|
34
|
+
command="/help",
|
|
35
|
+
command_usage="/help [command] - Show help",
|
|
36
|
+
keys="F1",
|
|
37
|
+
keys_description="Show help"
|
|
38
|
+
))
|
|
39
|
+
|
|
40
|
+
# OR using convenience method
|
|
41
|
+
|
|
42
|
+
registry.register_action(
|
|
43
|
+
name="show_help",
|
|
44
|
+
description="Show help information",
|
|
45
|
+
category="General",
|
|
46
|
+
handler=help_handler,
|
|
47
|
+
command="/help",
|
|
48
|
+
command_usage="/help [command] - Show help",
|
|
49
|
+
keys="F1",
|
|
50
|
+
keys_description="Show help"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Execute by command
|
|
54
|
+
registry.handle_command("/help")
|
|
55
|
+
|
|
56
|
+
# Execute by shortcut (in key binding)
|
|
57
|
+
registry.handle_shortcut("F1", event)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
"""Initialize the action registry with built-in actions."""
|
|
62
|
+
logger.trace("ActionRegistry.__init__() entry")
|
|
63
|
+
self.actions: Dict[str, Action] = {}
|
|
64
|
+
self.command_map: Dict[str, str] = {} # command -> action_name
|
|
65
|
+
self.key_map: Dict[str, str] = {} # key_combo -> action_name
|
|
66
|
+
self.handler_cache: Dict[str, Callable] = {}
|
|
67
|
+
self._backend = None
|
|
68
|
+
# Register built-in actions
|
|
69
|
+
self._register_builtin_actions()
|
|
70
|
+
logger.trace("ActionRegistry.__init__() exit")
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def backend(self):
|
|
74
|
+
"""The backend property getter."""
|
|
75
|
+
logger.trace("ActionRegistry.backend getter")
|
|
76
|
+
return self._backend
|
|
77
|
+
|
|
78
|
+
@backend.setter
|
|
79
|
+
def backend(self, value):
|
|
80
|
+
"""The backend property setter with validation."""
|
|
81
|
+
logger.trace("ActionRegistry.backend setter entry")
|
|
82
|
+
if not isinstance(value, AsyncBackend):
|
|
83
|
+
raise TypeError("Backend must implement AsyncBackend.") # pragma: no cover
|
|
84
|
+
self._backend = value # Set the actual private attribute
|
|
85
|
+
logger.trace("ActionRegistry.backend setter exit")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _register_builtin_actions(self) -> None:
|
|
89
|
+
"""Register essential built-in actions."""
|
|
90
|
+
logger.trace("ActionRegistry._register_builtin_actions() entry")
|
|
91
|
+
|
|
92
|
+
# Help action - Both command and shortcut
|
|
93
|
+
self.register_action(Action(
|
|
94
|
+
name="show_help",
|
|
95
|
+
description="Show help information for all actions or a specific action",
|
|
96
|
+
category="General",
|
|
97
|
+
handler=self._show_help,
|
|
98
|
+
command="/help",
|
|
99
|
+
command_usage="/help [action|command] - Show help for all actions or specific one",
|
|
100
|
+
keys="F1",
|
|
101
|
+
keys_description="Show help"
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
# List shortcuts action - command only
|
|
105
|
+
self.register_action(Action(
|
|
106
|
+
name="list_shortcuts",
|
|
107
|
+
description="List all available keyboard shortcuts",
|
|
108
|
+
category="General",
|
|
109
|
+
handler=self._list_shortcuts,
|
|
110
|
+
command="/shortcuts",
|
|
111
|
+
command_usage="/shortcuts - List all keyboard shortcuts"
|
|
112
|
+
))
|
|
113
|
+
|
|
114
|
+
# Exit actions - commands only (main loop handles these)
|
|
115
|
+
self.register_action(Action(
|
|
116
|
+
name="exit_repl",
|
|
117
|
+
description="Exit the REPL application",
|
|
118
|
+
category="Control",
|
|
119
|
+
handler=None, # Handled by main loop
|
|
120
|
+
command="/exit",
|
|
121
|
+
command_usage="/exit - Exit the application"
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
self.register_action(Action(
|
|
125
|
+
name="quit_repl",
|
|
126
|
+
description="Quit the REPL application",
|
|
127
|
+
category="Control",
|
|
128
|
+
handler=None, # Handled by main loop
|
|
129
|
+
command="/quit",
|
|
130
|
+
command_usage="/quit - Quit the application"
|
|
131
|
+
))
|
|
132
|
+
|
|
133
|
+
self.register_action(Action(
|
|
134
|
+
name="shell_command",
|
|
135
|
+
description="Drop to shell or execute shell command",
|
|
136
|
+
category="System",
|
|
137
|
+
handler=shell_command,
|
|
138
|
+
command="/shell",
|
|
139
|
+
command_usage="/shell [cmd [args [ ... ] ] ]"
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
logger.trace("ActionRegistry._register_builtin_actions() exit")
|
|
143
|
+
|
|
144
|
+
def register_action(self, *args, **kwargs) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Register an action in the registry.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
action: Action to register
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ActionValidationError: If action is invalid or conflicts exist
|
|
153
|
+
"""
|
|
154
|
+
logger.trace("ActionRegistry.register_action() entry")
|
|
155
|
+
|
|
156
|
+
action = None
|
|
157
|
+
if args and isinstance(args[0], Action):
|
|
158
|
+
action = args[0]
|
|
159
|
+
if kwargs:
|
|
160
|
+
for k, v in kwargs.items():
|
|
161
|
+
setattr(action, k, v)
|
|
162
|
+
else:
|
|
163
|
+
action = Action(**kwargs)
|
|
164
|
+
|
|
165
|
+
# Validate action
|
|
166
|
+
if action.name in self.actions:
|
|
167
|
+
raise ActionValidationError(f"Action '{action.name}' already exists") # pragma: no cover
|
|
168
|
+
|
|
169
|
+
if action.command and action.command in self.command_map:
|
|
170
|
+
existing_action = self.command_map[action.command]
|
|
171
|
+
raise ActionValidationError( # pragma: no cover
|
|
172
|
+
f"Command '{action.command}' already bound to action '{existing_action}'"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Check for key conflicts
|
|
176
|
+
for key_combo in action.get_keys_list():
|
|
177
|
+
if key_combo in self.key_map:
|
|
178
|
+
existing_action = self.key_map[key_combo]
|
|
179
|
+
raise ActionValidationError( # pragma: no cover
|
|
180
|
+
f"Key '{key_combo}' already bound to action '{existing_action}'"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Register action
|
|
184
|
+
self.actions[action.name] = action
|
|
185
|
+
|
|
186
|
+
# Register command mapping
|
|
187
|
+
if action.command:
|
|
188
|
+
self.command_map[action.command] = action.name
|
|
189
|
+
|
|
190
|
+
# Register key mappings
|
|
191
|
+
for key_combo in action.get_keys_list():
|
|
192
|
+
self.key_map[key_combo] = action.name
|
|
193
|
+
|
|
194
|
+
logger.debug(f"Registered action '{action.name}' with command='{action.command}' keys={action.keys}")
|
|
195
|
+
logger.trace("ActionRegistry.register_action() exit")
|
|
196
|
+
|
|
197
|
+
def get_action(self, name: str) -> Optional[Action]:
|
|
198
|
+
"""Get an action by name."""
|
|
199
|
+
logger.trace("ActionRegistry.get_action() entry")
|
|
200
|
+
result = self.actions.get(name)
|
|
201
|
+
logger.trace("ActionRegistry.get_action() exit")
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
def get_action_by_command(self, command: str) -> Optional[Action]:
|
|
205
|
+
"""Get an action by its command string."""
|
|
206
|
+
logger.trace("ActionRegistry.get_action_by_command() entry")
|
|
207
|
+
action_name = self.command_map.get(command)
|
|
208
|
+
result = self.actions.get(action_name) if action_name else None
|
|
209
|
+
logger.trace("ActionRegistry.get_action_by_command() exit")
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
def get_action_by_keys(self, keys: str) -> Optional[Action]:
|
|
213
|
+
"""Get an action by its key combination."""
|
|
214
|
+
logger.trace("ActionRegistry.get_action_by_keys() entry")
|
|
215
|
+
action_name = self.key_map.get(keys)
|
|
216
|
+
result = self.actions.get(action_name) if action_name else None
|
|
217
|
+
logger.trace("ActionRegistry.get_action_by_keys() exit")
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
def _resolve_handler(self, action: Action) -> Optional[Callable]:
|
|
221
|
+
"""
|
|
222
|
+
Resolve action handler to a callable function.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
action: Action whose handler to resolve
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Callable handler function or None for main-loop actions
|
|
229
|
+
"""
|
|
230
|
+
logger.trace("ActionRegistry._resolve_handler() entry")
|
|
231
|
+
|
|
232
|
+
if action.handler is None:
|
|
233
|
+
logger.trace("ActionRegistry._resolve_handler() exit - None handler")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
# Check cache first
|
|
237
|
+
cache_key = f"{action.name}:{action.handler}"
|
|
238
|
+
if cache_key in self.handler_cache:
|
|
239
|
+
logger.trace("ActionRegistry._resolve_handler() exit - cached")
|
|
240
|
+
return self.handler_cache[cache_key]
|
|
241
|
+
|
|
242
|
+
# If already callable, use it
|
|
243
|
+
if callable(action.handler):
|
|
244
|
+
self.handler_cache[cache_key] = action.handler
|
|
245
|
+
logger.trace("ActionRegistry._resolve_handler() exit - callable")
|
|
246
|
+
return action.handler
|
|
247
|
+
|
|
248
|
+
# If string, try to import
|
|
249
|
+
if isinstance(action.handler, str):
|
|
250
|
+
try:
|
|
251
|
+
module_path, func_name = action.handler.rsplit('.', 1)
|
|
252
|
+
module = importlib.import_module(module_path)
|
|
253
|
+
handler_func = getattr(module, func_name)
|
|
254
|
+
self.handler_cache[cache_key] = handler_func
|
|
255
|
+
logger.trace("ActionRegistry._resolve_handler() exit - imported")
|
|
256
|
+
return handler_func
|
|
257
|
+
except Exception as e: # pragma: no cover
|
|
258
|
+
logger.error(f"Failed to import handler '{action.handler}' for action '{action.name}': {e}") # pragma: no cover
|
|
259
|
+
raise ActionValidationError(f"Cannot resolve handler '{action.handler}'") # pragma: no cover
|
|
260
|
+
|
|
261
|
+
raise ActionValidationError(f"Invalid handler type for action '{action.name}': {type(action.handler)}") # pragma: no cover
|
|
262
|
+
|
|
263
|
+
def execute_action(self, action_name: str, context: ActionContext) -> None:
|
|
264
|
+
"""
|
|
265
|
+
Execute an action by name.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
action_name: Name of action to execute
|
|
269
|
+
context: Action context
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ActionError: If action is not found or execution fails
|
|
273
|
+
"""
|
|
274
|
+
logger.trace("ActionRegistry.execute_action() entry")
|
|
275
|
+
|
|
276
|
+
action = self.get_action(action_name)
|
|
277
|
+
if not action:
|
|
278
|
+
raise ActionError(f"Action '{action_name}' not found") # pragma: no cover
|
|
279
|
+
|
|
280
|
+
if not action.enabled:
|
|
281
|
+
logger.debug(f"Action '{action_name}' is disabled")
|
|
282
|
+
logger.trace("ActionRegistry.execute_action() exit - disabled")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# Resolve handler
|
|
286
|
+
handler = self._resolve_handler(action)
|
|
287
|
+
if handler is None:
|
|
288
|
+
# Main loop actions (like exit/quit) return without execution
|
|
289
|
+
logger.debug(f"Action '{action_name}' handled by main loop")
|
|
290
|
+
logger.trace("ActionRegistry.execute_action() exit - main loop")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
logger.debug(f"Executing action '{action_name}' via {context.triggered_by}")
|
|
295
|
+
|
|
296
|
+
# Execute handler synchronously
|
|
297
|
+
# If handler needs async operations, it can handle them internally
|
|
298
|
+
handler(context)
|
|
299
|
+
logger.trace("ActionRegistry.execute_action() exit - success")
|
|
300
|
+
|
|
301
|
+
except Exception as e: # pragma: no cover
|
|
302
|
+
logger.error(f"Error executing action '{action_name}': {e}") # pragma: no cover
|
|
303
|
+
logger.trace("ActionRegistry.execute_action() exit - exception")
|
|
304
|
+
raise ActionExecutionError(f"Failed to execute action '{action_name}': {e}", action_name) # pragma: no cover
|
|
305
|
+
|
|
306
|
+
def handle_command(self, command_string: str, **kwargs) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Handle a command string by mapping to appropriate action.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
command_string: Full command string (e.g., '/help topic')
|
|
312
|
+
"""
|
|
313
|
+
logger.trace("ActionRegistry.handle_command() entry")
|
|
314
|
+
logger.debug(f"Handling command: {command_string}")
|
|
315
|
+
|
|
316
|
+
# Parse command and arguments
|
|
317
|
+
parts = command_string.strip().split()
|
|
318
|
+
if not parts:
|
|
319
|
+
logger.trace("ActionRegistry.handle_command() exit - no parts")
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
command = parts[0]
|
|
323
|
+
args = parts[1:]
|
|
324
|
+
|
|
325
|
+
# Ensure command starts with /
|
|
326
|
+
if not command.startswith('/'):
|
|
327
|
+
command = f'/{command}'
|
|
328
|
+
|
|
329
|
+
# Look up action
|
|
330
|
+
action = self.get_action_by_command(command)
|
|
331
|
+
if not action:
|
|
332
|
+
print(f"Unknown command: {command}")
|
|
333
|
+
print("Use /help to see available commands.")
|
|
334
|
+
logger.trace("ActionRegistry.handle_command() exit - unknown command")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Create context and execute
|
|
338
|
+
context = ActionContext(
|
|
339
|
+
registry=self,
|
|
340
|
+
args=args,
|
|
341
|
+
backend=self.backend,
|
|
342
|
+
triggered_by="command",
|
|
343
|
+
user_input=command_string
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
vars(context).update(kwargs)
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
self.execute_action(action.name, context)
|
|
350
|
+
logger.trace("ActionRegistry.handle_command() exit - success")
|
|
351
|
+
except ActionError as e: # pragma: no cover
|
|
352
|
+
print(f"Error: {e}")
|
|
353
|
+
logger.trace("ActionRegistry.handle_command() exit - action error")
|
|
354
|
+
except Exception as e: # pragma: no cover
|
|
355
|
+
logger.error(f"Unexpected error handling command '{command_string}': {e}") # pragma: no cover
|
|
356
|
+
print(f"An unexpected error occurred: {e}")
|
|
357
|
+
logger.trace("ActionRegistry.handle_command() exit - unexpected error")
|
|
358
|
+
|
|
359
|
+
def handle_shortcut(self, key_combo: str, event: Any, **kwargs) -> None:
|
|
360
|
+
"""
|
|
361
|
+
Handle a keyboard shortcut by mapping to appropriate action.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
key_combo: Key combination string
|
|
365
|
+
event: Key press event from prompt_toolkit
|
|
366
|
+
"""
|
|
367
|
+
logger.trace("ActionRegistry.handle_shortcut() entry")
|
|
368
|
+
logger.debug(f"Handling shortcut: {key_combo}")
|
|
369
|
+
|
|
370
|
+
# Look up action
|
|
371
|
+
action = self.get_action_by_keys(key_combo)
|
|
372
|
+
if not action:
|
|
373
|
+
logger.debug(f"No action bound to key combination: {key_combo}")
|
|
374
|
+
logger.trace("ActionRegistry.handle_shortcut() exit - no action")
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
# Create context and execute
|
|
378
|
+
context = ActionContext(
|
|
379
|
+
registry=self,
|
|
380
|
+
backend=self.backend,
|
|
381
|
+
event=event,
|
|
382
|
+
triggered_by="shortcut"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
vars(context).update(kwargs)
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
self.execute_action(action.name, context)
|
|
389
|
+
logger.trace("ActionRegistry.handle_shortcut() exit - success")
|
|
390
|
+
except ActionError as e: # pragma: no cover
|
|
391
|
+
print(f"Error: {e}")
|
|
392
|
+
logger.trace("ActionRegistry.handle_shortcut() exit - action error")
|
|
393
|
+
except Exception as e: # pragma: no cover
|
|
394
|
+
logger.error(f"Unexpected error handling shortcut '{key_combo}': {e}") # pragma: no cover
|
|
395
|
+
print(f"An unexpected error occurred: {e}")
|
|
396
|
+
logger.trace("ActionRegistry.handle_shortcut() exit - unexpected error")
|
|
397
|
+
|
|
398
|
+
# ActionHandler protocol implementation
|
|
399
|
+
def validate_action(self, action_name: str) -> bool:
|
|
400
|
+
"""Validate if an action is supported."""
|
|
401
|
+
logger.trace("ActionRegistry.validate_action() entry/exit")
|
|
402
|
+
return action_name in self.actions
|
|
403
|
+
|
|
404
|
+
def list_actions(self) -> List[str]:
|
|
405
|
+
"""Return a list of all available action names."""
|
|
406
|
+
logger.trace("ActionRegistry.list_actions() entry/exit")
|
|
407
|
+
return list(self.actions.keys())
|
|
408
|
+
|
|
409
|
+
def list_commands(self) -> List[str]:
|
|
410
|
+
"""Return a list of all available commands."""
|
|
411
|
+
logger.trace("ActionRegistry.list_commands() entry/exit")
|
|
412
|
+
return list(self.command_map.keys())
|
|
413
|
+
|
|
414
|
+
def list_shortcuts(self) -> List[str]:
|
|
415
|
+
"""Return a list of all available keyboard shortcuts."""
|
|
416
|
+
logger.trace("ActionRegistry.list_shortcuts() entry/exit")
|
|
417
|
+
return list(self.key_map.keys())
|
|
418
|
+
|
|
419
|
+
def get_actions_by_category(self) -> Dict[str, List[Action]]:
|
|
420
|
+
"""Get actions organized by category."""
|
|
421
|
+
logger.trace("ActionRegistry.get_actions_by_category() entry")
|
|
422
|
+
categories = {}
|
|
423
|
+
for action in self.actions.values():
|
|
424
|
+
if action.hidden:
|
|
425
|
+
continue
|
|
426
|
+
if action.category not in categories:
|
|
427
|
+
categories[action.category] = []
|
|
428
|
+
categories[action.category].append(action)
|
|
429
|
+
logger.trace("ActionRegistry.get_actions_by_category() exit")
|
|
430
|
+
return categories
|
|
431
|
+
|
|
432
|
+
# Built-in action handlers
|
|
433
|
+
def _show_help(self, context: ActionContext) -> None:
|
|
434
|
+
"""Show help information."""
|
|
435
|
+
logger.trace("ActionRegistry._show_help() entry")
|
|
436
|
+
|
|
437
|
+
if context.args and len(context.args) > 0:
|
|
438
|
+
# Show help for specific action or command
|
|
439
|
+
target = context.args[0]
|
|
440
|
+
|
|
441
|
+
# Try as action name first
|
|
442
|
+
action = self.get_action(target)
|
|
443
|
+
if not action:
|
|
444
|
+
# Try as command (add / if missing)
|
|
445
|
+
if not target.startswith('/'):
|
|
446
|
+
target = f'/{target}'
|
|
447
|
+
action = self.get_action_by_command(target)
|
|
448
|
+
|
|
449
|
+
if action:
|
|
450
|
+
self._show_action_help(action)
|
|
451
|
+
else: # pragma: no cover
|
|
452
|
+
print(f"No help available for: {context.args[0]}")
|
|
453
|
+
else:
|
|
454
|
+
# Show general help
|
|
455
|
+
self._show_general_help()
|
|
456
|
+
|
|
457
|
+
logger.trace("ActionRegistry._show_help() exit")
|
|
458
|
+
|
|
459
|
+
def _show_action_help(self, action: Action) -> None:
|
|
460
|
+
"""Show detailed help for a specific action."""
|
|
461
|
+
logger.trace("ActionRegistry._show_action_help() entry")
|
|
462
|
+
# pragma: no cover
|
|
463
|
+
print(f"\n{action.description}") # pragma: no cover
|
|
464
|
+
print(f"Category: {action.category}")
|
|
465
|
+
|
|
466
|
+
if action.command: # pragma: no cover
|
|
467
|
+
print(f"Command: {action.command_usage or action.command}")
|
|
468
|
+
|
|
469
|
+
if action.keys:
|
|
470
|
+
keys_str = ", ".join(action.get_keys_list())
|
|
471
|
+
desc = f" - {action.keys_description}" if action.keys_description else "" # pragma: no cover
|
|
472
|
+
print(f"Shortcut: {keys_str}{desc}")
|
|
473
|
+
|
|
474
|
+
if not action.enabled: # pragma: no cover
|
|
475
|
+
print("Status: Disabled") # pragma: no cover
|
|
476
|
+
print()
|
|
477
|
+
|
|
478
|
+
logger.trace("ActionRegistry._show_action_help() exit")
|
|
479
|
+
|
|
480
|
+
def _show_general_help(self) -> None:
|
|
481
|
+
"""Show general help with all actions organized by category."""
|
|
482
|
+
logger.trace("ActionRegistry._show_general_help() entry")
|
|
483
|
+
# pragma: no cover
|
|
484
|
+
print("\nAvailable Actions:") # pragma: no cover
|
|
485
|
+
print("=" * 50)
|
|
486
|
+
|
|
487
|
+
categories = self.get_actions_by_category()
|
|
488
|
+
|
|
489
|
+
for category, actions in sorted(categories.items()): # pragma: no cover
|
|
490
|
+
print(f"\n{category}:")
|
|
491
|
+
for action in sorted(actions, key=lambda a: a.name):
|
|
492
|
+
# Format display line
|
|
493
|
+
parts = []
|
|
494
|
+
|
|
495
|
+
if action.command:
|
|
496
|
+
parts.append(f"{action.command:<20}")
|
|
497
|
+
else:
|
|
498
|
+
parts.append(" " * 20)
|
|
499
|
+
|
|
500
|
+
if action.keys:
|
|
501
|
+
keys_str = ", ".join(action.get_keys_list())
|
|
502
|
+
parts.append(f"{keys_str:<15}")
|
|
503
|
+
else:
|
|
504
|
+
parts.append(" " * 15)
|
|
505
|
+
|
|
506
|
+
parts.append(action.description)
|
|
507
|
+
# pragma: no cover
|
|
508
|
+
print(" " + "".join(parts))
|
|
509
|
+
# pragma: no cover
|
|
510
|
+
print(f"\nUse '/help <command>' for detailed information about a specific action.") # pragma: no cover
|
|
511
|
+
print(f"Use '/shortcuts' to see only keyboard shortcuts.") # pragma: no cover
|
|
512
|
+
print()
|
|
513
|
+
|
|
514
|
+
logger.trace("ActionRegistry._show_general_help() exit")
|
|
515
|
+
|
|
516
|
+
def _list_shortcuts(self, context: ActionContext) -> None:
|
|
517
|
+
"""List all keyboard shortcuts."""
|
|
518
|
+
logger.trace("ActionRegistry._list_shortcuts() entry")
|
|
519
|
+
# pragma: no cover
|
|
520
|
+
print("\nKeyboard Shortcuts:") # pragma: no cover
|
|
521
|
+
print("=" * 50)
|
|
522
|
+
|
|
523
|
+
categories = self.get_actions_by_category()
|
|
524
|
+
|
|
525
|
+
for category, actions in sorted(categories.items()):
|
|
526
|
+
shortcuts_in_category = [a for a in actions if a.keys]
|
|
527
|
+
if not shortcuts_in_category:
|
|
528
|
+
continue
|
|
529
|
+
# pragma: no cover
|
|
530
|
+
print(f"\n{category}:")
|
|
531
|
+
for action in sorted(shortcuts_in_category, key=lambda a: a.name):
|
|
532
|
+
keys_str = ", ".join(action.get_keys_list())
|
|
533
|
+
desc = action.keys_description or action.description # pragma: no cover
|
|
534
|
+
print(f" {keys_str:<15} {desc}")
|
|
535
|
+
# pragma: no cover
|
|
536
|
+
print()
|
|
537
|
+
|
|
538
|
+
logger.trace("ActionRegistry._list_shortcuts() exit")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from .action import ActionContext
|
|
7
|
+
|
|
8
|
+
def shell_command(context: ActionContext) -> None: # pragma: no cover
|
|
9
|
+
"""Built-in shell command handler."""
|
|
10
|
+
logger.trace("shell_command() entry")
|
|
11
|
+
|
|
12
|
+
if not context.args:
|
|
13
|
+
# No arguments - drop to interactive shell
|
|
14
|
+
logger.debug("Dropping to interactive shell. Type 'exit' to return to REPL.")
|
|
15
|
+
shell = os.environ.get('SHELL', '/bin/sh' if os.name != 'nt' else 'cmd')
|
|
16
|
+
subprocess.run(shell, check=False)
|
|
17
|
+
logger.debug("Returned from shell.")
|
|
18
|
+
else:
|
|
19
|
+
# Execute shell command and add output to input buffer
|
|
20
|
+
command_args = context.args
|
|
21
|
+
command_str = ' '.join(command_args)
|
|
22
|
+
|
|
23
|
+
logger.debug(f"Executing: {command_str}")
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
command_args,
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
timeout=30
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if result.stdout:
|
|
33
|
+
# Add output directly to prompt_toolkit input buffer
|
|
34
|
+
from prompt_toolkit.application import get_app
|
|
35
|
+
app = get_app()
|
|
36
|
+
current_buffer = app.current_buffer
|
|
37
|
+
|
|
38
|
+
logger.debug(f"Current buffer before adding output: {current_buffer.text!r}")
|
|
39
|
+
|
|
40
|
+
output_text = result.stdout.strip()
|
|
41
|
+
logger.debug(f"Output_text: {output_text!r}")
|
|
42
|
+
current_buffer.insert_text(f"{output_text}")
|
|
43
|
+
|
|
44
|
+
# Force the application to redraw to show the inserted text
|
|
45
|
+
app.invalidate()
|
|
46
|
+
|
|
47
|
+
logger.debug(f"Output added to input buffer and display invalidated")
|
|
48
|
+
|
|
49
|
+
if result.stderr:
|
|
50
|
+
print("Error output:", result.stderr)
|
|
51
|
+
|
|
52
|
+
if result.returncode != 0:
|
|
53
|
+
logger.warning(f"Command exited with code: {result.returncode}")
|
|
54
|
+
|
|
55
|
+
except subprocess.TimeoutExpired:
|
|
56
|
+
logger.warning("Command timed out after 30 seconds.")
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
logger.warning(f"Command not found: {command_args[0]}")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.warning(f"Error executing command: {e}")
|
|
61
|
+
|
|
62
|
+
logger.trace("shell_command() exit")
|