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