fix(cli): handle CJK wide chars in TUI input height
This commit is contained in:
18
cli.py
18
cli.py
@@ -7469,18 +7469,26 @@ class HermesCLI:
|
|||||||
# wrapping of long lines so the input area always fits its content.
|
# wrapping of long lines so the input area always fits its content.
|
||||||
def _input_height():
|
def _input_height():
|
||||||
try:
|
try:
|
||||||
|
from prompt_toolkit.application import get_app
|
||||||
|
from prompt_toolkit.utils import get_cwidth
|
||||||
|
|
||||||
doc = input_area.buffer.document
|
doc = input_area.buffer.document
|
||||||
prompt_width = max(2, len(self._get_tui_prompt_text()))
|
prompt_width = max(2, get_cwidth(self._get_tui_prompt_text()))
|
||||||
available_width = shutil.get_terminal_size().columns - prompt_width
|
try:
|
||||||
|
available_width = get_app().output.get_size().columns - prompt_width
|
||||||
|
except Exception:
|
||||||
|
available_width = shutil.get_terminal_size((80, 24)).columns - prompt_width
|
||||||
if available_width < 10:
|
if available_width < 10:
|
||||||
available_width = 40
|
available_width = 40
|
||||||
visual_lines = 0
|
visual_lines = 0
|
||||||
for line in doc.lines:
|
for line in doc.lines:
|
||||||
# Each logical line takes at least 1 visual row; long lines wrap
|
# Each logical line takes at least 1 visual row; long lines wrap.
|
||||||
if len(line) == 0:
|
# Use prompt_toolkit's cell width so CJK wide characters count as 2.
|
||||||
|
line_width = get_cwidth(line)
|
||||||
|
if line_width <= 0:
|
||||||
visual_lines += 1
|
visual_lines += 1
|
||||||
else:
|
else:
|
||||||
visual_lines += max(1, -(-len(line) // available_width)) # ceil division
|
visual_lines += max(1, -(-line_width // available_width)) # ceil division
|
||||||
return min(max(visual_lines, 1), 8)
|
return min(max(visual_lines, 1), 8)
|
||||||
except Exception:
|
except Exception:
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from cli import HermesCLI
|
from cli import HermesCLI
|
||||||
|
|
||||||
@@ -78,6 +79,92 @@ class TestCLIStatusBar:
|
|||||||
assert "$0.06" not in text # cost hidden by default
|
assert "$0.06" not in text # cost hidden by default
|
||||||
assert "15m" in text
|
assert "15m" in text
|
||||||
|
|
||||||
|
def test_input_height_counts_wide_characters_using_cell_width(self):
|
||||||
|
cli_obj = _make_cli()
|
||||||
|
|
||||||
|
class _Doc:
|
||||||
|
lines = ["你" * 10]
|
||||||
|
|
||||||
|
class _Buffer:
|
||||||
|
document = _Doc()
|
||||||
|
|
||||||
|
input_area = SimpleNamespace(buffer=_Buffer())
|
||||||
|
|
||||||
|
def _input_height():
|
||||||
|
try:
|
||||||
|
from prompt_toolkit.application import get_app
|
||||||
|
from prompt_toolkit.utils import get_cwidth
|
||||||
|
|
||||||
|
doc = input_area.buffer.document
|
||||||
|
prompt_width = max(2, get_cwidth(cli_obj._get_tui_prompt_text()))
|
||||||
|
try:
|
||||||
|
available_width = get_app().output.get_size().columns - prompt_width
|
||||||
|
except Exception:
|
||||||
|
import shutil
|
||||||
|
available_width = shutil.get_terminal_size((80, 24)).columns - prompt_width
|
||||||
|
if available_width < 10:
|
||||||
|
available_width = 40
|
||||||
|
visual_lines = 0
|
||||||
|
for line in doc.lines:
|
||||||
|
line_width = get_cwidth(line)
|
||||||
|
if line_width <= 0:
|
||||||
|
visual_lines += 1
|
||||||
|
else:
|
||||||
|
visual_lines += max(1, -(-line_width // available_width))
|
||||||
|
return min(max(visual_lines, 1), 8)
|
||||||
|
except Exception:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
mock_app = MagicMock()
|
||||||
|
mock_app.output.get_size.return_value = MagicMock(columns=14)
|
||||||
|
with patch.object(HermesCLI, "_get_tui_prompt_text", return_value="❯ "), \
|
||||||
|
patch("prompt_toolkit.application.get_app", return_value=mock_app):
|
||||||
|
assert _input_height() == 2
|
||||||
|
|
||||||
|
def test_input_height_uses_prompt_toolkit_width_over_shutil(self):
|
||||||
|
cli_obj = _make_cli()
|
||||||
|
|
||||||
|
class _Doc:
|
||||||
|
lines = ["你" * 10]
|
||||||
|
|
||||||
|
class _Buffer:
|
||||||
|
document = _Doc()
|
||||||
|
|
||||||
|
input_area = SimpleNamespace(buffer=_Buffer())
|
||||||
|
|
||||||
|
def _input_height():
|
||||||
|
try:
|
||||||
|
from prompt_toolkit.application import get_app
|
||||||
|
from prompt_toolkit.utils import get_cwidth
|
||||||
|
|
||||||
|
doc = input_area.buffer.document
|
||||||
|
prompt_width = max(2, get_cwidth(cli_obj._get_tui_prompt_text()))
|
||||||
|
try:
|
||||||
|
available_width = get_app().output.get_size().columns - prompt_width
|
||||||
|
except Exception:
|
||||||
|
import shutil
|
||||||
|
available_width = shutil.get_terminal_size((80, 24)).columns - prompt_width
|
||||||
|
if available_width < 10:
|
||||||
|
available_width = 40
|
||||||
|
visual_lines = 0
|
||||||
|
for line in doc.lines:
|
||||||
|
line_width = get_cwidth(line)
|
||||||
|
if line_width <= 0:
|
||||||
|
visual_lines += 1
|
||||||
|
else:
|
||||||
|
visual_lines += max(1, -(-line_width // available_width))
|
||||||
|
return min(max(visual_lines, 1), 8)
|
||||||
|
except Exception:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
mock_app = MagicMock()
|
||||||
|
mock_app.output.get_size.return_value = MagicMock(columns=14)
|
||||||
|
with patch.object(HermesCLI, "_get_tui_prompt_text", return_value="❯ "), \
|
||||||
|
patch("prompt_toolkit.application.get_app", return_value=mock_app), \
|
||||||
|
patch("shutil.get_terminal_size") as mock_shutil:
|
||||||
|
assert _input_height() == 2
|
||||||
|
mock_shutil.assert_not_called()
|
||||||
|
|
||||||
def test_build_status_bar_text_no_cost_in_status_bar(self):
|
def test_build_status_bar_text_no_cost_in_status_bar(self):
|
||||||
cli_obj = _attach_agent(
|
cli_obj = _attach_agent(
|
||||||
_make_cli(),
|
_make_cli(),
|
||||||
|
|||||||
Reference in New Issue
Block a user