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:
co-authored by
Claude Opus 5
parent
019c93a587
commit
8631a78229
+35
-1
@@ -610,14 +610,48 @@ static int consoleKeyFromEscape(void) {
|
|||||||
|
|
||||||
// One key from standard input: a byte as it arrived, or one of the console's own key values
|
// 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.
|
// where a terminal sent a sequence meaning one.
|
||||||
|
// ---- The terminal's erase key is not the one this machine knows ----
|
||||||
|
//
|
||||||
|
// SplitBit's Backspace is 0x08. That is what Voyager's keyboard sends and what CosmOS's line
|
||||||
|
// editor looks for. A POSIX terminal sends whatever ITS erase character is, and on most of
|
||||||
|
// them that is 0x7F.
|
||||||
|
//
|
||||||
|
// It went unnoticed for as long as the terminal was doing the editing: in canonical mode the
|
||||||
|
// tty eats its own erase character and hands over a finished line. Key mode turns ICANON off,
|
||||||
|
// which is the point of it, and from then on the byte arrives raw and means nothing to the
|
||||||
|
// editor. So Backspace worked in Voyager and stopped working in the console-only emulator on
|
||||||
|
// the day the shell started editing the line itself.
|
||||||
|
//
|
||||||
|
// VERASE IS ASKED RATHER THAN 0x7F ASSUMED, because the terminal is what knows: it was saved
|
||||||
|
// on the way into raw mode, some terminals really are set to 0x08, and a person who has moved
|
||||||
|
// their erase key somewhere else has said where it is.
|
||||||
|
//
|
||||||
|
// ONLY WHEN STANDARD INPUT IS A TERMINAL. A file or a pipe holding 0x7F holds a byte that
|
||||||
|
// somebody wrote, not a key somebody pressed, and rewriting it would corrupt input that has
|
||||||
|
// nothing to do with terminals. consoleTerminalSaved is exactly that question: it is only
|
||||||
|
// ever set after an isatty succeeded.
|
||||||
|
static int consoleEraseNormalized(int got) {
|
||||||
|
if (!consoleTerminalSaved || got < 0) {
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
const cc_t erase = consoleSavedTerminal.c_cc[VERASE];
|
||||||
|
// A terminal with no erase key at all, which says so this way, has nothing to translate.
|
||||||
|
if (erase == _POSIX_VDISABLE) {
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
return got == (int)erase ? 0x08 : got;
|
||||||
|
}
|
||||||
|
|
||||||
static int consoleKeyFromInput(int mayWait) {
|
static int consoleKeyFromInput(int mayWait) {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int got;
|
int got;
|
||||||
if (consoleHeldByte >= 0) {
|
if (consoleHeldByte >= 0) {
|
||||||
|
// Already normalized on the way in, and doing it twice would be wrong the day
|
||||||
|
// 0x08 is somebody's erase character: it is its own answer.
|
||||||
got = consoleHeldByte;
|
got = consoleHeldByte;
|
||||||
consoleHeldByte = -1;
|
consoleHeldByte = -1;
|
||||||
} else {
|
} else {
|
||||||
got = consoleFromInput(mayWait);
|
got = consoleEraseNormalized(consoleFromInput(mayWait));
|
||||||
}
|
}
|
||||||
if (got < 0) {
|
if (got < 0) {
|
||||||
return got;
|
return got;
|
||||||
|
|||||||
@@ -307,6 +307,8 @@ Backspace is 0x08 and always has been. It is a different key from Delete and doe
|
|||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
**Backspace is part of that, and the terminal says what it is.** A terminal has an erase character of its own, and while it is doing the editing it consumes that character itself and hands over a finished line. Key mode stops it editing, so from then on the byte arrives as it was sent - usually 0x7F, sometimes 0x08, and whatever the person at the keyboard has configured. The console asks the terminal which one it is rather than assuming, and delivers 0x08. Forward Delete stays 0x86: they are two keys and they stay two keys.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
**But asking the status port in line mode does not throw one away.** A key line mode will not deliver is not the same as a key that is gone, because the mode can change: a program that looks at the status port and then asks for key mode - which is exactly what a system does before it reads a line - would otherwise find that the first key it was reaching for had been swallowed by the looking. So the console holds it and delivers it as soon as something is willing to take it. Reading the data port in line mode does discard it, and must: that read is the delivery, and a key held there would be met again forever.
|
**But asking the status port in line mode does not throw one away.** A key line mode will not deliver is not the same as a key that is gone, because the mode can change: a program that looks at the status port and then asks for key mode - which is exactly what a system does before it reads a line - would otherwise find that the first key it was reaching for had been swallowed by the looking. So the console holds it and delivers it as soon as something is willing to take it. Reading the data port in line mode does discard it, and must: that read is the delivery, and a key held there would be met again forever.
|
||||||
|
|||||||
+50
-2
@@ -91,6 +91,9 @@ show:
|
|||||||
INIB 0x86
|
INIB 0x86
|
||||||
XOR
|
XOR
|
||||||
BRQ showDelete
|
BRQ showDelete
|
||||||
|
INIB 0x08
|
||||||
|
XOR
|
||||||
|
BRQ showBack
|
||||||
INIB 0x1B
|
INIB 0x1B
|
||||||
XOR
|
XOR
|
||||||
BRQ showEscape
|
BRQ showEscape
|
||||||
@@ -109,6 +112,10 @@ showDelete:
|
|||||||
INIA 0x44 ; 'D'
|
INIA 0x44 ; 'D'
|
||||||
OUTA 0x00
|
OUTA 0x00
|
||||||
RET
|
RET
|
||||||
|
showBack:
|
||||||
|
INIA 0x42 ; 'B'
|
||||||
|
OUTA 0x00
|
||||||
|
RET
|
||||||
showEscape:
|
showEscape:
|
||||||
INIA 0x45 ; 'E'
|
INIA 0x45 ; 'E'
|
||||||
OUTA 0x00
|
OUTA 0x00
|
||||||
@@ -134,6 +141,7 @@ python3 - "$ROOT" "$BUILD" <<'PY'
|
|||||||
import os
|
import os
|
||||||
import pty
|
import pty
|
||||||
import select
|
import select
|
||||||
|
import termios
|
||||||
import signal
|
import signal
|
||||||
import re
|
import re
|
||||||
import subprocess
|
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
|
# 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
|
# 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.
|
# 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
|
"""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()
|
pid, fd = pty.fork()
|
||||||
if pid == 0:
|
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")])
|
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
|
# 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
|
# 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",
|
report(seen == b"ULD", "a terminal's escape sequences arrive as keys",
|
||||||
"" if seen == b"ULD" else "expected ULD, saw %r" % seen)
|
"" 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 ----
|
# ---- And pressing Escape is still pressing Escape ----
|
||||||
#
|
#
|
||||||
# Which is the whole difficulty: Up and Escape both begin with 0x1B and the only thing
|
# Which is the whole difficulty: Up and Escape both begin with 0x1B and the only thing
|
||||||
|
|||||||
Reference in New Issue
Block a user