mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-01-27 14:02:50 +03:00
This provides two fixes: - proper handling of keypad keys on certain terminals (e.g. iTerm2) - fix problems pasting certain emojis or east asian text on Windows Terminal
929 lines
24 KiB
Go
929 lines
24 KiB
Go
// Copyright 2025 The TCell Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use 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.
|
|
|
|
package tcell
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unicode/utf16"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type inpState int
|
|
|
|
const (
|
|
inpStateInit = inpState(iota)
|
|
inpStateUtf
|
|
inpStateEsc
|
|
inpStateCsi // control sequence introducer
|
|
inpStateOsc // operating system command
|
|
inpStateDcs // device control string
|
|
inpStateSos // start of string (unused)
|
|
inpStatePm // privacy message (unused)
|
|
inpStateApc // application program command
|
|
inpStateSt // string terminator
|
|
inpStateSs2 // single shift 2
|
|
inpStateSs3 // single shift 3
|
|
inpStateLFK // linux F-key (not ECMA-48 compliant - bogus CSI)
|
|
)
|
|
|
|
type InputProcessor interface {
|
|
ScanUTF8([]byte)
|
|
ScanUTF16([]uint16)
|
|
SetSize(rows, cols int)
|
|
}
|
|
|
|
func NewInputProcessor(eq chan<- Event) InputProcessor {
|
|
return &inputProcessor{
|
|
evch: eq,
|
|
buf: make([]rune, 0, 128),
|
|
}
|
|
}
|
|
|
|
type inputProcessor struct {
|
|
ut8 []byte
|
|
ut16 []uint16
|
|
buf []rune
|
|
scratch []byte
|
|
csiParams []byte
|
|
csiInterm []byte
|
|
escaped bool
|
|
btnDown bool // mouse button tracking for broken terms
|
|
state inpState
|
|
strState inpState // saved str state (needed for ST)
|
|
timer *time.Timer
|
|
expire time.Time
|
|
l sync.Mutex
|
|
encBuf []rune
|
|
evch chan<- Event
|
|
rows int // used for clipping mouse coordinates
|
|
cols int // used for clipping mouse coordinates
|
|
surrogate rune
|
|
nested *inputProcessor
|
|
}
|
|
|
|
func (ip *inputProcessor) 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 *inputProcessor) post(ev Event) {
|
|
if ip.escaped {
|
|
ip.escaped = false
|
|
if ke, ok := ev.(*EventKey); ok {
|
|
ev = NewEventKey(ke.Key(), ke.Rune(), ke.Modifiers()|ModAlt)
|
|
}
|
|
} 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 *inputProcessor) escTimeout() {
|
|
ip.l.Lock()
|
|
defer ip.l.Unlock()
|
|
if ip.state == inpStateEsc && ip.expire.Before(time.Now()) {
|
|
// post it
|
|
ip.state = inpStateInit
|
|
ip.escaped = false
|
|
ip.post(NewEventKey(KeyEsc, 0, ModNone))
|
|
}
|
|
}
|
|
|
|
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: '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: '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: 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: 2}: {Key: KeyInsert, Mod: ModShift},
|
|
{M: '$', P: 3}: {Key: KeyDelete, 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
|
|
|
|
// TODO: Media keys
|
|
}
|
|
|
|
// windows virtual key codes per microsoft
|
|
var winKeys = map[int]Key{
|
|
0x03: KeyCancel, // vkCancel
|
|
0x08: KeyBackspace, // vkBackspace
|
|
0x09: KeyTab, // vkTab
|
|
0x0c: KeyClear, // vClear
|
|
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,
|
|
'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 *inputProcessor) scan() {
|
|
for _, r := range ip.buf {
|
|
ip.buf = ip.buf[1:]
|
|
if r > 0x7F {
|
|
// 8-bit extended Unicode we just treat as such - this will swallow anything else queued up
|
|
ip.state = inpStateInit
|
|
ip.post(NewEventKey(KeyRune, r, ModNone))
|
|
continue
|
|
}
|
|
switch ip.state {
|
|
case inpStateInit:
|
|
switch r {
|
|
case '\x1b':
|
|
// escape.. pending
|
|
ip.state = inpStateEsc
|
|
if len(ip.buf) == 0 && ip.nested == nil {
|
|
ip.expire = time.Now().Add(time.Millisecond * 50)
|
|
ip.timer = time.AfterFunc(time.Millisecond*60, ip.escTimeout)
|
|
}
|
|
case '\t':
|
|
ip.post(NewEventKey(KeyTab, 0, ModNone))
|
|
case '\b', '\x7F':
|
|
ip.post(NewEventKey(KeyBackspace, 0, ModNone))
|
|
case '\r':
|
|
ip.post(NewEventKey(KeyEnter, 0, ModNone))
|
|
default:
|
|
// Control keys - legacy handling
|
|
if r < ' ' {
|
|
ip.post(NewEventKey(KeyCtrlSpace+Key(r), 0, ModCtrl))
|
|
} else {
|
|
ip.post(NewEventKey(KeyRune, r, ModNone))
|
|
}
|
|
}
|
|
case inpStateEsc:
|
|
switch r {
|
|
case '[':
|
|
ip.state = inpStateCsi
|
|
ip.csiInterm = nil
|
|
ip.csiParams = nil
|
|
case ']':
|
|
ip.state = inpStateOsc
|
|
ip.scratch = nil
|
|
case 'N':
|
|
ip.state = inpStateSs2 // no known uses
|
|
ip.scratch = nil
|
|
case 'O':
|
|
ip.state = inpStateSs3
|
|
ip.scratch = nil
|
|
case 'X':
|
|
ip.state = inpStateSos
|
|
ip.scratch = nil
|
|
case '^':
|
|
ip.state = inpStatePm
|
|
ip.scratch = nil
|
|
case '_':
|
|
ip.state = inpStateApc
|
|
ip.scratch = nil
|
|
case '\\':
|
|
// string terminator reached, (orphaned?)
|
|
ip.state = inpStateInit
|
|
case '\t':
|
|
// Linux console only, does not conform to ECMA
|
|
ip.state = inpStateInit
|
|
ip.post(NewEventKey(KeyBacktab, 0, ModNone))
|
|
default:
|
|
if r == '\x1b' {
|
|
// leading ESC to capture alt
|
|
ip.escaped = true
|
|
} else {
|
|
// treat as alt-key ... legacy emulators only (no CSI-u or other)
|
|
ip.state = inpStateInit
|
|
mod := ModAlt
|
|
if r < ' ' {
|
|
mod |= ModCtrl
|
|
r += 0x60
|
|
}
|
|
ip.post(NewEventKey(KeyRune, r, mod))
|
|
}
|
|
}
|
|
case inpStateCsi:
|
|
// usual case for incoming keys
|
|
if r >= 0x30 && r <= 0x3F { // parameter bytes
|
|
ip.csiParams = append(ip.csiParams, byte(r))
|
|
} 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 = inpStateInit
|
|
}
|
|
case inpStateSs2:
|
|
// No known uses for SS2
|
|
ip.state = inpStateInit
|
|
|
|
case inpStateSs3: // typically application mode keys or older terminals
|
|
ip.state = inpStateInit
|
|
if k, ok := ss3Keys[r]; ok {
|
|
ip.post(NewEventKey(k, 0, ModNone))
|
|
}
|
|
|
|
case inpStatePm, inpStateApc, inpStateSos, inpStateDcs: // these we just eat
|
|
switch r {
|
|
case '\x1b':
|
|
ip.strState = ip.state
|
|
ip.state = inpStateSt
|
|
case '\x07': // bell - some send this instead of ST
|
|
ip.state = inpStateInit
|
|
}
|
|
|
|
case inpStateOsc: // not sure if used
|
|
switch r {
|
|
case '\x1b':
|
|
ip.strState = ip.state
|
|
ip.state = inpStateSt
|
|
case '\x07':
|
|
ip.handleOsc(string(ip.scratch))
|
|
default:
|
|
ip.scratch = append(ip.scratch, byte(r&0x7f))
|
|
}
|
|
case inpStateSt:
|
|
if r == '\\' || r == '\x07' {
|
|
ip.state = inpStateInit
|
|
switch ip.strState {
|
|
case inpStateOsc:
|
|
ip.handleOsc(string(ip.scratch))
|
|
case inpStatePm, inpStateApc, inpStateSos, inpStateDcs:
|
|
ip.state = inpStateInit
|
|
}
|
|
} else {
|
|
ip.scratch = append(ip.scratch, '\x1b', byte(r))
|
|
ip.state = ip.strState
|
|
}
|
|
case inpStateLFK:
|
|
// linux console does not follow ECMA
|
|
if k, ok := linuxFKeys[r]; ok {
|
|
ip.post(NewEventKey(k, 0, ModNone))
|
|
}
|
|
ip.state = inpStateInit
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ip *inputProcessor) handleOsc(str string) {
|
|
ip.state = inpStateInit
|
|
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 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 (ip *inputProcessor) handleMouse(x, y, btn int, down bool) *EventMouse {
|
|
func (ip *inputProcessor) 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.
|
|
x := max(min(params[1]-1, ip.cols-1), 0)
|
|
y := max(min(params[2]-1, ip.rows-1), 0)
|
|
motion := (btn & 0x20) != 0
|
|
scroll := (btn & 0x42) == 0x40
|
|
btn &^= 0x20
|
|
if mode == 'm' {
|
|
// mouse release, clear all buttons
|
|
btn |= 3
|
|
btn &^= 0x40
|
|
ip.btnDown = false
|
|
} else if motion {
|
|
/*
|
|
* Some broken terminals appear to send
|
|
* mouse button one motion events, instead of
|
|
* encoding 35 (no buttons) into these events.
|
|
* We resolve these by looking for a non-motion
|
|
* event first.
|
|
*/
|
|
if !ip.btnDown {
|
|
btn |= 3
|
|
btn &^= 0x40
|
|
}
|
|
} else if !scroll {
|
|
ip.btnDown = true
|
|
}
|
|
|
|
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.
|
|
switch btn & 0x43 {
|
|
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
|
|
}
|
|
|
|
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 *inputProcessor) 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 {
|
|
// key up event ignore ignore
|
|
return
|
|
}
|
|
|
|
if P[0] == 0 && P[1] == 0 && P[2] > 0 && P[2] < 0x80 { // only ASCII in win32-input-mode
|
|
if ip.nested == nil {
|
|
ip.nested = &inputProcessor{
|
|
evch: ip.evch,
|
|
rows: ip.rows,
|
|
cols: ip.cols,
|
|
}
|
|
}
|
|
|
|
ip.nested.ScanUTF8([]byte{byte(P[2])})
|
|
return
|
|
}
|
|
|
|
key := KeyRune
|
|
chr := rune(P[2])
|
|
mod := ModNone
|
|
rpt := max(1, P[5])
|
|
if k1, ok := winKeys[P[0]]; ok {
|
|
chr = 0
|
|
key = k1
|
|
} else if chr == 0 && P[0] >= 0x30 && P[0] <= 0x39 {
|
|
chr = rune(P[0])
|
|
} else if chr < ' ' && P[0] >= 0x41 && P[0] <= 0x5a {
|
|
key = Key(P[0])
|
|
chr = 0
|
|
|
|
} else if chr >= 0xD800 && chr <= 0xDBFF {
|
|
// high surrogate pair
|
|
ip.surrogate = chr
|
|
return
|
|
} else if chr >= 0xDC00 && chr <= 0xDFFF {
|
|
// low surrogate pair
|
|
chr = utf16.DecodeRune(ip.surrogate, chr)
|
|
} else if P[0] == 0x10 || P[0] == 0x11 || P[0] == 0x12 || P[0] == 0x14 {
|
|
// lone modifiers
|
|
ip.surrogate = 0
|
|
return
|
|
}
|
|
|
|
ip.surrogate = 0
|
|
|
|
// Modifiers
|
|
if P[4]&0x010 != 0 {
|
|
mod |= ModShift
|
|
}
|
|
if P[4]&0x000c != 0 {
|
|
mod |= ModCtrl
|
|
}
|
|
if P[4]&0x0003 != 0 {
|
|
mod |= ModAlt
|
|
}
|
|
if key == KeyRune && chr > ' ' && mod == ModShift {
|
|
// 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
|
|
}
|
|
|
|
for range rpt {
|
|
if key != KeyRune || chr != 0 {
|
|
ip.post(NewEventKey(key, chr, mod))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ip *inputProcessor) handleCsi(mode rune, params []byte, intermediate []byte) {
|
|
|
|
// reset state
|
|
ip.state = inpStateInit
|
|
|
|
if len(intermediate) != 0 {
|
|
// we don't know what to do with these for now
|
|
return
|
|
}
|
|
|
|
var parts []string
|
|
var P []int
|
|
hasLT := false
|
|
pstr := string(params)
|
|
// extract numeric parameters
|
|
if strings.HasPrefix(pstr, "<") {
|
|
hasLT = true
|
|
pstr = pstr[1:]
|
|
}
|
|
if pstr != "" && pstr[0] >= '0' && pstr[0] <= '9' {
|
|
parts = strings.Split(pstr, ";")
|
|
for i := range parts {
|
|
if parts[i] != "" {
|
|
if n, e := strconv.ParseInt(parts[i], 10, 32); e == nil {
|
|
P = append(P, int(n))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var P0 int
|
|
if len(P) > 0 {
|
|
P0 = P[0]
|
|
}
|
|
|
|
if hasLT {
|
|
switch mode {
|
|
case 'm', 'M': // mouse event, we only do SGR tracking
|
|
ip.handleMouse(mode, P)
|
|
}
|
|
}
|
|
|
|
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 = inpStateLFK
|
|
return
|
|
case 'u':
|
|
// CSI-u kitty keyboard protocol
|
|
if len(P) > 0 && !hasLT {
|
|
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])
|
|
}
|
|
ip.post(NewEventKey(key, chr, mod))
|
|
}
|
|
return
|
|
case '_':
|
|
if len(intermediate) == 0 && len(P) > 0 {
|
|
ip.handleWinKey(P)
|
|
return
|
|
}
|
|
case '~':
|
|
if len(intermediate) == 0 && len(P) >= 2 {
|
|
mod := calcModifier(P[1])
|
|
if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok {
|
|
ip.post(NewEventKey(ks.Key, 0, mod))
|
|
return
|
|
}
|
|
if P0 == 27 && len(P) > 2 && P[2] > 0 && P[2] <= 0xff {
|
|
if P[2] < ' ' || P[2] == 0x7F {
|
|
ip.post(NewEventKey(Key(P[2]), 0, mod))
|
|
} else {
|
|
ip.post(NewEventKey(KeyRune, rune(P[2]), mod))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok && !hasLT {
|
|
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.post(NewEventKey(ks.Key, 0, 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.post(NewEventKey(k, 0, calcModifier(P[1])))
|
|
return
|
|
}
|
|
// if we got here we just swallow the unknown sequence
|
|
}
|
|
|
|
func (ip *inputProcessor) ScanUTF8(b []byte) {
|
|
ip.l.Lock()
|
|
defer ip.l.Unlock()
|
|
|
|
ip.ut8 = append(ip.ut8, b...)
|
|
for len(ip.ut8) > 0 {
|
|
// fast path, basic ascii
|
|
if ip.ut8[0] < 0x7F {
|
|
ip.buf = append(ip.buf, rune(ip.ut8[0]))
|
|
ip.ut8 = ip.ut8[1:]
|
|
} else {
|
|
r, len := utf8.DecodeRune(ip.ut8)
|
|
if r == utf8.RuneError {
|
|
r = rune(ip.ut8[0])
|
|
len = 1
|
|
}
|
|
ip.buf = append(ip.buf, r)
|
|
ip.ut8 = ip.ut8[len:]
|
|
}
|
|
}
|
|
|
|
ip.scan()
|
|
}
|
|
|
|
func (ip *inputProcessor) ScanUTF16(u []uint16) {
|
|
ip.l.Lock()
|
|
defer ip.l.Unlock()
|
|
ip.ut16 = append(ip.ut16, u...)
|
|
for len(ip.ut16) > 0 {
|
|
if !utf16.IsSurrogate(rune(ip.ut16[0])) {
|
|
ip.buf = append(ip.buf, rune(ip.ut16[0]))
|
|
ip.ut16 = ip.ut16[1:]
|
|
} else if len(ip.ut16) > 1 {
|
|
ip.buf = append(ip.buf, utf16.DecodeRune(rune(ip.ut16[0]), rune(ip.ut16[1])))
|
|
ip.ut16 = ip.ut16[2:]
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|