Revert the share card QR experiments — back to half-blocks

Braille (one dot per module) would not scan, and the quadrant mosaic
reads worse on screen than the plain half-block form. The original
qr_text render scans and stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5 committed 2026-07-26 10:53:16 -04:00
1 parent a00fbe5639
commit bf992979ef
4 files changed
+3 -110

No files matched your search

+2 -2
View File
@@ -815,13 +815,13 @@ async def show_contact_card(
node_type: The MeshCore advert type byte (1 companion, 2 repeater, 3 room,
4 sensor).
"""
from .qr import qr_quad
from .qr import qr_text
url = contact_share_url(name, public_key, node_type)
body = Group(
Text("Scan to add this node as a contact:", style="muted"),
Text(""),
qr_quad(url),
qr_text(url),
Text(""),
Text(url, style="accent"),
)
-65
View File
@@ -5,13 +5,6 @@ Each character cell stacks two vertical modules — ``▀`` (upper only), ``▄`
one and comfortably fits an interactive window. Modules are always drawn black on a white
background (with the mandatory quiet-zone border), so a phone camera reads the code no
matter what colour theme the terminal itself is using.
:func:`qr_quad` is the dense variant: quadrant mosaic glyphs (``▘▚▛█``…) pack a 2×2
block of modules into every cell — half the columns of the half-block form at the same
row count. Ink stays solid (every dark module is a filled quarter-cell), so it scans
exactly as well as the half-block render, just drawn smaller. (A braille one-dot-per-
module form was tried first: it drew even smaller but the dots' white surrounds starve
the scanner of ink, and phones would not read it.)
"""
from __future__ import annotations
@@ -34,28 +27,6 @@ _GLYPH = {
#: contrast holds regardless of the surrounding terminal palette.
_STYLE = "black on white"
#: Quadrant glyph for each (top-left, top-right, bottom-left, bottom-right) dark-module
#: quartet — the sixteen 2×2 mosaics Unicode provides (space and the half/full blocks
#: fill the patterns the Block Elements quadrant range skips).
_QUAD = {
(False, False, False, False): " ",
(True, False, False, False): "▘",
(False, True, False, False): "▝",
(True, True, False, False): "▀",
(False, False, True, False): "▖",
(True, False, True, False): "▌",
(False, True, True, False): "▞",
(True, True, True, False): "▛",
(False, False, False, True): "▗",
(True, False, False, True): "▚",
(False, True, False, True): "▐",
(True, True, False, True): "▜",
(False, False, True, True): "▄",
(True, False, True, True): "▙",
(False, True, True, True): "▟",
(True, True, True, True): "█",
}
def qr_text(data: str, *, error: str = "m") -> Text:
"""Render ``data`` as a QR code built from half-block characters.
@@ -83,39 +54,3 @@ def qr_text(data: str, *, error: str = "m") -> Text:
text.append(line, style=_STYLE)
text.append("\n")
return text
def qr_quad(data: str, *, error: str = "m") -> Text:
"""Render ``data`` as a QR code built from quadrant mosaics, 2×2 modules per cell.
Half the width of :func:`qr_text` at the same row count, and every dark module is
still a solid quarter-cell of ink — the property a scanner actually needs — so the
smaller draw reads as well as the half-block form.
Args:
data: The string to encode.
error: QR error-correction level — ``l``/``m``/``q``/``h`` (default ``m``).
Returns:
A Rich :class:`~rich.text.Text` (no-wrap) whose lines draw the code.
"""
import segno
qr = segno.make(data, error=error)
rows = [[bool(v) for v in row] for row in qr.matrix_iter(border=_BORDER)]
# Pad to whole cells with light modules: out to an even row and column count.
if len(rows) % 2:
rows.append([False] * len(rows[0]))
if len(rows[0]) % 2:
for row in rows:
row.append(False)
text = Text(no_wrap=True)
for top, bottom in zip(rows[0::2], rows[1::2], strict=True):
line = "".join(
_QUAD[(top[x], top[x + 1], bottom[x], bottom[x + 1])]
for x in range(0, len(top), 2)
)
text.append(line, style=_STYLE)
text.append("\n")
return text
+1 -2
View File
@@ -91,8 +91,7 @@ async def test_show_contact_card_pops_the_qr_over_the_link() -> None:
console.print(renderable)
out = console.export_text()
assert f"meshcore://contact/add?name=Hub&public_key={'ab' * 32}&type=2" in out
# The QR actually drew its modules — quadrant mosaics, 2×2 modules per glyph.
assert "█" in out
assert "█" in out # the QR actually drew its modules
# -- coordinate parsing ----------------------------------------------------------
-41
View File
@@ -1,41 +0,0 @@
"""The terminal QR renderers: half-block and quadrant-mosaic forms of the same matrix."""
import segno
from meshterm.ui.qr import _BORDER, _QUAD, qr_quad, qr_text
URL = "meshcore://contact/add?name=Test&public_key=" + "ab" * 32 + "&type=1"
def _matrix(data: str) -> list[list[bool]]:
qr = segno.make(data, error="m")
return [[bool(v) for v in row] for row in qr.matrix_iter(border=_BORDER)]
def test_qr_text_matches_matrix() -> None:
rows = _matrix(URL)
lines = qr_text(URL).plain.rstrip("\n").split("\n")
glyph_bits = {"█": (True, True), "▀": (True, False), "▄": (False, True), " ": (False, False)}
for y, row in enumerate(rows):
for x, dark in enumerate(row):
top, bottom = glyph_bits[lines[y // 2][x]]
assert (top if y % 2 == 0 else bottom) == dark
def test_qr_quad_matches_matrix() -> None:
"""Every module maps to exactly its quarter of a mosaic glyph, raster order."""
rows = _matrix(URL)
lines = qr_quad(URL).plain.rstrip("\n").split("\n")
quarters = {glyph: quartet for quartet, glyph in _QUAD.items()}
for y, row in enumerate(rows):
for x, dark in enumerate(row):
quartet = quarters[lines[y // 2][x // 2]]
assert quartet[(y % 2) * 2 + (x % 2)] == dark
def test_qr_quad_geometry() -> None:
"""Whole-cell padding: equal-length lines at half the matrix width and height."""
rows = _matrix(URL)
lines = qr_quad(URL).plain.rstrip("\n").split("\n")
assert len(lines) == -(-len(rows) // 2)
assert all(len(line) == -(-len(rows[0]) // 2) for line in lines)