chore: ⬆️ updated deps
This commit is contained in:
+944
@@ -0,0 +1,944 @@
|
||||
// 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 == '\x1b' {
|
||||
// Per ECMA-48 §5.3.1, ESC restarts the escape
|
||||
// sequence machine from any intermediate state.
|
||||
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)
|
||||
}
|
||||
} else 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 r == '\x1b' {
|
||||
// Per ECMA-48 §5.3.1, ESC restarts the escape
|
||||
// sequence machine from any intermediate state.
|
||||
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)
|
||||
}
|
||||
} else 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user