132 lines
4.3 KiB
Python
132 lines
4.3 KiB
Python
"""Unified input handler for all platforms."""
|
|
|
|
import sys
|
|
import unicodedata
|
|
|
|
|
|
class TextInputHandler:
|
|
"""Input handler with platform-specific key code support."""
|
|
|
|
# Platform-specific key codes
|
|
if sys.platform == "win32":
|
|
# Windows uses \xe0 prefix for special keys when using msvcrt.getwch
|
|
DOWN_KEY = "\xe0P" # Down arrow
|
|
UP_KEY = "\xe0H" # Up arrow
|
|
LEFT_KEY = "\xe0K" # Left arrow
|
|
RIGHT_KEY = "\xe0M" # Right arrow
|
|
DELETE_KEY = "\xe0S" # Delete key
|
|
BACKSPACE_KEY = "\x08" # Backspace
|
|
TAB_KEY = "\t"
|
|
SHIFT_TAB_KEY = "\x00\x0f" # Shift+Tab
|
|
ENTER_KEY = "\r"
|
|
|
|
# Alternative codes that might be sent
|
|
ALT_BACKSPACE = "\x7f"
|
|
ALT_DELETE = "\x00S"
|
|
else:
|
|
# Unix/Linux key codes (ANSI escape sequences)
|
|
DOWN_KEY = "\x1b[B"
|
|
UP_KEY = "\x1b[A"
|
|
LEFT_KEY = "\x1b[D"
|
|
RIGHT_KEY = "\x1b[C"
|
|
BACKSPACE_KEY = "\x7f"
|
|
DELETE_KEY = "\x1b[3~"
|
|
TAB_KEY = "\t"
|
|
SHIFT_TAB_KEY = "\x1b[Z"
|
|
ENTER_KEY = "\r"
|
|
|
|
# Alternative codes
|
|
ALT_BACKSPACE = "\x08"
|
|
ALT_DELETE = None
|
|
|
|
def __init__(self):
|
|
self.text = ""
|
|
self._cursor_index = 0 # Character index in the text string
|
|
|
|
@property
|
|
def cursor_left(self) -> int:
|
|
"""Visual cursor position in display columns."""
|
|
return self._get_text_width(self.text[: self._cursor_index])
|
|
|
|
@staticmethod
|
|
def _get_char_width(char: str) -> int:
|
|
"""Get the display width of a character (1 for normal, 2 for CJK/fullwidth)."""
|
|
if not char:
|
|
return 0
|
|
|
|
# Check East Asian Width property
|
|
east_asian_width = unicodedata.east_asian_width(char)
|
|
# F (Fullwidth) and W (Wide) characters take 2 columns
|
|
if east_asian_width in ("F", "W"):
|
|
return 2
|
|
# A (Ambiguous) characters are typically 2 columns in CJK contexts
|
|
# but for simplicity we'll treat them as 1 (can be made configurable)
|
|
return 1
|
|
|
|
def _get_text_width(self, text: str) -> int:
|
|
"""Get the total display width of a text string."""
|
|
return sum(self._get_char_width(char) for char in text)
|
|
|
|
def _move_cursor_left(self) -> None:
|
|
self._cursor_index = max(0, self._cursor_index - 1)
|
|
|
|
def _move_cursor_right(self) -> None:
|
|
self._cursor_index = min(len(self.text), self._cursor_index + 1)
|
|
|
|
def _insert_char(self, char: str) -> None:
|
|
self.text = (
|
|
self.text[: self._cursor_index] + char + self.text[self._cursor_index :]
|
|
)
|
|
self._cursor_index += 1
|
|
|
|
def _delete_char(self) -> None:
|
|
"""Delete character before cursor (backspace)."""
|
|
if self._cursor_index == 0:
|
|
return
|
|
|
|
self.text = (
|
|
self.text[: self._cursor_index - 1] + self.text[self._cursor_index :]
|
|
)
|
|
self._cursor_index -= 1
|
|
|
|
def _delete_forward(self) -> None:
|
|
"""Delete character at cursor (delete key)."""
|
|
if self._cursor_index >= len(self.text):
|
|
return
|
|
|
|
self.text = (
|
|
self.text[: self._cursor_index] + self.text[self._cursor_index + 1 :]
|
|
)
|
|
|
|
def handle_key(self, key: str) -> None:
|
|
# Handle backspace (both possible codes)
|
|
if key == self.BACKSPACE_KEY or (
|
|
self.ALT_BACKSPACE and key == self.ALT_BACKSPACE
|
|
):
|
|
self._delete_char()
|
|
# Handle delete key
|
|
elif key == self.DELETE_KEY or (self.ALT_DELETE and key == self.ALT_DELETE):
|
|
self._delete_forward()
|
|
elif key == self.LEFT_KEY:
|
|
self._move_cursor_left()
|
|
elif key == self.RIGHT_KEY:
|
|
self._move_cursor_right()
|
|
elif key in (
|
|
self.UP_KEY,
|
|
self.DOWN_KEY,
|
|
self.ENTER_KEY,
|
|
self.SHIFT_TAB_KEY,
|
|
self.TAB_KEY,
|
|
):
|
|
pass
|
|
else:
|
|
# Handle regular text input
|
|
# Special keys on Windows start with \x00 or \xe0
|
|
if sys.platform == "win32" and key and key[0] in ("\x00", "\xe0"):
|
|
# Skip special key sequences
|
|
return
|
|
|
|
# Even if we call this handle_key, in some cases we might receive
|
|
# multiple keys at once (e.g., during paste operations)
|
|
for char in key:
|
|
self._insert_char(char)
|