Give the console colour and a cursor

COLOUR COSTS A NIBBLE AND NO HARDWARE. A glyph is drawn in palette indices 0 and 1, paper
and ink, and a cell's attribute nibble adds sixteen to both - so sixteen banks is already
sixteen ink and paper pairs, and all that was missing was a register saying which one the
console draws in. That is port 0x06, read as well as written like the rest.

The palette a machine wakes up with is arranged so that HIGHLIGHTING IS ONE BIT: banks 0 to
7 are colours on black, banks 8 to 15 are the same colours as paper with black ink. So
attribute XOR 8 turns any pair inside out. That is a convention rather than a rule of the
machine - the device only ever adds the nibble and looks the answer up - but it is the
convention that makes a highlighted line and a cursor free.

Bank 0 is still grey on black, so nothing that was written before this has changed colour.

THE CURSOR IS THE SAME BIT AGAIN. It is drawn by turning its cell inside out rather than by
putting a block over it, so the character underneath stays readable, which matters to
somebody editing a line. The device draws it rather than the window, because on a machine
with a screen a cursor is a hardware feature - one drawn by the presenter would not be in a
picture the machine saved.

It blinks on the machine's own clock, half a second each way, so the phase is a pure
function of the cycle count and a screen saved at a given cycle is the same screen every
time. A blink on the host's clock would have made every saved picture a matter of luck.

Off unless asked for, with bit 2 of the control port. That is right for a machine - a
program painting its own screen does not want something blinking in the middle of it - and
CosmOS asks for one at boot. It also asks again when it takes the console back from a
program that has stopped, because a program handing key mode back the way it was told to
writes zero, which turns the cursor off. The shell owns the prompt, so the shell is what
makes sure there is something blinking at it.

Nine more checks in Tests/video.sh, to 41: that the attribute colours the ink and not the
paper, that XOR 8 turns both, that it reads back, that a cursor appears where the registers
put it and only when asked for, and that it goes dark again half a million cycles later.

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-29 08:13:28 -04:00
co-authored by Claude Opus 5
parent 978aec4809
commit d6feddd1b6
7 changed files with 235 additions and 21 deletions
+36 -5
View File
@@ -26,6 +26,20 @@
// nothing needs to look further ahead than the byte it is about to take.
static int consoleKeyMode = 0;
static int cursorColumn = 0;
static int cursorRow = 0;
// What every cell the console draws is given. Zero is the pair the machine wakes up in,
// grey on black.
static uint8_t consoleAttribute = 0;
static int consoleCursorShown = 0;
// The device draws the cursor, so it has to be told where the console put it. Called
// wherever the cursor moves, which is every one of the few places that move it.
static void consoleCursorMoved(void) {
videoSetCursor(cursorRow, cursorColumn, consoleCursorShown);
}
static int consoleEnded = 0;
static int consolePushback = -1; // A byte already taken from the host, or -1.
static int consoleInterrupts = 0; // Whether an arriving byte puts the line up.
@@ -176,6 +190,8 @@ static void consoleSetControl(uint8_t control) {
// means one write can ask for line mode and interrupts together, which is an ordinary
// thing to want and would otherwise be undone in the same breath as it was asked for.
consoleSetMode((control & CONSOLE_MODE_KEY) != 0);
consoleCursorShown = (control & CONSOLE_CONTROL_CURSOR) != 0;
consoleCursorMoved();
int wantInterrupts = (control & CONSOLE_CONTROL_INTERRUPT) != 0;
if (!wantInterrupts) {
@@ -213,12 +229,12 @@ static void consoleShowWhatIsWritten(void) {
// hold Voyager to the same recorded results as SplitBit. It is also what makes --screen
// work on the plain machine: there is one console, and it drives everything it has.
static int cursorColumn = 0;
static int cursorRow = 0;
void consoleHome(void) {
cursorColumn = 0;
cursorRow = 0;
consoleAttribute = 0;
consoleCursorShown = 0;
consoleCursorMoved();
}
static void consoleNewLine(void) {
@@ -288,14 +304,17 @@ static void consoleDraw(uint8_t byte) {
switch (byte) {
case '\n':
consoleNewLine();
consoleCursorMoved();
return;
case '\r':
cursorColumn = 0;
consoleCursorMoved();
return;
case 0x08: // Backspace, which CosmOS sends when a line is being edited.
if (cursorColumn > 0) {
cursorColumn--;
videoPutCell(cursorRow, cursorColumn, 0, 0);
videoPutCell(cursorRow, cursorColumn, 0, consoleAttribute);
consoleCursorMoved();
}
return;
default:
@@ -306,10 +325,12 @@ static void consoleDraw(uint8_t byte) {
if (byte < CONSOLE_FONT_FIRST) {
return;
}
videoPutCell(cursorRow, cursorColumn, (uint8_t)(byte - CONSOLE_FONT_FIRST), 0);
videoPutCell(cursorRow, cursorColumn, (uint8_t)(byte - CONSOLE_FONT_FIRST),
consoleAttribute);
if (++cursorColumn >= videoColumns()) {
consoleNewLine();
}
consoleCursorMoved();
}
// ---- Waiting for a key when there is no terminal to wait on ----
@@ -538,6 +559,9 @@ static uint8_t consoleStatus(void) {
if (consoleInterrupts) {
status |= CONSOLE_STATUS_INTERRUPT;
}
if (consoleCursorShown) {
status |= CONSOLE_STATUS_CURSOR;
}
consoleFetch();
if (consoleEnded) {
// READY IS NOT SET HERE, although a read would answer immediately. The bit means
@@ -760,6 +784,9 @@ void setDiskLatency(unsigned long cycles) {
void deviceTick(unsigned long now) {
deviceNow = now;
// The screen blinks its cursor on the machine's own clock rather than the host's, so the
// picture is the same at the same cycle count however fast anything ran.
videoTick(now);
if (diskPending && now >= diskReadyAt) {
uint8_t command = diskPending;
diskPending = 0;
@@ -908,10 +935,12 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// obvious place to be, and stopping the machine over one is a poor trade.
cursorRow = DataByte < videoRows() ? DataByte : videoRows() - 1;
cursorTold = 0;
consoleCursorMoved();
break;
case CONSOLE_CURSOR_COLUMN:
cursorColumn = DataByte < videoColumns() ? DataByte : videoColumns() - 1;
cursorTold = 0;
consoleCursorMoved();
break;
case CONSOLE_COMMAND:
if (DataByte == CONSOLE_COMMAND_CLEAR) {
@@ -921,6 +950,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// Anything else does nothing. A command block reserved for later should be
// quiet rather than fatal, the same as the screen's spare registers.
break;
case CONSOLE_ATTRIBUTE: consoleAttribute = DataByte; break;
case CONSOLE_STATUS:
// Read only. A device saying how it is does not take instructions through the
// same hole, so a write here is ignored rather than meaning something.
@@ -988,6 +1018,7 @@ uint8_t InputHandler(uint8_t Address) {
case CONSOLE_STATUS: return consoleStatus();
case CONSOLE_CURSOR_ROW: return (uint8_t)cursorRow;
case CONSOLE_CURSOR_COLUMN: return (uint8_t)cursorColumn;
case CONSOLE_ATTRIBUTE: return consoleAttribute;
case CONSOLE_COMMAND:
// Write only. What it did is visible in the cursor and on the screen.
return 0;