forked from adamdruppe/arsd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
terminalemulator.d
5119 lines (4400 loc) · 144 KB
/
terminalemulator.d
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
/**
FIXME: writing a line in color then a line in ordinary does something
wrong.
# huh if i do underline then change color it undoes the underline
FIXME: make shift+enter send something special to the application
and shift+space, etc.
identify itself somehow too for client extensions
ctrl+space is supposed to send char 0.
ctrl+click on url pattern could open in browser perhaps
FIXME: scroll stuff should be higher level in the implementation.
so like scroll Rect, DirectionAndAmount
There should be a redraw thing that is given batches of instructions
in here that the other thing just implements.
FIXME: the save stack stuff should do cursor style too
This is an extendible unix terminal emulator and some helper functions to help actually implement one.
You'll have to subclass TerminalEmulator and implement the abstract functions as well as write a drawing function for it.
See nestedterminalemulator.d or main.d for how I did it.
*/
module arsd.terminalemulator;
/+
FIXME
terminal optimization:
first invalidated + last invalidated to slice the array
when looking for things that need redrawing.
+/
import arsd.color;
import std.algorithm : max;
enum extensionMagicIdentifier = "ARSD Terminal Emulator binary extension data follows:";
/+
The ;90 ones are my extensions.
90 - clipboard extensions
91 - image extensions
92 - hyperlink extensions
+/
enum terminalIdCode = "\033[?64;1;2;6;9;15;16;17;18;21;22;28;90;91;92c";
interface NonCharacterData {
//const(ubyte)[] serialize();
}
struct BinaryDataTerminalRepresentation {
int width;
int height;
TerminalEmulator.TerminalCell[] representation;
}
// old name, don't use in new programs anymore.
deprecated alias BrokenUpImage = BinaryDataTerminalRepresentation;
struct CustomGlyph {
TrueColorImage image;
dchar substitute;
}
void unknownEscapeSequence(in char[] esc) {
import std.file;
version(Posix) {
debug append("/tmp/arsd-te-bad-esc-sequences.txt", esc ~ "\n");
} else {
debug append("arsd-te-bad-esc-sequences.txt", esc ~ "\n");
}
}
// This is used for the double-click word selection
bool isWordSeparator(dchar ch) {
return ch == ' ' || ch == '"' || ch == '<' || ch == '>' || ch == '(' || ch == ')' || ch == ',';
}
TerminalEmulator.TerminalCell[] sliceTrailingWhitespace(TerminalEmulator.TerminalCell[] t) {
size_t end = t.length;
while(end >= 1) {
if(t[end-1].hasNonCharacterData || t[end-1].ch != ' ')
break;
end--;
}
t = t[0 .. end];
/*
import std.stdio;
foreach(ch; t)
write(ch.ch);
writeln("*");
*/
return t;
}
struct ScopeBuffer(T, size_t maxSize, bool allowGrowth = false) {
T[maxSize] bufferInternal;
T[] buffer;
size_t length;
bool isNull = true;
T[] opSlice() { return isNull ? null : buffer[0 .. length]; }
void opOpAssign(string op : "~")(in T rhs) {
if(buffer is null) buffer = bufferInternal[];
isNull = false;
static if(allowGrowth) {
if(this.length == buffer.length)
buffer.length = buffer.length * 2;
buffer[this.length++] = rhs;
} else {
if(this.length < buffer.length) // i am silently discarding more crap
buffer[this.length++] = rhs;
}
}
void opOpAssign(string op : "~")(in T[] rhs) {
if(buffer is null) buffer = bufferInternal[];
isNull = false;
buffer[this.length .. this.length + rhs.length] = rhs[];
this.length += rhs.length;
}
void opAssign(in T[] rhs) {
isNull = rhs is null;
if(buffer is null) buffer = bufferInternal[];
buffer[0 .. rhs.length] = rhs[];
this.length = rhs.length;
}
void opAssign(typeof(null)) {
isNull = true;
length = 0;
}
T opIndex(size_t idx) {
assert(!isNull);
assert(idx < length);
return buffer[idx];
}
void clear() {
isNull = true;
length = 0;
}
}
/**
An abstract class that does terminal emulation. You'll have to subclass it to make it work.
The terminal implements a subset of what xterm does and then, optionally, some special features.
Its linear mode (normal) screen buffer is infinitely long and infinitely wide. It is the responsibility
of your subclass to do line wrapping, etc., for display. This i think is actually incompatible with xterm but meh.
actually maybe it *should* automatically wrap them. idk. I think GNU screen does both. FIXME decide.
Its cellular mode (alternate) screen buffer can be any size you want.
*/
class TerminalEmulator {
/* override these to do stuff on the interface.
You might be able to stub them out if there's no state maintained on the target, since TerminalEmulator maintains its own internal state */
protected abstract void changeWindowTitle(string); /// the title of the window
protected abstract void changeIconTitle(string); /// the shorter window/iconified window
protected abstract void changeWindowIcon(IndexedImage); /// change the window icon. note this may be null
protected abstract void changeCursorStyle(CursorStyle); /// cursor style
protected abstract void changeTextAttributes(TextAttributes); /// current text output attributes
protected abstract void soundBell(); /// sounds the bell
protected abstract void sendToApplication(scope const(void)[]); /// send some data to the program running in the terminal, so keypresses etc.
protected abstract void copyToClipboard(string); /// copy the given data to the clipboard (or you can do nothing if you can't)
protected abstract void pasteFromClipboard(void delegate(in char[])); /// requests a paste. we pass it a delegate that should accept the data
protected abstract void copyToPrimary(string); /// copy the given data to the PRIMARY X selection (or you can do nothing if you can't)
protected abstract void pasteFromPrimary(void delegate(in char[])); /// requests a paste from PRIMARY. we pass it a delegate that should accept the data
abstract protected void requestExit(); /// the program is finished and the terminal emulator is requesting you to exit
/// Signal the UI that some attention should be given, e.g. blink the taskbar or sound the bell.
/// The default is to ignore the demand by instantly acknowledging it - if you override this, do NOT call super().
protected void demandAttention() {
attentionReceived();
}
/// After it demands attention, call this when the attention has been received
/// you may call it immediately to ignore the demand (the default)
public void attentionReceived() {
attentionDemanded = false;
}
protected final {
version(invalidator_2) {
int invalidatedMin;
int invalidatedMax;
}
void clearInvalidatedRange() {
version(invalidator_2) {
invalidatedMin = int.max;
invalidatedMax = 0;
}
}
void extendInvalidatedRange() {
version(invalidator_2) {
invalidatedMin = 0;
invalidatedMax = int.max;
}
}
void extendInvalidatedRange(int x, int y, int x2, int y2) {
version(invalidator_2) {
extendInvalidatedRange(y * screenWidth + x, y2 * screenWidth + x2);
}
}
void extendInvalidatedRange(int o1, int o2) {
version(invalidator_2) {
if(o1 < invalidatedMin)
invalidatedMin = o1;
if(o2 > invalidatedMax)
invalidatedMax = o2;
if(invalidatedMax < invalidatedMin)
invalidatedMin = invalidatedMax;
}
}
}
// I believe \033[50buffer[] and up are available for extensions everywhere.
// when keys are shifted, xterm sends them as \033[1;2F for example with end. but is this even sane? how would we do it with say, F5?
// apparently shifted F5 is ^[[15;2~
// alt + f5 is ^[[15;3~
// alt+shift+f5 is ^[[15;4~
private string pasteDataPending = null;
protected void justRead() {
if(pasteDataPending.length) {
sendPasteData(pasteDataPending);
import core.thread; Thread.sleep(50.msecs); // hack to keep it from closing, broken pipe i think
}
}
// my custom extension.... the data is the text content of the link, the identifier is some bits attached to the unit
public void sendHyperlinkData(scope const(dchar)[] data, uint identifier) {
if(bracketedHyperlinkMode) {
sendToApplication("\033[220~");
import std.conv;
// FIXME: that second 0 is a "command", like which menu option, which mouse button, etc.
sendToApplication(to!string(identifier) ~ ";0;" ~ to!string(data));
sendToApplication("\033[221~");
} else {
// without bracketed hyperlink, it simulates a paste
import std.conv;
sendPasteData(to!string(data));
}
}
public void sendPasteData(scope const(char)[] data) {
//if(pasteDataPending.length)
//throw new Exception("paste data being discarded, wtf, shouldnt happen");
// FIXME: i should put it all together so the brackets don't get separated by threads
if(bracketedPasteMode)
sendToApplication("\033[200~");
version(use_libssh2)
enum MAX_PASTE_CHUNK = 1024 * 40;
else
enum MAX_PASTE_CHUNK = 1024 * 1024 * 10;
if(data.length > MAX_PASTE_CHUNK) {
// need to chunk it in order to receive echos, etc,
// to avoid deadlocks
pasteDataPending = data[MAX_PASTE_CHUNK .. $].idup;
data = data[0 .. MAX_PASTE_CHUNK];
} else {
pasteDataPending = null;
}
if(data.length)
sendToApplication(data);
if(bracketedPasteMode)
sendToApplication("\033[201~");
}
private string overriddenSelection;
protected void cancelOverriddenSelection() {
if(overriddenSelection.length == 0)
return;
overriddenSelection = null;
sendToApplication("\033[27;0;987136~"); // fake "select none" key, see terminal.d's ProprietaryPseudoKeys for values.
// The reason that proprietary thing is ok is setting the selection is itself a proprietary extension
// so if it was ever set, it implies the user code is familiar with our magic.
}
public string getSelectedText() {
if(overriddenSelection.length)
return overriddenSelection;
return getPlainText(selectionStart, selectionEnd);
}
bool dragging;
int lastDragX, lastDragY;
public bool sendMouseInputToApplication(int termX, int termY, MouseEventType type, MouseButton button, bool shift, bool ctrl, bool alt) {
if(termX < 0)
termX = 0;
if(termX >= screenWidth)
termX = screenWidth - 1;
if(termY < 0)
termY = 0;
if(termY >= screenHeight)
termY = screenHeight - 1;
version(Windows) {
// I'm swapping these because my laptop doesn't have a middle button,
// and putty swaps them too by default so whatevs.
if(button == MouseButton.right)
button = MouseButton.middle;
else if(button == MouseButton.middle)
button = MouseButton.right;
}
int baseEventCode() {
int b;
// lol the xterm mouse thing sucks like javascript! unbelievable
// it doesn't support two buttons at once...
if(button == MouseButton.left)
b = 0;
else if(button == MouseButton.right)
b = 2;
else if(button == MouseButton.middle)
b = 1;
else if(button == MouseButton.wheelUp)
b = 64 | 0;
else if(button == MouseButton.wheelDown)
b = 64 | 1;
else
b = 3; // none pressed or button released
if(shift)
b |= 4;
if(ctrl)
b |= 16;
if(alt) // sending alt as meta
b |= 8;
return b;
}
if(type == MouseEventType.buttonReleased) {
// X sends press and release on wheel events, but we certainly don't care about those
if(button == MouseButton.wheelUp || button == MouseButton.wheelDown)
return false;
if(dragging) {
auto text = getSelectedText();
if(text.length) {
copyToPrimary(text);
} else if(!mouseButtonReleaseTracking || shift || (selectiveMouseTracking && ((!alternateScreenActive || scrollingBack) || termY != 0) && termY != cursorY)) {
// hyperlink check
int idx = termY * screenWidth + termX;
auto screen = (alternateScreenActive ? alternateScreen : normalScreen);
if(screen[idx].hyperlinkStatus & 0x01) {
// it is a link! need to find the beginning and the end
auto start = idx;
auto end = idx;
auto value = screen[idx].hyperlinkStatus;
while(start > 0 && screen[start].hyperlinkStatus == value)
start--;
if(screen[start].hyperlinkStatus != value)
start++;
while(end < screen.length && screen[end].hyperlinkStatus == value)
end++;
uint number;
dchar[64] buffer;
foreach(i, ch; screen[start .. end]) {
if(i >= buffer.length)
break;
if(!ch.hasNonCharacterData)
buffer[i] = ch.ch;
if(i < 16) {
number |= (ch.hyperlinkBit ? 1 : 0) << i;
}
}
sendHyperlinkData(buffer[0 .. end - start], number);
}
}
}
dragging = false;
if(mouseButtonReleaseTracking) {
int b = baseEventCode;
b |= 3; // always send none / button released
ScopeBuffer!(char, 16) buffer;
buffer ~= "\033[M";
buffer ~= cast(char) (b | 32);
addMouseCoordinates(buffer, termX, termY);
//buffer ~= cast(char) (termX+1 + 32);
//buffer ~= cast(char) (termY+1 + 32);
sendToApplication(buffer[]);
}
}
if(type == MouseEventType.motion) {
if(termX != lastDragX || termY != lastDragY) {
lastDragY = termY;
lastDragX = termX;
if(mouseMotionTracking || (mouseButtonMotionTracking && button)) {
int b = baseEventCode;
ScopeBuffer!(char, 16) buffer;
buffer ~= "\033[M";
buffer ~= cast(char) ((b | 32) + 32);
addMouseCoordinates(buffer, termX, termY);
//buffer ~= cast(char) (termX+1 + 32);
//buffer ~= cast(char) (termY+1 + 32);
sendToApplication(buffer[]);
}
if(dragging) {
auto idx = termY * screenWidth + termX;
// the no-longer-selected portion needs to be invalidated
int start, end;
if(idx > selectionEnd) {
start = selectionEnd;
end = idx;
} else {
start = idx;
end = selectionEnd;
}
if(start < 0 || end >= ((alternateScreenActive ? alternateScreen.length : normalScreen.length)))
return false;
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
cell.invalidated = true;
cell.selected = false;
}
extendInvalidatedRange(start, end);
cancelOverriddenSelection();
selectionEnd = idx;
// and the freshly selected portion needs to be invalidated
if(selectionStart > selectionEnd) {
start = selectionEnd;
end = selectionStart;
} else {
start = selectionStart;
end = selectionEnd;
}
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[start .. end]) {
cell.invalidated = true;
cell.selected = true;
}
extendInvalidatedRange(start, end);
return true;
}
}
}
if(type == MouseEventType.buttonPressed) {
// double click detection
import std.datetime;
static SysTime lastClickTime;
static int consecutiveClicks = 1;
if(button != MouseButton.wheelUp && button != MouseButton.wheelDown) {
if(Clock.currTime() - lastClickTime < dur!"msecs"(350))
consecutiveClicks++;
else
consecutiveClicks = 1;
lastClickTime = Clock.currTime();
}
// end dbl click
if(!(shift) && mouseButtonTracking) {
if(selectiveMouseTracking && termY != 0 && termY != cursorY) {
if(button == MouseButton.left || button == MouseButton.right)
goto do_default_behavior;
if((!alternateScreenActive || scrollingBack) && (button == MouseButton.wheelUp || button.MouseButton.wheelDown))
goto do_default_behavior;
}
// top line only gets special cased on full screen apps
if(selectiveMouseTracking && (!alternateScreenActive || scrollingBack) && termY == 0 && cursorY != 0)
goto do_default_behavior;
int b = baseEventCode;
int x = termX;
int y = termY;
x++; y++; // applications expect it to be one-based
ScopeBuffer!(char, 16) buffer;
buffer ~= "\033[M";
buffer ~= cast(char) (b | 32);
addMouseCoordinates(buffer, termX, termY);
//buffer ~= cast(char) (x + 32);
//buffer ~= cast(char) (y + 32);
sendToApplication(buffer[]);
} else {
do_default_behavior:
if(button == MouseButton.middle) {
pasteFromPrimary(&sendPasteData);
}
if(button == MouseButton.wheelUp) {
scrollback(alt ? 0 : (ctrl ? 10 : 1), alt ? -(ctrl ? 10 : 1) : 0);
return true;
}
if(button == MouseButton.wheelDown) {
scrollback(alt ? 0 : -(ctrl ? 10 : 1), alt ? (ctrl ? 10 : 1) : 0);
return true;
}
if(button == MouseButton.left) {
// we invalidate the old selection since it should no longer be highlighted...
makeSelectionOffsetsSane(selectionStart, selectionEnd);
cancelOverriddenSelection();
auto activeScreen = (alternateScreenActive ? &alternateScreen : &normalScreen);
foreach(ref cell; (*activeScreen)[selectionStart .. selectionEnd]) {
cell.invalidated = true;
cell.selected = false;
}
extendInvalidatedRange(selectionStart, selectionEnd);
if(consecutiveClicks == 1) {
selectionStart = termY * screenWidth + termX;
selectionEnd = selectionStart;
} else if(consecutiveClicks == 2) {
selectionStart = termY * screenWidth + termX;
selectionEnd = selectionStart;
while(selectionStart > 0 && !isWordSeparator((*activeScreen)[selectionStart-1].ch)) {
selectionStart--;
}
while(selectionEnd < (*activeScreen).length && !isWordSeparator((*activeScreen)[selectionEnd].ch)) {
selectionEnd++;
}
} else if(consecutiveClicks == 3) {
selectionStart = termY * screenWidth;
selectionEnd = selectionStart + screenWidth;
}
dragging = true;
lastDragX = termX;
lastDragY = termY;
// then invalidate the new selection as well since it should be highlighted
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[selectionStart .. selectionEnd]) {
cell.invalidated = true;
cell.selected = true;
}
extendInvalidatedRange(selectionStart, selectionEnd);
return true;
}
if(button == MouseButton.right) {
int changed1;
int changed2;
cancelOverriddenSelection();
auto click = termY * screenWidth + termX;
if(click < selectionStart) {
auto oldSelectionStart = selectionStart;
selectionStart = click;
changed1 = selectionStart;
changed2 = oldSelectionStart;
} else if(click > selectionEnd) {
auto oldSelectionEnd = selectionEnd;
selectionEnd = click;
changed1 = oldSelectionEnd;
changed2 = selectionEnd;
}
foreach(ref cell; (alternateScreenActive ? alternateScreen : normalScreen)[changed1 .. changed2]) {
cell.invalidated = true;
cell.selected = true;
}
extendInvalidatedRange(changed1, changed2);
auto text = getPlainText(selectionStart, selectionEnd);
if(text.length) {
copyToPrimary(text);
}
return true;
}
}
}
return false;
}
private void addMouseCoordinates(ref ScopeBuffer!(char, 16) buffer, int x, int y) {
// 1-based stuff and 32 is the base value
x += 1 + 32;
y += 1 + 32;
if(utf8MouseMode) {
import std.utf;
char[4] str;
foreach(char ch; str[0 .. encode(str, x)])
buffer ~= ch;
foreach(char ch; str[0 .. encode(str, y)])
buffer ~= ch;
} else {
buffer ~= cast(char) x;
buffer ~= cast(char) y;
}
}
protected void returnToNormalScreen() {
alternateScreenActive = false;
if(cueScrollback) {
showScrollbackOnScreen(normalScreen, 0, true, 0);
newLine(false);
cueScrollback = false;
}
notifyScrollbarRelevant(true, true);
extendInvalidatedRange();
}
protected void outputOccurred() { }
private int selectionStart; // an offset into the screen buffer
private int selectionEnd; // ditto
void requestRedraw() {}
private bool skipNextChar;
// assuming Key is an enum with members just like the one in simpledisplay.d
// returns true if it was handled here
protected bool defaultKeyHandler(Key)(Key key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
enum bool KeyHasNamedAscii = is(typeof(Key.A));
static string magic() {
string code;
foreach(member; __traits(allMembers, TerminalKey))
if(member != "Escape")
code ~= "case Key." ~ member ~ ": if(sendKeyToApplication(TerminalKey." ~ member ~ "
, shift ?true:false
, alt ?true:false
, ctrl ?true:false
, windows ?true:false
)) requestRedraw(); return true;";
return code;
}
void specialAscii(dchar what) {
if(!alt)
skipNextChar = true;
if(sendKeyToApplication(
cast(TerminalKey) what
, shift ? true:false
, alt ? true:false
, ctrl ? true:false
, windows ? true:false
)) requestRedraw();
}
static if(KeyHasNamedAscii) {
enum Space = Key.Space;
enum Enter = Key.Enter;
enum Backspace = Key.Backspace;
enum Tab = Key.Tab;
enum Escape = Key.Escape;
} else {
enum Space = ' ';
enum Enter = '\n';
enum Backspace = '\b';
enum Tab = '\t';
enum Escape = '\033';
}
switch(key) {
//// I want the escape key to send twice to differentiate it from
//// other escape sequences easily.
//case Key.Escape: sendToApplication("\033"); break;
/*
case Key.V:
case Key.C:
if(shift && ctrl) {
skipNextChar = true;
if(key == Key.V)
pasteFromClipboard(&sendPasteData);
else if(key == Key.C)
copyToClipboard(getSelectedText());
}
break;
*/
// expansion of my own for like shift+enter to terminal.d users
case Enter, Backspace, Tab, Escape:
if(shift || alt || ctrl) {
static if(KeyHasNamedAscii) {
specialAscii(
cast(TerminalKey) (
key == Key.Enter ? '\n' :
key == Key.Tab ? '\t' :
key == Key.Backspace ? '\b' :
key == Key.Escape ? '\033' :
0 /* assert(0) */
)
);
} else {
specialAscii(key);
}
return true;
}
break;
case Space:
if(alt) { // it used to be shift || alt here, but like shift+space is more trouble than it is worth in actual usage experience. too easily to accidentally type it in the middle of something else to be unambiguously useful. I wouldn't even set a hotkey on it so gonna just send it as plain space always.
// ctrl+space sends 0 per normal translation char rules
specialAscii(' ');
return true;
}
break;
mixin(magic());
static if(is(typeof(Key.Shift))) {
// modifiers are not ascii, ignore them here
case Key.Shift, Key.Ctrl, Key.Alt, Key.Windows, Key.Alt_r, Key.Shift_r, Key.Ctrl_r, Key.CapsLock, Key.NumLock:
// nor are these special keys that don't return characters
case Key.Menu, Key.Pause, Key.PrintScreen:
return false;
}
default:
// alt basically always get special treatment, since it doesn't
// generate anything from the char handler. but shift and ctrl
// do, so we'll just use that unless both are pressed, in which
// case I want to go custom to differentiate like ctrl+c from ctrl+shift+c and such.
// FIXME: xterm offers some control on this, see: https://invisible-island.net/xterm/xterm.faq.html#xterm_modother
if(alt || (shift && ctrl)) {
if(key >= 'A' && key <= 'Z')
key += 32; // always use lowercase for as much consistency as we can since the shift modifier need not apply here. Windows' keysyms are uppercase while X's are lowercase too
specialAscii(key);
if(!alt)
skipNextChar = true;
return true;
}
}
return true;
}
protected bool defaultCharHandler(dchar c) {
if(skipNextChar) {
skipNextChar = false;
return true;
}
endScrollback();
char[4] str;
char[5] send;
import std.utf;
//if(c == '\n') c = '\r'; // terminal seem to expect enter to send 13 instead of 10
auto data = str[0 .. encode(str, c)];
// on X11, the delete key can send a 127 character too, but that shouldn't be sent to the terminal since xterm shoots \033[3~ instead, which we handle in the KeyEvent handler.
if(c != 127)
sendToApplication(data);
return true;
}
/// Send a non-character key sequence
public bool sendKeyToApplication(TerminalKey key, bool shift = false, bool alt = false, bool ctrl = false, bool windows = false) {
bool redrawRequired = false;
if((!alternateScreenActive || scrollingBack) && key == TerminalKey.ScrollLock) {
toggleScrollLock();
return true;
}
/*
So ctrl + A-Z, [, \, ], ^, and _ are all chars 1-31
ctrl+5 send ^]
FIXME: for alt+keys and the other ctrl+them, send the xterm ascii magc thing terminal.d knows how to use
*/
// scrollback controls. Unlike xterm, I only want to do this on the normal screen, since alt screen
// doesn't have scrollback anyway. Thus the key will be forwarded to the application.
if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageUp && (shift || scrollLock)) {
scrollback(10);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.PageDown && (shift || scrollLock)) {
scrollback(-10);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Left && (shift || scrollLock)) {
scrollback(0, ctrl ? -10 : -1);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Right && (shift || scrollLock)) {
scrollback(0, ctrl ? 10 : 1);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Up && (shift || scrollLock)) {
scrollback(ctrl ? 10 : 1);
return true;
} else if((!alternateScreenActive || scrollingBack) && key == TerminalKey.Down && (shift || scrollLock)) {
scrollback(ctrl ? -10 : -1);
return true;
} else if((!alternateScreenActive || scrollingBack)) { // && ev.key != Key.Shift && ev.key != Key.Shift_r) {
if(endScrollback())
redrawRequired = true;
}
void sendToApplicationModified(string s, int key = 0) {
bool anyModifier = shift || alt || ctrl || windows;
if(!anyModifier || applicationCursorKeys)
sendToApplication(s); // FIXME: applicationCursorKeys can still be shifted i think but meh
else {
ScopeBuffer!(char, 16) modifierNumber;
char otherModifier = 0;
if(shift && alt && ctrl) modifierNumber = "8";
if(alt && ctrl && !shift) modifierNumber = "7";
if(shift && ctrl && !alt) modifierNumber = "6";
if(ctrl && !shift && !alt) modifierNumber = "5";
if(shift && alt && !ctrl) modifierNumber = "4";
if(alt && !shift && !ctrl) modifierNumber = "3";
if(shift && !alt && !ctrl) modifierNumber = "2";
// FIXME: meta and windows
// windows is an extension
if(windows) {
if(modifierNumber.length)
otherModifier = '2';
else
modifierNumber = "20";
/* // the below is what we're really doing
int mn = 0;
if(modifierNumber.length)
mn = modifierNumber[0] + '0';
mn += 20;
*/
}
string keyNumber;
char terminator;
if(s[$-1] == '~') {
keyNumber = s[2 .. $-1];
terminator = '~';
} else {
keyNumber = "1";
terminator = s[$ - 1];
}
ScopeBuffer!(char, 32) buffer;
buffer ~= "\033[";
buffer ~= keyNumber;
buffer ~= ";";
if(otherModifier)
buffer ~= otherModifier;
buffer ~= modifierNumber[];
if(key) {
buffer ~= ";";
import std.conv;
buffer ~= to!string(key);
}
buffer ~= terminator;
// the xterm style is last bit tell us what it is
sendToApplication(buffer[]);
}
}
alias TerminalKey Key;
import std.stdio;
// writefln("Key: %x", cast(int) key);
switch(key) {
case Key.Left: sendToApplicationModified(applicationCursorKeys ? "\033OD" : "\033[D"); break;
case Key.Up: sendToApplicationModified(applicationCursorKeys ? "\033OA" : "\033[A"); break;
case Key.Down: sendToApplicationModified(applicationCursorKeys ? "\033OB" : "\033[B"); break;
case Key.Right: sendToApplicationModified(applicationCursorKeys ? "\033OC" : "\033[C"); break;
case Key.Home: sendToApplicationModified(applicationCursorKeys ? "\033OH" : (1 ? "\033[H" : "\033[1~")); break;
case Key.Insert: sendToApplicationModified("\033[2~"); break;
case Key.Delete: sendToApplicationModified("\033[3~"); break;
// the 1? is xterm vs gnu screen. but i really want xterm compatibility.
case Key.End: sendToApplicationModified(applicationCursorKeys ? "\033OF" : (1 ? "\033[F" : "\033[4~")); break;
case Key.PageUp: sendToApplicationModified("\033[5~"); break;
case Key.PageDown: sendToApplicationModified("\033[6~"); break;
// the first one here is preferred, the second option is what xterm does if you turn on the "old function keys" option, which most apps don't actually expect
case Key.F1: sendToApplicationModified(1 ? "\033OP" : "\033[11~"); break;
case Key.F2: sendToApplicationModified(1 ? "\033OQ" : "\033[12~"); break;
case Key.F3: sendToApplicationModified(1 ? "\033OR" : "\033[13~"); break;
case Key.F4: sendToApplicationModified(1 ? "\033OS" : "\033[14~"); break;
case Key.F5: sendToApplicationModified("\033[15~"); break;
case Key.F6: sendToApplicationModified("\033[17~"); break;
case Key.F7: sendToApplicationModified("\033[18~"); break;
case Key.F8: sendToApplicationModified("\033[19~"); break;
case Key.F9: sendToApplicationModified("\033[20~"); break;
case Key.F10: sendToApplicationModified("\033[21~"); break;
case Key.F11: sendToApplicationModified("\033[23~"); break;
case Key.F12: sendToApplicationModified("\033[24~"); break;
case Key.Escape: sendToApplicationModified("\033"); break;
// my extensions, see terminator.d for the other side of it
case Key.ScrollLock: sendToApplicationModified("\033[70~"); break;
// xterm extension for arbitrary modified unicode chars
default:
sendToApplicationModified("\033[27~", key);
}
return redrawRequired;
}
/// if a binary extension is triggered, the implementing class is responsible for figuring out how it should be made to fit into the screen buffer
protected /*abstract*/ BinaryDataTerminalRepresentation handleBinaryExtensionData(const(ubyte)[]) {
return BinaryDataTerminalRepresentation();
}
/// If you subclass this and return true, you can scroll on command without needing to redraw the entire screen;
/// returning true here suppresses the automatic invalidation of scrolled lines (except the new one).
protected bool scrollLines(int howMany, bool scrollUp) {
return false;
}
// might be worth doing the redraw magic in here too.
// FIXME: not implemented
@disable protected void drawTextSection(int x, int y, TextAttributes attributes, in dchar[] text, bool isAllSpaces) {
// if you implement this it will always give you a continuous block on a single line. note that text may be a bunch of spaces, in that case you can just draw the bg color to clear the area
// or you can redraw based on the invalidated flag on the buffer
}
// FIXME: what about image sections? maybe it is still necessary to loop through them
/// Style of the cursor
enum CursorStyle {
block, /// a solid block over the position (like default xterm or many gui replace modes)
underline, /// underlining the position (like the vga text mode default)
bar, /// a bar on the left side of the cursor position (like gui insert modes)
}
// these can be overridden, but don't have to be
TextAttributes defaultTextAttributes() {
TextAttributes ta;
ta.foregroundIndex = 256; // terminal.d uses this as Color.DEFAULT
ta.backgroundIndex = 256;
import std.process;
// I'm using the environment for this because my programs and scripts
// already know this variable and then it gets nicely inherited. It is
// also easy to set without buggering with other arguments. So works for me.
version(with_24_bit_color) {
if(environment.get("ELVISBG") == "dark") {
ta.foreground = Color.white;
ta.background = Color.black;
} else {
ta.foreground = Color.black;
ta.background = Color.white;
}
}
return ta;
}
Color defaultForeground;
Color defaultBackground;