Group an address in fours, and say when a row was cut - #20
Conversation
A Bitcoin address is an unbroken run of base58 or bech32: no word shapes to count by, and characters chosen to look unlike each other rather than to group. It is exactly the shape a reader transcribing it loses their place in, and losing a place in an address is not a typo the reader finds out about cheaply. Drawn in groups of four now, with a gap at each boundary and the ink alternating between the body's white and the theme's orange - the same orange the back arrow and the selection bar already use, so this introduces no new colour. Two signals for one boundary on purpose: a reader who cannot tell the two inks apart still has the gap, and the grouping does not rest on colour alone. The address is never touched. Grouping happens at draw time, so what is compared, hashed and put in a QR is the same unbroken string it always was, and nothing downstream has to know to strip separators back out. Lines cut on a group boundary rather than at whatever pixel runs out, which is what keeps the alternation readable across a break - a line ending mid-group would put two same-coloured groups against each other with only the gap between them. The alternation is indexed from the start of the whole value, not from each line, for the same reason. The self-test walks four real addresses the way page_text_impl does and requires the pieces to rebuild the value character for character, plus the cut to land on a boundary. That is the invariant that matters: a group dropped or reordered at a line break would hand the reader an address that is not theirs, with nothing on screen to say so. Verified it fails when the boundary rounding is removed rather than passing vacuously. The line geometry is measured too - a grouped line is centred with its gaps counted in, so it can reach an edge a plain centred line would not. Only the address page groups. The xpub, the descriptor and the transcript are read rather than transcribed by hand, and the word list already has its own shape.
Every row of the address list is an address that does not fit, cut at whatever pixel ran out. A row that stops looks exactly like a row that ended, so the reader has nothing telling them the value continues - and on this list every value does. Rows that do not fit now end in an ellipsis. It is paid for out of the same width rather than drawn past it: the fit is recomputed against what is left after reserving room for the dots, so a truncated row is never wider than one that fits. This is in the list renderer rather than at the address list, so any row that overflows says so. In practice that is only this list - labels_fit_a_row already holds every menu label to fitting - which is why the change is narrow in effect while being general in the code. The self-test checks both halves, because either alone passes while the other is broken: that the dots stay inside the row's own column rather than running under the scrollbar, and that they are drawn at all. The second compares the rendered row against its own fit_row prefix - what a silent cut would have produced - and requires them to differ. Verified it fails when the ellipsis is removed.
|
Heads up before you merge: I have work in progress that builds directly on the grouping here, and it changes where it lives. Flagging now rather than after, so nothing lands as a surprise. What is on top of this branch locally The address flow becomes one screen instead of two. Opening an address lands on its QR, with the derivation path and the address itself drawn in the margin beside the code, in your groups of four with the alternating ink — two groups to a line, as a fixed grid. That reuses your work rather than replacing it: What this costs on your side: One thing worth knowing regardless of my branch. No objection to this merging as it stands — it is self-contained and I would rather rebase onto it than push into it. Just did not want you to find your grouping repurposed without hearing it from me first. |
A Bitcoin address is an unbroken run of base58 or bech32: no word shapes to
count by, no repeated forms to keep place with, and an alphabet chosen so that
characters look unlike each other rather than group into anything. It is
exactly the shape a reader transcribing it loses their place in — and losing a
place in an address is not a mistake that announces itself. The reader finds
out when the funds do not arrive.
Two changes, both about the same thing: telling the reader what they are
looking at.
The address page reads in fours
Groups of four, a gap at each boundary, and the ink alternating between the
body's own white and the theme's orange — the same orange the back arrow and
the selection bar already use, so no new colour enters the palette.
The gap and the colour say the same thing on purpose. A reader who cannot
tell the two inks apart still has the space; the grouping never rests on
colour alone. Two independent signals for one boundary is the point, not
redundancy to be optimised away.
The address is never touched. Grouping happens at draw time only. What is
compared, hashed and put in a QR is the same unbroken string it always was,
and nothing downstream has to know to strip separators back out. There is no
formatted copy of the value in memory to drift from the real one.
Two details that are not arbitrary:
ending mid-group would put two same-coloured groups against each other with
only the gap between them — and the gap is the signal the colour exists to
back up.
seedtool_render_fit_groupedrounds down to a whole group, exceptfor the value's own tail.
each line. Otherwise line two restarts in white and the break shows two
white groups running together, which is the one place the reader most needs
the boundary to be obvious.
Only the address page groups. The xpub, the descriptor and the transcript are
scanned or read rather than copied out by hand, and the word list already has
a shape of its own.
The address list says when a row was cut
Every row of
Addressesis an address that does not fit, cut at whateverpixel ran out. A row that stops looks exactly like a row that ended, so
nothing on screen tells the reader the value continues — and on this list,
every value does.
The ellipsis is paid for out of the same width, not drawn past it: the fit
is recomputed against what is left after reserving room for the dots, so a
truncated row is never wider than one that fits, and the dots never reach the
scrollbar's strip.
This went into the list renderer rather than into the address list, so any row
that overflows says so. In practice that is only this list —
labels_fit_a_rowalready holds every menu label to fitting — which is why the change is narrow
in effect while being general in the code.
Testing
Three new checks, and the reasoning behind each is the interesting part:
Grouping must not change the address. This is the only way this feature
can do real harm: a group dropped, doubled or reordered at a line break hands
the reader an address that is not theirs, with nothing on screen to say so.
The test walks four real addresses — both bech32 shapes and both base58 ones —
the way
page_text_implwalks them, and requires the pieces to rebuild thevalue character for character. It also requires each cut to land on a group
boundary.
The grouped line must clear both edges. It is centred with its gaps
counted in, so it can reach an edge a plain centred line would not. Measured
from the framebuffer rather than argued from the arithmetic.
The truncated row must both fit and be visible as truncated. Two halves,
because either alone passes while the other is broken. The second compares the
rendered row against its own
fit_rowprefix — precisely what a silent cutwould have produced — and requires them to differ.
Both of the load-bearing tests were checked against a deliberately broken
build before being trusted: removing the group-boundary rounding fails the
first, removing the ellipsis fails the third. Neither passes vacuously.
What none of this reaches is the same gap the nav work had: no test drives
navigation, so the flows around these screens were checked by hand in the
simulator.
The firmware build and its reproducibility check have not been run here; only
the host build and the Python suite.
-Wformat-truncationat the level thefirmware build errors on is clean for both changed files.
Notes
Branched from current
main, after #19. Touchesseedtool_render.{c,h}, both display backends,page_text_impl's split, andthe self-test.
SEEDTOOL_GROUP_LENis in the header rather than private to the renderer,because
page_text_implneeds it to say which group each line begins on.Four is what every wallet that groups at all has settled on; it divides the
eye's span without the gaps starting to outnumber the text.