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
@@ -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