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:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-10-05 04:21:38 -04:00
1 parent 44e60c4054
commit 7bcb1db360
4 files changed
+121 -36

No files matched your search

+3 -3
View File
@@ -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
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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