mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
Bumps [github.com/gdamore/tcell/v3](https://github.com/gdamore/tcell) from 3.4.1 to 3.4.2. - [Release notes](https://github.com/gdamore/tcell/releases) - [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv3.md) - [Commits](https://github.com/gdamore/tcell/compare/v3.4.1...v3.4.2) --- updated-dependencies: - dependency-name: github.com/gdamore/tcell/v3 dependency-version: 3.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
1592 lines
44 KiB
Go
1592 lines
44 KiB
Go
// 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
|
|
|
|
const (
|
|
// loneEscapeTimeout keeps bare Escape responsive when using legacy
|
|
// keyboard reporting, where ESC can also prefix an Alt-modified key.
|
|
loneEscapeTimeout = 200 * time.Millisecond
|
|
|
|
// escapeSequenceTimeout bounds incomplete escape sequences. Once a
|
|
// sequence introducer has arrived, it is no longer ambiguous with a lone
|
|
// Escape and can tolerate a substantially longer inter-byte delay.
|
|
escapeSequenceTimeout = time.Second
|
|
)
|
|
|
|
func newInputParser(eq chan<- Event) *inputParser {
|
|
return &inputParser{
|
|
evch: eq,
|
|
buf: make([]rune, 0, 128),
|
|
legacy: true,
|
|
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
|
|
legacy bool // keyboard protocol has ambiguous ESC prefixes
|
|
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
|
|
}
|
|
|
|
// waitDuration reports how long to wait for the next byte before resetting an
|
|
// incomplete escape sequence. A bare ESC is only ambiguous with legacy
|
|
// keyboard reporting; other protocols can use the longer sequence deadline.
|
|
func (ip *inputParser) waitDuration() time.Duration {
|
|
if ip.state == istInit {
|
|
return 0
|
|
}
|
|
if ip.state == istEsc && ip.legacy {
|
|
return loneEscapeTimeout
|
|
}
|
|
return escapeSequenceTimeout
|
|
}
|
|
|
|
func (ip *inputParser) WaitDuration() time.Duration {
|
|
ip.l.Lock()
|
|
defer ip.l.Unlock()
|
|
return ip.waitDuration()
|
|
}
|
|
|
|
func (ip *inputParser) SetKeyboardProtocol(protocol KeyProtocol) {
|
|
ip.l.Lock()
|
|
ip.legacy = protocol == LegacyKeyboard
|
|
nested := ip.nested
|
|
ip.l.Unlock()
|
|
if nested != nil {
|
|
nested.SetKeyboardProtocol(protocol)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
type ss3Key struct {
|
|
key Key
|
|
str string
|
|
}
|
|
|
|
// keys by their SS3 - used in application mode usually (legacy VT-style)
|
|
var ss3Keys = map[rune]ss3Key{
|
|
'A': {key: KeyUp},
|
|
'B': {key: KeyDown},
|
|
'C': {key: KeyRight},
|
|
'D': {key: KeyLeft},
|
|
'E': {key: KeyClear},
|
|
'F': {key: KeyEnd},
|
|
'H': {key: KeyHome},
|
|
'P': {key: KeyF1},
|
|
'Q': {key: KeyF2},
|
|
'R': {key: KeyF3},
|
|
'S': {key: KeyF4},
|
|
|
|
// DEC application-keypad sequences. The VT100 terminfo entry calls some
|
|
// of these F5-F10, but that is a terminfo naming artifact: a VT100 has
|
|
// only PF1-PF4. Decode them by their PC keypad navigation meanings.
|
|
'p': {key: KeyInsert},
|
|
'q': {key: KeyEnd},
|
|
'r': {key: KeyDown},
|
|
's': {key: KeyPgDn},
|
|
't': {key: KeyLeft},
|
|
'u': {key: KeyClear},
|
|
'v': {key: KeyRight},
|
|
'w': {key: KeyHome},
|
|
'x': {key: KeyUp},
|
|
'y': {key: KeyPgUp},
|
|
'M': {key: KeyEnter},
|
|
'n': {key: KeyDelete},
|
|
'j': {key: KeyRune, str: "*"},
|
|
'k': {key: KeyRune, str: "+"},
|
|
'l': {key: KeyRune, str: ","},
|
|
'm': {key: KeyRune, str: "-"},
|
|
'o': {key: KeyRune, str: "/"},
|
|
'X': {key: KeyRune, str: "="},
|
|
}
|
|
|
|
// 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.key, k.str, 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.key, k.str, calcModifier(m))
|
|
}
|
|
} else if m, err := strconv.Atoi(parts[0]); err == nil {
|
|
ip.postKey(k.key, k.str, 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 timeout := ip.waitDuration(); timeout > 0 && time.Since(ip.keyTime) > timeout {
|
|
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,
|
|
legacy: ip.legacy,
|
|
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.key, k.str, 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
|
|
}
|