@ 261105.1

260125.17 · 055d2d2ffa1d1d2a785d2ae98f651ccd6df52d07

feat(emulator): Implement keypad handling correctly

reading declarations

vt/emulate.go

@@ -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] < ' ') {

vt/layout.go

@@ -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, 

vt/tests/uslayout_test.go

@@ -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+" ")+		})+	} }

vt/tests/util.go

@@ -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 {