"""Terminal cell-width helpers shared by the popups and tmux labels. curses and tmux lay text out in cells: East Asian wide characters take two, combining marks and zero-width format characters none. Each popup used to carry its own copy of this logic with slightly different rules, and tmux labels were clipped by code point, so CJK names truncated inconsistently. """ from __future__ import annotations import unicodedata ZERO_WIDTH_CATEGORIES = frozenset({"Mn", "Me", "Cf", "Cc"}) def char_width(character: str) -> int: if ( unicodedata.combining(character) or unicodedata.category(character) in ZERO_WIDTH_CATEGORIES ): return 0 return 2 if unicodedata.east_asian_width(character) in {"W", "F"} else 1 def display_width(text: str) -> int: """Number of terminal cells ``text`` occupies.""" return sum(char_width(character) for character in text) def truncate_display(text: str, width: int) -> str: """Longest prefix of ``text`` that fits in ``width`` cells.""" if width <= 0: return "" used = 0 for index, character in enumerate(text): size = char_width(character) if used + size > width: return text[:index] used += size return text def display_tail(text: str, width: int) -> str: """Longest suffix of ``text`` that fits in ``width`` cells.""" if width <= 0: return "" used = 0 for index in range(len(text) - 1, -1, -1): size = char_width(text[index]) if used + size > width: return text[index + 1 :] used += size return text def pad_display(text: str, width: int) -> str: """Clip or space-pad ``text`` to exactly ``width`` cells.""" clipped = truncate_display(text, width) return clipped + " " * max(0, width - display_width(clipped)) def sanitize_display(text: str) -> str: """Replace control characters, which curses would render as ``^X``.""" return "".join( " " if unicodedata.category(character) == "Cc" else character for character in text ) def wrap_display(text: str, width: int) -> list[str]: """Hard-wrap at cell boundaries; newlines break, control characters blank.""" lines: list[str] = [] line = "" cells = 0 for character in text: if character == "\n": lines.append(line) line, cells = "", 0 continue if unicodedata.category(character) == "Cc": character = " " size = char_width(character) if cells + size > width and line: lines.append(line) line, cells = "", 0 line += character cells += size lines.append(line) return lines def input_display_line(prompt: str, value: str, width: int) -> str: """``prompt: value`` clipped to ``width`` cells, keeping the end of the value.""" prefix = f"{prompt}: " if display_width(prefix) >= width: return truncate_display(prefix, width) value_width = width - display_width(prefix) if display_width(value) <= value_width: visible_value = value elif value_width <= 1: visible_value = "…"[:value_width] else: visible_value = "…" + display_tail(value, value_width - 1) return prefix + visible_value