_input_handler.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """Unified input handler for all platforms."""
  2. import sys
  3. import unicodedata
  4. class TextInputHandler:
  5. """Input handler with platform-specific key code support."""
  6. # Platform-specific key codes
  7. if sys.platform == "win32":
  8. # Windows uses \xe0 prefix for special keys when using msvcrt.getwch
  9. DOWN_KEY = "\xe0P" # Down arrow
  10. UP_KEY = "\xe0H" # Up arrow
  11. LEFT_KEY = "\xe0K" # Left arrow
  12. RIGHT_KEY = "\xe0M" # Right arrow
  13. DELETE_KEY = "\xe0S" # Delete key
  14. BACKSPACE_KEY = "\x08" # Backspace
  15. TAB_KEY = "\t"
  16. SHIFT_TAB_KEY = "\x00\x0f" # Shift+Tab
  17. ENTER_KEY = "\r"
  18. # Alternative codes that might be sent
  19. ALT_BACKSPACE = "\x7f"
  20. ALT_DELETE = "\x00S"
  21. else:
  22. # Unix/Linux key codes (ANSI escape sequences)
  23. DOWN_KEY = "\x1b[B"
  24. UP_KEY = "\x1b[A"
  25. LEFT_KEY = "\x1b[D"
  26. RIGHT_KEY = "\x1b[C"
  27. BACKSPACE_KEY = "\x7f"
  28. DELETE_KEY = "\x1b[3~"
  29. TAB_KEY = "\t"
  30. SHIFT_TAB_KEY = "\x1b[Z"
  31. ENTER_KEY = "\r"
  32. # Alternative codes
  33. ALT_BACKSPACE = "\x08"
  34. ALT_DELETE = None
  35. def __init__(self):
  36. self.text = ""
  37. self._cursor_index = 0 # Character index in the text string
  38. @property
  39. def cursor_left(self) -> int:
  40. """Visual cursor position in display columns."""
  41. return self._get_text_width(self.text[: self._cursor_index])
  42. @staticmethod
  43. def _get_char_width(char: str) -> int:
  44. """Get the display width of a character (1 for normal, 2 for CJK/fullwidth)."""
  45. if not char:
  46. return 0
  47. # Check East Asian Width property
  48. east_asian_width = unicodedata.east_asian_width(char)
  49. # F (Fullwidth) and W (Wide) characters take 2 columns
  50. if east_asian_width in ("F", "W"):
  51. return 2
  52. # A (Ambiguous) characters are typically 2 columns in CJK contexts
  53. # but for simplicity we'll treat them as 1 (can be made configurable)
  54. return 1
  55. def _get_text_width(self, text: str) -> int:
  56. """Get the total display width of a text string."""
  57. return sum(self._get_char_width(char) for char in text)
  58. def _move_cursor_left(self) -> None:
  59. self._cursor_index = max(0, self._cursor_index - 1)
  60. def _move_cursor_right(self) -> None:
  61. self._cursor_index = min(len(self.text), self._cursor_index + 1)
  62. def _insert_char(self, char: str) -> None:
  63. self.text = (
  64. self.text[: self._cursor_index] + char + self.text[self._cursor_index :]
  65. )
  66. self._cursor_index += 1
  67. def _delete_char(self) -> None:
  68. """Delete character before cursor (backspace)."""
  69. if self._cursor_index == 0:
  70. return
  71. self.text = (
  72. self.text[: self._cursor_index - 1] + self.text[self._cursor_index :]
  73. )
  74. self._cursor_index -= 1
  75. def _delete_forward(self) -> None:
  76. """Delete character at cursor (delete key)."""
  77. if self._cursor_index >= len(self.text):
  78. return
  79. self.text = (
  80. self.text[: self._cursor_index] + self.text[self._cursor_index + 1 :]
  81. )
  82. def handle_key(self, key: str) -> None:
  83. # Handle backspace (both possible codes)
  84. if key == self.BACKSPACE_KEY or (
  85. self.ALT_BACKSPACE and key == self.ALT_BACKSPACE
  86. ):
  87. self._delete_char()
  88. # Handle delete key
  89. elif key == self.DELETE_KEY or (self.ALT_DELETE and key == self.ALT_DELETE):
  90. self._delete_forward()
  91. elif key == self.LEFT_KEY:
  92. self._move_cursor_left()
  93. elif key == self.RIGHT_KEY:
  94. self._move_cursor_right()
  95. elif key in (
  96. self.UP_KEY,
  97. self.DOWN_KEY,
  98. self.ENTER_KEY,
  99. self.SHIFT_TAB_KEY,
  100. self.TAB_KEY,
  101. ):
  102. pass
  103. else:
  104. # Handle regular text input
  105. # Special keys on Windows start with \x00 or \xe0
  106. if sys.platform == "win32" and key and key[0] in ("\x00", "\xe0"):
  107. # Skip special key sequences
  108. return
  109. # Even if we call this handle_key, in some cases we might receive
  110. # multiple keys at once (e.g., during paste operations)
  111. for char in key:
  112. self._insert_char(char)