diff --git a/pyproject.toml b/pyproject.toml index c410be6..5097f71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ description = """\ license = {text = "MIT"} requires-python = ">=3.8" -dependencies = ["typing_extensions"] +dependencies = ["wcwidth", "typing_extensions"] keywords = [ "terminal", diff --git a/pytermgui/regex.py b/pytermgui/regex.py index 23bc9ad..c5e8e1e 100644 --- a/pytermgui/regex.py +++ b/pytermgui/regex.py @@ -4,6 +4,8 @@ from functools import lru_cache from typing import Match +from wcwidth import wcswidth + RE_LINK = re.compile(r"(?:\x1b\]8;;([^\\]*)\x1b\\([^\\]*?)\x1b\]8;;\x1b\\)") RE_ANSI_NEW = re.compile(rf"(\x1b\[(.*?)[mH])|{RE_LINK.pattern}|(\x1b_G(.*?)\x1b\\)") RE_ANSI = re.compile(r"(?:\x1b\[(.*?)[mH])|(?:\x1b\](.*?)\x1b\\)|(?:\x1b_G(.*?)\x1b\\)") @@ -69,7 +71,7 @@ def real_length(text: str) -> int: The display-length of text. """ - return len(strip_ansi(text)) + return wcswidth(strip_ansi(text)) def escape_markup(text: str) -> str: diff --git a/pytermgui/widgets/input_field.py b/pytermgui/widgets/input_field.py index ac13ce2..7ae8659 100644 --- a/pytermgui/widgets/input_field.py +++ b/pytermgui/widgets/input_field.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from typing import Any, Iterator, Literal +from wcwidth import wcwidth + from ..ansi_interface import MouseAction, MouseEvent from ..enums import HorizontalAlignment from ..helpers import break_line @@ -83,6 +85,9 @@ def __init__( if "width" not in attrs: self.width = len(value) + if any(wcwidth(char) > 1 for char in value): + raise ValueError("InputField doesn't support wide unicode characters.") + self.prompt = prompt self.height = 1 self.tablength = tablength @@ -431,6 +436,7 @@ def move_cursor(self, new: tuple[int, int], *, absolute: bool = False) -> None: row, col = self.cursor line = self._lines[row] + width = len(line) # Going left, possibly upwards if col < 0: @@ -440,17 +446,17 @@ def move_cursor(self, new: tuple[int, int], *, absolute: bool = False) -> None: else: self.cursor.row -= 1 line = self._lines[self.cursor.row] - self.cursor.col = len(line) + self.cursor.col = width # Going right, possibly downwards - elif col > len(line) and line != "": + elif col > width and line != "": if len(self._lines) > row + 1: self.cursor.row += 1 self.cursor.col = 0 line = self._lines[self.cursor.row] - self.cursor.col = max(0, min(self.cursor.col, len(line))) + self.cursor.col = max(0, min(self.cursor.col, width)) def get_lines(self) -> list[str]: """Builds the input field's lines."""