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:
1 parent
20991acc91
commit
ee047f1212
3 files changed
+4
-110
No files matched your search
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user