Deliver the keys that are not characters
An arrow key has never reached this machine. Voyager threw it away for want of anywhere to put it, and a terminal sent ESC [ A, which arrived in the middle of whatever was being read and made it unrecognisable - typing Up at the CosmOS prompt put three bytes in the command line and got "I do not know". So the console names them: one byte each, 0x80 upward, above ASCII so nothing written before them can collide. Up, Down, Left, Right, Home, End and forward Delete, with room above for the paging and function keys. The console normalises, which is what it already does. Behind a window it turns the key somebody pressed into a byte; on a terminal it turns the sequence into the same byte. That is the act it has always performed on Return and Backspace, one layer further along, and it is why a program need not know which of the two it is talking to. What a key MEANS is not the console's business - that belongs to whoever is reading, the same way what is on a disk belongs to the system and what a drive is belongs to the machine. Translated only when standard input really is a terminal. Nothing else sends these sequences, a pipe holds exactly the bytes somebody put in it, and it keeps the Escape-or-Up timing problem out of every test here: a test writes the key values themselves. Line mode drops them, in both front ends, because line mode delivers characters and a line somebody else has finished editing cannot be moved about in. Press.sbx says what it was handed, in hexadecimal and by name, and reads a line before it reads keys so both halves of that rule are checked. Two recordings, one fed as standard input and one as a keyboard, agreeing byte for byte; each break fails exactly one of them. Three checks in terminal.sh type real escape sequences at a pseudo-terminal, which is the only place they are ever read as sequences: that they arrive as keys, that Escape alone is still Escape, and that a character typed straight after an escape is held rather than swallowed. Five recordings re-blessed for Press.sbx appearing on the shared disk, and the whole of that diff is the file's own line and the counts above it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
2a29cebc6b
commit
b3726c950a
@@ -0,0 +1,178 @@
|
||||
; What the console just handed over, in hexadecimal and by name.
|
||||
;
|
||||
; The keys that are not characters - the arrows, Home, End and forward Delete - arrive as
|
||||
; the console's own values above ASCII rather than as the escape sequences a terminal sends
|
||||
; or as nothing at all, which is what a window used to make of them. This is what shows
|
||||
; that, and it shows both halves of the rule in one run:
|
||||
;
|
||||
; A LINE FIRST, read the way everything reads one. Line mode delivers characters, so the
|
||||
; keys are dropped before they reach the buffer and what comes back is what a person could
|
||||
; have typed. Pressing Up while something else is collecting a line does nothing, which is
|
||||
; an improvement on putting an escape and a bracket in the middle of it.
|
||||
;
|
||||
; THEN THE KEYS, in key mode, where a program has asked for every keystroke as it happens
|
||||
; and these are keystrokes like any other.
|
||||
;
|
||||
; Written by Anachronaut
|
||||
|
||||
#Include services.asm
|
||||
|
||||
#Program
|
||||
|
||||
#Base 0x4000
|
||||
|
||||
start:
|
||||
SETD.0 LineText
|
||||
CALL printString
|
||||
CALL newLine
|
||||
|
||||
SETD.0 Buffer
|
||||
INIB 0d63
|
||||
CALL readLine
|
||||
|
||||
SETD.0 Buffer
|
||||
CALL showBytes
|
||||
|
||||
SETD.0 KeyText
|
||||
CALL printString
|
||||
CALL newLine
|
||||
|
||||
INIA 0x01
|
||||
OUTA 0x02 ; Key mode. Nothing echoes, so everything below says what it saw.
|
||||
keyLoop:
|
||||
INA 0x00
|
||||
INIB 0xFF
|
||||
XOR
|
||||
BRQ keyDone ; Nothing more is coming.
|
||||
INIB 0x71 ; q, which is how this is stopped.
|
||||
XOR
|
||||
BRQ keyDone
|
||||
CALL showKey
|
||||
BRI keyLoop
|
||||
|
||||
keyDone:
|
||||
RSTA
|
||||
OUTA 0x02 ; Line mode, the way it was found.
|
||||
SETD.0 DoneText
|
||||
CALL printString
|
||||
CALL newLine
|
||||
RSTA
|
||||
SWI osExit
|
||||
|
||||
; DP0 names a string of bytes ending in a zero. Prints each as two hexadecimal digits, so
|
||||
; that what is in the buffer can be read rather than guessed at.
|
||||
showBytes:
|
||||
LDA.0
|
||||
BRA showBytesDone
|
||||
CALL printByteHex
|
||||
INIA 0x20
|
||||
OUTA 0x00
|
||||
INCD.0
|
||||
BRI showBytes
|
||||
showBytesDone:
|
||||
CALL newLine
|
||||
RET
|
||||
|
||||
; A holds a key. Prints its value and then what it is.
|
||||
;
|
||||
; A survives a CALL, so the byte is still here after printing it - but only until something
|
||||
; else is put in A, which the space below does. So it is kept where the naming can find it.
|
||||
showKey:
|
||||
SETD.1 KeyByte
|
||||
STA.1
|
||||
CALL printByteHex
|
||||
INIA 0x20
|
||||
OUTA 0x00
|
||||
SETD.1 KeyByte
|
||||
LDA.1
|
||||
|
||||
; XOR leaves the answer in Q and A alone, so one load stands for the whole ladder.
|
||||
INIB 0x80
|
||||
XOR
|
||||
BRQ keyUp
|
||||
INIB 0x81
|
||||
XOR
|
||||
BRQ keyDown
|
||||
INIB 0x82
|
||||
XOR
|
||||
BRQ keyLeft
|
||||
INIB 0x83
|
||||
XOR
|
||||
BRQ keyRight
|
||||
INIB 0x84
|
||||
XOR
|
||||
BRQ keyHome
|
||||
INIB 0x85
|
||||
XOR
|
||||
BRQ keyEnd
|
||||
INIB 0x86
|
||||
XOR
|
||||
BRQ keyDelete
|
||||
|
||||
; An ordinary character, which is its own best name.
|
||||
OUTA 0x00
|
||||
CALL newLine
|
||||
RET
|
||||
|
||||
keyUp:
|
||||
SETD.0 UpText
|
||||
BRI keySay
|
||||
keyDown:
|
||||
SETD.0 DownText
|
||||
BRI keySay
|
||||
keyLeft:
|
||||
SETD.0 LeftText
|
||||
BRI keySay
|
||||
keyRight:
|
||||
SETD.0 RightText
|
||||
BRI keySay
|
||||
keyHome:
|
||||
SETD.0 HomeText
|
||||
BRI keySay
|
||||
keyEnd:
|
||||
SETD.0 EndText
|
||||
BRI keySay
|
||||
keyDelete:
|
||||
SETD.0 DeleteText
|
||||
keySay:
|
||||
CALL printString
|
||||
CALL newLine
|
||||
RET
|
||||
|
||||
#Data
|
||||
|
||||
#Base 0x2000
|
||||
|
||||
LineText:
|
||||
"a line, then keys. q stops."
|
||||
KeyText:
|
||||
"keys:"
|
||||
DoneText:
|
||||
"done"
|
||||
|
||||
UpText:
|
||||
"up"
|
||||
DownText:
|
||||
"down"
|
||||
LeftText:
|
||||
"left"
|
||||
RightText:
|
||||
"right"
|
||||
HomeText:
|
||||
"home"
|
||||
EndText:
|
||||
"end"
|
||||
DeleteText:
|
||||
"delete"
|
||||
|
||||
KeyByte:
|
||||
0x00
|
||||
|
||||
Buffer:
|
||||
#Reserve 0d64
|
||||
|
||||
#Vectors
|
||||
|
||||
Boot start
|
||||
|
||||
#Include console.asm
|
||||
+184
-28
@@ -432,7 +432,13 @@ static int consoleGatherLine(void) {
|
||||
// No room, or nothing a line is made of. A control byte that means something to a
|
||||
// terminal means nothing here yet, and putting it in the line would only hand a
|
||||
// program something it cannot use.
|
||||
if (got < 0x20 || consoleLineLength >= CONSOLE_LINE_BYTES - 1) {
|
||||
//
|
||||
// THE CONSOLE'S OWN KEYS GO THE SAME WAY, from above rather than below: this is the
|
||||
// gatherer, which is what line mode IS behind a window, and line mode delivers
|
||||
// characters. An arrow key pressed while something else is collecting the line
|
||||
// arrived too late to move anything.
|
||||
if (got < 0x20 || (got >= CONSOLE_KEY_FIRST && got <= CONSOLE_KEY_LAST) ||
|
||||
consoleLineLength >= CONSOLE_LINE_BYTES - 1) {
|
||||
continue;
|
||||
}
|
||||
consoleLine[consoleLineLength++] = (unsigned char)got;
|
||||
@@ -444,6 +450,168 @@ void consoleSetInputHook(int (*hook)(int mayWait)) {
|
||||
inputHook = hook;
|
||||
}
|
||||
|
||||
// ---- Reading standard input, in one place ----
|
||||
//
|
||||
// The blocking read and the poll behind the status port each reached for read() themselves,
|
||||
// which was fine while one byte from the host was one byte for the program. It stopped
|
||||
// being fine the moment a key could arrive as several: a sequence half taken by one path
|
||||
// and half by the other is not a key, it is two pieces of rubbish. So there is one way in
|
||||
// now, and the translation below sits on top of it.
|
||||
//
|
||||
// Answers a byte, CONSOLE_NOTHING_YET when nothing is waiting and it was told not to wait,
|
||||
// or CONSOLE_GONE at the end of input. Deliberately the same three answers a front end's
|
||||
// hook gives, so that everything above this treats a terminal and a window alike.
|
||||
static int consoleFromInput(int mayWait) {
|
||||
if (!mayWait) {
|
||||
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
||||
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
|
||||
return CONSOLE_NOTHING_YET;
|
||||
}
|
||||
}
|
||||
unsigned char byte;
|
||||
for (;;) {
|
||||
const ssize_t got = read(STDIN_FILENO, &byte, 1);
|
||||
if (got == 1) {
|
||||
return byte;
|
||||
}
|
||||
if (got == 0) {
|
||||
return CONSOLE_GONE;
|
||||
}
|
||||
if (errno != EINTR) {
|
||||
return CONSOLE_GONE;
|
||||
}
|
||||
// Interrupted before anything arrived, so ask again.
|
||||
}
|
||||
}
|
||||
|
||||
// ---- What a terminal sends for a key that is not a character ----
|
||||
//
|
||||
// ESC [ A and its neighbours. A window hands over the key somebody pressed; a terminal
|
||||
// hands over the sequence it was taught to send decades ago, and something has to turn one
|
||||
// into the other. It happens here because the console is already the thing that turns a
|
||||
// window's Enter key into a newline, and because doing this once in C is a great deal
|
||||
// better than doing it in every program that wants an arrow key.
|
||||
//
|
||||
// ONLY WHEN THERE IS A TERMINAL. Nothing else sends these: a file or a pipe holds exactly
|
||||
// the bytes somebody put in it, and translating there would mean an escape byte followed by
|
||||
// a bracket could never be read back as what it is. It also keeps the timing problem below
|
||||
// out of every test this machine has, which is the larger gain - a test writes the key
|
||||
// values themselves and no terminal is involved in reading them.
|
||||
//
|
||||
// THE TIMING PROBLEM, which only the terminal case has: pressing Escape and pressing Up
|
||||
// both begin with 0x1B, and the difference is that Up is followed immediately by more. So
|
||||
// after an escape the console waits a moment to see whether anything is. A keyboard
|
||||
// delivers a whole sequence in one go, so the wait is only ever spent when somebody really
|
||||
// did press Escape, and it is far shorter than the gap between two keystrokes.
|
||||
#define CONSOLE_ESCAPE_WAIT_MS 30
|
||||
|
||||
static int consoleTerminalKnown = 0; // isatty has been asked.
|
||||
static int consoleHasTerminal = 0; // and this is what it said.
|
||||
|
||||
static int consoleTranslatingKeys(void) {
|
||||
if (!consoleTerminalKnown) {
|
||||
consoleHasTerminal = isatty(STDIN_FILENO);
|
||||
consoleTerminalKnown = 1;
|
||||
}
|
||||
return consoleHasTerminal;
|
||||
}
|
||||
|
||||
// A byte taken while deciding what an escape was, which turned out not to belong to it.
|
||||
// One is enough: it is only ever the byte immediately after an escape that can be both
|
||||
// part of a sequence and an ordinary character.
|
||||
static int consoleHeldByte = -1;
|
||||
|
||||
// The next byte of a sequence, or below zero if the terminal has stopped talking. The wait
|
||||
// is what tells a sequence from a keypress, so this is the one read in the console that
|
||||
// times out rather than blocking.
|
||||
static int consoleSequenceByte(void) {
|
||||
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
||||
if (poll(&waiting, 1, CONSOLE_ESCAPE_WAIT_MS) <= 0 || (waiting.revents & POLLIN) == 0) {
|
||||
return -1;
|
||||
}
|
||||
return consoleFromInput(1);
|
||||
}
|
||||
|
||||
// An escape has just been read. Answers the key it began, 0x1B if it was the Escape key
|
||||
// itself, or below zero for a sequence this console has no key for.
|
||||
static int consoleKeyFromEscape(void) {
|
||||
const int intro = consoleSequenceByte();
|
||||
if (intro < 0) {
|
||||
// Nothing followed it, so somebody pressed Escape.
|
||||
return 0x1B;
|
||||
}
|
||||
if (intro != '[' && intro != 'O') {
|
||||
// An escape and then something else, close enough together to look like one thing.
|
||||
// It was two: the escape is delivered now and the other byte waits its turn rather
|
||||
// than being dropped, because it is an ordinary character somebody typed.
|
||||
consoleHeldByte = intro;
|
||||
return 0x1B;
|
||||
}
|
||||
// ESC [ 3 ~ carries a number and ESC [ A does not, and both end in a byte that says
|
||||
// which kind it was. So the digits are collected and the ending decides.
|
||||
int number = 0;
|
||||
int final = consoleSequenceByte();
|
||||
while (final >= '0' && final <= '9') {
|
||||
number = number * 10 + (final - '0');
|
||||
final = consoleSequenceByte();
|
||||
}
|
||||
switch (final) {
|
||||
case 'A': return CONSOLE_KEY_UP;
|
||||
case 'B': return CONSOLE_KEY_DOWN;
|
||||
case 'C': return CONSOLE_KEY_RIGHT;
|
||||
case 'D': return CONSOLE_KEY_LEFT;
|
||||
// Terminals disagree about Home and End more than about anything else here, so both
|
||||
// spellings of each are taken: the lettered one, and the numbered one that the
|
||||
// terminals which do not use letters send instead.
|
||||
case 'H': return CONSOLE_KEY_HOME;
|
||||
case 'F': return CONSOLE_KEY_END;
|
||||
case '~':
|
||||
switch (number) {
|
||||
case 1: case 7: return CONSOLE_KEY_HOME;
|
||||
case 4: case 8: return CONSOLE_KEY_END;
|
||||
case 3: return CONSOLE_KEY_DELETE;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
// A sequence this console has no key for. THE WHOLE OF IT GOES rather than the bytes
|
||||
// being handed on, because it is a control sequence and not text: a program given the
|
||||
// tail of one would put a bracket and a letter into whatever it was reading, which is
|
||||
// the exact fault this whole translation exists to end.
|
||||
return -1;
|
||||
}
|
||||
|
||||
// One key from standard input: a byte as it arrived, or one of the console's own key values
|
||||
// where a terminal sent a sequence meaning one.
|
||||
static int consoleKeyFromInput(int mayWait) {
|
||||
for (;;) {
|
||||
int got;
|
||||
if (consoleHeldByte >= 0) {
|
||||
got = consoleHeldByte;
|
||||
consoleHeldByte = -1;
|
||||
} else {
|
||||
got = consoleFromInput(mayWait);
|
||||
}
|
||||
if (got < 0) {
|
||||
return got;
|
||||
}
|
||||
if (got == 0x1B && consoleTranslatingKeys()) {
|
||||
got = consoleKeyFromEscape();
|
||||
}
|
||||
// Nothing to hand over: either that sequence meant nothing here, or it meant a key
|
||||
// and line mode does not deliver keys. Both are the same answer to a caller - there
|
||||
// is still no byte - so a blocking read asks again and a poll says so and leaves.
|
||||
if (got < 0 || (!consoleKeyMode && got >= CONSOLE_KEY_FIRST && got <= CONSOLE_KEY_LAST)) {
|
||||
if (!mayWait) {
|
||||
return CONSOLE_NOTHING_YET;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return got;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t consoleReadByte(void) {
|
||||
// Taking the byte answers whatever the console was asking about, so the line comes
|
||||
// down here as well as when the CPU acknowledges it. Otherwise a program that reads
|
||||
@@ -485,25 +653,15 @@ uint8_t consoleReadByte(void) {
|
||||
consoleWaited();
|
||||
}
|
||||
}
|
||||
unsigned char byte;
|
||||
for (;;) {
|
||||
ssize_t got = read(STDIN_FILENO, &byte, 1);
|
||||
if (got == 1) {
|
||||
return byte;
|
||||
}
|
||||
if (got == 0) {
|
||||
// End of input. Still 0xFF, which is what getchar's EOF became when this was
|
||||
// the only answer available, so nothing written against the old behaviour
|
||||
// changes. The ENDED bit is the new way to know it was not a real byte.
|
||||
consoleEnded = 1;
|
||||
return 0xFF;
|
||||
}
|
||||
if (errno != EINTR) {
|
||||
consoleEnded = 1;
|
||||
return 0xFF;
|
||||
}
|
||||
// Interrupted before anything arrived, so ask again.
|
||||
const int got = consoleKeyFromInput(1);
|
||||
if (got >= 0) {
|
||||
return (uint8_t)got;
|
||||
}
|
||||
// End of input. Still 0xFF, which is what getchar's EOF became when this was the only
|
||||
// answer available, so nothing written against the old behaviour changes. The ENDED bit
|
||||
// is the new way to know it was not a real byte.
|
||||
consoleEnded = 1;
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
// Asking the host whether anything is waiting, and TAKING IT IF THERE IS. The byte goes
|
||||
@@ -551,16 +709,14 @@ static void consoleFetch(void) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
||||
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
|
||||
return;
|
||||
}
|
||||
unsigned char byte;
|
||||
ssize_t got = read(STDIN_FILENO, &byte, 1);
|
||||
if (got == 1) {
|
||||
consolePushback = byte;
|
||||
} else if (got == 0) {
|
||||
const int got = consoleKeyFromInput(0);
|
||||
if (got >= 0) {
|
||||
consolePushback = got;
|
||||
} else if (got == CONSOLE_GONE) {
|
||||
consoleEnded = 1;
|
||||
} else {
|
||||
// Nothing waiting, which is the ordinary answer and not news.
|
||||
return;
|
||||
}
|
||||
// A read that failed for any other reason is left alone: the next attempt asks again,
|
||||
// and an interrupted poll is not news.
|
||||
|
||||
@@ -207,6 +207,54 @@
|
||||
// can ask the console to be, it can also ask the console what it currently is.
|
||||
#define CONSOLE_STATUS_CURSOR 0x10
|
||||
|
||||
// ---- Keys that are not characters ----
|
||||
//
|
||||
// An arrow key is not a letter and there is no byte for it, which is why it has never
|
||||
// reached this machine at all: a window threw it away for want of anywhere to put it, and
|
||||
// a terminal sent an escape sequence that arrived in a command line and made it
|
||||
// unrecognisable.
|
||||
//
|
||||
// So the console names them. These are the values it delivers, one byte each, and they are
|
||||
// the console's own: NOT ASCII, and deliberately above it, so nothing that existed before
|
||||
// them can collide. A program reads one the same way it reads a letter.
|
||||
//
|
||||
// THE CONSOLE NORMALISES, WHICH IS WHAT IT ALREADY DOES. Behind a window it turns the key
|
||||
// somebody pressed into a byte; on a terminal it turns the sequence the terminal sent into
|
||||
// the same byte. That is the same act it has always performed on Return and Backspace, one
|
||||
// layer further along, and it is why a program does not have to know which it is talking to.
|
||||
//
|
||||
// WHAT IT DOES NOT DO is decide what they mean. Where the cursor goes, what a line looks
|
||||
// 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.
|
||||
#define CONSOLE_KEY_UP 0x80
|
||||
#define CONSOLE_KEY_DOWN 0x81
|
||||
#define CONSOLE_KEY_LEFT 0x82
|
||||
#define CONSOLE_KEY_RIGHT 0x83
|
||||
#define CONSOLE_KEY_HOME 0x84
|
||||
#define CONSOLE_KEY_END 0x85
|
||||
// Forward delete, which is the character UNDER the cursor and not the one before it.
|
||||
// Backspace is 0x08 and always has been; these two are different keys that do different
|
||||
// things, and a terminal has always sent different bytes for them.
|
||||
#define CONSOLE_KEY_DELETE 0x86
|
||||
|
||||
// The range, so that anything wanting to know whether a byte is one of these can ask
|
||||
// without naming them all. Room is left above DELETE on purpose: function keys and the
|
||||
// paging keys are the obvious next ones, and adding one should disturb nothing.
|
||||
#define CONSOLE_KEY_FIRST 0x80
|
||||
#define CONSOLE_KEY_LAST 0x8F
|
||||
|
||||
// ---- Only in key mode ----
|
||||
//
|
||||
// LINE MODE DELIVERS CHARACTERS, and these are not characters. A program in line mode is
|
||||
// being handed a line that something else has already finished editing, so a key that means
|
||||
// "move the cursor left" arrived too late to mean anything and putting it in the line would
|
||||
// only corrupt it - which is precisely what an untranslated escape sequence used to do.
|
||||
//
|
||||
// So the console drops them in line mode, wherever it is reading from. That is also what a
|
||||
// real terminal does: canonical mode gives a program backspace and line kill, and has never
|
||||
// given it arrow keys.
|
||||
|
||||
// Puts the terminal back the way it was found. Registered with atexit and called from a
|
||||
// handler for every signal that can end this process and be caught, because a machine that
|
||||
// stops in key mode and does not undo it leaves the shell that started it unusable, which
|
||||
|
||||
@@ -122,6 +122,19 @@ static void drainKeyboard(void) {
|
||||
case KEY_BACKSPACE: keyPush(0x08); break;
|
||||
case KEY_TAB: keyPush('\t'); break;
|
||||
case KEY_ESCAPE: keyPush(0x1B); break;
|
||||
// ---- And the keys that are not characters at all ----
|
||||
//
|
||||
// These used to fall through the default below and vanish, because there was no
|
||||
// byte to turn them into. There is now, and it is the console's rather than
|
||||
// this window's - a terminal reaches the same values by a different road, and a
|
||||
// program is entitled not to know which of the two it is talking to.
|
||||
case KEY_UP: keyPush(CONSOLE_KEY_UP); break;
|
||||
case KEY_DOWN: keyPush(CONSOLE_KEY_DOWN); break;
|
||||
case KEY_LEFT: keyPush(CONSOLE_KEY_LEFT); break;
|
||||
case KEY_RIGHT: keyPush(CONSOLE_KEY_RIGHT); break;
|
||||
case KEY_HOME: keyPush(CONSOLE_KEY_HOME); break;
|
||||
case KEY_END: keyPush(CONSOLE_KEY_END); break;
|
||||
case KEY_DELETE: keyPush(CONSOLE_KEY_DELETE); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,10 +281,36 @@ The control port's two bits are independent, and one write sets both. Everything
|
||||
|
||||
In **line mode**, which is how the machine starts, the terminal holds what is typed until Return and does the echoing and the backspacing on the way. A program reading the data port gets a finished line, one byte at a time. This is what the machine has always done and what a shell wants.
|
||||
|
||||
In **key mode** the terminal stops holding the line. Keys arrive as they are pressed, and nothing echoes them, so a program that wants them seen has to send them back out itself. The editing goes with the echo: there is no backspace, because backspace was the terminal's doing and the terminal is no longer involved. That is not a choice this machine makes, it is what asking for keys means, and a program that wants keys is expected to want it.
|
||||
In **key mode** the terminal stops holding the line. Keys arrive as they are pressed, and nothing echoes them, so a program that wants them seen has to send them back out itself. This is also the only mode in which the keys that are not characters arrive at all - see below. The editing goes with the echo: there is no backspace, because backspace was the terminal's doing and the terminal is no longer involved. That is not a choice this machine makes, it is what asking for keys means, and a program that wants keys is expected to want it.
|
||||
|
||||
A program is expected to put the console back in line mode before it finishes. CosmOS also does it whenever a program returns, because a program that stops early would otherwise hand back a shell with no echo, and a shell has no way to find out that happened.
|
||||
|
||||
### Keys That Are Not Characters:
|
||||
|
||||
An arrow key is not a letter, and for a long time there was no byte for one, so it did not reach this machine at all: a window threw it away for want of anywhere to put it, and a terminal sent an escape sequence which arrived in the middle of whatever was being read and made it unrecognisable.
|
||||
|
||||
The console names them now. Each arrives as one byte, above ASCII so that nothing written before them can collide:
|
||||
|
||||
| Byte | Key |
|
||||
| --- | --- |
|
||||
| 0x80 | Up |
|
||||
| 0x81 | Down |
|
||||
| 0x82 | Left |
|
||||
| 0x83 | Right |
|
||||
| 0x84 | Home |
|
||||
| 0x85 | End |
|
||||
| 0x86 | Delete, meaning the character under the cursor |
|
||||
|
||||
Backspace is 0x08 and always has been. It is a different key from Delete and does a different thing, which is why they are two values and not one.
|
||||
|
||||
0x80 to 0x8F belong to the console, so a program can tell a key from a character by testing that range. The values above 0x86 are not used yet.
|
||||
|
||||
**The console normalises, which is what it has always done.** Behind a window it turns the key somebody pressed into a byte; on a terminal it turns `ESC [ A` and its neighbours into the same byte. That is the same act it performs on Return and Backspace, and it is why a program does not have to know which of the two it is talking to. The translation happens only when there really is a terminal: a file or a pipe holds exactly the bytes somebody put in it, and a program reading one gets those bytes untouched - which is also how a test presses an arrow key.
|
||||
|
||||
**These arrive in key mode only.** Line mode delivers characters, and a program in line mode is being handed a line that something else has already finished editing, so a key meaning "move the cursor left" arrived too late to mean anything. The console drops them there. This is what a terminal does too: it has always given a program in line mode backspace and line kill, and has never given it arrow keys.
|
||||
|
||||
**What a key means is not the console's business.** Where the cursor goes, what the line looks like afterwards and what was typed before are all decisions, and decisions belong to whatever is reading - which on this machine is usually CosmOS, whose shell edits its own line. The console says which key was pressed and stops there, exactly as the disk says what a drive is and says nothing about what should be on it.
|
||||
|
||||
### Reading Without Waiting:
|
||||
|
||||
Reading the data port waits in **both** modes. The status port is how a program declines to wait, and keeping that in one place is deliberate: a read that sometimes blocked and sometimes did not, depending on a mode set somewhere else, would be a program that works until it does not.
|
||||
|
||||
+11
-1
@@ -79,7 +79,7 @@ from `make`, not from here.
|
||||
### 1. Recorded output
|
||||
|
||||
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
|
||||
everything it printed against a file in `Tests/expected`. 186 tests, of which 124 run, 35
|
||||
everything it printed against a file in `Tests/expected`. 188 tests, of which 126 run, 35
|
||||
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
|
||||
given at all.
|
||||
|
||||
@@ -219,6 +219,16 @@ that a prompt arrives before input is read, that a keystroke arrives without Ret
|
||||
the terminal is handed back however the machine dies - SIGHUP, SIGINT, SIGQUIT, SIGABRT,
|
||||
SIGSEGV, SIGTERM - and that suspending and resuming leave it as they found it.
|
||||
|
||||
**And it is the only place an escape sequence is ever read as one.** A terminal sends
|
||||
`ESC [ A` for the Up key and the console turns that into a byte of its own, but only when
|
||||
standard input really is a terminal - everywhere else in this suite the input is a file,
|
||||
which holds exactly the bytes somebody put in it and goes straight past the translation. So
|
||||
three checks here type at a pseudo-terminal: that the sequences arrive as keys, that Escape
|
||||
pressed on its own is still Escape, and that an ordinary character typed straight after an
|
||||
escape is held rather than swallowed with the sequence that never was. The recorded tests
|
||||
cover the other half - what a program does with the key values - by writing them into the
|
||||
input file directly.
|
||||
|
||||
It also asks the one question about *waiting* that nothing else can, since the count is
|
||||
stripped from every recorded result: whether a program on a slow disk slept through the wait
|
||||
or spun on it. Both print the same characters and take the same elapsed time. Only the split
|
||||
|
||||
@@ -13,6 +13,7 @@ Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 559
|
||||
Press.sbx 872
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
@@ -24,7 +25,7 @@ outer.script 376
|
||||
inner.script 44
|
||||
loop.script 35
|
||||
crossed.txt 560
|
||||
18 files, 1 directory
|
||||
19 files, 1 directory
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -8,6 +8,7 @@ Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 559
|
||||
Press.sbx 872
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
@@ -18,7 +19,7 @@ nonl.script 38
|
||||
outer.script 376
|
||||
inner.script 44
|
||||
loop.script 35
|
||||
17 files, 1 directory
|
||||
18 files, 1 directory
|
||||
> > other.txt 28
|
||||
notes <dir>
|
||||
2things <dir>
|
||||
|
||||
@@ -12,6 +12,7 @@ Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 559
|
||||
Press.sbx 872
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
@@ -22,7 +23,7 @@ nonl.script 38
|
||||
outer.script 376
|
||||
inner.script 44
|
||||
loop.script 35
|
||||
17 files, 1 directory
|
||||
18 files, 1 directory
|
||||
> halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
CosmOS
|
||||
> a line, then keys. q stops.
|
||||
61 62 63 64
|
||||
keys:
|
||||
80 up
|
||||
82 left
|
||||
86 delete
|
||||
5A Z
|
||||
done
|
||||
finished
|
||||
> > halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -0,0 +1,13 @@
|
||||
CosmOS
|
||||
> a line, then keys. q stops.
|
||||
61 62 63 64
|
||||
keys:
|
||||
80 up
|
||||
82 left
|
||||
86 delete
|
||||
5A Z
|
||||
done
|
||||
finished
|
||||
> > halted
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -8,6 +8,7 @@ Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 559
|
||||
Press.sbx 872
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
@@ -18,7 +19,7 @@ nonl.script 38
|
||||
outer.script 376
|
||||
inner.script 44
|
||||
loop.script 35
|
||||
17 files, 1 directory
|
||||
18 files, 1 directory
|
||||
> load what?
|
||||
> no such file
|
||||
> not a program
|
||||
|
||||
@@ -7,6 +7,7 @@ Keys.sbx 664
|
||||
Say.sbx 156
|
||||
Break.sbx 149
|
||||
Grid.sbx 559
|
||||
Press.sbx 872
|
||||
notes.txt 21
|
||||
Apps <dir>
|
||||
hi.script 121
|
||||
@@ -17,7 +18,7 @@ nonl.script 38
|
||||
outer.script 376
|
||||
inner.script 44
|
||||
loop.script 35
|
||||
17 files, 1 directory
|
||||
18 files, 1 directory
|
||||
> loaded, starting at 4000
|
||||
> it says: the disk took its time
|
||||
finished
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Press
|
||||
ab€cd
|
||||
€‚†Zq
|
||||
exit
|
||||
@@ -112,6 +112,12 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Grid.asm" -o "$WORK/Grid.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Grid.sbx" >/dev/null
|
||||
# Press.sbx says what the console handed it, which is how the keys that are not characters
|
||||
# are checked at all. It reads a line first and then keys, because line mode drops them and
|
||||
# key mode delivers them and both halves of that rule want testing.
|
||||
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
|
||||
"$ROOT/Programs/CosmOS/Apps/Press.asm" -o "$WORK/Press.sbx" >/dev/null
|
||||
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Press.sbx" >/dev/null
|
||||
printf 'this is not a program' > notes.txt
|
||||
"$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null
|
||||
|
||||
|
||||
@@ -294,6 +294,17 @@ cosmosSnake | CosmOS/Source/cosmos.asm | run | cosmosSna
|
||||
# console's slot at FE00 is zero again, and CosmOS's own disk handler further along is
|
||||
# untouched by a program having installed over the top of it.
|
||||
cosmosKeys | CosmOS/Source/cosmos.asm | run | cosmosKeys.in | - | disks/cosmos.img
|
||||
# The keys that are not characters, both halves of the rule in one run: a line read in line
|
||||
# mode with an arrow key in the middle of it, which arrives with the arrow dropped because
|
||||
# line mode delivers characters, and then the same keys in key mode where they are what the
|
||||
# program asked for. Fed as STANDARD INPUT, so the console is reading bytes that are exactly
|
||||
# what is in the file - no terminal is involved and no escape sequence is translated, which
|
||||
# is what makes an arrow key testable at all.
|
||||
cosmosPress | CosmOS/Source/cosmos.asm | run | cosmosPress.in | - | disks/cosmos.img
|
||||
# The same program and the same keys, fed as a KEYBOARD instead. That is the other front
|
||||
# end - the window's path through the gatherer - and it has its own copy of the line mode
|
||||
# rule, so it needs its own test. The two recordings should agree, which is the point.
|
||||
cosmosPressKeys | CosmOS/Source/cosmos.asm | run | - | - | disks/cosmos.img | cosmosPress.in
|
||||
# The shell taking things off a disk and calling them something else, which is the last of
|
||||
# CosmOS's original four verbs to be built and the first time anything has changed a disk
|
||||
# from the shell. Its own image, because it changes what is on it: a fixture named with a
|
||||
|
||||
@@ -57,6 +57,66 @@ ASM
|
||||
|
||||
"$ROOT/Assembler" "$BUILD/keywait.asm" -o "$BUILD/keywait.bin" >/dev/null 2>&1 || {
|
||||
echo "Could not assemble the terminal test programs."; exit 1; }
|
||||
|
||||
# ---- And a program that says which key it was given ----
|
||||
#
|
||||
# A terminal sends ESC [ A for the Up key and the console turns that into one byte of its
|
||||
# own. NOTHING BUT A REAL TERMINAL CAN TEST THAT: the translation deliberately happens only
|
||||
# when standard input is one, so every recorded test in the suite - which feed a file - goes
|
||||
# straight past it and would pass against a console that translated nothing at all.
|
||||
#
|
||||
# It prints a letter per key rather than the byte, because the bytes are not printable and
|
||||
# a recorded escape byte is no easier to read than the sequence it came from.
|
||||
cat > "$BUILD/escape.asm" <<'ASM'
|
||||
#Program
|
||||
start:
|
||||
INIA 0x01
|
||||
OUTA 0x02 ; Key mode, or the terminal holds everything until Return.
|
||||
CALL show
|
||||
CALL show
|
||||
CALL show
|
||||
RSTA
|
||||
OUTA 0x02
|
||||
HALT
|
||||
|
||||
; One key, named. XOR leaves the answer in Q and A alone, so one read stands for the ladder.
|
||||
show:
|
||||
INA 0x00
|
||||
INIB 0x80
|
||||
XOR
|
||||
BRQ showUp
|
||||
INIB 0x82
|
||||
XOR
|
||||
BRQ showLeft
|
||||
INIB 0x86
|
||||
XOR
|
||||
BRQ showDelete
|
||||
INIB 0x1B
|
||||
XOR
|
||||
BRQ showEscape
|
||||
INIA 0x3F ; '?', for anything else.
|
||||
OUTA 0x00
|
||||
RET
|
||||
showUp:
|
||||
INIA 0x55 ; 'U'
|
||||
OUTA 0x00
|
||||
RET
|
||||
showLeft:
|
||||
INIA 0x4C ; 'L'
|
||||
OUTA 0x00
|
||||
RET
|
||||
showDelete:
|
||||
INIA 0x44 ; 'D'
|
||||
OUTA 0x00
|
||||
RET
|
||||
showEscape:
|
||||
INIA 0x45 ; 'E'
|
||||
OUTA 0x00
|
||||
RET
|
||||
ASM
|
||||
|
||||
"$ROOT/Assembler" "$BUILD/escape.asm" -o "$BUILD/escape.bin" >/dev/null 2>&1 || {
|
||||
echo "Could not assemble the terminal test programs."; exit 1; }
|
||||
"$ROOT/Assembler" "$BUILD/prompt.asm" -o "$BUILD/prompt.bin" >/dev/null 2>&1 || {
|
||||
echo "Could not assemble the terminal test programs."; exit 1; }
|
||||
|
||||
@@ -185,6 +245,74 @@ except (ProcessLookupError, ChildProcessError):
|
||||
os.close(fd)
|
||||
|
||||
|
||||
# ---- The keys that are not characters, as a terminal actually sends them ----
|
||||
#
|
||||
# A window hands the console the key somebody pressed. A terminal hands it ESC [ A and
|
||||
# expects the far end to know what that means, and the console is the far end. Every other
|
||||
# test in this suite feeds a file, where the translation deliberately does not happen, so
|
||||
# this is the only place the sequences are ever read as sequences.
|
||||
def typedAt(typing, seconds=4):
|
||||
"""Runs the naming program under a pseudo-terminal, types at it, and gives back the
|
||||
letters it printed."""
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.execv(emulator, [emulator, "--fast", os.path.join(build, "escape.bin")])
|
||||
# The machine has to have asked for key mode before anything is typed at it. Until it
|
||||
# does, the terminal is still holding what arrives until Return and the sequences would
|
||||
# sit in it unread.
|
||||
time.sleep(0.5)
|
||||
for chunk, pause in typing:
|
||||
os.write(fd, chunk)
|
||||
time.sleep(pause)
|
||||
seen = b""
|
||||
end = time.time() + seconds
|
||||
while time.time() < end:
|
||||
ready, _, _ = select.select([fd], [], [], 0.2)
|
||||
if ready:
|
||||
try:
|
||||
data = os.read(fd, 1024)
|
||||
except OSError:
|
||||
break
|
||||
if not data:
|
||||
break
|
||||
seen += data
|
||||
elif os.waitpid(pid, os.WNOHANG)[0]:
|
||||
break
|
||||
try:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
os.waitpid(pid, 0)
|
||||
except (ProcessLookupError, ChildProcessError):
|
||||
pass
|
||||
os.close(fd)
|
||||
# Everything before the emulator says how it stopped. The letters have no newline after
|
||||
# them, so they arrive stuck to whatever the machine printed on its way out.
|
||||
return seen.split(b"Execution")[0].strip()
|
||||
|
||||
|
||||
seen = typedAt([(b"\x1b[A", 0.2), (b"\x1b[D", 0.2), (b"\x1b[3~", 0.2)])
|
||||
report(seen == b"ULD", "a terminal's escape sequences arrive as keys",
|
||||
"" if seen == b"ULD" else "expected ULD, saw %r" % seen)
|
||||
|
||||
# ---- And pressing Escape is still pressing Escape ----
|
||||
#
|
||||
# Which is the whole difficulty: Up and Escape both begin with 0x1B and the only thing
|
||||
# telling them apart is whether anything follows immediately. A console that waited for the
|
||||
# rest of a sequence that was never coming would swallow the key; one that did not wait at
|
||||
# all would never see a sequence.
|
||||
seen = typedAt([(b"\x1b", 0.3), (b"\x1b", 0.3), (b"\x1b", 0.3)])
|
||||
report(seen == b"EEE", "Escape on its own is still Escape",
|
||||
"" if seen == b"EEE" else "expected EEE, saw %r" % seen)
|
||||
|
||||
# ---- And what follows an escape that was not a sequence is not eaten ----
|
||||
#
|
||||
# Escape and then an ordinary character, close enough together to look like one thing. It is
|
||||
# two, and the second one is somebody's keystroke: the console holds it and hands it over
|
||||
# next rather than throwing it away with the sequence that never was.
|
||||
seen = typedAt([(b"\x1ba", 0.3), (b"\x1b", 0.3)])
|
||||
report(seen == b"E?E", "a character after an escape is not swallowed",
|
||||
"" if seen == b"E?E" else "expected E?E, saw %r" % seen)
|
||||
|
||||
|
||||
# ---- The terminal is handed back however the machine dies ----
|
||||
#
|
||||
# atexit covers stopping on purpose and nothing else: it does not run when a process is
|
||||
|
||||
Reference in New Issue
Block a user