/
githubmirror
/
lazygit
Обзор
Документация
Войти
/
githubmirror
/
lazygit
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
vendor/github.com/gdamore/tcell/v3/input.go
1 527 строк
42 KB
Stefan Haller
Bump tcell to v3.4.1 to fix drawing on a suspended screen
20 июл 2026, 15:23
20 июл 2026, 15:23
94e5f57
Код
Авторство
О чём код?
// Copyright 2026 The TCell Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This file describes a generic VT input processor. It parses key sequences, // (input bytes) and loads them into events. It expects UTF-8 or UTF-16 as the input // feed, along with ECMA-48 sequences. The assumption here is that all potential // key sequences are unambiguous between terminal variants (analysis of extant terminfo // data appears to support this conjecture). This allows us to implement this once, // in the most efficient and terminal-agnostic way possible. // // There is unfortunately *one* conflict, with aixterm, for CSI-P - which is KeyDelete // in aixterm, but F1 in others. //go:build (!js && !wasm) || (js && wasm) // +build !js,!wasm js,wasm package tcell import ( "encoding/base64" "os" "strconv" "strings" "sync" "time" "unicode/utf16" "unicode/utf8" "github.com/gdamore/tcell/v3/vt" ) type inputState int const ( istInit = inputState(iota) istUtf // utf8 state istEsc // escape istCsi // control sequence introducer istOsc // operating system command istDcs // device control string istSos // start of string (unused) istPm // privacy message (unused) istApc // application program command istSt // string terminator istSs2 // single shift 2 istSs3 // single shift 3 istLnx // linux F-key (not ECMA-48 compliant - bogus CSI) istXda // extended device attributes (ESC P Ps ST) ) // defaultControlStringLimit caps inbound OSC/XDA control-string payloads // before they can grow without bound while waiting for a string terminator. const defaultControlStringLimit = 64 * 1024 func newInputParser(eq chan<- Event) *inputParser { return &inputParser{ evch: eq, buf: make([]rune, 0, 128), controlStringMax: defaultControlStringLimit, } } type inputParser struct { buf []rune // bytes to process (ingest data) utfBuf []byte // accrued UTF8 bytes strBuf []byte // accrued string data (for ST, OSC, etc.) csiParams []byte // accrued parameter bytes for CSI (and SS3) csiInterm []byte // accrued intermediate bytes for CSI escChar byte // last byte for escape escaped bool // true if next key should be modified by ESC btnsDown ButtonMask // mouse buttons down (excludes wheel buttons) state inputState // tracks processor state strState inputState // saved str state (needed for ST) l sync.Mutex // protects local state evch chan<- Event // where events are routed rows int // used for clipping mouse coordinates cols int // used for clipping mouse coordinates pixelMouse bool // mouse reports in pixels (CSI ?1016h); skip cell clipping keyTime time.Time // time of last key press / byte ingested nested *inputParser // for buggy win32-input-mode implementations surrogate rune // high surrogate pair seen (for Win32 input mode) advanced bool // use advanced key reporting semantics controlStringMax int // maximum inbound OSC/XDA payload size; 0 means unlimited discardString bool // drop the rest of an over-limit OSC/XDA sequence } func keyFromInt(n int) (Key, bool) { if n < 0 || n > 32767 { return 0, false } return Key(n), true } func keyFromRune(r rune) (Key, bool) { if r < 0 || r > 32767 { return 0, false } return Key(r), true } func asciiByteFromInt(n int) (byte, bool) { if n <= 0 || n >= 0x80 { return 0, false } return byte(n), true } // Waiting returns true if the processor is waiting for // some more input (i.e. we are not in in the initial state.) // This can occur when we have ambiguous escape sequences, such // as the lone escape. If this is typed, we expect at least a minimal // inter-key delay before the next stroke occurs, and the caller // should check for waiting, and call Scan() or ScanUTF8() to // finish the processing. (Typically after a delay of around 100ms.) func (ip *inputParser) Waiting() bool { ip.l.Lock() defer ip.l.Unlock() return ip.state != istInit } // SetPixelMouse toggles whether SGR mouse reports are interpreted as // pixel coordinates (CSI ?1016h) rather than character cells (CSI ?1006h). // When enabled, mouse coordinates are not clipped to the screen size. // The setting is also forwarded to the lazily-created nested parser used // for win32-input-mode, if one exists, so both stay in sync. func (ip *inputParser) SetPixelMouse(on bool) { ip.l.Lock() ip.pixelMouse = on nested := ip.nested ip.l.Unlock() if nested != nil { nested.SetPixelMouse(on) } } func (ip *inputParser) SetSize(w, h int) { if ip.nested != nil { ip.nested.SetSize(w, h) return } go func() { ip.l.Lock() ip.rows = h ip.cols = w ip.post(NewEventResize(w, h)) ip.l.Unlock() }() } func (ip *inputParser) post(ev Event) { if ip.escaped { ip.escaped = false if ke, ok := ev.(*EventKey); ok { ev = ip.newKey(ke.Key(), ke.Str(), ke.Modifiers()|ModAlt, ke.Pressed(), ke.Physical(), ke.Repeat()) } } else if ke, ok := ev.(*EventKey); ok { switch ke.Key() { case keyPasteStart: ev = NewEventPaste(true) case keyPasteEnd: ev = NewEventPaste(false) } } ip.evch <- ev } func (ip *inputParser) newKey(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int) *EventKey { if ip.advanced { return NewEventKeyEx(k, str, mod, pressed, physical, repeat) } return NewEventKey(k, str, mod) } func (ip *inputParser) postKey(k Key, str string, mod ModMask) { ip.post(ip.newKey(k, str, mod, true, 0, 1)) } func (ip *inputParser) postKeyEx(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int) { ip.post(ip.newKey(k, str, mod, pressed, physical, repeat)) } func (ip *inputParser) postControlKey(r rune, mod ModMask) { if r == 0 { ip.postKeyEx(KeyRune, " ", mod|ModCtrl, true, Key(' '), 1) } else if ip.advanced && r >= 1 && r <= 26 { ip.postKeyEx(KeyRune, string('a'+r-1), mod|ModCtrl, true, Key('a'+r-1), 1) } else { ip.postKey(KeyRune, string(r+0x40), mod|ModCtrl) } } type csiParamMode struct { M rune // Mode P int // Parameter (first) } type keyMap struct { Key Key Mod ModMask Rune rune } var csiAllKeys = map[csiParamMode]keyMap{ {M: 'A'}: {Key: KeyUp}, {M: 'B'}: {Key: KeyDown}, {M: 'C'}: {Key: KeyRight}, {M: 'D'}: {Key: KeyLeft}, {M: 'E'}: {Key: KeyClear}, {M: 'F'}: {Key: KeyEnd}, {M: 'H'}: {Key: KeyHome}, {M: 'L'}: {Key: KeyInsert}, {M: 'P'}: {Key: KeyF1}, // except for aixterm, where this is Delete {M: 'Q'}: {Key: KeyF2}, {M: 'R'}: {Key: KeyF3}, {M: 'S'}: {Key: KeyF4}, {M: 'Z'}: {Key: KeyBacktab}, {M: 'a'}: {Key: KeyUp, Mod: ModShift}, {M: 'b'}: {Key: KeyDown, Mod: ModShift}, {M: 'c'}: {Key: KeyRight, Mod: ModShift}, {M: 'd'}: {Key: KeyLeft, Mod: ModShift}, {M: 'q', P: 1}: {Key: KeyF1}, // all these 'q' are for aixterm {M: 'q', P: 2}: {Key: KeyF2}, {M: 'q', P: 3}: {Key: KeyF3}, {M: 'q', P: 4}: {Key: KeyF4}, {M: 'q', P: 5}: {Key: KeyF5}, {M: 'q', P: 6}: {Key: KeyF6}, {M: 'q', P: 7}: {Key: KeyF7}, {M: 'q', P: 8}: {Key: KeyF8}, {M: 'q', P: 9}: {Key: KeyF9}, {M: 'q', P: 10}: {Key: KeyF10}, {M: 'q', P: 11}: {Key: KeyF11}, {M: 'q', P: 12}: {Key: KeyF12}, {M: 'q', P: 13}: {Key: KeyF13}, {M: 'q', P: 14}: {Key: KeyF14}, {M: 'q', P: 15}: {Key: KeyF15}, {M: 'q', P: 16}: {Key: KeyF16}, {M: 'q', P: 17}: {Key: KeyF17}, {M: 'q', P: 18}: {Key: KeyF18}, {M: 'q', P: 19}: {Key: KeyF19}, {M: 'q', P: 20}: {Key: KeyF20}, {M: 'q', P: 21}: {Key: KeyF21}, {M: 'q', P: 22}: {Key: KeyF22}, {M: 'q', P: 23}: {Key: KeyF23}, {M: 'q', P: 24}: {Key: KeyF24}, {M: 'q', P: 25}: {Key: KeyF25}, {M: 'q', P: 26}: {Key: KeyF26}, {M: 'q', P: 27}: {Key: KeyF27}, {M: 'q', P: 28}: {Key: KeyF28}, {M: 'q', P: 29}: {Key: KeyF29}, {M: 'q', P: 30}: {Key: KeyF30}, {M: 'q', P: 31}: {Key: KeyF31}, {M: 'q', P: 32}: {Key: KeyF32}, {M: 'q', P: 33}: {Key: KeyF33}, {M: 'q', P: 34}: {Key: KeyF34}, {M: 'q', P: 35}: {Key: KeyF35}, {M: 'q', P: 36}: {Key: KeyF36}, {M: 'q', P: 144}: {Key: KeyClear}, {M: 'q', P: 146}: {Key: KeyEnd}, {M: 'q', P: 150}: {Key: KeyPgUp}, {M: 'q', P: 154}: {Key: KeyPgDn}, {M: 'z', P: 214}: {Key: KeyHome}, {M: 'z', P: 216}: {Key: KeyPgUp}, {M: 'z', P: 220}: {Key: KeyEnd}, {M: 'z', P: 222}: {Key: KeyPgDn}, {M: 'z', P: 224}: {Key: KeyF1}, {M: 'z', P: 225}: {Key: KeyF2}, {M: 'z', P: 226}: {Key: KeyF3}, {M: 'z', P: 227}: {Key: KeyF4}, {M: 'z', P: 228}: {Key: KeyF5}, {M: 'z', P: 229}: {Key: KeyF6}, {M: 'z', P: 230}: {Key: KeyF7}, {M: 'z', P: 231}: {Key: KeyF8}, {M: 'z', P: 232}: {Key: KeyF9}, {M: 'z', P: 233}: {Key: KeyF10}, {M: 'z', P: 234}: {Key: KeyF11}, {M: 'z', P: 235}: {Key: KeyF12}, {M: 'z', P: 247}: {Key: KeyInsert}, {M: '^', P: 1}: {Key: KeyHome, Mod: ModCtrl}, {M: '^', P: 2}: {Key: KeyInsert, Mod: ModCtrl}, {M: '^', P: 3}: {Key: KeyDelete, Mod: ModCtrl}, {M: '^', P: 4}: {Key: KeyEnd, Mod: ModCtrl}, {M: '^', P: 5}: {Key: KeyPgUp, Mod: ModCtrl}, {M: '^', P: 6}: {Key: KeyPgDn, Mod: ModCtrl}, {M: '^', P: 7}: {Key: KeyHome, Mod: ModCtrl}, {M: '^', P: 8}: {Key: KeyEnd, Mod: ModCtrl}, {M: '^', P: 11}: {Key: KeyF23}, {M: '^', P: 12}: {Key: KeyF24}, {M: '^', P: 13}: {Key: KeyF25}, {M: '^', P: 14}: {Key: KeyF26}, {M: '^', P: 15}: {Key: KeyF27}, {M: '^', P: 17}: {Key: KeyF28}, // 16 is a gap {M: '^', P: 18}: {Key: KeyF29}, {M: '^', P: 19}: {Key: KeyF30}, {M: '^', P: 20}: {Key: KeyF31}, {M: '^', P: 21}: {Key: KeyF32}, {M: '^', P: 23}: {Key: KeyF33}, // 22 is a gap {M: '^', P: 24}: {Key: KeyF34}, {M: '^', P: 25}: {Key: KeyF35}, {M: '^', P: 26}: {Key: KeyF36}, // 27 is a gap {M: '^', P: 28}: {Key: KeyF37}, {M: '^', P: 29}: {Key: KeyF38}, // 30 is a gap {M: '^', P: 31}: {Key: KeyF39}, {M: '^', P: 32}: {Key: KeyF40}, {M: '^', P: 33}: {Key: KeyF41}, {M: '^', P: 34}: {Key: KeyF42}, {M: '@', P: 23}: {Key: KeyF43}, {M: '@', P: 24}: {Key: KeyF44}, {M: '@', P: 1}: {Key: KeyHome, Mod: ModShift | ModCtrl}, {M: '@', P: 2}: {Key: KeyInsert, Mod: ModShift | ModCtrl}, {M: '@', P: 3}: {Key: KeyDelete, Mod: ModShift | ModCtrl}, {M: '@', P: 4}: {Key: KeyEnd, Mod: ModShift | ModCtrl}, {M: '@', P: 5}: {Key: KeyPgUp, Mod: ModShift | ModCtrl}, {M: '@', P: 6}: {Key: KeyPgDn, Mod: ModShift | ModCtrl}, {M: '@', P: 7}: {Key: KeyHome, Mod: ModShift | ModCtrl}, {M: '@', P: 8}: {Key: KeyEnd, Mod: ModShift | ModCtrl}, {M: '$', P: 1}: {Key: KeyHome, Mod: ModShift}, {M: '$', P: 2}: {Key: KeyInsert, Mod: ModShift}, {M: '$', P: 3}: {Key: KeyDelete, Mod: ModShift}, {M: '$', P: 5}: {Key: KeyPgUp, Mod: ModShift}, {M: '$', P: 6}: {Key: KeyPgDn, Mod: ModShift}, {M: '$', P: 7}: {Key: KeyHome, Mod: ModShift}, {M: '$', P: 8}: {Key: KeyEnd, Mod: ModShift}, {M: '$', P: 23}: {Key: KeyF21}, {M: '$', P: 24}: {Key: KeyF22}, {M: '~', P: 1}: {Key: KeyHome}, {M: '~', P: 2}: {Key: KeyInsert}, {M: '~', P: 3}: {Key: KeyDelete}, {M: '~', P: 4}: {Key: KeyEnd}, {M: '~', P: 5}: {Key: KeyPgUp}, {M: '~', P: 6}: {Key: KeyPgDn}, {M: '~', P: 7}: {Key: KeyHome}, {M: '~', P: 8}: {Key: KeyEnd}, {M: '~', P: 11}: {Key: KeyF1}, {M: '~', P: 12}: {Key: KeyF2}, {M: '~', P: 13}: {Key: KeyF3}, {M: '~', P: 14}: {Key: KeyF4}, {M: '~', P: 15}: {Key: KeyF5}, {M: '~', P: 17}: {Key: KeyF6}, {M: '~', P: 18}: {Key: KeyF7}, {M: '~', P: 19}: {Key: KeyF8}, {M: '~', P: 20}: {Key: KeyF9}, {M: '~', P: 21}: {Key: KeyF10}, {M: '~', P: 23}: {Key: KeyF11}, {M: '~', P: 24}: {Key: KeyF12}, {M: '~', P: 25}: {Key: KeyF13}, {M: '~', P: 26}: {Key: KeyF14}, {M: '~', P: 28}: {Key: KeyF15}, // aka KeyHelp {M: '~', P: 29}: {Key: KeyF16}, {M: '~', P: 31}: {Key: KeyF17}, {M: '~', P: 32}: {Key: KeyF18}, {M: '~', P: 33}: {Key: KeyF19}, {M: '~', P: 34}: {Key: KeyF20}, {M: '~', P: 200}: {Key: keyPasteStart}, {M: '~', P: 201}: {Key: keyPasteEnd}, } // keys reported using Kitty csi-u protocol var csiUKeys = map[int]keyMap{ 27: {Key: KeyESC}, 9: {Key: KeyTAB}, 13: {Key: KeyEnter}, 127: {Key: KeyBS}, 57358: {Key: KeyCapsLock}, 57359: {Key: KeyScrollLock}, 57360: {Key: KeyNumLock}, 57361: {Key: KeyPrint}, 57362: {Key: KeyPause}, 57363: {Key: KeyMenu}, 57376: {Key: KeyF13}, 57377: {Key: KeyF14}, 57378: {Key: KeyF15}, 57379: {Key: KeyF16}, 57380: {Key: KeyF17}, 57381: {Key: KeyF18}, 57382: {Key: KeyF19}, 57383: {Key: KeyF20}, 57384: {Key: KeyF21}, 57385: {Key: KeyF22}, 57386: {Key: KeyF23}, 57387: {Key: KeyF24}, 57388: {Key: KeyF25}, 57389: {Key: KeyF26}, 57390: {Key: KeyF27}, 57391: {Key: KeyF28}, 57392: {Key: KeyF29}, 57393: {Key: KeyF30}, 57394: {Key: KeyF31}, 57395: {Key: KeyF32}, 57396: {Key: KeyF33}, 57397: {Key: KeyF34}, 57398: {Key: KeyF35}, 57399: {Key: KeyRune, Rune: '0'}, // KP 0 57400: {Key: KeyRune, Rune: '1'}, // KP 1 57401: {Key: KeyRune, Rune: '2'}, // KP 2 57402: {Key: KeyRune, Rune: '3'}, // KP 3 57403: {Key: KeyRune, Rune: '4'}, // KP 4 57404: {Key: KeyRune, Rune: '5'}, // KP 5 57405: {Key: KeyRune, Rune: '6'}, // KP 6 57406: {Key: KeyRune, Rune: '7'}, // KP 7 57407: {Key: KeyRune, Rune: '8'}, // KP 8 57408: {Key: KeyRune, Rune: '9'}, // KP 9 57409: {Key: KeyRune, Rune: '.'}, // KP_DECIMAL 57410: {Key: KeyRune, Rune: '/'}, // KP_DIVIDE 57411: {Key: KeyRune, Rune: '*'}, // KP_MULTIPLY 57412: {Key: KeyRune, Rune: '-'}, // KP_SUBTRACT 57413: {Key: KeyRune, Rune: '+'}, // KP_ADD 57414: {Key: KeyEnter}, // KP_ENTER 57415: {Key: KeyRune, Rune: '='}, // KP_EQUAL 57416: {Key: KeyClear}, // KP_SEPARATOR 57417: {Key: KeyLeft}, // KP_LEFT 57418: {Key: KeyRight}, // KP_RIGHT 57419: {Key: KeyUp}, // KP_UP 57420: {Key: KeyDown}, // KP_DOWN 57421: {Key: KeyPgUp}, // KP_PG_UP 57422: {Key: KeyPgDn}, // KP_PG_DN 57423: {Key: KeyHome}, // KP_HOME 57424: {Key: KeyEnd}, // KP_END 57425: {Key: KeyInsert}, // KP_INSERT 57426: {Key: KeyDelete}, // KP_DELETE // 57427: {Key: KeyBegin}, // KP_BEGIN 57441: {Key: KeyShift}, // LEFT_SHIFT 57442: {Key: KeyCtrl}, // LEFT_CONTROL 57443: {Key: KeyAlt}, // LEFT_ALT 57444: {Key: KeyMeta}, // LEFT_SUPER 57447: {Key: KeyShift}, // RIGHT_SHIFT 57448: {Key: KeyCtrl}, // RIGHT_CONTROL 57449: {Key: KeyAlt}, // RIGHT_ALT 57450: {Key: KeyMeta}, // RIGHT_SUPER // TODO: Media keys } // windows virtual key codes per microsoft var winKeys = map[int]Key{ 0x03: KeyCancel, // vkCancel 0x08: KeyBackspace, // vkBackspace 0x09: KeyTab, // vkTab 0x0d: KeyEnter, // vkReturn 0x13: KeyPause, // vkPause 0x1b: KeyEscape, // vkEscape 0x21: KeyPgUp, // vkPrior 0x22: KeyPgDn, // vkNext 0x23: KeyEnd, // vkEnd 0x24: KeyHome, // vkHome 0x25: KeyLeft, // vkLeft 0x26: KeyUp, // vkUp 0x27: KeyRight, // vkRight 0x28: KeyDown, // vkDown 0x2a: KeyPrint, // vkPrint 0x2c: KeyPrint, // vkPrtScr 0x2d: KeyInsert, // vkInsert 0x2e: KeyDelete, // vkDelete 0x2f: KeyHelp, // vkHelp 0x70: KeyF1, // vkF1 0x71: KeyF2, // vkF2 0x72: KeyF3, // vkF3 0x73: KeyF4, // vkF4 0x74: KeyF5, // vkF5 0x75: KeyF6, // vkF6 0x76: KeyF7, // vkF7 0x77: KeyF8, // vkF8 0x78: KeyF9, // vkF9 0x79: KeyF10, // vkF10 0x7a: KeyF11, // vkF11 0x7b: KeyF12, // vkF12 0x7c: KeyF13, // vkF13 0x7d: KeyF14, // vkF14 0x7e: KeyF15, // vkF15 0x7f: KeyF16, // vkF16 0x80: KeyF17, // vkF17 0x81: KeyF18, // vkF18 0x82: KeyF19, // vkF19 0x83: KeyF20, // vkF20 0x84: KeyF21, // vkF21 0x85: KeyF22, // vkF22 0x86: KeyF23, // vkF23 0x87: KeyF24, // vkF24 } // keys by their SS3 - used in application mode usually (legacy VT-style) var ss3Keys = map[rune]Key{ 'A': KeyUp, 'B': KeyDown, 'C': KeyRight, 'D': KeyLeft, 'E': KeyClear, 'F': KeyEnd, 'H': KeyHome, 'P': KeyF1, 'Q': KeyF2, 'R': KeyF3, 'S': KeyF4, 't': KeyF5, 'u': KeyF6, 'v': KeyF7, 'l': KeyF8, 'w': KeyF9, 'x': KeyF10, } // linux terminal uses these non ECMA keys prefixed by CSI-[ var linuxFKeys = map[rune]Key{ 'A': KeyF1, 'B': KeyF2, 'C': KeyF3, 'D': KeyF4, 'E': KeyF5, } func (ip *inputParser) scan() { for _, r := range ip.buf { ip.buf = ip.buf[1:] ip.escChar = 0 ip.keyTime = time.Now() if r >= 0xA0 { // 8-bit extended Unicode we just treat as such - this will swallow anything else queued up ip.state = istInit physical, _ := keyFromRune(r) ip.postKeyEx(KeyRune, string(r), ModNone, true, physical, 1) continue } else if r >= 0x80 { // ISO 2022 control chars ip.state = istEsc r -= 0x40 // we fall through so it will be treated as the 7-bit equivalent } switch ip.state { case istInit: switch r { case '\x1b': // escape.. pending ip.state = istEsc ip.escChar = 0 case '\t': ip.postKey(KeyTab, "", ModNone) case '\b', '\x7F': ip.postKey(KeyBackspace, "", ModNone) case '\r': ip.postKey(KeyEnter, "", ModNone) default: // Control keys - legacy handling if r == 0 { ip.postControlKey(r, ModNone) } else if r < ' ' { ip.postControlKey(r, ModNone) } else { physical, _ := keyFromRune(r) ip.postKeyEx(KeyRune, string(r), ModNone, true, physical, 1) } } case istEsc: switch r { case '[': ip.state = istCsi ip.csiInterm = nil ip.csiParams = nil ip.escChar = byte(r) case ']': ip.state = istOsc ip.strBuf = nil ip.discardString = false ip.escChar = byte(r) case 'N': ip.state = istSs2 // no known uses ip.strBuf = nil ip.escChar = byte(r) case 'O': ip.state = istSs3 ip.csiParams = nil ip.strBuf = nil ip.escChar = byte(r) case 'P': ip.state = istXda ip.csiParams = nil ip.strBuf = nil ip.discardString = false ip.escChar = byte(r) case 'X': ip.state = istSos ip.strBuf = nil ip.escChar = byte(r) case '^': ip.state = istPm ip.strBuf = nil ip.escChar = byte(r) case '_': ip.state = istApc ip.strBuf = nil ip.escChar = byte(r) case '\\': // string terminator reached, (orphaned?) ip.state = istInit case '\t': // Linux console only, does not conform to ECMA ip.state = istInit ip.postKey(KeyBacktab, "", ModNone) default: if r == '\x1b' { // leading ESC to capture alt ip.escaped = true ip.escChar = byte(r) } else { // treat as alt-key ... legacy emulators only (no CSI-u or other) ip.state = istInit mod := ModAlt if r < ' ' { mod |= ModCtrl r += 0x60 } physical, _ := keyFromRune(r) ip.postKeyEx(KeyRune, string(r), mod, true, physical, 1) } } case istCsi: // usual case for incoming keys // NB: rxvt uses terminating '$' which is not a legal CSI terminator, // for certain shifted key sequences. We special case this, and it's ok // because no other terminal seems to use this for CSI intermediates from // the terminal to the host (queries in the other direction can use it.) // However, this is only true if the first parameter does not have a "?", // because it *does* collide with DEC private mode queries otherwise. if r == '\x1b' { // Per ECMA-48 §5.3.1, ESC restarts the escape // sequence machine from any intermediate state. ip.state = istEsc ip.escChar = 0 } else if r >= 0x30 && r <= 0x3F { // parameter bytes ip.csiParams = append(ip.csiParams, byte(r)) } else if r == '$' && len(ip.csiParams) > 0 && ip.csiParams[0] != '?' { // rxvt non-standard ip.handleCsi(r, ip.csiParams, ip.csiInterm) } else if r >= 0x20 && r <= 0x2F { // intermediate bytes, rarely used ip.csiInterm = append(ip.csiInterm, byte(r)) } else if r >= 0x40 && r <= 0x7F { // final byte ip.handleCsi(r, ip.csiParams, ip.csiInterm) } else { // bad parse, just swallow it all ip.state = istInit } case istSs2: // No known uses for SS2 ip.state = istInit case istSs3: // typically application mode keys or older terminals ip.state = istInit // some SS3 sequences (old VTE) encode modifiers here just like CSI if r == '\x1b' { // Per ECMA-48 §5.3.1, ESC restarts the escape // sequence machine from any intermediate state. ip.state = istEsc ip.escChar = 0 } else if r >= 0x30 && r <= 0x3F { ip.csiParams = append(ip.csiParams, byte(r)) ip.state = istSs3 } else if k, ok := ss3Keys[r]; ok { // If there are no parameters, then it's simple without modifiers. // The options for parameters are "1;<modifiers>" , or ";modifiers" (empty // first parameter defaults to 1), or just <modifiers>. If a sequence has // parameters that do not match one of these forms, we just discard it. if len(ip.csiParams) == 0 { // simple SS3 case ip.postKey(k, "", ModNone) } else if parts := strings.Split(string(ip.csiParams), ";"); len(parts) >= 1 { // SS3 with modifier (old style). Note old terminfo would declare these as high // numbered function keys, but we encode as modified since that's how they are entered. if len(parts) >= 2 { if m, err := strconv.Atoi(parts[1]); err == nil && (parts[0] == "1" || parts[0] == "") { ip.postKey(k, "", calcModifier(m)) } } else if m, err := strconv.Atoi(parts[0]); err == nil { ip.postKey(k, "", calcModifier(m)) } } } case istPm, istApc, istSos, istDcs: // these we just eat switch r { case '\x1b': ip.strState = ip.state ip.state = istSt case '\x07': // bell - some send this instead of ST ip.state = istInit } case istXda: switch r { case '\x1b': ip.strState = ip.state ip.state = istSt case '\x07': if ip.discardString { ip.discardString = false ip.state = istInit } else { ip.handleXda(string(ip.strBuf)) } default: if !ip.discardString { ip.appendStringBytes(byte(r & 0x7f)) } } case istOsc: // not sure if used switch r { case '\x1b': ip.strState = ip.state ip.state = istSt case '\x07': if ip.discardString { ip.discardString = false ip.state = istInit } else { ip.handleOsc(string(ip.strBuf)) } default: if !ip.discardString { ip.appendStringBytes(byte(r & 0x7f)) } } case istSt: if r == '\\' || r == '\x07' { ip.state = istInit if ip.discardString { ip.discardString = false } else { switch ip.strState { case istOsc: ip.handleOsc(string(ip.strBuf)) case istXda: ip.handleXda(string(ip.strBuf)) case istPm, istApc, istSos, istDcs: ip.state = istInit } } } else { if !ip.discardString { ip.appendStringBytes('\x1b', byte(r)) } ip.state = ip.strState } case istLnx: // linux console does not follow ECMA if k, ok := linuxFKeys[r]; ok { ip.postKey(k, "", ModNone) } ip.state = istInit } } if ip.state != istInit && time.Since(ip.keyTime) > time.Millisecond*50 { if ip.state == istEsc { ip.postKey(KeyEscape, "", ModNone) } else if ec := ip.escChar; ec != 0 { ip.postKey(KeyRune, string(ec), ModAlt) } // if we take too long between bytes, reset the state machine. ip.state = istInit ip.discardString = false } } func (ip *inputParser) appendStringBytes(bs ...byte) { if ip.controlStringMax > 0 && len(ip.strBuf)+len(bs) > ip.controlStringMax { ip.strBuf = nil ip.discardString = true return } ip.strBuf = append(ip.strBuf, bs...) } func (ip *inputParser) handleOsc(str string) { ip.state = istInit if content, ok := strings.CutPrefix(str, "52;c;"); ok { decoded := make([]byte, base64.StdEncoding.DecodedLen(len(content))) if count, err := base64.StdEncoding.Decode(decoded, []byte(content)); err == nil { ip.post(NewEventClipboard(decoded[:count])) return } } } func (ip *inputParser) handleXda(str string) { ip.state = istInit if content, ok := strings.CutPrefix(str, ">|"); ok { // two approaches, one with version like (1.23) another with just spaces if name, vers, ok := strings.Cut(content, "("); ok && strings.HasSuffix(vers, ")") { name = strings.TrimSpace(name) vers = strings.TrimSpace(strings.TrimSuffix(vers, ")")) ip.post(&eventTermName{Name: name, Version: vers}) } else if name, vers, ok = strings.Cut(content, " "); ok { ip.post(&eventTermName{Name: name, Version: vers}) } } } func calcModifier(n int) ModMask { n-- m := ModNone if n&1 != 0 { m |= ModShift } if n&2 != 0 { m |= ModAlt } if n&4 != 0 { m |= ModCtrl } if n&8 != 0 { m |= ModMeta // kitty calls this Super } if n&16 != 0 { m |= ModHyper } if n&32 != 0 { m |= ModMeta // for now not separating from Super } // Not doing (kitty only): // caps_lock 0b1000000 (64) // num_lock 0b10000000 (128) return m } func calcWinModifier(n int, advanced bool) ModMask { m := ModNone if n&0x010 != 0 { m |= ModShift } if advanced { // Bits through 0x0100 match Win32 dwControlKeyState. 0x0040 and // 0x0080 are ScrollLock and CapsLock, not Meta. The 0x0200 and // 0x0400 bits are tcell extensions used by the WASM browser shim, // which has Meta keys but no native Win32 bit assignment for them. if n&0x0008 != 0 { m |= ModLCtrl } if n&0x0004 != 0 { m |= ModRCtrl } if n&0x0002 != 0 { m |= ModLAlt } if n&0x0001 != 0 { m |= ModRAlt } if n&0x0200 != 0 { m |= ModLMeta } if n&0x0400 != 0 { m |= ModRMeta } } else { if n&0x000c != 0 { m |= ModCtrl } if n&0x0003 != 0 { m |= ModAlt } } return m } func winModifierKey(vk int) (Key, ModMask, bool) { switch vk { case 0x10: return KeyShift, ModShift, true case 0xa0: return KeyShift, ModLShift, true case 0xa1: return KeyShift, ModRShift, true case 0x11: return KeyCtrl, ModCtrl, true case 0xa2: return KeyCtrl, ModLCtrl, true case 0xa3: return KeyCtrl, ModRCtrl, true case 0x12: return KeyAlt, ModAlt, true case 0xa4: return KeyAlt, ModLAlt, true case 0xa5: return KeyAlt, ModRAlt, true case 0x5b: return KeyMeta, ModLMeta, true case 0x5c: return KeyMeta, ModRMeta, true case 0x14: return KeyCapsLock, ModNone, true default: return 0, ModNone, false } } func kittyModifierKey(code int) ModMask { switch code { case 57441: return ModLShift case 57447: return ModRShift case 57442: return ModLCtrl case 57448: return ModRCtrl case 57443: return ModLAlt case 57449: return ModRAlt case 57444: return ModLMeta case 57450: return ModRMeta default: return ModNone } } func (ip *inputParser) handleMouse(mode rune, params []int) { // XTerm mouse events only report at most one button at a time, // which may include a wheel button. Wheel motion events are // reported as single impulses, while other button events are reported // as separate press & release events. if len(params) < 3 { return } btn := params[0] // Some terminals will report mouse coordinates outside the // screen, especially with click-drag events. Clip the coordinates // to the screen in that case. In pixel-reporting mode (CSI ?1016h) // the values are already pixels rather than cells, so skip the clip // and pass them through unchanged for the application to interpret. x := params[1] - 1 y := params[2] - 1 if !ip.pixelMouse { x = max(min(x, ip.cols-1), 0) y = max(min(y, ip.rows-1), 0) } button := ButtonNone mod := ModNone // Mouse wheel has bit 6 set, no release events. It should be noted // that wheel events are sometimes misdelivered as mouse button events // during a click-drag, so we debounce these, considering them to be // button press events unless we see an intervening release event. // This excludes motion (bit 5) and modifiers (bits 2, 3, 4) for now. switch btn & 0xC3 { case 0: button = Button1 case 1: button = Button3 // Note we prefer to treat right as button 2 case 2: button = Button2 // And the middle button as button 3 case 3: button = ButtonNone case 0x40: button = WheelUp case 0x41: button = WheelDown case 0x42: button = WheelLeft case 0x43: button = WheelRight case 0x80: button = Button4 case 0x81: button = Button5 case 0x82: button = Button6 case 0x83: button = Button7 } switch mode { case 'm': if (ip.btnsDown & button) == 0 { // a release without a corresponding press, so clear it button = ButtonNone } else { ip.btnsDown &^= button button = ip.btnsDown } case 'M': if btn&0x20 != 0 && button != ButtonNone && (ip.btnsDown&button) == 0 { // Ghostty may send out motion signals that indicate a button has // been pressed, even when the button is not actually pressed. // Do not create a synthetic button-down state from these packets. button = ip.btnsDown break } // record this press ip.btnsDown |= button // and use the full set so can see chords button = ip.btnsDown // mice wheel do not have release events ip.btnsDown &^= (WheelDown | WheelUp | WheelLeft | WheelRight) } if btn&0x4 != 0 { mod |= ModShift } if btn&0x8 != 0 { mod |= ModAlt } if btn&0x10 != 0 { mod |= ModCtrl } ip.post(NewEventMouse(x, y, button, mod)) } func (ip *inputParser) handleWinKey(P []int) { // win32-input-mode // ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _ // Vk: the value of wVirtualKeyCode - any number. If omitted, defaults to '0'. // Sc: the value of wVirtualScanCode - any number. If omitted, defaults to '0'. // Uc: the decimal value of UnicodeChar - for example, NUL is "0", LF is // "10", the character 'A' is "65". If omitted, defaults to '0'. // Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'. // Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'. // Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'. // // Note that some 3rd party terminal emulators (not Terminal) suffer from a bug // where other events, such as mouse events, are doubly encoded, using Vk 0 // for each character. (So a CSI-M sequence is encoded as a series of CSI-_ // sequences.) We consider this a bug in those terminal emulators -- Windows 11 // Terminal does not suffer this brain damage. (We've observed this with both Alacritty // and WezTerm.) for len(P) < 6 { P = append(P, 0) // ensure sufficient length } if P[3] == 0 && !ip.advanced { // key up event ignore ignore return } // these terminals never send ambiguous escapes ip.escaped = false if P[0] == 0 && P[1] == 0 { // only ASCII in win32-input-mode if b, ok := asciiByteFromInt(P[2]); ok { if ip.nested == nil { ip.nested = &inputParser{ evch: ip.evch, rows: ip.rows, cols: ip.cols, advanced: ip.advanced, pixelMouse: ip.pixelMouse, controlStringMax: ip.controlStringMax, } } ip.nested.ScanUTF8([]byte{b}) return } } key := KeyRune chr := rune(P[2]) mod := ModNone rpt := max(1, P[5]) decoded := false if k1, ok := winKeys[P[0]]; ok { chr = 0 key = k1 decoded = true } else if ip.advanced { if k1, mod1, ok := winModifierKey(P[0]); ok { key = k1 mod = mod1 chr = 0 decoded = true } } if decoded { // Already decoded. } else if chr == 0 && P[0] >= 0x30 && P[0] <= 0x39 { chr = rune(P[0]) } else if chr < ' ' && P[0] >= 0x41 && P[0] <= 0x5a { if ip.advanced { key = KeyRune chr = rune(P[0] + 0x20) } else { var ok bool if key, ok = keyFromInt(P[0]); !ok { return } chr = 0 } } else if chr >= 0xD800 && chr <= 0xDBFF { // high surrogate pair if ip.surrogate != 0 { ip.postKeyEx(KeyRune, string(utf8.RuneError), mod, P[3] != 0, 0, rpt) } ip.surrogate = chr return } else if chr >= 0xDC00 && chr <= 0xDFFF { // low surrogate pair if ip.surrogate == 0 { chr = utf8.RuneError } else { chr = utf16.DecodeRune(ip.surrogate, chr) } } else if ip.surrogate != 0 { ip.postKeyEx(KeyRune, string(utf8.RuneError), mod, P[3] != 0, 0, rpt) } else if _, _, ok := winModifierKey(P[0]); ok { // Lone modifier releases are ignored unless advanced mode is enabled. ip.surrogate = 0 return } ip.surrogate = 0 mod |= calcWinModifier(P[4], ip.advanced) if key == KeyRune && chr > ' ' && mod == ModShift && !ip.advanced { // filter out lone shift for printable chars mod = ModNone } if chr != 0 && mod&(ModCtrl|ModAlt) == ModCtrl|ModAlt { // Filter out ctrl+alt (it means AltGr) mod = ModNone } physical := key if key == KeyRune && chr != 0 { physical, _ = keyFromRune(chr) if ip.advanced && P[0] >= 0x41 && P[0] <= 0x5a { physical, _ = keyFromInt(P[0] + 0x20) } } if key != KeyRune { ip.postKeyEx(key, "", mod, P[3] != 0, physical, rpt) } else if chr != 0 { ip.postKeyEx(KeyRune, string(chr), mod, P[3] != 0, physical, rpt) } } func (ip *inputParser) handlePrimaryDA(params []int) { if len(params) < 1 { return } evDA := &eventPrimaryAttributes{Class: params[0]} params = params[1:] if evDA.Class >= 60 { for _, v := range params { switch v { case 3: evDA.ReGIS = true case 4: evDA.Sixel = true case 9: evDA.National = true case 12: evDA.SerboCroation = true case 22: evDA.Color = true case 23: evDA.Greek = true case 24: evDA.Turkish = true case 42: evDA.Latin2 = true case 52: evDA.Clipboard = true } } } ip.post(evDA) } func (ip *inputParser) handlePrivateModeResponse(params []int) { for len(params) < 2 { params = append(params, 0) } if params[1] >= 0 && params[1] <= 4 { ev := &eventPrivateMode{ Mode: vt.PrivateMode(params[0]), Status: vt.ModeStatus(params[1]), } ip.post(ev) } } func (ip *inputParser) handleKittyMode(params []int) { if len(params) == 1 && params[0] >= 0 && params[0] < 32 { ev := &eventKittyKbdMode{ Mode: KittyKbdMode(params[0] & 0xffff), } ip.post(ev) } } func (ip *inputParser) handleXTermMode(params []int) { if len(params) >= 1 && params[0] == 4 { if len(params) == 1 { params = append(params, 0) } ev := &eventXTermKbdMode{ Mode: XtermKbdMode(params[1] & 0x3), } ip.post(ev) } } func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte) { // reset state ip.state = istInit var P []int hasLT := false hasQM := false hasGT := false pstr := string(params) // extract numeric parameters if strings.HasPrefix(pstr, "<") { hasLT = true pstr = pstr[1:] } else if strings.HasPrefix(pstr, "?") { hasQM = true pstr = pstr[1:] } else if strings.HasPrefix(pstr, ">") { hasGT = true pstr = pstr[1:] } pressed := true repeat := 1 physical := Key(0) if pstr != "" && pstr[0] >= '0' && pstr[0] <= '9' { var PSubs [][]int parts := strings.Split(pstr, ";") for i := range parts { subparts := strings.Split(parts[i], ":") if subparts[0] != "" { if n, e := strconv.ParseInt(subparts[0], 10, 32); e == nil { P = append(P, int(n)) } else { P = append(P, 0) } } else { P = append(P, 0) } subs := []int{} for _, sub := range subparts[1:] { if sub != "" { if n, e := strconv.ParseInt(sub, 10, 32); e == nil { subs = append(subs, int(n)) } } else { subs = append(subs, 0) } } PSubs = append(PSubs, subs) } if len(PSubs) > 1 && len(PSubs[1]) > 0 { switch PSubs[1][0] { case 2: repeat = 2 case 3: pressed = false } } if len(PSubs) > 0 && len(PSubs[0]) > 0 { base := PSubs[0][0] if baseKey, ok := csiUKeys[base]; ok { physical = baseKey.Key if physical == KeyRune && baseKey.Rune != 0 { physical, _ = keyFromRune(baseKey.Rune) } } else if base != 0 { physical, _ = keyFromInt(base) } } } var P0 int if len(P) > 0 { P0 = P[0] } if hasLT && len(intermediate) == 0 { switch mode { case 'm', 'M': // mouse event, we only do SGR tracking ip.handleMouse(mode, P) } return } if hasQM { switch mode { case 'c': if len(intermediate) == 0 { ip.handlePrimaryDA(P) } case 'y': if string(intermediate) == "$" { ip.handlePrivateModeResponse(P) } case 'u': if len(intermediate) == 0 { ip.handleKittyMode(P) } } return } if hasGT { switch mode { case 'm': if len(intermediate) == 0 { ip.handleXTermMode(P) } } return } if len(intermediate) != 0 { // we don't know what to do with these for now return } switch mode { case 'I': // focus in ip.post(NewEventFocus(true)) return case 'O': // focus out ip.post(NewEventFocus(false)) return case '[': // linux console F-key - CSI-[ modifies next key ip.state = istLnx return case 'u': // CSI-u kitty keyboard protocol, is unambiguous if len(P) > 0 { mod := ModNone key := KeyRune chr := rune(0) if k1, ok := csiUKeys[P0]; ok { key = k1.Key chr = k1.Rune } else { chr = rune(P0) } if len(P) > 1 { mod = calcModifier(P[1]) } if mod1 := kittyModifierKey(P0); mod1 != ModNone { mod |= mod1 } if key != KeyRune { ip.postKeyEx(key, "", mod, pressed, physical, repeat) } else if chr != 0 { ip.postKeyEx(KeyRune, string(chr), mod, pressed, physical, repeat) } return } case '_': if len(P) > 0 { ip.handleWinKey(P) return } case 't': if len(P) < 1 { break } switch P[0] { case 8: if len(P) > 2 { // window size report h := P[1] w := P[2] if h != ip.rows || w != ip.cols { ip.SetSize(w, h) } return } case 48: if len(P) > 2 { // window resize report ip.post(NewEventResize(P[2], P[1])) return } } case '~': if len(P) >= 2 { mod := calcModifier(P[1]) if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok { ip.postKeyEx(ks.Key, "", mod, pressed, 0, repeat) return } if P0 == 27 && len(P) > 2 && P[2] > 0 && P[2] <= utf8.MaxRune { if P[2] < ' ' || P[2] == 0x7F { if key, ok := keyFromInt(P[2]); ok { ip.postKey(key, "", mod) } } else { physical, _ := keyFromRune(rune(P[2])) ip.postKeyEx(KeyRune, string(rune(P[2])), mod, true, physical, 1) } return } } } if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok { if mode == '~' && len(P) > 1 && ks.Mod == ModNone { // apply modifiers if present ks.Mod = calcModifier(P[1]) } else if mode == 'P' && os.Getenv("TERM") == "aixterm" { ks.Key = KeyDelete // aixterm hack - conflicts with kitty protocol } ip.postKey(ks.Key, "", ks.Mod) return } // this might have been an SS3 style key with modifiers applied if k, ok := ss3Keys[mode]; ok && P0 == 1 && len(P) > 1 { ip.postKeyEx(k, "", calcModifier(P[1]), pressed, 0, repeat) return } // if we got here we just swallow the unknown sequence } func (ip *inputParser) ScanUTF8(b []byte) { ip.l.Lock() defer ip.l.Unlock() ip.utfBuf = append(ip.utfBuf, b...) for len(ip.utfBuf) > 0 { // fast path, basic ascii, also includes ISO2022 8-bit controls if ip.utfBuf[0] < 0xA0 { ip.buf = append(ip.buf, rune(ip.utfBuf[0])) ip.utfBuf = ip.utfBuf[1:] } else { r, utfLen := utf8.DecodeRune(ip.utfBuf) if r == utf8.RuneError { // discard the leading byte as bad, // hopefully it will recover. utfLen = 1 } else { ip.buf = append(ip.buf, r) } ip.utfBuf = ip.utfBuf[utfLen:] } } ip.scan() } // Scan scans the existing input, but does not take new content. // This is typically called after a delay when Waiting() is true. func (ip *inputParser) Scan() { ip.l.Lock() ip.scan() ip.l.Unlock() } // Private events between input and tscreen. // eventPrimaryAttributes is for primary device attributes -- this should be // the last event returned during initial handshaking type eventPrimaryAttributes struct { EventTime Class int // Terminal class, 1 is vt100, vt101, 6 is vt102, > 60 for vt200 and up ReGIS bool // Terminal supports ReGIS graphics (DA 3) Sixel bool // Terminal supports Sixel graphics (DA 4) National bool // Terminal supports national replacement character sets (DA 9) SerboCroation bool // Serbo-Croatian(DA 12) Color bool // Terminal supports color (DA 22) Greek bool // Greek (DA 23) Turkish bool // Turkish (DA 24) Latin2 bool // ISO Latin-2 (DA 42) Clipboard bool // OSC 52 support (DA 52) } // eventTermName is for extended attributes type eventTermName struct { EventTime Name string Version string } type eventPrivateMode struct { EventTime Mode vt.PrivateMode // numeric mode e.g. 7 for auto-margin, 1006 for SGR mouse reports, etc Status vt.ModeStatus // value of status } type KittyKbdMode uint16 const ( KittyKbdModeOff = KittyKbdMode(0) // Disable Kitty keyboard mode KittyKbdModeBase = KittyKbdMode(1) // Enable disambiguated keys KittyKbdModeEvents = KittyKbdMode(2) // Report event types (e.g. key release) KittyKbdModeAlternate = KittyKbdMode(4) // Report alternate keys KittyKbdModeAll = KittyKbdMode(8) // Report all keys using kitty keyboard protocol KittyKbdModeText = KittyKbdMode(16) // Report associated text ) type eventKittyKbdMode struct { EventTime Mode KittyKbdMode } type XtermKbdMode uint16 const ( XtermKbdModeOff = XtermKbdMode(0) // Disabled XtermKbdModeBase = XtermKbdMode(1) // Enabled except for ones with legacy behavior XtermKbdModeExt = XtermKbdMode(2) // Enabled for all modified keys XtermKbdModeAll = XtermKbdMode(3) // Send all keys (including unmodified) ) type eventXTermKbdMode struct { EventTime Mode XtermKbdMode }