Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10484,21 +10484,6 @@ def handle_ctrl_c(event):
self._should_exit = True
event.app.exit()

@kb.add('c-S-c') # Ctrl+Shift+C
def handle_ctrl_shift_c(event):
"""Copy text to clipboard (terminal-native).

This is a no-op at the application level. Terminal emulators
handle the actual copy operation when Ctrl+Shift+C is pressed.
This binding prevents Hermes from intercepting the keystroke
as an interrupt signal.

On macOS the standard copy shortcut is Cmd+C (no Hermes binding
needed). On Linux/Windows Ctrl+Shift+C is the conventional
terminal copy shortcut.
"""
return # No-op — let the terminal perform native copy

@kb.add('c-q') # Ctrl+Q
def handle_ctrl_q(event):
"""Alternative interrupt/exit shortcut (Ctrl+Q).
Expand Down
53 changes: 53 additions & 0 deletions tests/hermes_cli/test_cli_keybindings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Regression tests for prompt_toolkit CLI key bindings."""

import ast
from pathlib import Path

import pytest
from prompt_toolkit.key_binding import KeyBindings


CLI_PATH = Path(__file__).resolve().parents[2] / "cli.py"


def _literal_key_sequences_from_cli():
"""Return literal key sequences passed to ``*.add(...)`` in cli.py.

prompt_toolkit validates key names when decorators are registered. A bad
key name in the interactive CLI prevents ``hermes`` from starting, so keep
this lightweight static regression test around for startup safety.
"""
tree = ast.parse(CLI_PATH.read_text(), filename=str(CLI_PATH))
sequences = []

for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
if not isinstance(node.func, ast.Attribute) or node.func.attr != "add":
continue
if not isinstance(node.func.value, ast.Name) or node.func.value.id != "kb":
continue

literal_keys = []
for arg in node.args:
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
literal_keys.append(arg.value)
else:
# Dynamic bindings such as kb.add(str(_num), ...) are covered
# by runtime tests; this check is for hard-coded key names.
literal_keys = []
break
if literal_keys:
sequences.append(tuple(literal_keys))

return sequences


@pytest.mark.parametrize("keys", _literal_key_sequences_from_cli())
def test_cli_literal_keybindings_are_valid_prompt_toolkit_keys(keys):
"""Every hard-coded key binding in cli.py must be accepted by prompt_toolkit."""
kb = KeyBindings()

@kb.add(*keys)
def _handler(event): # pragma: no cover - registration is the assertion
return None