The battery shows its last digit on Linux and macOS
A full T1000-E read 9 or 10% in the header on Ubuntu, while the radio reported 4.21 V. The battery was never the fault. A full pack's "100" lost its last zero, and 99% lost its 9 to the % sign that followed it. MeshTerm adds one column to the width the terminal reports, because prompt_toolkit's Windows console output reports one column fewer than the window has, on purpose. That was done on every desktop, but a Linux or macOS terminal reports its width exactly. There the extra column fell off the screen. prompt_toolkit draws with autowrap off, so each row's last character landed on top of the one before it, and whatever sat flush right lost its end. The battery gauge sits flush right. The column is now added only where the terminal hid one, which is the Windows console. The full_width preference and MESHTERM_FULL_WIDTH still override either way. Driven into an exact 80-column terminal under --mock, the header now ends "100" at 4.21 V and "98%" at 4.18 V, where it ended "10" and "9%". In a real Windows console the reported 119 columns still widen to 120. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
44e60c4054
commit
7bcb1db360
4 files changed
+121
-36
No files matched your search
@@ -157,11 +157,11 @@ _SILENCE_CHOICES: dict[int, str] = {
|
||||
#: scheduler's own choices so the two never drift.
|
||||
_QUIET_CHOICES: dict[int, str] = {s: f"{s} s" for s in QUIET_CHOICES_S}
|
||||
|
||||
#: How the terminal-width reclaim can be asked for: follow the platform's own verdict, or
|
||||
#: How the terminal-width reclaim can be asked for: follow the terminal's own size probe, or
|
||||
#: overrule it in either direction. The row's description asks a yes/no question ("use the
|
||||
#: column ... ?"), so the values answer it in those words rather than naming the mechanism
|
||||
#: twice. See :func:`meshterm.ui.tui.session._reclaim_last_column` for why a terminal may
|
||||
#: need to disagree with its platform.
|
||||
#: twice. See :func:`meshterm.ui.tui.session._reclaim_last_column` for which terminals hide
|
||||
#: a column, and what reclaiming one that isn't hidden costs.
|
||||
_WIDTH_CHOICES: dict[str, str] = {"auto": "auto", "yes": "yes", "no": "no"}
|
||||
|
||||
#: How many colours to send the terminal. ``auto`` is the honest default and the only
|
||||
|
||||
@@ -97,10 +97,12 @@ class Platform:
|
||||
own, so one can change without the other following. :attr:`footer_fkeys` is the
|
||||
yes/no reading of it.
|
||||
width_reclaim: Whether the session may report one extra terminal column (see
|
||||
:class:`~meshterm.ui.tui.session._WidthExtendedOutput`). Correctness-critical
|
||||
on PicoCalc's exact-width console: a phantom extra column tears the frame, so
|
||||
this is ``False`` there. ``MESHTERM_FULL_WIDTH`` remains an explicit override
|
||||
on top of this default.
|
||||
:class:`~meshterm.ui.tui.session._WidthExtendedOutput`). ``True`` only *lets*
|
||||
it: the column is reclaimed where the terminal's size probe hid one, which is
|
||||
prompt_toolkit's Windows console output and nothing else
|
||||
(:func:`~meshterm.ui.tui.session._probe_hides_last_column`). ``False`` on
|
||||
PicoCalc's exact-width console, where a phantom extra column tears the frame.
|
||||
``MESHTERM_FULL_WIDTH`` remains an explicit override on top of this default.
|
||||
emoji: Whether emoji icons render at all. ``False`` means every icon funnel routes
|
||||
through a compact single-BMP-glyph table instead (P3), and neither half of the
|
||||
emoji alignment runs — the reserve-two measurement
|
||||
|
||||
+46
-18
@@ -228,24 +228,28 @@ def _read_clipboard() -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _reclaim_last_column() -> bool:
|
||||
"""Whether to reclaim the terminal's final column.
|
||||
def _reclaim_last_column() -> bool | None:
|
||||
"""Whether to reclaim the terminal's final column — or ``None``, to ask the terminal.
|
||||
|
||||
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.
|
||||
prompt_toolkit's Windows console output reports the window one column narrower than it
|
||||
really is, on purpose (see :func:`_probe_hides_last_column`), 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 — which is exactly PicoCalc's exact-width console, so
|
||||
:data:`~meshterm.platforms.PICOCALC_LYRA` defaults this off.
|
||||
It is *correct* only where the probe hid the column. Everywhere else the extra column
|
||||
falls off the real screen, and since prompt_toolkit draws with autowrap off, each row's
|
||||
phantom last cell is written over the real one before it: whatever sits flush right
|
||||
loses its final character. On Linux that cut the header's battery gauge, so a full pack's
|
||||
bare ``100`` read ``10`` and 99% read ``9%``. Hence the default is a question rather than
|
||||
an answer: ``None`` means "reclaim if, and only if, the output this session draws on is
|
||||
one whose probe hides the column", decided once that output exists.
|
||||
|
||||
Three sources, in confidence order, because the person at the keyboard can see the
|
||||
column and the platform can only guess at it: ``MESHTERM_FULL_WIDTH=0``/``=1`` for a
|
||||
one-off, then the ``full_width`` preference (``yes``/``no``; ``auto`` — the default —
|
||||
steps aside), then the platform's own verdict.
|
||||
column and the code can only infer it: ``MESHTERM_FULL_WIDTH=0``/``=1`` for a one-off,
|
||||
then the ``full_width`` preference (``yes``/``no``; ``auto`` — the default — steps
|
||||
aside), then the platform, which answers ``False`` outright where its console is exact
|
||||
(:data:`~meshterm.platforms.PICOCALC_LYRA`) and otherwise leaves it to the terminal.
|
||||
|
||||
Read fresh on every call (never cached at import time) so it reflects whichever platform
|
||||
:func:`~meshterm.platforms.set_platform` installed for this process — see that module's
|
||||
@@ -259,7 +263,26 @@ def _reclaim_last_column() -> bool:
|
||||
preferred = current_preferences().full_width
|
||||
if preferred != "auto":
|
||||
return preferred == "yes"
|
||||
return get_platform().width_reclaim
|
||||
return None if get_platform().width_reclaim else False
|
||||
|
||||
|
||||
def _probe_hides_last_column(output: Any) -> bool:
|
||||
"""Whether prompt_toolkit sizes ``output`` one column narrower than its window.
|
||||
|
||||
True of its Windows console output and nothing else. ``Win32Output.get_size`` takes the
|
||||
visible width as ``srWindow.Right - srWindow.Left`` (an inclusive span, so one short) and
|
||||
then caps it below the buffer's right margin — "windows will wrap otherwise" — and
|
||||
``Windows10_Output`` and ``ConEmuOutput`` both hand their size to the ``Win32Output``
|
||||
they hold. A POSIX terminal's ``Vt100_Output`` reads ``TIOCGWINSZ``, which is exact, so
|
||||
there is nothing there to reclaim.
|
||||
"""
|
||||
if sys.platform != "win32":
|
||||
return False # prompt_toolkit's win32 module asserts the platform on import
|
||||
from prompt_toolkit.output.win32 import Win32Output
|
||||
|
||||
return isinstance(output, Win32Output) or isinstance(
|
||||
getattr(output, "win32_output", None), Win32Output
|
||||
)
|
||||
|
||||
|
||||
#: What each ``color_depth`` preference value asks for. ``auto`` is deliberately absent: it
|
||||
@@ -1789,18 +1812,23 @@ class TuiSession:
|
||||
* :class:`~meshterm.ui.tui.colsnap.PinnedOutput`, where
|
||||
:func:`~meshterm.ui.tui.colsnap.enabled` — every glyph prompt_toolkit's renderer
|
||||
writes lands in the column that renderer measured it into.
|
||||
* :class:`_WidthExtendedOutput`, where :func:`_reclaim_last_column` — outermost, since
|
||||
the size it reports is what both renderers and the compositor lay out against.
|
||||
* :class:`_WidthExtendedOutput`, where :func:`_reclaim_last_column` — or, left to the
|
||||
terminal, where :func:`_probe_hides_last_column` — outermost, since the size it
|
||||
reports is what both renderers and the compositor lay out against.
|
||||
|
||||
With neither, this is ``None`` and prompt_toolkit builds its own output as before.
|
||||
"""
|
||||
pin = colsnap.enabled()
|
||||
reclaim = _reclaim_last_column()
|
||||
if self._output is not None or not (pin or reclaim):
|
||||
if self._output is not None or not (pin or reclaim is not False):
|
||||
return self._output
|
||||
from prompt_toolkit.output.defaults import create_output
|
||||
|
||||
output: Any = create_output()
|
||||
if reclaim is None:
|
||||
reclaim = _probe_hides_last_column(output)
|
||||
if not (pin or reclaim):
|
||||
return None
|
||||
if pin:
|
||||
output = colsnap.PinnedOutput(output)
|
||||
if reclaim:
|
||||
|
||||
+66
-11
@@ -1,24 +1,42 @@
|
||||
# SPDX-License-Identifier: Apache-2.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.
|
||||
prompt_toolkit's Windows console output reports 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.
|
||||
|
||||
The default comes from the active :class:`~meshterm.platforms.Platform` (on, desktop; off,
|
||||
PicoCalc — reclaiming a phantom column on its exact-width console would tear the frame), and
|
||||
``MESHTERM_FULL_WIDTH`` remains an explicit override on top of that default either way.
|
||||
The reclaim is right only where the probe hid the column. On an exact-width terminal (every
|
||||
POSIX one) the phantom column overprints the real last cell, which on Linux turned a full
|
||||
battery's ``100`` into ``10``. So by default the terminal's own output decides; the platform
|
||||
can only rule it out (PicoCalc's exact-width console), and ``MESHTERM_FULL_WIDTH`` remains an
|
||||
explicit override on top of either.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from prompt_toolkit.data_structures import Size
|
||||
from prompt_toolkit.output.vt100 import Vt100_Output
|
||||
|
||||
from meshterm.platforms import PICOCALC_LYRA, REGULAR, set_platform
|
||||
from meshterm.ui.tui.session import TuiSession, _WidthExtendedOutput
|
||||
from meshterm.ui.tui.session import TuiSession, _probe_hides_last_column, _WidthExtendedOutput
|
||||
|
||||
|
||||
def _vt100(columns: int = 100) -> Vt100_Output:
|
||||
"""A POSIX terminal's output, sized exactly as ``TIOCGWINSZ`` would size it."""
|
||||
size = Size(rows=30, columns=columns)
|
||||
return Vt100_Output(io.StringIO(), lambda: size, term="xterm-256color", enable_cpr=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def windows_console(monkeypatch):
|
||||
"""Make whatever the session builds read as prompt_toolkit's Windows console output."""
|
||||
monkeypatch.setattr("meshterm.ui.tui.session._probe_hides_last_column", lambda _out: True)
|
||||
|
||||
|
||||
def test_width_extended_output_reports_one_more_column() -> None:
|
||||
@@ -41,8 +59,8 @@ def test_session_leaves_a_supplied_output_untouched() -> None:
|
||||
assert session._resolve_output() is dummy
|
||||
|
||||
|
||||
def test_session_wraps_the_real_terminal_on_regular(monkeypatch) -> None:
|
||||
"""No supplied output + REGULAR (today's default) → the real terminal is width-extended."""
|
||||
def test_session_widens_a_windows_console_on_regular(monkeypatch, windows_console) -> None:
|
||||
"""No supplied output + REGULAR + a probe that hid a column → the column is reclaimed."""
|
||||
fake = SimpleNamespace(get_size=lambda: Size(rows=30, columns=100))
|
||||
monkeypatch.setattr("prompt_toolkit.output.defaults.create_output", lambda: fake)
|
||||
monkeypatch.delenv("MESHTERM_FULL_WIDTH", raising=False)
|
||||
@@ -52,7 +70,44 @@ def test_session_wraps_the_real_terminal_on_regular(monkeypatch) -> None:
|
||||
assert out.get_size() == Size(rows=30, columns=101)
|
||||
|
||||
|
||||
def test_session_pins_the_real_terminal_inside_the_width_extension(monkeypatch) -> None:
|
||||
def test_session_leaves_an_exact_terminal_its_own_width_on_regular(monkeypatch) -> None:
|
||||
"""A POSIX terminal is sized exactly, so REGULAR claims no phantom column on it.
|
||||
|
||||
The phantom column used to be claimed here too. With autowrap off, its cell overprinted the
|
||||
real last one, so the flush-right battery gauge lost its final character on Linux: a full
|
||||
pack read ``10``, and 99% read ``9%``.
|
||||
"""
|
||||
terminal = _vt100(columns=100)
|
||||
monkeypatch.setattr("prompt_toolkit.output.defaults.create_output", lambda: terminal)
|
||||
monkeypatch.delenv("MESHTERM_FULL_WIDTH", raising=False)
|
||||
monkeypatch.delenv("MESHTERM_COLUMN_SNAP", raising=False)
|
||||
set_platform(REGULAR)
|
||||
out = TuiSession()._resolve_output()
|
||||
assert not isinstance(out, _WidthExtendedOutput)
|
||||
assert out.get_size() == Size(rows=30, columns=100)
|
||||
|
||||
# And with pinning off too, nothing is left to wrap: prompt_toolkit builds its own.
|
||||
monkeypatch.setenv("MESHTERM_COLUMN_SNAP", "0")
|
||||
assert TuiSession()._resolve_output() is None
|
||||
|
||||
|
||||
def test_only_the_windows_console_probe_hides_a_column() -> None:
|
||||
"""The probe question is answered by the output's kind: exact everywhere but Win32."""
|
||||
assert _probe_hides_last_column(_vt100()) is False
|
||||
assert _probe_hides_last_column(SimpleNamespace()) is False
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
from prompt_toolkit.output.win32 import Win32Output
|
||||
|
||||
win32 = object.__new__(Win32Output) # no console needed to ask what kind it is
|
||||
assert _probe_hides_last_column(win32) is True
|
||||
# Windows10_Output and ConEmuOutput hold one and hand their size to it.
|
||||
assert _probe_hides_last_column(SimpleNamespace(win32_output=win32)) is True
|
||||
|
||||
|
||||
def test_session_pins_the_real_terminal_inside_the_width_extension(
|
||||
monkeypatch, windows_console
|
||||
) -> None:
|
||||
"""Both wraps on REGULAR, reclaim outermost: its size is what everything lays out against."""
|
||||
from meshterm.ui.tui.colsnap import PinnedOutput
|
||||
|
||||
|
||||
Reference in new issue
Block a user