260125.17 · 055d2d2ffa1d1d2a785d2ae98f651ccd6df52d07
feat(emulator): Implement keypad handling correctly
reading declarations
@@ -209,6 +209,7 @@ type emulator struct { buffering uint // reference count - number of (re-entrant) buffering calls autoWrap bool // next character will wrap (auto margin, deferred until char emitted) sevenOnly bool // only allow 7-bit escapes (needed for KOI8, ShiftJIS, etc.)+ appKeyPad bool // use application key pad keys? name string // name of this emulator (used for extended attributes) vers string // version string of this emulator (used for extended attributes) savedPos Coord // saved via DECSC@@ -352,6 +353,10 @@ func (em *emulator) inbEsc(b byte) { em.inb = em.inbStr case '_': // application program command (APC) em.inb = em.inbStr+ case '=':+ em.appKeyPad = true+ case '>':+ em.appKeyPad = false case 'D': // down one line (IND) em.processIndex() case 'E': // next line (NEL)@@ -2114,6 +2119,7 @@ var legacyKeys = map[Key]struct { KeyBackspace: {K: "\x7f", S: "\x7f", C: "\x08", CS: "\x08"}, KeySpace: {K: " ", S: " ", C: "\x00", CS: "\x00"}, KeyEnter: {K: "\r", S: "\r", CS: "\r"}, // NB: consider using kitty encoding here+ KeyPadEnter: {K: "\r", S: "\r", CS: "\r"}, // NB: consider using kitty encoding here KeyEsc: {K: "\x1b", S: "\x1b", C: "\x1b"}, } @@ -2132,6 +2138,31 @@ var legacyControls = map[Key]string{ KeyRBrace: "\x1d", } +// legacyPadKeys are keys that are on the keypad, when not in numeric keypad mode.+// Note that num lock overrides this.+var legacyPadKeys = map[Key]struct {+ app string+ num string+}{+ KeyPadEnter: {"\x1bOM", "\r"},+ KeyPadMul: {"\x1bOj", "*"},+ KeyPadAdd: {"\x1bOk", "+"},+ KeyPadSub: {"\x1bOm", "-"},+ KeyPadDiv: {"\x1bOo", "/"},+ KeyPadDec: {"\x1b[3~", "."}, // Del+ KeyPad0: {"\x1b[2~", "0"}, // Ins+ KeyPad1: {"\x1bOF", "1"}, // End+ KeyPad2: {"\x1b[B", "2"}, // Down+ KeyPad3: {"\x1b[6~", "3"}, // PgDn+ KeyPad4: {"\x1b[D", "4"}, // Left+ KeyPad5: {"\x1b[E", "5"}, // Clear/Begin+ KeyPad6: {"\x1b[C", "6"}, // Right+ KeyPad7: {"\x1bOH", "7"}, // Home+ KeyPad8: {"\x1b[A", "8"}, // Up+ KeyPad9: {"\x1b[5~", "9"}, // PgUp+ KeyPadEqual: {"\x1bOX", "="},+}+ // keyLegacy handles a keyboard event when in legacy vt220 style mode. func (em *emulator) keyLegacy(ev KeyEvent) { if !ev.Down { // legacy protocol does not support key release@@ -2151,6 +2182,20 @@ func (em *emulator) keyLegacy(ev KeyEvent) { } } + // keypad sequences+ if v, ok := legacyPadKeys[ev.Key]; ok {+ if ev.Mod&ModNumLock == 0 {+ if em.appKeyPad {+ em.SendRaw([]byte(v.app))+ } else {+ em.SendRaw([]byte(v.num))+ }+ return+ } else {+ ev.Utf = v.num+ }+ }+ // For control keys (e.g. control-J) we never emit a rune directly -- but we might later // add after decoding the key accordingly. if ev.Utf != "" && (ev.Mod == ModCtrl || ev.Utf[0] < ' ') {
@@ -245,6 +245,7 @@ const DeadRune = 0x101000 // registering an instance of this. type Layout struct { // Name is the name of the keyboard layout.+ // We prefer to use the same names that Microsoft uses for keyboard layouts. Name string // Base is a base keyboard layout, so that we can simplify by only@@ -393,7 +394,7 @@ var KeysPadOps = map[Key]rune{ // KeyboardANSI is the base PC keyboard used in ANSI (US) systems. var KeyboardANSI = &Layout{- Name: "US English",+ Name: "US", Base: nil,
@@ -311,5 +311,109 @@ func TestDeadKeys(t *testing.T) { CheckRead(t, term, cases[i].want+" ") }) }+}++func TestUsCapsLock(t *testing.T) {++ term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4})+ defer MustClose(t, term)+ term.SetLayout(vt.GetLayout("US International"))+ MustStart(t, term)++ cases := []struct {+ name string+ keys []vt.Key+ caps string+ base string+ }{+ {"A", []vt.Key{vt.KeyA}, "A", "a"},+ {"B", []vt.Key{vt.KeyB}, "B", "b"},+ {"1", []vt.Key{vt.Key1}, "1", "!"},+ {"Z", []vt.Key{vt.KeyZ}, "Z", "z"},+ {"plus", []vt.Key{vt.KeyEqual}, "=", "+"},+ }+ for i := range cases {+ t.Run(cases[i].name, func(t *testing.T) {++ // set caps lock+ term.KeyTap(vt.KeyCapsLock)++ term.KeyTap(cases[i].keys...)+ term.KeyTap(vt.KeySpace)+ CheckRead(t, term, cases[i].caps+" ")++ term.KeyTap(append([]vt.Key{vt.KeyLShift}, cases[i].keys...)...)+ term.KeyTap(vt.KeySpace)+ CheckRead(t, term, cases[i].base+" ")++ // release caps lock+ term.KeyTap(vt.KeyCapsLock)+ })+ }+}++func TestUsNumLock(t *testing.T) { + term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4})+ defer MustClose(t, term)+ term.SetLayout(vt.GetLayout("US International"))+ MustStart(t, term)++ cases := []struct {+ name string+ keys []vt.Key+ lock string+ app string+ num string+ }{+ {"1", []vt.Key{vt.KeyPad1}, "1", "\x1bOF", "1"},+ {"2", []vt.Key{vt.KeyPad2}, "2", "\x1b[B", "2"},+ {"3", []vt.Key{vt.KeyPad3}, "3", "\x1b[6~", "3"},+ {"4", []vt.Key{vt.KeyPad4}, "4", "\x1b[D", "4"},+ {"5", []vt.Key{vt.KeyPad5}, "5", "\x1b[E", "5"},+ {"6", []vt.Key{vt.KeyPad6}, "6", "\x1b[C", "6"},+ {"7", []vt.Key{vt.KeyPad7}, "7", "\x1bOH", "7"},+ {"8", []vt.Key{vt.KeyPad8}, "8", "\x1b[A", "8"},+ {"9", []vt.Key{vt.KeyPad9}, "9", "\x1b[5~", "9"},+ {"0", []vt.Key{vt.KeyPad0}, "0", "\x1b[2~", "0"},+ {"dec", []vt.Key{vt.KeyPadDec}, ".", "\x1b[3~", "."},+ {"add", []vt.Key{vt.KeyPadAdd}, "+", "\x1bOk", "+"},+ {"sub", []vt.Key{vt.KeyPadSub}, "-", "\x1bOm", "-"},+ {"mul", []vt.Key{vt.KeyPadMul}, "*", "\x1bOj", "*"},+ {"div", []vt.Key{vt.KeyPadDiv}, "/", "\x1bOo", "/"},+ {"equal", []vt.Key{vt.KeyPadEqual}, "=", "\x1bOX", "="},+ {"enter", []vt.Key{vt.KeyPadEnter}, "\r", "\x1bOM", "\r"},+ }+ for i := range cases {+ t.Run(cases[i].name, func(t *testing.T) {++ // set num lock+ term.KeyTap(vt.KeyNumLock)++ // set application mode+ WriteF(t, term, "\x1b=")+ term.KeyTap(cases[i].keys...)+ term.KeyTap(vt.KeySpace)+ CheckRead(t, term, cases[i].lock+" ")+ // clear application mode - should not change (NumLock takes precedence)+ WriteF(t, term, "\x1b>")+ term.KeyTap(cases[i].keys...)+ term.KeyTap(vt.KeySpace)+ CheckRead(t, term, cases[i].lock+" ")++ // release num lock+ term.KeyTap(vt.KeyNumLock)++ // set application mode+ WriteF(t, term, "\x1b=")+ term.KeyTap(cases[i].keys...)+ term.KeyTap(vt.KeySpace)+ CheckRead(t, term, cases[i].app+" ")++ WriteF(t, term, "\x1b>")+ term.KeyTap(cases[i].keys...)+ term.KeyTap(vt.KeySpace)+ CheckRead(t, term, cases[i].num+" ")+ })+ } }
@@ -47,6 +47,7 @@ func WriteF(t *testing.T, term MockTerm, str string, args ...any) { // ReadF reads content from the term and returns it as a string. func ReadF(t *testing.T, term MockTerm) string {+ t.Helper() buf := make([]byte, 128) n, err := term.Read(buf) if err != nil {