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."