-
-
Notifications
You must be signed in to change notification settings - Fork 360
Expand file tree
/
Copy pathinput.go
More file actions
1526 lines (1440 loc) · 41.6 KB
/
Copy pathinput.go
File metadata and controls
1526 lines (1440 loc) · 41.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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: '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
}