diff options
| author | historia <historiavg@proton.me> | 2026-08-28 04:45:29 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-28 04:45:29 -0400 |
| commit | 5ed31309b6d0db94ccd566914653d367f4577c64 (patch) | |
| tree | b909445125629181eb8aa38e4805f672bdaf9c20 /app/ui | |
| parent | 0dda9a5921eafd03853d233e077138ff72d64e4d (diff) | |
| download | tts-audiobook-generator-5ed31309b6d0db94ccd566914653d367f4577c64.tar.gz | |
fix: japanese characters not truncated correctly in tui terminal output
Diffstat (limited to 'app/ui')
| -rw-r--r-- | app/ui/tui.py | 27 | ||||
| -rw-r--r-- | app/ui/viewkit.py | 8 |
2 files changed, 25 insertions, 10 deletions
diff --git a/app/ui/tui.py b/app/ui/tui.py index b22a186..60e2dda 100644 --- a/app/ui/tui.py +++ b/app/ui/tui.py @@ -37,6 +37,7 @@ import contextlib import os import re import textwrap +import unicodedata from pathlib import Path from typing import Callable, List, Optional, Sequence, Tuple, Union @@ -255,13 +256,31 @@ def _hline(scr, y: int, x: int, n: int, attr: int = 0) -> None: pass +def _char_width(ch: str) -> int: + """Terminal cells CH occupies (East Asian Wide/Fullwidth count 2).""" + return 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 + + +def _disp_width(text: str) -> int: + """Terminal cells TEXT occupies (East Asian Wide/Fullwidth count 2).""" + return sum(_char_width(ch) for ch in text) + + def _fit(text: str, width: int) -> str: """Truncate TEXT to WIDTH columns, appending '~' when cut.""" if width < 1: return "" - if len(text) <= width: + if _disp_width(text) <= width: return text - return text[: max(0, width - 1)] + "~" + out: List[str] = [] + used = 0 + for ch in text: + w = _char_width(ch) + if used + w > width - 1: + break + out.append(ch) + used += w + return "".join(out) + "~" def _wrap_segments(segments: Sequence[Tuple[str, int]], width: int @@ -283,13 +302,13 @@ def _wrap_segments(segments: Sequence[Tuple[str, int]], width: int current: List[Tuple[str, int]] = [] used = 0 for text, attr in tokens: - if current and used + len(text) > width: + if current and used + _disp_width(text) > width: lines.append(current) current, used = [], 0 if text.isspace(): continue # the break eats the whitespace it happens at current.append((text, attr)) - used += len(text) + used += _disp_width(text) if current: lines.append(current) return lines or [[]] diff --git a/app/ui/viewkit.py b/app/ui/viewkit.py index 301a0af..e54aada 100644 --- a/app/ui/viewkit.py +++ b/app/ui/viewkit.py @@ -217,11 +217,7 @@ def _sep(scr, curses, theme, y, width) -> None: def _fit(text: str, width: int) -> str: """Truncate TEXT to WIDTH columns, appending '~' when cut.""" - if width < 1: - return "" - if len(text) <= width: - return text - return text[: max(0, width - 1)] + "~" + return tui._fit(text, width) def _wrap(text: str, width: int) -> List[str]: @@ -230,7 +226,7 @@ def _wrap(text: str, width: int) -> List[str]: current = "" for word in text.split(): candidate = f"{current} {word}".strip() - if len(candidate) <= max(10, width): + if tui._disp_width(candidate) <= max(10, width): current = candidate else: if current: |
