Backspace, whatever the terminal calls it

CosmOS's line editor looks for 0x08, which is what Voyager's keyboard
sends. A POSIX terminal sends its own erase character instead, and on
most of them that is 0x7F.

It stayed hidden while the terminal was doing the editing: canonical
mode consumes the erase character itself and hands over a finished
line. Key mode turns ICANON off, which is the point of it, so from the
day the shell started editing its own line - 7360374 - Backspace worked
in Voyager and did nothing at all in the console-only emulator. The
Programming Manual already claimed the console normalised Backspace on a
terminal the way it normalises the arrow keys; the code did not.

The erase character is read from the terminal's own VERASE rather than
assumed to be 0x7F, because some terminals are set to 0x08 and a person
who has moved their erase key has said where it is. Forward Delete is
untouched and stays 0x86: two keys, two values.

Only when standard input really is a terminal. A file or a pipe holding
0x7F holds a byte somebody wrote, not a key somebody pressed.

Three checks in terminal.sh under a pseudo-terminal whose VERASE is set
on the slave side: erase at 0x7F arrives as Backspace, erase at 0x08
still does, and a 0x7F read from a file is left alone. The last one is
what fails on the obvious wrong fix of translating 0x7F unconditionally,
which was verified with break.sh along with the fix's removal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-05 11:56:17 -04:00
co-authored by Claude Opus 5
parent 019c93a587
commit 8631a78229
3 changed files with 87 additions and 3 deletions
+50 -2
View File
@@ -91,6 +91,9 @@ show:
INIB 0x86
XOR
BRQ showDelete
INIB 0x08
XOR
BRQ showBack
INIB 0x1B
XOR
BRQ showEscape
@@ -109,6 +112,10 @@ showDelete:
INIA 0x44 ; 'D'
OUTA 0x00
RET
showBack:
INIA 0x42 ; 'B'
OUTA 0x00
RET
showEscape:
INIA 0x45 ; 'E'
OUTA 0x00
@@ -134,6 +141,7 @@ python3 - "$ROOT" "$BUILD" <<'PY'
import os
import pty
import select
import termios
import signal
import re
import subprocess
@@ -251,11 +259,17 @@ os.close(fd)
# 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):
def typedAt(typing, seconds=4, erase=None):
"""Runs the naming program under a pseudo-terminal, types at it, and gives back the
letters it printed."""
letters it printed. erase sets the terminal's own erase character first, which is set on
the SLAVE side in the child: that is the terminal the machine will find and save, and
configuring the master would be configuring something else."""
pid, fd = pty.fork()
if pid == 0:
if erase is not None:
attributes = termios.tcgetattr(0)
attributes[6][termios.VERASE] = erase
termios.tcsetattr(0, termios.TCSANOW, attributes)
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
@@ -293,6 +307,40 @@ 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)
# ---- Backspace, whatever this terminal calls it ----
#
# SplitBit's Backspace is 0x08 and CosmOS's line editor looks for that. A POSIX terminal
# sends its own erase character, which is usually 0x7F, and while the terminal was doing the
# editing nobody could tell: canonical mode ate the byte and handed over a finished line. Key
# mode turns that off, so from the day the shell started editing its own line, Backspace
# worked in Voyager and did nothing at all in the console-only emulator.
#
# Both settings are tried because the fix reads VERASE rather than assuming 0x7F, and a
# terminal set to 0x08 has to keep working - it is already sending the right byte.
seen = typedAt([(b"\x7f", 0.3), (b"\x1b[3~", 0.2)], erase=0x7F)
report(seen == b"BD", "a terminal's erase character arrives as Backspace",
"" if seen == b"BD" else "expected BD, saw %r" % seen)
seen = typedAt([(b"\x08", 0.3), (b"\x1b[3~", 0.2)], erase=0x08)
report(seen == b"BD", "and a terminal that already sends 0x08 still works",
"" if seen == b"BD" else "expected BD, saw %r" % seen)
# ---- And nothing is rewritten when there is no terminal ----
#
# The translation is a thing done TO A TERMINAL, because a terminal is the only thing that
# has an erase character. A file or a pipe holding 0x7F holds a byte somebody wrote, and a
# console that rewrote it would corrupt input that has nothing to do with keys.
fed = os.path.join(build, "erase.keys")
with open(fed, "wb") as f:
f.write(b"\x7f\x7f\x7f")
ran = subprocess.run([emulator, "--fast", os.path.join(build, "escape.bin")],
stdin=open(fed, "rb"), capture_output=True)
said = ran.stdout.split(b"Execution")[0].strip()
report(said == b"???", "a 0x7F from a file is left alone",
"" if said == b"???" else "expected ??? for three unnamed bytes, saw %r" % said)
# ---- And pressing Escape is still pressing Escape ----
#
# Which is the whole difficulty: Up and Escape both begin with 0x1B and the only thing