From 019c93a587ab4ec8947c74ec07cd57ae69e619d5 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Sat, 5 Sep 2026 11:55:23 -0400 Subject: [PATCH] The console draws a tab instead of dropping it consoleDraw gave meanings to newline, carriage return and backspace and dropped every other byte below the first glyph. A tab was one of those, so it left no mark on the screen at all - while the same byte went down the serial line, where a host terminal laid it out perfectly. That is why a tab separated file read correctly and displayed wrongly. Type and More were never at fault: they hand the file's bytes to the console unchanged, and the console is where the tabs stopped. An assembler symbol table came out with its fields run together. A tab now moves the cursor to the next stop, eight columns apart, and wraps when the next stop would reach or pass the last column - which is what an ordinary character does at the edge, rather than a rule only tabs obey. It MOVES rather than writing spaces, the way a terminal does: a carriage return followed by a tab steps over what is on the line and leaves it. Kept in the console rather than expanded by Type, More, and every future program that prints text. Three checks in video.sh, each of which fails on a different mistake: a tab renders the same screen as the spaces it stands for, one that runs off the edge renders the same screen as a newline, and sixteen letters tabbed across still have their ink - which is the one that fails if a tab is implemented by writing spaces. Verified with break.sh both ways. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Source/Emulator/io.c | 25 +++++++ Source/Emulator/io.h | 6 ++ SplitBit Programming Manual.md | 7 +- Tests/video.sh | 132 +++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 2 deletions(-) diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index ba1ee2b..58d19e6 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -332,6 +332,31 @@ static void consoleDraw(uint8_t byte) { consoleCursorMoved(); } return; + case '\t': { + // ---- A tab moves the cursor and marks nothing ---- + // + // The byte reaches standard output either way, so anything reading the serial + // line has always seen tabs and a host terminal has always laid them out. The + // screen dropped them, which is why a tab separated file - an assembler symbol + // table, say - came out of Type or More as its fields run together. + // + // MOVING RATHER THAN WRITING SPACES is what a terminal does, and the difference + // shows the moment anything is already on the line: a tab that wrote blanks + // would rub out what it passed over. + // + // Eight columns is the conventional stop and the one everything that prints a + // tab assumes. Reaching or passing the right edge wraps, which is what an + // ordinary character does at the edge - the alternative, stopping in the last + // column, invents a rule that only tabs obey. + int stop = (cursorColumn / CONSOLE_TAB_WIDTH + 1) * CONSOLE_TAB_WIDTH; + if (stop >= videoColumns()) { + consoleNewLine(); + } else { + cursorColumn = (uint8_t)stop; + } + consoleCursorMoved(); + } + return; default: break; } diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index fa10e79..797f815 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -227,6 +227,12 @@ // like afterwards and what was typed before are the system's business - see the shell, // which edits its own line - exactly as what is on a disk is the system's business and what // a drive IS belongs to the machine. +// How far apart the console's tab stops are, on the screen. The serial line is not this +// device's business - a host terminal lays a tab out with whatever it was configured for - +// but the character screen has to be told, and eight is what everything that prints a tab +// expects. +#define CONSOLE_TAB_WIDTH 8 + #define CONSOLE_KEY_UP 0x80 #define CONSOLE_KEY_DOWN 0x81 #define CONSOLE_KEY_LEFT 0x82 diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 3f75915..59fda33 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -888,15 +888,18 @@ A console on a machine with a screen sends every byte to both, because a machine At reset the font is expanded into tile memory and the palette is given sixteen ink and paper pairs. See Colour below. -The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes below that have no glyph and are not drawn; three of them do something instead. +The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes below that have no glyph and are not drawn; four of them do something instead. | Byte | Does | | --- | --- | | 0x0A | Newline. The cursor goes to the start of the next row, and at the last row the screen scrolls instead. | | 0x0D | Carriage return. The cursor goes to the start of the row it is on. | | 0x08 | Backspace. The cursor steps back and rubs out what was there. | +| 0x09 | Tab. The cursor moves to the next stop, eight columns apart. | -Writing past the last column wraps to the next row, the same as a newline. +Writing past the last column wraps to the next row, the same as a newline. So does a tab whose next stop would reach or pass the last column. + +**A tab moves the cursor and marks nothing.** It does not write spaces, which matters as soon as there is anything on the line already: a carriage return followed by a tab steps over what is there and leaves it, exactly as a terminal does. Something that wants the columns cleared has to write the spaces itself. ### Colour: diff --git a/Tests/video.sh b/Tests/video.sh index 8524bc7..a943dd8 100755 --- a/Tests/video.sh +++ b/Tests/video.sh @@ -3117,6 +3117,138 @@ else result no "a fault puts the screen back where text can be seen" "no picture was written" fi +# ---- A tab moves the cursor to the next stop ---- +# +# The console gave newline, carriage return and backspace meanings and dropped every other +# byte below the first glyph, so a tab left no mark on the screen at all - while the same +# byte went down the serial line, where a host terminal laid it out. A tab separated file +# read perfectly and displayed as its fields run together, which is exactly what Type and +# More did to an assembler symbol table. +# +# CHECKED AS A PICTURE AGAINST THE SPACES IT STANDS FOR, rather than by reading a cell. If a +# tab from column one reaches column eight, then "A" tab "B" and "A" seven spaces "B" put the +# same glyphs in the same cells and leave the cursor in the same place, so the two screens are +# the same bytes. That tests where the cursor landed without this suite having to learn how a +# glyph is stored. +run tabstop <<'ASM' +#Program +start: + INIA 0x41 ; 'A' + OUTA 0x00 + INIA 0x09 ; Tab, which should reach column eight. + OUTA 0x00 + INIA 0x42 ; 'B' + OUTA 0x00 + HALT +ASM +run tabspaces <<'ASM' +#Program +start: + INIA 0x41 ; 'A' + OUTA 0x00 + INIB 0d7 ; The seven spaces that get to the same place. +spaces: + INIA 0x20 + OUTA 0x00 + DECB + BNB spaces + INIA 0x42 ; 'B' + OUTA 0x00 + HALT +ASM +if cmp -s "$BUILD/tabstop.ppm" "$BUILD/tabspaces.ppm"; then + result ok "a tab reaches the next stop" "the same screen as the spaces it stands for" +else + result no "a tab reaches the next stop" "the screen differs from the same line spaced out" +fi + +# ---- And a tab that would leave the screen wraps, the way a character does ---- +# +# The screen is forty columns and the stops are eight apart, so from column thirty three the +# next stop is forty, which is off the end. An ordinary character at the edge wraps, and a +# tab that stopped in the last column instead would be inventing a rule only tabs obey. +run tabwrap <<'ASM' +#Program +start: + INIB 0d33 ; Columns nought to thirty two, so the cursor is at thirty three. +fill: + INIA 0x41 + OUTA 0x00 + DECB + BNB fill + INIA 0x09 ; The next stop is forty, which is the edge. + OUTA 0x00 + INIA 0x42 + OUTA 0x00 + HALT +ASM +run tabwrapped <<'ASM' +#Program +start: + INIB 0d33 +fill: + INIA 0x41 + OUTA 0x00 + DECB + BNB fill + INIA 0x0A ; A new line, which is where the tab should have gone. + OUTA 0x00 + INIA 0x42 + OUTA 0x00 + HALT +ASM +if cmp -s "$BUILD/tabwrap.ppm" "$BUILD/tabwrapped.ppm"; then + result ok "and one that runs off the edge wraps" "the same screen as a new line" +else + result no "and one that runs off the edge wraps" "it did not go to the next row" +fi + +# ---- A tab does not rub out what it passes over ---- +# +# The other way to make a tab visible is to write spaces to the next stop, and it is wrong +# for the reason a terminal does not do it: a cursor moved back to the start of a line and +# tabbed forward would erase the line it moved over. This writes a row, returns to the front +# of it, and tabs. +run tabkeeps <<'ASM' +#Program +start: + INIB 0d16 +fill: + INIA 0x41 ; Sixteen letters. + OUTA 0x00 + DECB + BNB fill + INIA 0x0D ; Back to the front of the line, without leaving it. + OUTA 0x00 + INIA 0x09 ; And a tab across the letters already there. + OUTA 0x00 + INIA 0x42 ; Which lands in column eight, on top of one of them. + OUTA 0x00 + HALT +ASM +LETTERS="$(python3 - "$BUILD/tabkeeps.ppm" <<'PY2' +import sys +data = open(sys.argv[1], "rb").read() +fields = data.split(b"\n", 3) +width, height = (int(n) for n in fields[1].split()) +body = fields[3] +# The first row of cells is eight pixels tall. A cell that still holds a letter has ink in +# it; one a tab blanked has none. Counting ink across the row says how many survived. +ink = 0 +for cell in range(16): + lit = False + for y in range(8): + for x in range(cell * 8, cell * 8 + 8): + at = (y * width + x) * 3 + if body[at:at + 3] != body[0:3]: + lit = True + if lit: + ink += 1 +print(ink) +PY2 +)" +[ "$LETTERS" = "16" ] && result ok "and does not rub out what it moves over" "all sixteen letters are still there" || result no "and does not rub out what it moves over" "$LETTERS of sixteen cells still have ink" + echo if [ "$FAIL" -eq 0 ]; then echo "All $PASS video checks passed."