diff --git a/meshterm/ui/tui/glyphwidth.py b/meshterm/ui/tui/glyphwidth.py index f7ceb6c..3de26a8 100644 --- a/meshterm/ui/tui/glyphwidth.py +++ b/meshterm/ui/tui/glyphwidth.py @@ -24,10 +24,9 @@ ordinary differential painting stays synced and nothing lingers — no whole-fra compositing tricks. CJK and fullwidth glyphs, which terminals *do* draw two cells wide, are left at two; node-type marks and braille are already one cell everywhere and are untouched. -This is the sibling of :class:`~meshterm.ui.tui.session._WidthExtendedOutput`: both correct a -width assumption the terminal breaks, in the one place the whole pipeline reads it. Gated by -``MESHTERM_NARROW_EMOJI`` (default on); set it to ``0`` on a terminal that genuinely draws -emoji two cells wide, where the alignment would instead *introduce* a one-cell error. +Correcting the width in the one place the whole pipeline reads it, rather than per screen. +Gated by ``MESHTERM_NARROW_EMOJI`` (default on); set it to ``0`` on a terminal that genuinely +draws emoji two cells wide, where the alignment would instead *introduce* a one-cell error. """ from __future__ import annotations diff --git a/meshterm/ui/tui/session.py b/meshterm/ui/tui/session.py index 60ac727..9bc8329 100644 --- a/meshterm/ui/tui/session.py +++ b/meshterm/ui/tui/session.py @@ -12,12 +12,10 @@ and pop it — the push/await/pop model behind every prompt. from __future__ import annotations import asyncio -import os from contextlib import asynccontextmanager from typing import Any, AsyncIterator, Callable, Optional from prompt_toolkit.application import Application -from prompt_toolkit.data_structures import Size from prompt_toolkit.filters import Condition from prompt_toolkit.formatted_text import ANSI from prompt_toolkit.key_binding import KeyBindings @@ -73,49 +71,12 @@ _KEY_ACTIONS: dict[Any, str] = { } -#: Reclaim the terminal's final column. Some terminals (and prompt_toolkit's size probe on -#: them) report the window one column narrower than it really is, so the frame is drawn to -#: ``columns - 1`` and the true last column sits unused — visibly selectable to the right of -#: the border. When this is on, :class:`_WidthExtendedOutput` tells both the renderer and the -#: frame compositor the window is one column wider, and that final column gets drawn. -#: -#: This is a gate, not a certainty: it is *correct* only when the probe under-reports. On a -#: terminal whose width probe is already right, the extra column falls off the real screen and -#: the frame would wrap and tear, so it can be switched off with ``MESHTERM_FULL_WIDTH=0``. -#: Kept as an env-var gate for now so it can graduate to a user setting once confirmed across -#: terminals (TODO: thread through ``settings`` and the config editor). -_RECLAIM_LAST_COLUMN = os.environ.get("MESHTERM_FULL_WIDTH", "1") != "0" - #: How many stacked dialog layers the layout can float over the background at once. A fixed #: pool of centered-box floats (see :meth:`TuiSession._build_app`), sized well past the deepest #: real nesting — a tool's list, an item's detail popup, and a confirm over that is only three. _MAX_DIALOG_LAYERS = 8 -class _WidthExtendedOutput: - """A prompt_toolkit ``Output`` proxy that reports one extra terminal column. - - Wraps the real output and forwards everything untouched *except* :meth:`get_size`, which - adds a column. Because the whole render pipeline — prompt_toolkit's differential renderer - and MeshTerm's own frame compositor (via :meth:`TuiSession._size`) — keys off - ``output.get_size()``, this single override makes both use the reclaimed column in - lock-step. See :data:`_RECLAIM_LAST_COLUMN` for when this is right (and when it isn't). - """ - - def __init__(self, inner: Any) -> None: - """Wrap ``inner`` (the concrete prompt_toolkit output for the real terminal).""" - self._inner = inner - - def get_size(self) -> Size: - """The wrapped size with one column added, so the last column is claimed.""" - size = self._inner.get_size() - return Size(rows=size.rows, columns=size.columns + 1) - - def __getattr__(self, name: str) -> Any: - """Forward every other attribute/method straight to the wrapped output.""" - return getattr(self._inner, name) - - def _message_border(message: "Text | str") -> str: """Pick a message dialog's border style from the strongest tone in the text. @@ -921,23 +882,9 @@ class TuiSession: mouse_support=False, refresh_interval=1.0, # keep the live monitor counter in the header ticking input=self._input, - output=self._resolve_output(), + output=self._output, ) - def _resolve_output(self) -> Any: - """The output the app renders to — optionally widened to reclaim the last column. - - Only the *real* terminal (``self._output is None``, so prompt_toolkit would build its - own output) is wrapped, and only when :data:`_RECLAIM_LAST_COLUMN` is on: a test that - supplies its own output keeps the exact size it set, so headless rendering stays - deterministic. See :class:`_WidthExtendedOutput` for what the wrap does. - """ - if self._output is not None or not _RECLAIM_LAST_COLUMN: - return self._output - from prompt_toolkit.output.defaults import create_output - - return _WidthExtendedOutput(create_output()) - # --- rendering ----------------------------------------------------------- def _size(self) -> tuple[int, int]: diff --git a/tests/test_session_width.py b/tests/test_session_width.py deleted file mode 100644 index 12857d2..0000000 --- a/tests/test_session_width.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Session width tests: reclaiming the terminal's final column. - -Some terminals (and prompt_toolkit's size probe on them) report the window one column -narrower than it really is, so the frame's right border lands one short and the true last -column sits unused. The session can report one extra column to close that gap. The wrapping -output is pure and the resolver is a small branch, so both are assertable without a terminal. -""" - -from __future__ import annotations - -from types import SimpleNamespace - -from prompt_toolkit.data_structures import Size - -import meshterm.ui.tui.session as session_mod -from meshterm.ui.tui.session import TuiSession, _WidthExtendedOutput - - -def test_width_extended_output_reports_one_more_column() -> None: - """get_size() gains a column; every other attribute forwards to the wrapped output.""" - inner = SimpleNamespace( - get_size=lambda: Size(rows=24, columns=80), - write=lambda s: f"wrote:{s}", - encoding="utf-8", - ) - out = _WidthExtendedOutput(inner) - assert out.get_size() == Size(rows=24, columns=81) # the reclaimed column - assert out.write("x") == "wrote:x" # forwarded method - assert out.encoding == "utf-8" # forwarded attribute - - -def test_session_leaves_a_supplied_output_untouched() -> None: - """A test-supplied output is never wrapped — headless sizes stay exactly as set.""" - dummy = SimpleNamespace(get_size=lambda: Size(rows=10, columns=40)) - session = TuiSession(output=dummy) - assert session._resolve_output() is dummy - - -def test_session_wraps_the_real_terminal_when_enabled(monkeypatch) -> None: - """No supplied output + gate on → the real terminal output is width-extended.""" - fake = SimpleNamespace(get_size=lambda: Size(rows=30, columns=100)) - monkeypatch.setattr("prompt_toolkit.output.defaults.create_output", lambda: fake) - monkeypatch.setattr(session_mod, "_RECLAIM_LAST_COLUMN", True) - out = TuiSession()._resolve_output() - assert isinstance(out, _WidthExtendedOutput) - assert out.get_size() == Size(rows=30, columns=101) - - -def test_session_gate_off_uses_the_bare_terminal(monkeypatch) -> None: - """Gate off → the real terminal output is used untouched (pt builds its own).""" - monkeypatch.setattr(session_mod, "_RECLAIM_LAST_COLUMN", False) - assert TuiSession()._resolve_output() is None