Replace the escape parser with cursor registers

The console had grown an ANSI parser, and that was the wrong shape. ANSI exists because a
screen used to be on the other end of a serial line and a byte stream was the only channel
there was. This screen is memory the program can already address, so reaching it by sending
characters for a state machine to take apart is a middleman for something the machine does
better - and it meant accepting an open protocol somebody else defines, in hardware, with no
natural end to it. Everything else on this machine is registers.

So the console gets three: cursor row at 0x03, cursor column at 0x04, and a command port at
0x05 where 1 clears the screen. Both cursor registers are READ as well as written, which is
the thing an escape cannot do without sending a query and parsing a reply - a routine that
wants to put the cursor back where it found it can now ask.

Clearing is one command against a thousand cells walked one at a time. Snake and Life are
smaller for it: 2,168 bytes to 2,163 and 1,410 to 1,396.

A HOST TERMINAL STILL SPEAKS ANSI, and bridging to the host is the emulator's job, the same
job it does reading standard input. So the escapes are now GENERATED, outbound, for the set
this device chooses, rather than parsed inbound as though the machine were a terminal. The
set cannot grow behind our backs because we are the ones saying it. The cursor is announced
lazily, at the next character rather than at the register write, so setting a row and a
column costs one sequence rather than two.

The console's block widens from three ports to six, which registryTest noticed: it had been
asking about port 0x05 precisely BECAUSE nothing was there, and the console had just moved
in. Re-blessing it would have left it checking nothing, so it asks about 0x80 instead -
clear of the console, the disk, the screen, the controller, and the sound device coming to
0x40.

Six checks in Tests/video.sh swapped from the sequences to the registers, including that the
cursor reads back and that one sent past the edge is clamped rather than refusing. Those
checks also stopped counting bytes from the ends of a file, which had quietly started
measuring an escape the moment the console began announcing the cursor.

SplitLint caught the one thing worth catching in the port: the clear command leaves A at 1
and key mode is also 1, so the second load looks redundant. Acting on it would tie a console
command to a console mode by coincidence, and break silently if either ever moved, so it is
suppressed with that reason rather than removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-28 23:08:52 -04:00
co-authored by Claude Opus 5
parent bdb2d0d8e6
commit 43a05b3df1
17 changed files with 316 additions and 294 deletions
+15 -8
View File
@@ -506,7 +506,7 @@ If nothing is installed for the vector a device refused with, the machine stops
| Port | Device | Class |
| --- | --- | --- |
| 0x00 - 0x02 | The console. See The Console. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 |
| 0x00 - 0x05 | The console. See The Console. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 |
| 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 |
| 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 |
| 0x20 - 0x23 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 |
@@ -589,19 +589,26 @@ The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes b
Writing past the last column wraps to the next row, the same as a newline.
**And it understands the escape sequences this machine already sends.** Every program here that moves a cursor does it with ANSI escapes, because until there was a screen the thing on the other end was somebody's terminal. A controller that ignored them would draw `[2J` on the screen and leave the picture underneath it, so it parses them - which is what a video terminal did.
### Moving The Cursor:
| Sequence | Does |
Three more registers, because that is how this machine talks to everything else.
| Port | Register |
| --- | --- |
| `ESC [ 2 J` | Clears the whole screen. The cursor does not move. |
| `ESC [ J` | Clears from the cursor to the end of the screen. |
| `ESC [ H` | Puts the cursor in the corner. |
| `ESC [ row ; column H` | Puts the cursor there, counting from one. |
| 0x03 | Cursor row. Read and write. |
| 0x04 | Cursor column. Read and write. |
| 0x05 | Command. Write 1 to clear the screen. |
Anything else in that shape - an escape, a bracket, some numbers, a letter - is recognised and **swallowed rather than drawn**. A sequence nobody implemented should leave no marks, which is what a terminal does with one it does not know, and drawing it would be worse than ignoring it.
Both counted from zero, and both **readable**, which is the thing worth having: a routine that wants to put the cursor back where it found it asks where that was.
A cursor sent past the edge is clamped rather than refused. It has an obvious place to be, and stopping the machine over one would be a poor trade.
Clearing does not touch the scrollback. It clears what is on the screen, and what has already gone off the top is still in the map where the Scroll register can find it.
**There is no escape sequence here, and there should not be.** ANSI exists because a screen used to be on the other end of a serial line and a byte stream was the only channel there was. This screen is memory the program can already address, and reaching it by sending characters for a parser to take apart is a middleman for something the machine does better - clearing by writing 1 to a port costs one command, against a thousand cells walked one at a time.
What a program on the other end of an actual serial line sees is a different question, and the answer is that the console sends it the escapes it needs. That is the emulator bridging to a host terminal, the same job it does reading standard input, and it is not part of this machine.
**Scrolling moves the video device's Scroll register and no memory at all.** The row that comes into view at the bottom is cleared, because the map is a ring and it is holding whatever was there 128 rows ago. The rows that go off the top are *not* cleared, and that is the point: a hundred rows of what has already been said are still in the map, so a machine has scrollback without anything having to keep it.
**It is one screen.** A program that writes its own tiles and its own map has taken the screen, and a console still writing characters into it will scribble on what that program drew. This is not an oversight to be worked around - it is what one screen means, and it is why a program that wants the screen takes it.