Drop the +1 last-column width reclaim

The MESHTERM_FULL_WIDTH reclaim reported the terminal one column wider than
probed, to draw a final column some terminals under-report. With the emoji
width now aligned to the terminal, that extra column pushes the frame past the
real screen edge and tears the layout. Remove it: the size probe is used as-is
again (_WidthExtendedOutput, _RECLAIM_LAST_COLUMN, _resolve_output, and the
MESHTERM_FULL_WIDTH gate all gone), and the app renders to prompt_toolkit's
default output. Drops tests/test_session_width.py with the feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 4.8 committed 2026-07-16 23:26:02 -04:00
1 parent 20991acc91
commit ee047f1212
3 files changed
+4 -110

No files matched your search

+3 -4
View File
@@ -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
+1 -54
View File
@@ -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]:
-52
View File
@@ -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