Unify terminal display-width helpers

key_help, selector and control each carried their own East Asian width
and truncation logic with slightly different rules, and cli clipped tmux
labels by code point, so a CJK display name ran twice as wide as an
ASCII one before its ellipsis appeared.

Move the cell-width helpers into herdr_hub.text, make truncate_label
cell-aware, and sanitise control characters before curses draws a row
so an escape sequence in a remote error cannot be rendered as ^[ and
misalign the list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 03:09:28 +00:00
co-authored by Claude Fable 5.1
parent d2228ed5fb
commit ac6bb30f47
7 changed files with 194 additions and 136 deletions
+3 -24
View File
@@ -15,12 +15,12 @@ import threading
import time
from typing import TYPE_CHECKING, Callable, Sequence
import uuid
import unicodedata
from .remotes import (
RemoteEntry, clip_utf8_tail, decode_remote_output, parse_remote_session_result,
remote_control_command, remote_session_command,
)
from .text import wrap_display
if TYPE_CHECKING:
from .cli import Endpoint
@@ -431,27 +431,6 @@ class HerdrControl:
return tab_id
def display_chunks(text: str, width: int) -> list[str]:
chunks: list[str] = []
line = ""
cells = 0
for character in text:
if character == "\n":
chunks.append(line)
line, cells = "", 0
continue
if ord(character) < 32 or ord(character) == 127:
character = " "
size = 0 if unicodedata.combining(character) else (2 if unicodedata.east_asian_width(character) in ("W", "F") else 1)
if cells + size > width and line:
chunks.append(line)
line, cells = "", 0
line += character
cells += size
chunks.append(line)
return chunks
class ControlMenu:
"""Small action menu; all subprocess work stays off the curses thread."""
ACTIONS = (
@@ -570,7 +549,7 @@ class ControlMenu:
self._add(screen, y + 1, 4, detail, curses.A_DIM)
status_top = action_top + len(self.ACTIONS) * action_stride + (0 if compact else 1)
status_lines = display_chunks(status, max(1, width - 5)) if status else []
status_lines = wrap_display(status, max(1, width - 5)) if status else []
visible_lines = max(1, height - status_top - 1)
status_offset = min(status_offset, max(0, len(status_lines) - visible_lines))
if future is not None and phase in ("update", "update-restart"):
@@ -664,7 +643,7 @@ class ControlMenu:
height, width = screen.getmaxyx()
if y < 0 or y >= height or x >= width:
return
chunks = display_chunks(value, max(1, width - x - 1))
chunks = wrap_display(value, max(1, width - x - 1))
try:
screen.addstr(y, x, chunks[0] if chunks else "", attribute)
except curses.error: