Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
556 lines
23 KiB
Python
556 lines
23 KiB
Python
# 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)
|