@@ -75,6 +75,12 @@ _Static_assert(ADDRESS_SHOWN_ROWS <= ADDRESS_LIST_ROWS, "more address rows are s
7575_Static_assert (ADDRESS_INDEX_DIGITS == 2 && SEEDTOOL_MAX_ADDRESS_INDEX == 99 ,
7676 "the address-index keypad width and the derived address range must be changed together" );
7777
78+ /* The word-number field draws one box per digit, and the renderer bounds how
79+ * many of those fit across the display. Widening a word number without the
80+ * boxes to show it would run the row off both edges, which clips silently. */
81+ _Static_assert (SEEDTOOL_MAX_WORD_DIGITS <= SEEDTOOL_DIGIT_BOXES_MAX ,
82+ "a word number has more digits than the digit field can draw boxes for" );
83+
7884/* Every key is found by its character rather than by where it sits, so the
7985 * layouts are QWERTY, or anything else, purely by changing the strings above. */
8086#define PASSPHRASE_PAGES SEEDTOOL_PASSPHRASE_PAGES
@@ -368,7 +374,15 @@ static int enter_value(const char* title, const unsigned position, const unsigne
368374 * mis-keyed roll is caught against the paper now rather than at the end
369375 * of ninety-nine of them. Only its tail fits, which is the part that
370376 * just changed. */
371- if (progress ) {
377+ /* A plain number scrolls in its own box, the same shape the word-number
378+ * field uses - up and down are the value, and there is nothing else on
379+ * screen to hunt through. A formatted value (a card, "A Clubs") is too
380+ * wide for a box and stays on the text screen, where the running
381+ * transcript sits under it. */
382+ if (!format ) {
383+ seedtool_display_value_box (
384+ heading , shown , on_back , chord_learned ? NULL : NAV_FOOTER , progress );
385+ } else if (progress ) {
372386 seedtool_display_dice_screen (
373387 heading , on_back ? "[back]" : shown , history , chord_learned ? NULL : NAV_FOOTER , progress );
374388 } else {
@@ -901,92 +915,157 @@ static int enter_word(
901915 * chosen, 0 when the user deleted back out of this word, -1 on timeout or
902916 * overflow. Only digits that still lead to a word number are reachable, and the
903917 * number is shown as its word before it is accepted: a digit misread off paper
904- * is otherwise a different seed with no sign that anything went wrong. */
918+ * is otherwise a different seed with no sign that anything went wrong.
919+ *
920+ * `allowed` narrows that further on a mnemonic's last word, where the checksum
921+ * leaves only a handful of words possible. It is the same set the letter
922+ * keyboard narrows by, and it has to be applied here too: a restore that
923+ * refuses a word by letter and accepts it by number is worse than one that does
924+ * neither, because the reader who used the number pad has been told nothing.
925+ *
926+ * Up to four boxes, scrolled one at a time, and a number ends where the reader
927+ * says it does: `240` is typed as three digits, not as `0240`. The ring for the
928+ * current box carries the digits that still lead somewhere, then OK once what
929+ * is typed is already a word number, then backspace - the same ring Jade's
930+ * index entry uses, where a short number also has to be able to stop early.
931+ *
932+ * A fixed four-box field was tried first and was wrong: it made 240 a dead end,
933+ * since no fourth digit takes 240x into 1..2048, so the reader who typed it
934+ * found a box with nothing in it but backspace. Ending the number early is not
935+ * a convenience here, it is what makes three-digit numbers reachable at all.
936+ *
937+ * Replaces a twelve-key keypad the reader had to walk a cursor across. Two
938+ * buttons that mean "the digit goes up" and "the digit goes down" need no
939+ * hunting, and the row of boxes shows how much is left to type without a
940+ * separate readout. */
905941static int enter_word_number (
906942 const size_t position , const size_t total , const seedtool_wordset_t * allowed , char * output , const size_t output_len )
907943{
908944 char digits [SEEDTOOL_MAX_WORD_DIGITS + 1 ] = { 0 };
909- size_t digits_len = 0 ;
910- size_t selected = seedtool_layout_center ( WORD_NUMBER_LAYOUT ) ;
911- const size_t accept_index = layout_key_index ( WORD_NUMBER_LAYOUT , SEEDTOOL_KEY_ACCEPT ) ;
945+ /* What each box shows, including the one being scrolled. */
946+ char shown [ SEEDTOOL_MAX_WORD_DIGITS + 1 ] = { 0 } ;
947+ size_t at = 0 ;
912948 char title [24 ];
913949 (void )snprintf (title , sizeof (title ), "Word %u/%u" , (unsigned )position , (unsigned )total );
914950
915- for (;;) {
916- bool reachable [SEEDTOOL_DIGITS ] = { false };
917- bool enabled [WORD_NUMBER_KEYS ] = { false };
918- (void )seedtool_next_digits_in (allowed , digits , digits_len , reachable );
919- const unsigned number = seedtool_word_number_in (allowed , digits , digits_len );
920- for (size_t i = 0 ; i < WORD_NUMBER_KEYS ; ++ i ) {
921- const char key = seedtool_layout_key (WORD_NUMBER_LAYOUT , i );
922- enabled [i ] = key == SEEDTOOL_KEY_BACKSPACE ? true
923- : key == SEEDTOOL_KEY_ACCEPT ? number != 0
924- : reachable [key - '0' ];
925- }
926- /* Once the typed digits are themselves a complete valid number and
927- * cannot be extended any further, every digit key goes dark at once
928- * and the cursor has to land somewhere. nearest_enabled's ring
929- * distance to Accept vs. Backspace from here depends on exactly
930- * which key the reader was last on - circumstantial, and it lands on
931- * Backspace as often as not. Accept is the deliberate target
932- * whenever it's actually reachable; only fall back to the generic
933- * search when it isn't (still mid-prefix, no complete number yet). */
934- selected = !enabled [selected ] && number ? accept_index : nearest_enabled (enabled , WORD_NUMBER_KEYS , selected );
935- bool picked = false;
936- while (!picked ) {
937- seedtool_display_keyboard (
938- title , digits_len ? digits : "-" , WORD_NUMBER_LAYOUT , enabled , selected , position , total );
939- switch (wait_key ()) {
940- case KEY_SELECT :
941- picked = true;
942- break ;
943- case KEY_PREV :
944- selected = step_key (enabled , WORD_NUMBER_KEYS , selected , false);
945- break ;
946- case KEY_NEXT :
947- selected = step_key (enabled , WORD_NUMBER_KEYS , selected , true);
948- break ;
949- case KEY_REDRAW :
950- break ;
951- default :
952- seedtool_zero (digits , sizeof (digits ));
953- return -1 ;
954- }
955- }
951+ /* The ring for the current box: the digits still reachable, then backspace.
952+ * Rebuilt whenever the box changes, since which digits are reachable
953+ * depends on the ones already set. */
954+ char ring [SEEDTOOL_DIGITS + 2 ];
955+ size_t ring_len = 0 , on = 0 ;
956+ bool rebuild = true;
956957
957- const char pressed = seedtool_layout_key ( WORD_NUMBER_LAYOUT , selected );
958- if (pressed == SEEDTOOL_KEY_ACCEPT ) {
959- const char * const word = seedtool_word ( number - 1 ) ;
960- char counted [ 32 ];
961- ( void ) snprintf ( counted , sizeof ( counted ), "Number %u of %u" , number , ( unsigned ) SEEDTOOL_WORDLIST_LEN );
962- const seedtool_key_t key = confirm ( title , word , counted );
963- seedtool_zero ( counted , sizeof ( counted ) );
964- if ( key == KEY_SELECT ) {
965- const int result = ! word || strlen ( word ) + 1 > output_len ? -1 : 1 ;
966- if (result == 1 ) {
967- strcpy ( output , word );
958+ for (;;) {
959+ if (rebuild ) {
960+ bool reachable [ SEEDTOOL_DIGITS ] = { false } ;
961+ /* Narrowed by `allowed`, exactly as the letter keyboard is. The
962+ * ring is built per box, so a digit that cannot begin an allowed
963+ * word never appears to be scrolled to in the first place. */
964+ ( void ) seedtool_next_digits_in ( allowed , digits , at , reachable );
965+ ring_len = 0 ;
966+ for ( size_t d = 0 ; d < SEEDTOOL_DIGITS ; ++ d ) {
967+ if (reachable [ d ] ) {
968+ ring [ ring_len ++ ] = ( char )( '0' + d );
968969 }
969- seedtool_zero (digits , sizeof (digits ));
970- return result ;
971970 }
972- if (key == KEY_TIMEOUT ) {
973- seedtool_zero (digits , sizeof (digits ));
974- return -1 ;
971+ /* OK only once the digits already name a word - so it is absent on
972+ * an empty field and on a prefix like `20`, which is a real number
973+ * but is offered here as a step towards 200x rather than as 20
974+ * itself... which it also is. Both readings are live, which is
975+ * exactly why the reader has to say which one they meant. */
976+ if (seedtool_word_number_in (allowed , digits , at )) {
977+ ring [ring_len ++ ] = SEEDTOOL_KEY_ACCEPT ;
975978 }
976- /* Went back: the number is cleared rather than left half-entered, so
977- * what is on screen is always the whole of what was typed. */
979+ ring [ring_len ++ ] = SEEDTOOL_KEY_BACKSPACE ;
980+ on = 0 ;
981+ rebuild = false;
982+ }
983+
984+ memset (shown , 0 , sizeof (shown ));
985+ memcpy (shown , digits , at );
986+ shown [at ] = ring [on ];
987+ for (size_t i = at + 1 ; i < SEEDTOOL_MAX_WORD_DIGITS ; ++ i ) {
988+ shown [i ] = ' ' ;
989+ }
990+ seedtool_display_digits (title , shown , SEEDTOOL_MAX_WORD_DIGITS , at , ACK_FOOTER , NULL );
991+
992+ seedtool_key_t key = wait_key ();
993+ if (key == KEY_REDRAW ) {
994+ continue ;
995+ }
996+ if (key == KEY_TIMEOUT ) {
978997 seedtool_zero (digits , sizeof (digits ));
979- digits_len = 0 ;
980- } else if (pressed == SEEDTOOL_KEY_BACKSPACE ) {
981- if (!digits_len ) {
998+ seedtool_zero (shown , sizeof (shown ));
999+ return -1 ;
1000+ }
1001+ if (key == KEY_PREV ) {
1002+ on = on ? on - 1 : ring_len - 1 ;
1003+ continue ;
1004+ }
1005+ if (key == KEY_NEXT ) {
1006+ on = on + 1 < ring_len ? on + 1 : 0 ;
1007+ continue ;
1008+ }
1009+
1010+ /* KEY_SELECT: commit whatever the box is showing. */
1011+ if (ring [on ] == SEEDTOOL_KEY_BACKSPACE ) {
1012+ if (!at ) {
9821013 seedtool_zero (digits , sizeof (digits ));
1014+ seedtool_zero (shown , sizeof (shown ));
9831015 return 0 ;
9841016 }
985- digits [-- digits_len ] = '\0' ;
986- } else if (digits_len < SEEDTOOL_MAX_WORD_DIGITS ) {
987- digits [digits_len ++ ] = pressed ;
988- digits [digits_len ] = '\0' ;
1017+ digits [-- at ] = '\0' ;
1018+ rebuild = true;
1019+ continue ;
1020+ }
1021+ bool typed_last = false;
1022+ if (ring [on ] != SEEDTOOL_KEY_ACCEPT ) {
1023+ digits [at ++ ] = ring [on ];
1024+ digits [at ] = '\0' ;
1025+ rebuild = true;
1026+ /* A fourth digit has nowhere left to go, so it confirms itself
1027+ * rather than asking for an OK the field has no box to show. */
1028+ if (at < SEEDTOOL_MAX_WORD_DIGITS ) {
1029+ continue ;
1030+ }
1031+ typed_last = true;
1032+ }
1033+
1034+ /* Narrowed here too, not only where the ring is built. The ring should
1035+ * already have made a disallowed number untypeable - but this is the
1036+ * gate a number passes through to become a word, and a gate that
1037+ * inherits its guarantee from the screen upstream is one bug away from
1038+ * accepting what that screen was supposed to have refused. A number
1039+ * outside the set reads as no word at all, which sends the reader back
1040+ * to the field rather than into a confirmation they should never see. */
1041+ const unsigned number = seedtool_word_number_in (allowed , digits , at );
1042+ const char * const word = number ? seedtool_word (number - 1 ) : NULL ;
1043+ char counted [32 ];
1044+ (void )snprintf (counted , sizeof (counted ), "Number %u of %u" , number , (unsigned )SEEDTOOL_WORDLIST_LEN );
1045+ const seedtool_key_t answer = word ? confirm (title , word , counted ) : KEY_PREV ;
1046+ seedtool_zero (counted , sizeof (counted ));
1047+ if (answer == KEY_SELECT ) {
1048+ const int result = strlen (word ) + 1 > output_len ? -1 : 1 ;
1049+ if (result == 1 ) {
1050+ strcpy (output , word );
1051+ }
1052+ seedtool_zero (digits , sizeof (digits ));
1053+ seedtool_zero (shown , sizeof (shown ));
1054+ return result ;
1055+ }
1056+ if (answer == KEY_TIMEOUT ) {
1057+ seedtool_zero (digits , sizeof (digits ));
1058+ seedtool_zero (shown , sizeof (shown ));
1059+ return -1 ;
1060+ }
1061+ /* Went back from the confirmation. A number that confirmed itself on a
1062+ * fourth digit reopens that digit, so a wrong last one costs a press
1063+ * rather than the whole number; one the reader ended with OK returns to
1064+ * the field exactly as it was, with the OK still on the ring. */
1065+ if (typed_last ) {
1066+ digits [-- at ] = '\0' ;
9891067 }
1068+ rebuild = true;
9901069 }
9911070}
9921071
0 commit comments