diff --git a/Programs/CosmOS/Apps/Press.asm b/Programs/CosmOS/Apps/Press.asm new file mode 100644 index 0000000..f71bf88 --- /dev/null +++ b/Programs/CosmOS/Apps/Press.asm @@ -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 diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 0395233..5c3a09a 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -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. diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index 976f8f7..7012030 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -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 diff --git a/Source/Emulator/voyager.c b/Source/Emulator/voyager.c index 60510c1..4237fbf 100644 --- a/Source/Emulator/voyager.c +++ b/Source/Emulator/voyager.c @@ -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; } } diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 28b44fb..b74dc37 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -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. diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index f0eb2b6..3350fa1 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -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 diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 4cfdf3f..1b98d67 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -13,6 +13,7 @@ Keys.sbx 664 Say.sbx 156 Break.sbx 149 Grid.sbx 559 +Press.sbx 872 notes.txt 21 Apps 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] diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index 8bf2a61..7b7452a 100644 --- a/Tests/expected/cosmosDrives.out +++ b/Tests/expected/cosmosDrives.out @@ -8,6 +8,7 @@ Keys.sbx 664 Say.sbx 156 Break.sbx 149 Grid.sbx 559 +Press.sbx 872 notes.txt 21 Apps 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 2things diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 69ffa41..fcbbcb8 100644 --- a/Tests/expected/cosmosGrid.out +++ b/Tests/expected/cosmosGrid.out @@ -12,6 +12,7 @@ Keys.sbx 664 Say.sbx 156 Break.sbx 149 Grid.sbx 559 +Press.sbx 872 notes.txt 21 Apps 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] diff --git a/Tests/expected/cosmosPress.out b/Tests/expected/cosmosPress.out new file mode 100644 index 0000000..b42f3b5 --- /dev/null +++ b/Tests/expected/cosmosPress.out @@ -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] diff --git a/Tests/expected/cosmosPressKeys.out b/Tests/expected/cosmosPressKeys.out new file mode 100644 index 0000000..b42f3b5 --- /dev/null +++ b/Tests/expected/cosmosPressKeys.out @@ -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] diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 20b9a3c..b4afbb4 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -8,6 +8,7 @@ Keys.sbx 664 Say.sbx 156 Break.sbx 149 Grid.sbx 559 +Press.sbx 872 notes.txt 21 Apps 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 diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index d55f474..a118e46 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -7,6 +7,7 @@ Keys.sbx 664 Say.sbx 156 Break.sbx 149 Grid.sbx 559 +Press.sbx 872 notes.txt 21 Apps 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 diff --git a/Tests/input/cosmosPress.in b/Tests/input/cosmosPress.in new file mode 100644 index 0000000..f673609 --- /dev/null +++ b/Tests/input/cosmosPress.in @@ -0,0 +1,4 @@ +Press +ab€cd +€‚†Zq +exit diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 566620c..0fa183a 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -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 diff --git a/Tests/manifest b/Tests/manifest index 9b46a5d..cbfef25 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -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 diff --git a/Tests/terminal.sh b/Tests/terminal.sh index b91b967..643d285 100755 --- a/Tests/terminal.sh +++ b/Tests/terminal.sh @@ -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