Skip to content

Commit a76cafe

Browse files
committed
feat: Complete legacy keyboard protocol for extra keys
1 parent 561bb68 commit a76cafe

2 files changed

Lines changed: 163 additions & 7 deletions

File tree

mock/term_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,73 @@ func TestKbdEventLegacy(t *testing.T) {
433433
if result != want {
434434
t.Errorf("key responses failed: %q != %q", result, want)
435435
}
436+
437+
// SS3 based F-keys
438+
clear(buf)
439+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF1, Down: true}) // SS3 P
440+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF1, Down: false}) // none
441+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF1, Mod: vt.ModShift, Down: true}) // CSI 1 ; 2 P
442+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF2, Mod: vt.ModCtrl, Down: true}) // CSI 1 ; 5 Q
443+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF3, Mod: vt.ModAlt | vt.ModShift | vt.ModCtrl, Down: true}) // ESC CSI 1 ; 6 R
444+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF4, Mod: vt.ModAlt | vt.ModCtrl, Down: true}) // ESC CSI 1 ; 5 S
445+
want = "\x1bOP"
446+
want += "\x1b[1;2P"
447+
want += "\x1b[1;5Q"
448+
want += "\x1b\x1b[1;6R"
449+
want += "\x1b\x1b[1;5S"
450+
n, err = trm.Read(buf)
451+
if err != nil {
452+
t.Errorf("failed read: %v", err)
453+
}
454+
result = string(buf[:n])
455+
if result != want {
456+
t.Errorf("key responses failed: %q != %q", result, want)
457+
}
458+
459+
// CSI based F-keys
460+
buf = make([]byte, 256)
461+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF5, Down: true}) // CSI 15 ~
462+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF5, Down: false}) // none
463+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF6, Mod: vt.ModShift, Down: true}) // CSI 17 ; 2 ~
464+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF7, Mod: vt.ModCtrl, Down: true}) // CSI 18 ; 5 ~
465+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF8, Mod: vt.ModAlt | vt.ModShift | vt.ModCtrl, Down: true}) // ESC CSI 19 ; 6 ~
466+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcF9, Mod: vt.ModAlt | vt.ModCtrl, Down: true}) // ESC CSI 20 ; 5 ~
467+
want = "\x1b[15~"
468+
want += "\x1b[17;2~"
469+
want += "\x1b[18;5~"
470+
want += "\x1b\x1b[19;6~"
471+
want += "\x1b\x1b[20;5~"
472+
n, err = trm.Read(buf)
473+
if err != nil {
474+
t.Errorf("failed read: %v", err)
475+
}
476+
result = string(buf[:n])
477+
if result != want {
478+
t.Errorf("key responses failed: %q != %q", result, want)
479+
}
480+
481+
// Misc other keys
482+
clear(buf)
483+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcReturn, Down: true}) // \r
484+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcTab, Down: true}) // \t
485+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcTab, Mod: vt.ModShift, Down: true}) // CSI Z
486+
trm.KeyEvent(vt.KbdEvent{Code: 'm', Mod: vt.ModCtrl, Down: true}) // \r
487+
trm.KeyEvent(vt.KbdEvent{Code: 'l', Mod: vt.ModCtrl, Down: true}) // \x0c
488+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcBackspace, Down: true}) // \x7f
489+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcBackspace, Mod: vt.ModCtrl, Down: true}) // \x08
490+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcBackspace, Mod: vt.ModShift | vt.ModCtrl, Down: true}) // \x08
491+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcSpace, Mod: vt.ModCtrl, Down: true}) // \x00
492+
trm.KeyEvent(vt.KbdEvent{Code: vt.KcSpace, Down: true}) // ' '
493+
494+
want = "\r\t\x1b[Z\r\x0c\x7f\x08\x08\x00 "
495+
n, err = trm.Read(buf)
496+
if err != nil {
497+
t.Errorf("failed read: %v", err)
498+
}
499+
result = string(buf[:n])
500+
if result != want {
501+
t.Errorf("key responses failed: %q != %q", result, want)
502+
}
436503
}
437504

438505
// TestSgrAttr tests a variety of combinations of Sgr settings.

vt/emulate.go

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -913,25 +913,114 @@ func (em *emulator) KeyEvent(ev KbdEvent) {
913913
em.keyLegacy(ev)
914914
}
915915

916+
var legacyKeys = map[KeyCode]struct {
917+
K string // unmodified key
918+
P string // unmodified in keypad mode (smkx)
919+
S string // with shift (if empty use regular modifier)
920+
C string // with control (if empty use regular modifier)
921+
CS string // with ctrl-shift
922+
}{
923+
KcF1: {K: "\x1bOP"}, // SS3 P
924+
KcF2: {K: "\x1bOQ"}, // SS3 Q
925+
KcF3: {K: "\x1bOR"}, // SS3 R
926+
KcF4: {K: "\x1bOS"}, // SS3 S
927+
KcF5: {K: "\x1b[15~"},
928+
KcF6: {K: "\x1b[17~"},
929+
KcF7: {K: "\x1b[18~"},
930+
KcF8: {K: "\x1b[19~"},
931+
KcF9: {K: "\x1b[20~"},
932+
KcF10: {K: "\x1b[21~"},
933+
KcF11: {K: "\x1b[23~"},
934+
KcF12: {K: "\x1b[24~"},
935+
KcUp: {K: "\x1b[A", P: "\x1bOA"},
936+
KcDown: {K: "\x1b[B", P: "\x1bOB"},
937+
KcRight: {K: "\x1b[C", P: "\x1bOC"},
938+
KcLeft: {K: "\x1b[D", P: "\x1bOD"},
939+
KcHome: {K: "\x1b[H", P: "\x1bOH"},
940+
KcEnd: {K: "\x1b[F", P: "\x1bOF"},
941+
KcPgUp: {K: "\x1b[5~"},
942+
KcPgDn: {K: "\x1b[6~"},
943+
KcDel: {K: "\x1b[3~"},
944+
KcIns: {K: "\x1b[2~"},
945+
KcMenu: {K: "\x1b[29~"}, // also F15
946+
KcTab: {K: "\t", S: "\x1b[Z", CS: "\x1b[Z"},
947+
KcBackspace: {K: "\x7f", S: "\x7f", C: "\x08", CS: "\x08"},
948+
KcDelete: {K: "\x08", S: "\x08", C: "\x7f", CS: "\x7f"},
949+
KcSpace: {K: " ", S: " ", C: "\x00", CS: "\x00"},
950+
KcReturn: {K: "\r", S: "\r", CS: "\r"},
951+
KcEsc: {K: "\x1b", S: "\x1b", C: "\x1b"},
952+
}
953+
916954
func (em *emulator) keyLegacy(ev KbdEvent) {
917955
if !ev.Down { // legacy protocol does not support key release
918956
return
919957
}
920-
if ev.Code > KcSpace && ev.Code < 0x7F && (ev.Mod == ModNone || ev.Mod == ModShift) {
921-
em.SendRaw([]byte{byte(ev.Code)})
958+
if ev.Mod&(ModHyper|ModMeta) != 0 { // legacy protocol does not support these
922959
return
923960
}
924-
switch ev.Code {
925-
case KcSpace, KcEsc, KcReturn, KcTab, KcBackspace, KcDelete:
926-
if ev.Mod == ModNone {
927-
em.SendRaw([]byte{byte(ev.Code)})
928-
return
961+
962+
if v, ok := legacyKeys[ev.Code]; ok {
963+
str := ""
964+
match := false
965+
switch ev.Mod & (ModShift | ModCtrl) {
966+
case ModNone:
967+
// TODO: key for keypad mode
968+
str = v.K
969+
match = true
970+
case ModShift:
971+
if str = v.S; str != "" {
972+
match = true
973+
}
974+
case ModCtrl:
975+
if str = v.C; str != "" {
976+
match = true
977+
}
978+
case ModCtrl | ModShift:
979+
if str = v.CS; str != "" {
980+
match = true
981+
}
982+
}
983+
if !match {
984+
// No specific modifiers present, lets add them. There are two cases,
985+
// one for SS3 based keys and another for CSI based keys. SS3 based
986+
// keys are converted to CSI - 1 ; mod ; final
987+
// Note: legacy encoding does not use modifiers for alt or super - alt will be
988+
// determined by sending an esc prefix.
989+
mod := 0
990+
if ev.Mod&ModShift != 0 {
991+
mod |= 1
992+
}
993+
if ev.Mod&ModCtrl != 0 {
994+
mod |= 4
995+
}
996+
if strings.HasPrefix(v.K, "\x1bO") {
997+
str = fmt.Sprintf("\x1b[1;%d%c", mod+1, v.K[len(v.K)-1])
998+
} else {
999+
str = fmt.Sprintf("%s;%d%c", v.K[:len(v.K)-1], mod+1, v.K[len(v.K)-1])
1000+
}
1001+
}
1002+
if ev.Mod&ModAlt != 0 {
1003+
em.SendRaw([]byte{'\x1b'}) // alt sends leading esc
9291004
}
1005+
em.SendRaw([]byte(str))
1006+
return
9301007
}
9311008

9321009
// fallback control key handling
9331010
if ev.Code >= 'a' && ev.Code <= 'z' && ev.Mod == ModCtrl {
1011+
if ev.Mod&ModAlt != 0 {
1012+
em.SendRaw([]byte{'\x1b'})
1013+
}
9341014
em.SendRaw([]byte{byte(ev.Code) - 'a' + 1})
1015+
return
1016+
}
1017+
1018+
if ev.Code > KcSpace && ev.Code < 0x7F && ev.Mod&ModCtrl == ModNone {
1019+
if ev.Mod&ModAlt != 0 {
1020+
em.SendRaw([]byte{'\x1b'})
1021+
}
1022+
em.SendRaw([]byte{byte(ev.Code)})
1023+
return
9351024
}
9361025
}
9371026

0 commit comments

Comments
 (0)