Kaynağa Gözat

editor: the command keys from a text file (TP3.KEY)

The other half of what TINST configures - without patching the binary.  The
editor's single-key command map is now data:

  - Keys (new): TP3.KEY, one NAME=value per line, values decoded exactly like
    TP3.TRM's (\e, \r, \n, \t, \\, ^X) and required to be ONE byte.  Unknown
    names ignored; missing/invalid file leaves the defaults, which are the keys
    the editor used before there was a key file (so behaviour does not move).
  - Editor.MainLoop dispatches on keys.quit / keys.charLeft / ... instead of
    C_Q / C_S / ..., so the whole single-key table - cursor, insert/delete,
    block prefix, literal, repeat, auto-tab - is configurable.  `Del' and `^J'
    remain aliases of DELETELEFT/NEWLINE and are not configurable in this slice.
  - shell/TP3.KEY is the committed default map.
  - KeysTest + key_default/key_custom fixtures assert the loader (including an
    unknown key being ignored) byte for byte; tests/term_config.sh now runs
    four cases: term_ansi, term_vt52, key_default, key_custom.

NOT in this slice: the Ctrl-K sub-commands (KCommand) and the Esc/ANSI cursor
sequences (HandleEsc) are still hard-coded; Keys is the pattern to extend to
them.  TERMINAL.md says so.

run_all.sh: ALL PASS - compile matrix 71/71, terminal config 4/4, execution
60/60 under qemu and Exec86, and all three UI paths (the editor behaves
identically with the default map).
Eric Streit 2 gün önce
ebeveyn
işleme
35d5012135

+ 1 - 0
.gitignore

@@ -25,3 +25,4 @@ tmp/
 
 # terminal-config test harness (tests/term_config.sh)
 shell/tests/termcfgtest
+shell/tests/keycfgtest

+ 4 - 3
SUMMARY.md

@@ -2074,9 +2074,10 @@ change. Items 1-4 below are independent of both.
 
 5. Terminal configuration is a **text file** now, not a patched binary:
    `shell/TP3.TRM` is read by `tpshell` at startup, and `tp3cfg` writes it
-   (ANSI/VT100/VT52 so far). See `TERMINAL.md`. `tests/term_config.sh` asserts
-   the loader/decoder byte for byte. Key remapping (the rest of TINST) is a
-   separate step.
+   (ANSI/VT100/VT52 so far). The editor's **single-key command map** is
+   `TP3.KEY`, read by `Editor.Run` (defaults = the old hard-coded keys); the
+   Ctrl-K sub-commands and Esc/ANSI sequences are the remaining slice. See
+   `TERMINAL.md`; `tests/term_config.sh` asserts both loaders byte for byte.
 
 The 4 KiB code window is no longer a documented limitation but an enforced
 one (`v-TP3-CODEWINDOW`): `Ebyte` reports TP3's error 98 when the code

+ 53 - 5
TERMINAL.md

@@ -61,6 +61,54 @@ A missing `TP3.TRM` means the built-in ANSI defaults (the sequences the shell
 had before there was a config). A malformed line leaves the defaults (and
 anything read before it) in place; it never silently half-configure the screen.
 
+## The editor's keys: `TP3.KEY`
+
+The same design extends to the editor's command keys. `Editor.Run` reads
+`TP3.KEY` (or keeps the built-in defaults) and the main command dispatch uses
+the loaded map instead of compiled-in constants.
+
+```
+; TP3.KEY
+QUIT=^Q
+CANCEL=^U
+PREFIX=^K
+LITERAL=^P
+WORDLEFT=^A
+CHARLEFT=^S
+CHARRIGHT=^D
+WORDRIGHT=^F
+LINEUP=^E
+LINEDOWN=^X
+SCROLLUP=^W
+SCROLLDOWN=^Z
+PAGEUP=^R
+PAGEDOWN=^C
+INSERTTOGGLE=^V
+DELETECHAR=^G
+DELETELEFT=^H
+DELETEWORD=^T
+INSERTBREAK=^N
+DELETELINE=^Y
+REPEAT=^L
+AUTOTAB=^I
+NEWLINE=^M
+```
+
+Values decode exactly like `TP3.TRM` values and must be **one byte**; a value
+that is not one byte makes the load report not-ok (the defaults stand).
+Unknown names are ignored. `Del` and `^J` remain aliases of `DELETELEFT` and
+`NEWLINE` and are not configurable in this slice.
+
+`tests/term_config.sh` asserts both the escape loader and the key loader byte
+for byte (`term_ansi`, `term_vt52`, `key_default`, `key_custom`).
+
+### Not in this slice
+
+TINST also remaps the **Ctrl-K sub-commands** and the **Esc/ANSI cursor
+sequences** (`HandleEsc`). Those are two further tables in `Editor.mod`; the
+same `Keys` pattern extends to them, as a separate step. The single-key command
+map above is the bulk of them and is what this slice covers.
+
 ## `tp3cfg`
 
 The configurator - TINST's role, without the patch. It lists the terminals it
@@ -95,9 +143,9 @@ That is deliberately not done by transcription: the sequences are what the
 original writes, and the diff is the evidence. Until it is done, the table
 above is the curated subset the shell needs.
 
-## Not in this step: key remapping
+## Not in this step: the Ctrl-K and Esc sub-tables
 
-TINST also remaps the **editing keys** (the cursor/insert/delete/block command
-table visible in `TINST.MSG`). The shell's editor hard-codes its keys today.
-The same design extends to it - a `KEYS` section in the config read by
-`Editor.mod` - and is a separate step.
+The editor's **Ctrl-K sub-commands** and its **Esc/ANSI cursor sequences**
+(`KCommand`, `HandleEsc`) are still hard-coded.  The single-key command map is
+configurable now; those two tables are the remaining part of TINST's editor
+configuration, and `Keys` is the pattern to extend.

+ 31 - 26
shell/Editor.mod

@@ -12,6 +12,8 @@ FROM Posix IMPORT
 
 FROM SYSTEM IMPORT ADR, BYTE ;
 
+IMPORT Keys ;
+
 CONST
    Broad    = 80 ;                (* columns on the screen *)
    TopRow   = 1 ;
@@ -34,6 +36,7 @@ VAR
    drive      : CHAR ;
    fname      : ARRAY [0..255] OF CHAR ;
    modified   : BOOLEAN ;
+   keys       : Keys.Map ;        (* the key map, from TP3.KEY *)
 
    curLine, curCol : CARDINAL ;   (* 0-based cursor *)
    topLine    : CARDINAL ;        (* first visible line *)
@@ -1508,59 +1511,57 @@ BEGIN
       GetCh (ch) ;
       IF ch = Esc THEN
          HandleEsc
-      ELSIF ch = C_Q THEN
+      ELSIF ch = keys.quit THEN
          QCommand
-      ELSIF ch = C_K THEN
+      ELSIF ch = keys.prefix THEN
          KCommand
-      ELSIF ch = C_P THEN
+      ELSIF ch = keys.literal THEN
          GetCh (ch) ;
-         IF NOT ((ch = Esc) OR (ch = C_U)) THEN
+         IF NOT ((ch = Esc) OR (ch = keys.charLeft)) THEN
             BufInsert (CursorOff (), ch) ;
             INC (curCol) ;
             restoreOn := FALSE ;
             modified := TRUE
          END
-      ELSIF ch = C_A THEN
+      ELSIF ch = keys.wordLeft THEN
          WordLeft
-      ELSIF ch = C_S THEN
+      ELSIF ch = keys.charLeft THEN
          CharLeft
-      ELSIF ch = C_D THEN
+      ELSIF ch = keys.charRight THEN
          CharRight
-      ELSIF ch = C_F THEN
+      ELSIF ch = keys.wordRight THEN
          WordRight
-      ELSIF ch = C_E THEN
+      ELSIF ch = keys.lineUp THEN
          LineUp
-      ELSIF ch = C_X THEN
+      ELSIF ch = keys.lineDown THEN
          LineDown
-      ELSIF ch = C_W THEN
+      ELSIF ch = keys.scrollUp THEN
          ScrollUp
-      ELSIF ch = C_Z THEN
+      ELSIF ch = keys.scrollDown THEN
          ScrollDown
-      ELSIF ch = C_R THEN
+      ELSIF ch = keys.pageUp THEN
          PageUp
-      ELSIF ch = C_C THEN
+      ELSIF ch = keys.pageDown THEN
          PageDown
-      ELSIF ch = C_V THEN
+      ELSIF ch = keys.insertToggle THEN
          insertMode := NOT insertMode
-      ELSIF ch = C_G THEN
+      ELSIF ch = keys.deleteChar THEN
          DeleteChar
-      ELSIF ch = Del THEN
-         DeleteLeft
-      ELSIF ch = C_H THEN
+      ELSIF (ch = Del) OR (ch = keys.deleteLeft) THEN
          DeleteLeft
-      ELSIF ch = C_T THEN
+      ELSIF ch = keys.deleteWord THEN
          DeleteWord
-      ELSIF ch = C_N THEN
+      ELSIF ch = keys.insertBreak THEN
          InsertBreak
-      ELSIF ch = C_Y THEN
+      ELSIF ch = keys.deleteLine THEN
          DeleteLine
-      ELSIF ch = C_L THEN
+      ELSIF ch = keys.repeatLast THEN
          RepeatLast
-      ELSIF ch = C_U THEN
+      ELSIF ch = keys.cancel THEN
          Beep
-      ELSIF ch = C_I THEN
+      ELSIF ch = keys.autoTab THEN
          AutoTab
-      ELSIF (ch = C_M) OR (ch = C_J) THEN
+      ELSIF (ch = keys.newLine) OR (ch = C_J) THEN
          NewLine
       ELSIF (ch >= " ") AND (ch <= "~") THEN
          TypeChar (ch)
@@ -1574,7 +1575,11 @@ END MainLoop ;
 PROCEDURE Run (drv : CHAR ; VAR fileName : ARRAY OF CHAR ;
                VAR changed : BOOLEAN) ;
 VAR i : CARDINAL ;
+    kok : BOOLEAN ;
 BEGIN
+   (* The key map: TP3.KEY beside the shell, or the built-in defaults.  Read
+      once per Run (the file can change between edits without restarting). *)
+   Keys.Load ("TP3.KEY", keys, kok) ;
    drive := drv ;
    i := 0 ;
    LOOP

+ 52 - 0
shell/Keys.def

@@ -0,0 +1,52 @@
+DEFINITION MODULE Keys ;
+
+(* The editor's key map, loaded from a TEXT file.  This is the other half of
+   what TINST configures: TINST patches TURBO.COM's editor command table; this
+   port reads TP3.KEY.
+
+   FILE FORMAT - one NAME=value per line, ';' comments, values decoded by
+   TermCfg.ParseValue (\e, \r, \n, \t, \\, ^X).  Each value must be exactly ONE
+   byte.  Names are case-insensitive; unknown names are ignored.
+
+      QUIT=^Q
+      PREFIX=^K
+      WORDLEFT=^A
+      ...
+
+   A missing file leaves the defaults, which are exactly the keys the editor
+   used before there was a key file. *)
+
+TYPE
+   Map = RECORD
+      quit        : CHAR ;
+      cancel      : CHAR ;       (* ^U *)
+      prefix      : CHAR ;       (* Ctrl-K *)
+      literal     : CHAR ;       (* Ctrl-P: insert the next key literally *)
+      wordLeft    : CHAR ;
+      charLeft    : CHAR ;
+      charRight   : CHAR ;
+      wordRight   : CHAR ;
+      lineUp      : CHAR ;
+      lineDown    : CHAR ;
+      scrollUp    : CHAR ;
+      scrollDown  : CHAR ;
+      pageUp      : CHAR ;
+      pageDown    : CHAR ;
+      insertToggle: CHAR ;
+      deleteChar  : CHAR ;
+      deleteLeft  : CHAR ;
+      deleteWord  : CHAR ;
+      insertBreak : CHAR ;
+      deleteLine  : CHAR ;
+      repeatLast  : CHAR ;
+      autoTab     : CHAR ;
+      newLine     : CHAR ;
+   END ;
+
+PROCEDURE Default (VAR m : Map) ;
+
+PROCEDURE Load (path : ARRAY OF CHAR ; VAR m : Map ; VAR ok : BOOLEAN) ;
+(* Default(m), then every NAME=value in path.  ok is FALSE if the file could
+   not be opened or a value is not exactly one byte. *)
+
+END Keys.

+ 179 - 0
shell/Keys.mod

@@ -0,0 +1,179 @@
+IMPLEMENTATION MODULE Keys ;
+
+FROM Posix IMPORT open, read, close ;
+FROM SYSTEM IMPORT ADR, BYTE ;
+FROM TermCfg IMPORT ParseValue ;
+
+CONST
+   MaxFile = 4095 ;
+   CR = 13 ;
+   LF = 10 ;
+
+PROCEDURE Up (ch : CHAR) : CHAR ;
+BEGIN
+   IF (ch >= 'a') AND (ch <= 'z') THEN
+      RETURN CHR (ORD (ch) - 32)
+   END ;
+   RETURN ch
+END Up ;
+
+PROCEDURE Eq (a, b : ARRAY OF CHAR) : BOOLEAN ;
+VAR i : CARDINAL ;
+BEGIN
+   i := 0 ;
+   LOOP
+      IF (i > HIGH (a)) OR (i > HIGH (b)) THEN
+         RETURN FALSE
+      END ;
+      IF Up (a [i]) # Up (b [i]) THEN
+         RETURN FALSE
+      END ;
+      IF b [i] = 0C THEN
+         RETURN TRUE
+      END ;
+      INC (i)
+   END
+END Eq ;
+
+PROCEDURE One (val : ARRAY OF CHAR ; VAR ch : CHAR ; VAR good : BOOLEAN) ;
+(* exactly one decoded byte *)
+VAR tmp : ARRAY [0..63] OF CHAR ;
+BEGIN
+   ParseValue (val, tmp) ;
+   IF (tmp [0] # 0C) AND (tmp [1] = 0C) THEN
+      ch := tmp [0] ;
+      good := TRUE
+   ELSE
+      good := FALSE
+   END
+END One ;
+
+PROCEDURE Apply (VAR m : Map ; name, val : ARRAY OF CHAR ; VAR bad : BOOLEAN) ;
+VAR good : BOOLEAN ;
+BEGIN
+   good := TRUE ;
+   IF Eq (name, "QUIT") THEN One (val, m.quit, good)
+   ELSIF Eq (name, "CANCEL") THEN One (val, m.cancel, good)
+   ELSIF Eq (name, "PREFIX") THEN One (val, m.prefix, good)
+   ELSIF Eq (name, "LITERAL") THEN One (val, m.literal, good)
+   ELSIF Eq (name, "WORDLEFT") THEN One (val, m.wordLeft, good)
+   ELSIF Eq (name, "CHARLEFT") THEN One (val, m.charLeft, good)
+   ELSIF Eq (name, "CHARRIGHT") THEN One (val, m.charRight, good)
+   ELSIF Eq (name, "WORDRIGHT") THEN One (val, m.wordRight, good)
+   ELSIF Eq (name, "LINEUP") THEN One (val, m.lineUp, good)
+   ELSIF Eq (name, "LINEDOWN") THEN One (val, m.lineDown, good)
+   ELSIF Eq (name, "SCROLLUP") THEN One (val, m.scrollUp, good)
+   ELSIF Eq (name, "SCROLLDOWN") THEN One (val, m.scrollDown, good)
+   ELSIF Eq (name, "PAGEUP") THEN One (val, m.pageUp, good)
+   ELSIF Eq (name, "PAGEDOWN") THEN One (val, m.pageDown, good)
+   ELSIF Eq (name, "INSERTTOGGLE") THEN One (val, m.insertToggle, good)
+   ELSIF Eq (name, "DELETECHAR") THEN One (val, m.deleteChar, good)
+   ELSIF Eq (name, "DELETELEFT") THEN One (val, m.deleteLeft, good)
+   ELSIF Eq (name, "DELETEWORD") THEN One (val, m.deleteWord, good)
+   ELSIF Eq (name, "INSERTBREAK") THEN One (val, m.insertBreak, good)
+   ELSIF Eq (name, "DELETELINE") THEN One (val, m.deleteLine, good)
+   ELSIF Eq (name, "REPEAT") THEN One (val, m.repeatLast, good)
+   ELSIF Eq (name, "AUTOTAB") THEN One (val, m.autoTab, good)
+   ELSIF Eq (name, "NEWLINE") THEN One (val, m.newLine, good)
+   END ;
+   IF NOT good THEN
+      bad := TRUE
+   END
+END Apply ;
+
+PROCEDURE Default (VAR m : Map) ;
+BEGIN
+   m.quit         := CHR (17) ;   (* ^Q *)
+   m.cancel       := CHR (21) ;   (* ^U *)
+   m.prefix       := CHR (11) ;   (* ^K *)
+   m.literal      := CHR (16) ;   (* ^P *)
+   m.wordLeft     := CHR (1) ;    (* ^A *)
+   m.charLeft     := CHR (19) ;   (* ^S *)
+   m.charRight    := CHR (4) ;    (* ^D *)
+   m.wordRight    := CHR (6) ;    (* ^F *)
+   m.lineUp       := CHR (5) ;    (* ^E *)
+   m.lineDown     := CHR (24) ;   (* ^X *)
+   m.scrollUp     := CHR (23) ;   (* ^W *)
+   m.scrollDown   := CHR (26) ;   (* ^Z *)
+   m.pageUp       := CHR (18) ;   (* ^R *)
+   m.pageDown     := CHR (3) ;    (* ^C *)
+   m.insertToggle := CHR (22) ;   (* ^V *)
+   m.deleteChar   := CHR (7) ;    (* ^G *)
+   m.deleteLeft   := CHR (8) ;    (* ^H *)
+   m.deleteWord   := CHR (20) ;   (* ^T *)
+   m.insertBreak  := CHR (14) ;   (* ^N *)
+   m.deleteLine   := CHR (25) ;   (* ^Y *)
+   m.repeatLast   := CHR (12) ;   (* ^L *)
+   m.autoTab      := CHR (9) ;    (* ^I *)
+   m.newLine      := CHR (13)     (* ^M *)
+END Default ;
+
+PROCEDURE Load (path : ARRAY OF CHAR ; VAR m : Map ; VAR ok : BOOLEAN) ;
+VAR fd : INTEGER ;
+    k : LONGINT ;
+    b : BYTE ;
+    buf : ARRAY [0..MaxFile] OF CHAR ;
+    n, i, ls, le, eq, j : CARDINAL ;
+    name : ARRAY [0..31] OF CHAR ;
+    val : ARRAY [0..63] OF CHAR ;
+    bad : BOOLEAN ;
+BEGIN
+   Default (m) ;
+   ok := FALSE ;
+   fd := open (ADR (path), 0, 0) ;
+   IF fd < 0 THEN
+      RETURN
+   END ;
+   n := 0 ;
+   LOOP
+      k := read (fd, ADR (b), 1) ;
+      IF k # 1 THEN EXIT END ;
+      IF b = 26 THEN EXIT END ;
+      IF n <= HIGH (buf) THEN
+         buf [n] := CHR (ORD (b)) ;
+         INC (n)
+      END
+   END ;
+   k := close (fd) ;
+   bad := FALSE ;
+   i := 0 ;
+   WHILE i < n DO
+      ls := i ;
+      WHILE (i < n) AND (buf [i] # CHR (CR)) AND (buf [i] # CHR (LF)) DO
+         INC (i)
+      END ;
+      le := i ;
+      WHILE (i < n) AND ((buf [i] = CHR (CR)) OR (buf [i] = CHR (LF))) DO
+         INC (i)
+      END ;
+      WHILE (ls < le) AND ((buf [ls] = ' ') OR (buf [ls] = CHR (9))) DO
+         INC (ls)
+      END ;
+      WHILE (le > ls) AND ((buf [le - 1] = ' ') OR (buf [le - 1] = CHR (9))) DO
+         DEC (le)
+      END ;
+      IF (ls < le) AND (buf [ls] # ';') THEN
+         eq := ls ;
+         WHILE (eq < le) AND (buf [eq] # '=') DO INC (eq) END ;
+         IF eq = le THEN
+            bad := TRUE
+         ELSE
+            j := 0 ;
+            WHILE (ls < eq) AND (j <= HIGH (name)) DO
+               name [j] := buf [ls] ; INC (j) ; INC (ls)
+            END ;
+            name [j] := 0C ;
+            INC (eq) ;
+            j := 0 ;
+            WHILE (eq < le) AND (j <= HIGH (val)) DO
+               val [j] := buf [eq] ; INC (j) ; INC (eq)
+            END ;
+            val [j] := 0C ;
+            Apply (m, name, val, bad)
+         END
+      END
+   END ;
+   ok := NOT bad
+END Load ;
+
+END Keys.

+ 6 - 3
shell/Makefile

@@ -6,8 +6,8 @@ FLAGS = -fiso
 # pass-3 rollup on a compiler-size program ("too many errors in pass 3"), so
 # the import closure is generated first and the link then consumes it.  Phase 1
 # exiting 1 is the expected rollup, hence the `|| true`.
-MODS  = Shell Term TermCfg Posix TextBuf Editor Compiler Runtime Linker Exec86
-LINK  = Shell.mod Term.o TermCfg.o Posix.o TextBuf.o Editor.o Compiler.o Runtime.o Linker.o Exec86.o
+MODS  = Shell Term TermCfg Keys Posix TextBuf Editor Compiler Runtime Linker Exec86
+LINK  = Shell.mod Term.o TermCfg.o Keys.o Posix.o TextBuf.o Editor.o Compiler.o Runtime.o Linker.o Exec86.o
 
 all: tpshell tp3cfg
 
@@ -33,10 +33,13 @@ Term.o: Term.mod Term.def TermCfg.def Posix.def
 TermCfg.o: TermCfg.mod TermCfg.def Posix.def
 	$(GM2) $(FLAGS) -c TermCfg.mod
 
+Keys.o: Keys.mod Keys.def TermCfg.def Posix.def
+	$(GM2) $(FLAGS) -c Keys.mod
+
 TextBuf.o: TextBuf.mod TextBuf.def
 	$(GM2) $(FLAGS) -c TextBuf.mod
 
-Editor.o: Editor.mod Editor.def Term.def TextBuf.def Posix.def
+Editor.o: Editor.mod Editor.def Term.def TextBuf.def Posix.def Keys.def
 	$(GM2) $(FLAGS) -c Editor.mod
 
 Posix.o: Posix.c Posix.def

+ 30 - 0
shell/TP3.KEY

@@ -0,0 +1,30 @@
+; TP3 key map -- read by tpshell's editor at startup (Keys.Load).
+;
+; The editor's single-key commands.  Values are decoded like TP3.TRM's:
+; \e, \r, \n, \t, \\, ^X.  Each must be exactly one byte.
+;
+; Not here (yet): the Ctrl-K sub-commands and the Esc/ANSI cursor sequences.
+; Those are the next slice; see TERMINAL.md.
+QUIT=^Q
+CANCEL=^U
+PREFIX=^K
+LITERAL=^P
+WORDLEFT=^A
+CHARLEFT=^S
+CHARRIGHT=^D
+WORDRIGHT=^F
+LINEUP=^E
+LINEDOWN=^X
+SCROLLUP=^W
+SCROLLDOWN=^Z
+PAGEUP=^R
+PAGEDOWN=^C
+INSERTTOGGLE=^V
+DELETECHAR=^G
+DELETELEFT=^H
+DELETEWORD=^T
+INSERTBREAK=^N
+DELETELINE=^Y
+REPEAT=^L
+AUTOTAB=^I
+NEWLINE=^M

+ 78 - 0
shell/tests/KeysTest.mod

@@ -0,0 +1,78 @@
+MODULE KeysTest ;
+
+(* Read a key map and print it as hex, so the loader/decoder is asserted byte
+   for byte by tests/term_config.sh (which copies the fixture to the fixed
+   path). *)
+
+FROM Keys IMPORT Map, Load ;
+FROM Posix IMPORT write ;
+FROM SYSTEM IMPORT ADR ;
+
+VAR
+   m : Map ;
+   ok : BOOLEAN ;
+   n : LONGINT ;
+   ch : CHAR ;
+
+PROCEDURE Out (s : ARRAY OF CHAR) ;
+VAR i : CARDINAL ;
+BEGIN
+   i := 0 ;
+   WHILE (i <= HIGH (s)) AND (s [i] # 0C) DO
+      ch := s [i] ;
+      n := write (1, ADR (ch), 1) ;
+      INC (i)
+   END
+END Out ;
+
+PROCEDURE NL ;
+BEGIN
+   ch := CHR (10) ;
+   n := write (1, ADR (ch), 1)
+END NL ;
+
+PROCEDURE HexDig (d : CARDINAL) ;
+BEGIN
+   IF d < 10 THEN
+      ch := CHR (ORD ('0') + d)
+   ELSE
+      ch := CHR (ORD ('A') + d - 10)
+   END ;
+   n := write (1, ADR (ch), 1)
+END HexDig ;
+
+PROCEDURE K (label : ARRAY OF CHAR ; c : CHAR) ;
+BEGIN
+   Out (label) ; Out ("=") ;
+   HexDig (ORD (c) DIV 16) ; HexDig (ORD (c) MOD 16) ;
+   NL
+END K ;
+
+BEGIN
+   Load ("../tmp/key_test.key", m, ok) ;
+   IF ok THEN Out ("ok=1") ELSE Out ("ok=0") END ;
+   NL ;
+   K ("QUIT", m.quit) ;
+   K ("CANCEL", m.cancel) ;
+   K ("PREFIX", m.prefix) ;
+   K ("LITERAL", m.literal) ;
+   K ("WORDLEFT", m.wordLeft) ;
+   K ("CHARLEFT", m.charLeft) ;
+   K ("CHARRIGHT", m.charRight) ;
+   K ("WORDRIGHT", m.wordRight) ;
+   K ("LINEUP", m.lineUp) ;
+   K ("LINEDOWN", m.lineDown) ;
+   K ("SCROLLUP", m.scrollUp) ;
+   K ("SCROLLDOWN", m.scrollDown) ;
+   K ("PAGEUP", m.pageUp) ;
+   K ("PAGEDOWN", m.pageDown) ;
+   K ("INSERTTOGGLE", m.insertToggle) ;
+   K ("DELETECHAR", m.deleteChar) ;
+   K ("DELETELEFT", m.deleteLeft) ;
+   K ("DELETEWORD", m.deleteWord) ;
+   K ("INSERTBREAK", m.insertBreak) ;
+   K ("DELETELINE", m.deleteLine) ;
+   K ("REPEAT", m.repeatLast) ;
+   K ("AUTOTAB", m.autoTab) ;
+   K ("NEWLINE", m.newLine)
+END KeysTest.

+ 24 - 0
shell/tests/fixtures/key_custom.hex

@@ -0,0 +1,24 @@
+ok=1
+QUIT=18
+CANCEL=14
+PREFIX=0B
+LITERAL=10
+WORDLEFT=02
+CHARLEFT=13
+CHARRIGHT=04
+WORDRIGHT=06
+LINEUP=05
+LINEDOWN=18
+SCROLLUP=17
+SCROLLDOWN=1A
+PAGEUP=12
+PAGEDOWN=03
+INSERTTOGGLE=16
+DELETECHAR=07
+DELETELEFT=08
+DELETEWORD=14
+INSERTBREAK=0E
+DELETELINE=19
+REPEAT=0C
+AUTOTAB=09
+NEWLINE=0D

+ 5 - 0
shell/tests/fixtures/key_custom.key

@@ -0,0 +1,5 @@
+; a remap: QUIT and WORDLEFT and CANCEL change; NOTAKEY is ignored
+QUIT=^X
+WORDLEFT=^B
+CANCEL=^T
+NOTAKEY=^Z

+ 24 - 0
shell/tests/fixtures/key_default.hex

@@ -0,0 +1,24 @@
+ok=1
+QUIT=11
+CANCEL=15
+PREFIX=0B
+LITERAL=10
+WORDLEFT=01
+CHARLEFT=13
+CHARRIGHT=04
+WORDRIGHT=06
+LINEUP=05
+LINEDOWN=18
+SCROLLUP=17
+SCROLLDOWN=1A
+PAGEUP=12
+PAGEDOWN=03
+INSERTTOGGLE=16
+DELETECHAR=07
+DELETELEFT=08
+DELETEWORD=14
+INSERTBREAK=0E
+DELETELINE=19
+REPEAT=0C
+AUTOTAB=09
+NEWLINE=0D

+ 30 - 0
shell/tests/fixtures/key_default.key

@@ -0,0 +1,30 @@
+; TP3 key map -- read by tpshell's editor at startup (Keys.Load).
+;
+; The editor's single-key commands.  Values are decoded like TP3.TRM's:
+; \e, \r, \n, \t, \\, ^X.  Each must be exactly one byte.
+;
+; Not here (yet): the Ctrl-K sub-commands and the Esc/ANSI cursor sequences.
+; Those are the next slice; see TERMINAL.md.
+QUIT=^Q
+CANCEL=^U
+PREFIX=^K
+LITERAL=^P
+WORDLEFT=^A
+CHARLEFT=^S
+CHARRIGHT=^D
+WORDRIGHT=^F
+LINEUP=^E
+LINEDOWN=^X
+SCROLLUP=^W
+SCROLLDOWN=^Z
+PAGEUP=^R
+PAGEDOWN=^C
+INSERTTOGGLE=^V
+DELETECHAR=^G
+DELETELEFT=^H
+DELETEWORD=^T
+INSERTBREAK=^N
+DELETELINE=^Y
+REPEAT=^L
+AUTOTAB=^I
+NEWLINE=^M

+ 25 - 0
shell/tests/term_config.sh

@@ -52,6 +52,31 @@ for name in term_ansi term_vt52; do
     fi
 done
 
+# ---- the editor's key map ------------------------------------------------
+if [ ! -f Keys.o ] || [ Keys.mod -nt Keys.o ]; then
+    $GM2 -fiso -c Keys.mod >../tmp/termcfg_c_keys 2>&1 || {
+        echo "COMPILE_FAIL Keys"; grep -m5 "error:" ../tmp/termcfg_c_keys; exit 1; }
+fi
+if [ ! -x tests/keycfgtest ] || [ tests/KeysTest.mod -nt tests/keycfgtest ] \
+   || [ Keys.o -nt tests/keycfgtest ] || [ TermCfg.o -nt tests/keycfgtest ]; then
+    rm -f tests/keycfgtest
+    $GM2 -fiso -o tests/keycfgtest tests/KeysTest.mod Keys.o TermCfg.o Posix.o \
+        >../tmp/keycfg_link 2>&1 || {
+        echo "LINK_FAIL KeysTest"; grep -m5 -E "error:|undefined" ../tmp/keycfg_link; exit 1; }
+fi
+for name in key_default key_custom; do
+    cp "tests/fixtures/$name.key" ../tmp/key_test.key || exit 1
+    ./tests/keycfgtest > "../tmp/$name.got" 2>&1
+    if diff -u "tests/fixtures/$name.hex" "../tmp/$name.got" > "../tmp/$name.diff"; then
+        printf '  %-20s PASS\n' "$name"
+        pass=$((pass + 1))
+    else
+        printf '  %-20s FAIL\n' "$name"
+        head -20 "../tmp/$name.diff"
+        fail=$((fail + 1))
+    fi
+done
+
 echo "----------------------------------------------------------------"
 echo "terminal config: $pass passed, $fail failed (of $((pass + fail)))"
 [ "$fail" -eq 0 ] || exit 1