cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / e2e / keys_test.go
keys_test.go 5.3K
1package e2e23import (4 "bytes"5 "regexp"67 ghostty "go.mitchellh.com/libghostty"8)910// Keys as a terminal sends them. The program asks for the kitty keyboard11// protocol (Bubble Tea asks for its first level, 012 for key events too)12// and libghostty encodes keys for what it turned on, so sessions are13// Ghostty's. A session with legacyKeys never sees those requests, as a14// terminal without the protocol ignores them, and sends keys the legacy15// way.1617// keyboardModes are the sequences that push, pop, set and query the kitty18// keyboard protocol's flags, and turn modifyOtherKeys on or off.19var keyboardModes = regexp.MustCompile(`\x1b\[(?:[>=<?][0-9;]*u|>4(?:;[0-9]*)?m)`)2021// keyFilter takes keyboardModes out of the program's output, keeping a22// sequence split between reads until its end arrives.23type keyFilter struct {24 carry []byte25}2627// filter returns data without keyboardModes. A nil filter changes28// nothing.29func (f *keyFilter) filter(data []byte) []byte {30 if f == nil {31 return data32 }33 data = append(f.carry, data...)34 f.carry = nil35 if i := bytes.LastIndexByte(data, 0x1b); i >= 0 && unfinished(data[i:]) {36 f.carry = append([]byte(nil), data[i:]...)37 data = data[:i]38 }39 return keyboardModes.ReplaceAll(data, nil)40}4142// unfinished reports whether seq, which starts with Esc, may be the43// start of a CSI sequence yet to end.44func unfinished(seq []byte) bool {45 if len(seq) < 2 {46 return true47 }48 if seq[1] != '[' {49 return false50 }51 for _, c := range seq[2:] {52 if c >= 0x40 && c <= 0x7e {53 return false54 }55 }56 return true57}5859// legacyBytes is what a terminal without the kitty keyboard protocol or60// modifyOtherKeys sends for the keys libghostty tells apart even then,61// and whether key is one: no release at all, Enter whatever the62// modifiers, Ctrl and a letter as a control character, and Alt and a63// letter as Esc before the letter.64func legacyBytes(action ghostty.KeyAction, key ghostty.Key, mods ghostty.Mods, unshifted rune) ([]byte, bool) {65 letter := unshifted >= 'a' && unshifted <= 'z'66 switch {67 case action == ghostty.KeyActionRelease:68 return nil, true69 case key == ghostty.KeyEnter && mods&ghostty.ModAlt != 0:70 return []byte("\x1b\r"), true71 case key == ghostty.KeyEnter:72 return []byte("\r"), true73 case letter && mods == ghostty.ModCtrl:74 return []byte{byte(unshifted) & 0x1f}, true75 case letter && mods == ghostty.ModAlt:76 return []byte{0x1b, byte(unshifted)}, true77 case letter && mods == ghostty.ModAlt|ghostty.ModShift:78 return []byte{0x1b, byte(unshifted) - 'a' + 'A'}, true79 }80 return nil, false81}8283// hold presses a key written as for keys ("<space>", "<shift+enter>")84// and keeps it down: the terminal repeats it once, as it does for a key85// held.86func (s *session) hold(name string) {87 s.t.Helper()88 s.keys(name)89 s.namedKeyAction(ghostty.KeyActionRepeat, name)90}9192// release lets go of a key held.93func (s *session) release(name string) {94 s.t.Helper()95 s.namedKeyAction(ghostty.KeyActionRelease, name)96}9798// namedKeyAction sends a repeat or release of a key in angle brackets.99func (s *session) namedKeyAction(action ghostty.KeyAction, name string) {100 s.t.Helper()101 name = name[1 : len(name)-1]102 key, ok := namedKeys[name]103 if !ok {104 s.t.Fatalf("unknown key <%s>", name)105 }106 var cp rune107 if name == "space" {108 cp = ' '109 }110 s.keyEvent(action, key, 0, "", cp)111}112113// kittyFlags are the keyboard protocol flags the program has on.114func (s *session) kittyFlags() ghostty.KittyKeyFlags {115 s.mu.Lock()116 defer s.mu.Unlock()117 f, _ := s.vt.KittyKeyboardFlags()118 return f119}120121var namedKeys = map[string]ghostty.Key{122 "enter": ghostty.KeyEnter, "esc": ghostty.KeyEscape, "backspace": ghostty.KeyBackspace,123 "tab": ghostty.KeyTab, "delete": ghostty.KeyDelete, "home": ghostty.KeyHome, "end": ghostty.KeyEnd,124 "up": ghostty.KeyArrowUp, "down": ghostty.KeyArrowDown,125 "left": ghostty.KeyArrowLeft, "right": ghostty.KeyArrowRight,126 "pgup": ghostty.KeyPageUp, "pgdown": ghostty.KeyPageDown,127 "f1": ghostty.KeyF1, "f2": ghostty.KeyF2, "f5": ghostty.KeyF5, "f9": ghostty.KeyF9, "f10": ghostty.KeyF10, "f11": ghostty.KeyF11, "f12": ghostty.KeyF12,128 "space": ghostty.KeySpace,129}130131type physKey struct {132 key ghostty.Key133 mods ghostty.Mods134 base rune // unshifted character, for shifted symbols135}136137// charKeys maps typed characters to physical keys on a US layout.138var charKeys = func() map[rune]physKey {139 m := map[rune]physKey{' ': {key: ghostty.KeySpace}}140 for i := range 26 {141 k := ghostty.KeyA + ghostty.Key(i)142 m[rune('a'+i)] = physKey{key: k}143 m[rune('A'+i)] = physKey{key: k, mods: ghostty.ModShift, base: rune('a' + i)}144 }145 digits := ")!@#$%^&*("146 for i := range 10 {147 k := ghostty.KeyDigit0 + ghostty.Key(i)148 m[rune('0'+i)] = physKey{key: k}149 m[rune(digits[i])] = physKey{key: k, mods: ghostty.ModShift, base: rune('0' + i)}150 }151 for _, p := range []struct {152 plain, shifted rune153 key ghostty.Key154 }{155 {'-', '_', ghostty.KeyMinus}, {'=', '+', ghostty.KeyEqual},156 {',', '<', ghostty.KeyComma}, {'.', '>', ghostty.KeyPeriod},157 {'/', '?', ghostty.KeySlash}, {';', ':', ghostty.KeySemicolon},158 {'\'', '"', ghostty.KeyQuote}, {'`', '~', ghostty.KeyBackquote},159 {'\\', '|', ghostty.KeyBackslash}, {'[', '{', ghostty.KeyBracketLeft}, {']', '}', ghostty.KeyBracketRight},160 } {161 m[p.plain] = physKey{key: p.key}162 m[p.shifted] = physKey{key: p.key, mods: ghostty.ModShift, base: p.plain}163 }164 return m165}()