Files
MeshTerm/tests/test_colsnap.py
T
2026-09-16 23:26:43 -04:00

556 lines
23 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: Apache-2.0
"""Column pinning: a row lands on the app's grid however wide the terminal draws its glyphs.
The reasoning lives in :mod:`meshterm.ui.tui.colsnap`. What these tests pin is the claim that
makes it worth having — that alignment no longer depends on the two width tables being *right*
about a glyph, only on the row saying where its columns are.
So the fixture here is a terminal that **disagrees on purpose** (:class:`_Terminal`, handed the
glyphs it draws at a width of its own). That is not a contrived case: it is the situation the
emoji-width module documents as unprobeable and unfixable-in-general, and the one a node name
off the air walks into every time somebody puts an emoji in it. Every assertion below is about
where a glyph *landed*, never about what was measured.
"""
from __future__ import annotations
import re
from rich.cells import cell_len
from meshterm.ui.tui import colsnap
from meshterm.ui.tui.emoji_width import clusters
_WAVE = "👋" # two cells by both stock tables; one cell on the reference terminal's font
_ROAD = "\U0001f6e3" # 🛣 one cell by both tables — the glyph a font may still draw wide
_CA = "🇨🇦" # a flag: one glyph out of two Regional Indicators
_FAMILY = "\U0001f468‍\U0001f469‍\U0001f467" # 👨‍👩‍👧 one glyph out of three joined people
_ROW = "│ ❯ Lakeside ● ▲ ★ ─── 5m │" # chrome only: nothing here needs an address
class _Terminal:
"""A terminal that lays out what it is written using a width table of its own.
Honours exactly one control sequence — ``CSI n G``, the absolute column address — and
ignores every other one, which is how a real terminal treats the theme's colour runs for
the purposes of column arithmetic.
Args:
width: The terminal's width in cells.
draws: Glyphs this terminal draws at a width the app did not measure, as
``{glyph: cells}``. Everything absent is drawn at the measured width, so a test
names its disagreement and nothing else.
"""
_CSI = re.compile(r"\x1b\[([0-9;]*)([A-Za-z])")
def __init__(self, width: int, *, draws: dict[str, int] | None = None) -> None:
"""Start with a blank row and the cursor in column 0."""
self.width = width
self.draws = draws or {}
self.cells: list[str] = [" "] * width
self.column = 0
def write(self, data: str) -> None:
"""Draw ``data``, obeying any column address in it."""
position = 0
for match in self._CSI.finditer(data):
self._draw(data[position : match.start()])
if match.group(2) == "G":
self.column = int(match.group(1) or "1") - 1
position = match.end()
self._draw(data[position:])
def _draw(self, text: str) -> None:
"""Place each glyph of one escape-free stretch, advancing by this terminal's widths."""
for cluster in clusters(text):
size = self.draws.get(cluster, cell_len(cluster))
if 0 <= self.column < self.width:
self.cells[self.column] = cluster
for overhang in range(1, size):
if self.column + overhang < self.width:
self.cells[self.column + overhang] = ""
self.column += size
def at(self, column: int) -> str:
"""The glyph drawn in ``column``."""
return self.cells[column]
def test_a_row_of_chrome_is_handed_back_untouched() -> None:
"""The common row costs one scan and no allocation — the same object comes back.
Borders, block elements, braille and the node marks are what the app draws by the
thousand, and pinning after each of them would be five bytes and a cluster walk to
address the column the cursor is already in.
"""
assert colsnap.snap_row(_ROW) is _ROW
assert colsnap.snap_row("plain ascii row") is "plain ascii row" # noqa: F632 - identity
assert colsnap.snap_row("⠁⠂⠃⡿ braille chart ▁▃▅█ ╭─╮") is not None
def test_an_emoji_is_followed_by_the_column_it_was_measured_into() -> None:
"""The whole mechanism: the glyph, then the one-based column the next glyph belongs in."""
assert cell_len(_WAVE) == 2, "the stock table measures the wave at two cells"
# "a" fills column 0, the wave is measured into columns 1-2, so "b" belongs in column 3.
assert colsnap.snap_row(f"a{_WAVE}b") == f"a\x1b[2X{_WAVE}\x1b[4Gb"
def test_nothing_is_pinned_after_the_last_glyph_on_the_row() -> None:
"""An address is held over until something drawable follows it, and dropped if none does.
A chat line ending on an emoji is the common case, and there is nothing after it whose
column could be wrong.
"""
assert colsnap.snap_row(f"hi {_WAVE}") == f"hi \x1b[2X{_WAVE}"
def test_a_style_change_occupies_no_column_and_stays_where_the_theme_put_it() -> None:
"""A colour run is copied through without advancing the column it would have shifted."""
pinned = colsnap.snap_row(f"\x1b[31m{_WAVE}\x1b[0m|")
assert pinned == f"\x1b[31m\x1b[2X{_WAVE}\x1b[0m\x1b[3G|"
def test_a_flag_and_a_joined_sequence_are_pinned_once_as_whole_glyphs() -> None:
"""One address per glyph, not per codepoint — the cut that halves a flag is never made."""
for glyph in (_CA, _FAMILY):
width = cell_len(glyph)
pinned = colsnap.snap_row(f"{glyph}|")
assert pinned == f"\x1b[{width}X{glyph}\x1b[{width + 1}G|"
assert pinned.count("G") == 1
def test_a_bare_text_emoji_is_pinned_even_though_both_tables_agree_about_it() -> None:
"""Agreement is not certainty: what a font draws is still the font's business.
``🛣`` is the glyph whose measurement must not be *forced* to two (that pulled the Trophy
case's border in). Pinning is the other answer to the same doubt: leave the measurement
alone and say where the next column is, so the row holds either way.
"""
assert cell_len(_ROAD) == 1
assert colsnap.snap_row(f"{_ROAD}|") == f"{_ROAD}\x1b[2G|"
def test_the_row_frames_flush_on_a_terminal_that_draws_the_glyph_narrow() -> None:
"""The notch, and its absence: a border lands in its measured column once pinned."""
row = f"│ {_WAVE} Lakeside │"
edge = cell_len(row) - 1
loose = _Terminal(cell_len(row), draws={_WAVE: 1})
loose.write(row)
assert loose.at(edge) != "│", "unpinned, the narrow glyph pulls the border a column in"
assert loose.column == cell_len(row) - 1
pinned = _Terminal(cell_len(row), draws={_WAVE: 1})
pinned.write(colsnap.snap_row(row))
assert pinned.at(edge) == "│"
assert pinned.column == cell_len(row)
def test_the_row_frames_flush_on_a_terminal_that_draws_the_glyph_wide() -> None:
"""The mirror case: the overhang is written over, and the row still ends where it should.
One cell of the glyph is lost to its neighbour, which is the trade — a cell of cosmetic
damage inside the glyph's own lane, in place of every lane after it moving.
"""
row = f"│ {_ROAD} Lakeside │"
edge = cell_len(row) - 1
loose = _Terminal(cell_len(row), draws={_ROAD: 2})
loose.write(row)
assert loose.at(edge) != "│"
pinned = _Terminal(cell_len(row), draws={_ROAD: 2})
pinned.write(colsnap.snap_row(row))
assert pinned.at(edge) == "│"
def test_every_lane_of_a_archive_preview_row_lands_where_the_screen_measured_it() -> None:
"""The screen this started on: a swept contact whose name carries an emoji.
The name lane is already fitted in display cells (:func:`~meshterm.ui.menus.fit_cells`),
which is the padding that cannot help — it is computed with the same measurement the
terminal disagrees with. What holds the evidence lanes under their headers is the pinning.
"""
from meshterm.core.contact_score import ContactSignals, ScoredContact
from meshterm.core.models import Contact
from meshterm.ui.sweep_screen import _NAME_W, _lane_widths, _victim_row
from meshterm.ui.tui.render import render_to_ansi
key = "ab" * 32
victim = ScoredContact(
contact=Contact(name=f"{_WAVE} Lakeside", public_key=key, key_prefix=key[:12]),
signals=ContactSignals(node=key[:12], packets=12, hops=2.0),
score=1.0,
percentile=3,
)
row = _victim_row(victim, _lane_widths([victim]))
measured = cell_len(row.plain)
ansi = render_to_ansi(row, 72, no_wrap=True)
loose = _Terminal(80, draws={_WAVE: 1})
loose.write(ansi)
assert loose.column != measured, "unpinned, one emoji shifts every lane after it"
pinned = _Terminal(80, draws={_WAVE: 1})
pinned.write(colsnap.snap_row(ansi))
assert pinned.column == measured
# Not just the right edge: *every* glyph of the row is in the column the screen's own
# cell arithmetic put it in, which is what keeps a lane under the header naming it.
column = 0
for cluster in clusters(row.plain):
if cluster != " ":
assert pinned.at(column) == cluster, f"column {column} holds the wrong glyph"
column += cell_len(cluster)
assert column == measured
assert _NAME_W == 22 # the name lane the evidence starts after, gap included
def test_pinning_is_on_with_an_escape_hatch(monkeypatch) -> None: # noqa: ANN001
"""On by default; only an explicit 0 writes rows the way they were composed."""
monkeypatch.delenv("MESHTERM_COLUMN_SNAP", raising=False)
assert colsnap.enabled()
monkeypatch.setenv("MESHTERM_COLUMN_SNAP", "0")
assert not colsnap.enabled()
def test_the_row_writer_pins_what_it_writes(monkeypatch) -> None: # noqa: ANN001
"""The wiring: a composed frame reaches the terminal carrying its column addresses."""
from prompt_toolkit.data_structures import Size
from prompt_toolkit.styles import Style
from meshterm.ui.tui.fastrender import FastRenderer
class _Capturing:
"""Just enough output for one paint: what was written, and how big the screen is."""
def __init__(self) -> None:
self.written: list[str] = []
def write_raw(self, data: str) -> None:
self.written.append(data)
def get_size(self) -> Size:
return Size(rows=26, columns=72)
def __getattr__(self, name: str): # noqa: ANN001, ANN204 - the rest is inert here
return lambda *args, **kwargs: None
frame = f"contact {_WAVE} 5m\nsecond row\n"
out = _Capturing()
renderer = FastRenderer(Style([]), out, full_screen=True, frame_source=lambda: frame)
renderer.render(None, None)
assert "\x1b[11G" in "".join(out.written) # the wave measured into columns 8-9
monkeypatch.setenv("MESHTERM_COLUMN_SNAP", "0")
off = _Capturing()
plain = FastRenderer(Style([]), off, full_screen=True, frame_source=lambda: frame)
plain.render(None, None)
assert "\x1b[11G" not in "".join(off.written)
def test_a_platform_that_draws_no_emoji_pins_nothing() -> None:
"""The PicoCalc draws only glyphs its own font has verified, so there is nothing to pin."""
from meshterm.platforms import PICOCALC, set_platform
assert colsnap.enabled()
set_platform(PICOCALC)
assert not colsnap.enabled()
# --- prompt_toolkit's renderer: the dialog path -------------------------------------------------
class _Tty:
"""A two-dimensional terminal, for the renderer that moves its cursor relatively.
Honours what prompt_toolkit's differential renderer and :class:`colsnap.PinnedOutput` send:
carriage return, newline, backspace, cursor save and restore, and the CSI cursor moves.
Colour and mode sequences are ignored, which is all a terminal does with them as far as
column arithmetic goes. Glyph widths come from prompt_toolkit's own measurement unless
``draws`` names a glyph this terminal disagrees about.
"""
_SEQUENCE = re.compile(r"\x1b(?:\[([0-9;?]*)([A-Za-z@`])|([78]))")
def __init__(self, cols: int, rows: int, *, draws: dict[str, int] | None = None) -> None:
"""Start blank, with the cursor home."""
self.cols, self.rows = cols, rows
self.draws = draws or {}
self.grid = [[" "] * cols for _ in range(rows)]
self.x = self.y = 0
self._saved = (0, 0)
def feed(self, data: str) -> None:
"""Interpret ``data`` as the terminal would."""
position = 0
for match in self._SEQUENCE.finditer(data):
self._text(data[position : match.start()])
position = match.end()
if match.group(3) == "7":
self._saved = (self.x, self.y)
elif match.group(3) == "8":
self.x, self.y = self._saved
else:
self._csi(match.group(1), match.group(2))
self._text(data[position:])
def _csi(self, params: str, final: str) -> None:
"""One control sequence: the cursor moves, the erases, and nothing else."""
if params.startswith("?"):
return
numbers = [int(part) if part else 0 for part in params.split(";")] if params else []
count = numbers[0] if numbers and numbers[0] else 1
if final == "A":
self.y = max(0, self.y - count)
elif final == "B":
self.y = min(self.rows - 1, self.y + count)
elif final == "C":
self.x = min(self.cols - 1, self.x + count)
elif final == "D":
self.x = max(0, self.x - count)
elif final == "G":
self.x = count - 1
elif final in "Hf":
row = numbers[0] if numbers and numbers[0] else 1
column = numbers[1] if len(numbers) > 1 and numbers[1] else 1
self.y, self.x = row - 1, column - 1
elif final == "X":
for column in range(self.x, min(self.cols, self.x + count)):
self.grid[self.y][column] = " "
elif final == "K":
self.grid[self.y][self.x :] = [" "] * (self.cols - self.x)
elif final == "J":
self.grid[self.y][self.x :] = [" "] * (self.cols - self.x)
for row in range(self.y + 1, self.rows):
self.grid[row] = [" "] * self.cols
def _text(self, text: str) -> None:
"""Place each glyph at the cursor, advancing by this terminal's idea of its width."""
from prompt_toolkit.utils import get_cwidth
for cluster in clusters(text):
if cluster == "\r":
self.x = 0
elif cluster == "\n":
self.y = min(self.rows - 1, self.y + 1)
elif cluster == "\b":
self.x = max(0, self.x - 1)
else:
size = self.draws.get(cluster, get_cwidth(cluster))
if self.x < self.cols:
self.grid[self.y][self.x] = cluster
for overhang in range(1, size):
if self.x + overhang < self.cols:
self.grid[self.y][self.x + overhang] = ""
self.x += size
def glyph_at(self, row: int, column: int) -> str:
"""The glyph drawn at ``(row, column)``."""
return self.grid[row][column]
def _screen(rows: list[str], cols: int): # noqa: ANN202 - a prompt_toolkit Screen
"""Lay ``rows`` out exactly as prompt_toolkit lays out a window's text."""
from prompt_toolkit.layout import Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.mouse_handlers import MouseHandlers
from prompt_toolkit.layout.screen import Screen, WritePosition
screen = Screen()
window = Window(FormattedTextControl("\n".join(rows)), wrap_lines=False)
window.write_to_screen(
screen, MouseHandlers(), WritePosition(0, 0, cols, len(rows)), "", False, None
)
return screen
def _paint(output, screen, previous, position, cols: int, rows: int): # noqa: ANN001, ANN202
"""One pass of prompt_toolkit's own differential writer over ``screen``."""
from types import SimpleNamespace
from prompt_toolkit.data_structures import Size
from prompt_toolkit.output.color_depth import ColorDepth
from prompt_toolkit.renderer import (
_output_screen_diff,
_StyleStringHasStyleCache,
_StyleStringToAttrsCache,
)
from prompt_toolkit.styles import DummyStyleTransformation, Style
attrs = _StyleStringToAttrsCache(Style([]).get_attrs_for_style_str, DummyStyleTransformation())
position, _style = _output_screen_diff(
SimpleNamespace(layout=SimpleNamespace(current_window=None)),
output,
screen,
position,
ColorDepth.DEPTH_24_BIT,
previous,
None,
False,
True,
attrs,
_StyleStringHasStyleCache(attrs),
Size(rows=rows, columns=cols),
cols if previous is not None else 0,
)
return position
def _vt100(cols: int, rows: int): # noqa: ANN202 - a Vt100_Output writing into memory
"""A real VT100 output, so every cursor move is spelled the way the terminal receives it."""
import io
from prompt_toolkit.data_structures import Size
from prompt_toolkit.output.vt100 import Vt100_Output
return Vt100_Output(io.StringIO(), lambda: Size(rows=rows, columns=cols), term="xterm")
def _drained(output) -> str: # noqa: ANN001
"""Everything written to ``output`` since the last drain."""
vt100 = getattr(output, "_inner", output)
data = "".join(vt100._buffer)
vt100._buffer.clear()
return data
def _assert_landed(tty: _Tty, row: int, text: str) -> None:
"""Every visible glyph of ``text`` is in the column prompt_toolkit measured it into."""
from prompt_toolkit.utils import get_cwidth
column = 0
for cluster in clusters(text):
if cluster != " ":
assert tty.glyph_at(row, column) == cluster, f"column {column} of row {row}"
column += get_cwidth(cluster)
def test_a_dialog_row_frames_flush_on_a_terminal_that_draws_the_glyph_narrow() -> None:
"""prompt_toolkit's renderer, unpinned and pinned, on a terminal that disagrees about 👋.
The renderer writes the whole row in one run after its first move, adding two cells for the
wave to a cursor of its own while the terminal adds one. Unpinned, the rest of the row is
drawn a column early. Pinned, every glyph is where the renderer measured it.
"""
cols, rows = 24, 2
text = [f"│ {_WAVE} Lakeside 5m │", "│ Plain row 5m │"]
screen = _screen(text, cols)
loose, loose_tty = _vt100(cols, rows), _Tty(cols, rows, draws={_WAVE: 1})
_paint(loose, screen, None, _point(), cols, rows)
loose_tty.feed(_drained(loose))
assert loose_tty.glyph_at(0, cell_len(text[0]) - 1) != "│", "unpinned, the border moves"
pinned, pinned_tty = (
colsnap.PinnedOutput(_vt100(cols, rows)),
_Tty(cols, rows, draws={_WAVE: 1}),
)
_paint(pinned, screen, None, _point(), cols, rows)
pinned_tty.feed(_drained(pinned))
for row, line in enumerate(text):
_assert_landed(pinned_tty, row, line)
def test_a_repaint_after_the_glyph_lands_where_the_renderer_measured_it() -> None:
"""The move that follows a pinned glyph is computed relatively, and still lands.
A second paint changes the glyph *and* a value later on the same row, with unchanged cells
between them, so the renderer writes the new glyph and then jumps forward by the distance its
own arithmetic says. That jump is only right if the terminal's cursor was put back in step
after the glyph — which is the whole claim of the relative pin.
"""
cols, rows = 24, 1
first = [f"│ {_WAVE} Lakeside 5m │"]
second = [f"│ {_ROCKET} Lakeside 7m │"]
before, after = _screen(first, cols), _screen(second, cols)
for pin, lands in ((False, False), (True, True)):
output = _vt100(cols, rows)
if pin:
output = colsnap.PinnedOutput(output)
tty = _Tty(cols, rows, draws={_WAVE: 1, _ROCKET: 1})
position = _paint(output, before, None, _point(), cols, rows)
tty.feed(_drained(output))
_paint(output, after, before, position, cols, rows)
tty.feed(_drained(output))
seven = cell_len(second[0].split("7m")[0])
assert (tty.glyph_at(0, seven) == "7") is lands
def test_a_glyph_drawn_narrow_leaves_no_character_from_the_last_frame_beside_it() -> None:
"""The cell a narrow-drawn glyph does not cover is blank, not what the last frame put there.
prompt_toolkit treats the cell after a wide glyph as part of it and never writes it, so a
repaint that puts a two-cell glyph where two letters were leaves the second letter standing
on a terminal that draws the glyph in one cell. The pin erases its reservation first.
"""
cols, rows = 16, 1
before, after = _screen(["│ ab Lakeside │"], cols), _screen([f"│ {_WAVE} Lakeside │"], cols)
for pin, stale in ((False, True), (True, False)):
output = _vt100(cols, rows)
if pin:
output = colsnap.PinnedOutput(output)
tty = _Tty(cols, rows, draws={_WAVE: 1})
position = _paint(output, before, None, _point(), cols, rows)
tty.feed(_drained(output))
_paint(output, after, before, position, cols, rows)
tty.feed(_drained(output))
assert tty.glyph_at(0, 2) == _WAVE
assert (tty.glyph_at(0, 3) == "b") is stale
def test_the_pinned_output_brackets_only_a_lone_uncertain_glyph() -> None:
"""What reaches the terminal for each kind of write prompt_toolkit's renderer makes."""
class _Recorder:
def __init__(self) -> None:
self.calls: list[tuple[str, str]] = []
def write(self, data: str) -> None:
self.calls.append(("write", data))
def write_raw(self, data: str) -> None:
self.calls.append(("raw", data))
recorder = _Recorder()
output = colsnap.PinnedOutput(recorder)
for data in ("a", "\r\n", "│", "⠿", "é", "ab"):
output.write(data)
assert recorder.calls == [
("write", "a"),
("write", "\r\n"),
("write", "│"),
("write", "⠿"),
("write", "é"),
("write", "ab"),
], "a cell of text passes straight through"
for glyph in (_WAVE, _FAMILY, _CA):
recorder.calls.clear()
output.write(glyph)
from prompt_toolkit.utils import get_cwidth
step = get_cwidth(glyph)
assert recorder.calls == [
("raw", f"\x1b7\x1b[{step}X"),
("write", glyph),
("raw", f"\x1b8\x1b[{step}C"),
]
recorder.calls.clear()
output.write(f"{_WAVE}{_WAVE}") # two glyphs is not a cell, and is not pinned
assert recorder.calls == [("write", f"{_WAVE}{_WAVE}")]
assert output.calls is recorder.calls # everything else forwards
_ROCKET = "\U0001f680" # 🚀 a second emoji the disagreeing terminal draws in one cell
def _point(): # noqa: ANN202 - a prompt_toolkit Point at the origin
"""The cursor at home, where a first paint starts."""
from prompt_toolkit.data_structures import Point
return Point(x=0, y=0)