- Notifications
You must be signed in to change notification settings - Fork 8.5k
/
Copy pathreadDataCooked.cpp
1732 lines (1536 loc) · 67.1 KB
/
readDataCooked.cpp
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 (c) Microsoft Corporation.
// Licensed under the MIT license.
#include"precomp.h"
#include"readDataCooked.hpp"
#include"alias.h"
#include"history.h"
#include"resource.h"
#include"stream.h"
#include"_stream.h"
#include"../interactivity/inc/ServiceLocator.hpp"
#defineCOOKED_READ_DEBUG0
#if COOKED_READ_DEBUG
#include<til/colorbrewer.h>
#endif
using Microsoft::Console::Interactivity::ServiceLocator;
using Microsoft::Console::VirtualTerminal::VtIo;
// Routine Description:
// - Constructs cooked read data class to hold context across key presses while a user is modifying their 'input line'.
// Arguments:
// - pInputBuffer - Buffer that data will be read from.
// - pInputReadHandleData - Context stored across calls from the same input handle to return partial data appropriately.
// - screenInfo - Output buffer that will be used for 'echoing' the line back to the user so they can see/manipulate it
// - UserBufferSize - The byte count of the buffer presented by the client
// - UserBuffer - The buffer that was presented by the client for filling with input data on read conclusion/return from server/host.
// - CtrlWakeupMask - Special client parameter to interrupt editing, end the wait, and return control to the client application
// - initialData - any text data that should be prepopulated into the buffer
// - pClientProcess - Attached process handle object
COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
_In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData,
SCREEN_INFORMATION& screenInfo,
_In_ size_t UserBufferSize,
_In_ char* UserBuffer,
_In_ ULONG CtrlWakeupMask,
_In_ const std::wstring_view exeName,
_In_ const std::wstring_view initialData,
_In_ ConsoleProcessHandle* const pClientProcess) :
ReadData(pInputBuffer, pInputReadHandleData),
_screenInfo{ screenInfo },
_userBuffer{ UserBuffer, UserBufferSize },
_exeName{ exeName },
_processHandle{ pClientProcess },
_history{ CommandHistory::s_Find(pClientProcess) },
_ctrlWakeupMask{ CtrlWakeupMask },
_insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() }
{
#ifndef UNIT_TESTING
// The screen buffer instance is basically a reference counted HANDLE given out to the user.
// We need to ensure that it stays alive for the duration of the read.
// Coincidentally this serves another important purpose: It checks whether we're allowed to read from
// the given buffer in the first place. If it's missing the FILE_SHARE_READ flag, we can't read from it.
//
// GH#16158: It's important that we hold a handle to the main instead of the alt buffer
// even if this cooked read targets the latter, because alt buffers are fake
// SCREEN_INFORMATION objects that are owned by the main buffer.
THROW_IF_FAILED(_screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, _tempHandle));
#endif
if (!initialData.empty())
{
// The console API around `nInitialChars` in `CONSOLE_READCONSOLE_CONTROL` is pretty weird.
// The way it works is that cmd.exe does a ReadConsole() with a `dwCtrlWakeupMask` that includes \t,
// so when you press tab it can autocomplete the prompt based on the available file names.
// The weird part is that it's not us who then prints the autocompletion. It's cmd.exe which calls WriteConsoleW().
// It then initiates another ReadConsole() where the `nInitialChars` is the amount of chars it wrote via WriteConsoleW().
//
// In other words, `nInitialChars` is a "trust me bro, I just wrote that in the buffer" API.
// This unfortunately means that the API is inherently broken: ReadConsole() visualizes control
// characters like Ctrl+X as "^X" and WriteConsoleW() doesn't and so the column counts don't match.
// Solving these issues is technically possible, but it's also quite difficult to do so correctly.
//
// But unfortunately (or fortunately) the initial implementation (from the 1990s up to 2023) looked something like that:
// cursor = cursor.GetPosition();
// cursor.x -= initialData.size();
// while (cursor.x < 0)
// {
// cursor.x += textBuffer.Width();
// cursor.y -= 1;
// }
//
// In other words, it assumed that the number of code units in the initial data corresponds 1:1 to
// the column count. This meant that the API never supported tabs for instance (nor wide glyphs).
//
//
// The new implementation is a lot more complex to be a little more correct.
// It replicates part of the _redisplay() logic to layout the text at various
// starting positions until it finds one that matches the current cursor position.
constauto cursorPos = _getViewportCursorPosition();
constauto size = _screenInfo.GetVtPageArea().Dimensions();
// Guess the initial cursor position based on the string length, assuming that 1 char = 1 column.
constauto columnRemainder = gsl::narrow_cast<til::CoordType>((initialData.size() % size.width));
constauto bestGuessColumn = (cursorPos.x - columnRemainder + size.width) % size.width;
std::wstring line;
LayoutResult res;
til::CoordType bestDistance = til::CoordTypeMax;
til::CoordType bestColumnBegin = 0;
til::CoordType bestNewlineCount = 0;
line.reserve(size.width);
// We're given an "end position" and a string and we need to find its starting position.
// The problem is that a wide glyph that doesn't fit into the last column of a row gets padded with a whitespace
// and then written on the next line. Because of this, multiple starting positions can result in the same end
// position and this prevents us from simply laying out the text backwards from the end position.
// To solve this, we do a brute force search for the best starting position that ends at the end position.
// The search is centered around `bestGuessColumn` with offsets 0, 1, -1, 2, -2, 3, -3, ...
for (til::CoordType i = 0, attempts = 2 * size.width; i <= attempts; i++)
{
// Hilarious bit-trickery that no one can read. But it works. Check it out in a debugger.
// The idea is to use bits 1:31 as the value (i >> 1) and bit 0 (i & 1) as a trigger to bit-flip the value.
// A bit-flipped positive number is negative, but offset by 1, so we add 1 at the end. Fun!
constauto offset = ((i >> 1) ^ ((i & 1) - 1)) + 1;
constauto columnBegin = bestGuessColumn + offset;
if (columnBegin < 0 || columnBegin >= size.width)
{
continue;
}
til::CoordType newlineCount = 0;
res.column = columnBegin;
for (size_t beg = 0; beg < initialData.size();)
{
line.clear();
res = _layoutLine(line, initialData, beg, res.column, size.width);
beg = res.offset;
if (res.column >= size.width)
{
res.column = 0;
newlineCount += 1;
}
}
constauto distance = abs(res.column - cursorPos.x);
if (distance < bestDistance)
{
bestDistance = distance;
bestColumnBegin = columnBegin;
bestNewlineCount = newlineCount;
}
if (distance == 0)
{
break;
}
}
auto originInViewport = cursorPos;
originInViewport.x = bestColumnBegin;
originInViewport.y = originInViewport.y - bestNewlineCount;
if (originInViewport.y < 0)
{
originInViewport = {};
}
// We can't mark the buffer as dirty because this messes up the cursor position for cmd
// somehow when the prompt is longer than the viewport height. I haven't investigated
// why that happens, but it works decently well enough that it's not too important.
_buffer.assign(initialData);
_bufferDirtyBeg = _buffer.size();
_bufferCursor = _buffer.size();
_originInViewport = originInViewport;
_pagerPromptEnd = cursorPos;
_pagerHeight = std::min(size.height, bestNewlineCount + 1);
}
}
// Routine Description:
// - This routine is called to complete a cooked read that blocked in ReadInputBuffer.
// - The context of the read was saved in the CookedReadData structure.
// - This routine is called when events have been written to the input buffer.
// - It is called in the context of the writing thread.
// - It may be called more than once.
// Arguments:
// - TerminationReason - if this routine is called because a ctrl-c or ctrl-break was seen, this argument
// contains CtrlC or CtrlBreak. If the owning thread is exiting, it will have ThreadDying. Otherwise 0.
// - fIsUnicode - Whether to convert the final data to A (using Console Input CP) at the end or treat everything as Unicode (UCS-2)
// - pReplyStatus - The status code to return to the client application that originally called the API (before it was queued to wait)
// - pNumBytes - The number of bytes of data that the server/driver will need to transmit back to the client process
// - pControlKeyState - For certain types of reads, this specifies which modifier keys were held.
// - pOutputData - not used
// Return Value:
// - true if the wait is done and result buffer/status code can be sent back to the client.
// - false if we need to continue to wait until more data is available.
boolCOOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason,
constboolfIsUnicode,
_Out_ NTSTATUS* const pReplyStatus,
_Out_ size_t* const pNumBytes,
_Out_ DWORD* const pControlKeyState,
_Out_ void* const/*pOutputData*/) noexcept
try
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
*pNumBytes = 0;
*pControlKeyState = 0;
*pReplyStatus = STATUS_SUCCESS;
// if ctrl-c or ctrl-break was seen, terminate read.
if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak)))
{
*pReplyStatus = STATUS_ALERTED;
gci.SetCookedReadData(nullptr);
returntrue;
}
// See if we were called because the thread that owns this wait block is exiting.
if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
{
*pReplyStatus = STATUS_THREAD_IS_TERMINATING;
gci.SetCookedReadData(nullptr);
returntrue;
}
// We must see if we were woken up because the handle is being closed. If
// so, we decrement the read count. If it goes to zero, we wake up the
// close thread. Otherwise, we wake up any other thread waiting for data.
if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing))
{
*pReplyStatus = STATUS_ALERTED;
gci.SetCookedReadData(nullptr);
returntrue;
}
if (Read(fIsUnicode, *pNumBytes, *pControlKeyState))
{
gci.SetCookedReadData(nullptr);
returntrue;
}
returnfalse;
}
NT_CATCH_RETURN()
void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(constvoid* oldBuffer, void* newBuffer) noexcept
{
// See the comment in WaitBlock.cpp for more information.
if (_userBuffer.data() == oldBuffer)
{
_userBuffer = { static_cast<char*>(newBuffer), _userBuffer.size() };
}
}
// Routine Description:
// - Method that actually retrieves a character/input record from the buffer (key press form)
// and determines the next action based on the various possible cooked read modes.
// - Mode options include the F-keys popup menus, keyboard manipulation of the edit line, etc.
// - This method also does the actual copying of the final manipulated data into the return buffer.
// Arguments:
// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done.
// - numBytes - On in, the number of bytes available in the client
// buffer. On out, the number of bytes consumed in the client buffer.
// - controlKeyState - For some types of reads, this is the modifier key state with the last button press.
boolCOOKED_READ_DATA::Read(constbool isUnicode, size_t& numBytes, ULONG& controlKeyState)
{
controlKeyState = 0;
_readCharInputLoop();
// NOTE: Don't call _flushBuffer in a wil::scope_exit/defer.
// It may throw and throwing during an ongoing exception is a bad idea.
_redisplay();
if (_state == State::Accumulating)
{
returnfalse;
}
_handlePostCharInputLoop(isUnicode, numBytes, controlKeyState);
returntrue;
}
// Printing wide glyphs at the end of a row results in a forced line wrap and a padding whitespace to be inserted.
// When the text buffer resizes these padding spaces may vanish and the _distanceCursor and _distanceEnd measurements become inaccurate.
// To fix this, this function is called before a resize and will clear the input line. Afterward, RedrawAfterResize() will restore it.
voidCOOKED_READ_DATA::EraseBeforeResize()
{
// If we've already erased the buffer, we don't need to do it again.
if (_redrawPending)
{
return;
}
// If we don't have an origin, we've never had user input, and consequently there's nothing to erase.
if (!_originInViewport)
{
return;
}
_redrawPending = true;
// Position the cursor the start of the prompt before reflow.
// Then, after reflow, we'll be able to ask the buffer where it went (the new origin).
// This uses the buffer APIs directly, so that we don't emit unnecessary VT into ConPTY's output.
auto& textBuffer = _screenInfo.GetTextBuffer();
auto& cursor = textBuffer.GetCursor();
auto cursorPos = *_originInViewport;
_screenInfo.GetVtPageArea().ConvertFromOrigin(&cursorPos);
cursor.SetPosition(cursorPos);
}
// The counter-part to EraseBeforeResize().
voidCOOKED_READ_DATA::RedrawAfterResize()
{
if (!_redrawPending)
{
return;
}
_redrawPending = false;
// Get the new cursor position after the reflow, since it may have changed.
if (_originInViewport)
{
_originInViewport = _getViewportCursorPosition();
}
// Ensure that we don't use any scroll sequences or try to clear previous pager contents.
// They have all been erased with the CSI J above.
_pagerHeight = 0;
// Ensure that the entire buffer content is rewritten after the above CSI J.
_bufferDirtyBeg = 0;
_dirty = !_buffer.empty();
// Let _redisplay() know to inject a CSI J at the start of the output.
// This ensures we fully erase the previous contents, that are now in disarray.
_clearPending = true;
_redisplay();
}
voidCOOKED_READ_DATA::SetInsertMode(bool insertMode) noexcept
{
_insertMode = insertMode;
}
boolCOOKED_READ_DATA::IsEmpty() constnoexcept
{
return _buffer.empty() && _popups.empty();
}
boolCOOKED_READ_DATA::PresentingPopup() constnoexcept
{
return !_popups.empty();
}
til::point_span COOKED_READ_DATA::GetBoundaries() noexcept
{
constauto viewport = _screenInfo.GetViewport();
constauto virtualViewport = _screenInfo.GetVtPageArea();
staticconstexpr til::point min;
const til::point max{ viewport.RightInclusive(), viewport.BottomInclusive() };
// Convert from VT-viewport-relative coordinate space back to the console one.
auto beg = _getOriginInViewport();
virtualViewport.ConvertFromOrigin(&beg);
// Since the pager may be longer than the viewport is tall, we need to clamp the coordinates to still remain within
// the current viewport (the pager doesn't write outside of the viewport, since that's not supported by VT).
auto end = _pagerPromptEnd;
end.y -= _pagerContentTop;
end = std::clamp(end, min, max);
end.y += beg.y;
return { beg, end };
}
// _wordPrev and _wordNext implement the classic Windows word-wise cursor movement algorithm, as traditionally used by
// conhost, notepad, Visual Studio and other "old" applications. If you look closely you can see how they're the exact
// same "skip 1 char, skip x, skip not-x", but since the "x" between them is different (non-words for _wordPrev and
// words for _wordNext) it results in the inconsistent feeling that these have compared to more modern algorithms.
// TODO: GH#15787
size_tCOOKED_READ_DATA::_wordPrev(const std::wstring_view& chars, size_t position)
{
if (position != 0)
{
--position;
while (position != 0 && chars[position] == L'')
{
--position;
}
constauto dc = DelimiterClass(chars[position]);
while (position != 0 && DelimiterClass(chars[position - 1]) == dc)
{
--position;
}
}
return position;
}
size_tCOOKED_READ_DATA::_wordNext(const std::wstring_view& chars, size_t position)
{
if (position < chars.size())
{
++position;
constauto dc = DelimiterClass(chars[position - 1]);
while (position != chars.size() && dc == DelimiterClass(chars[position]))
{
++position;
}
while (position != chars.size() && chars[position] == L'')
{
++position;
}
}
return position;
}
// Reads text off of the InputBuffer and dispatches it to the current popup or otherwise into the _buffer contents.
voidCOOKED_READ_DATA::_readCharInputLoop()
{
while (_state == State::Accumulating)
{
constauto hasPopup = !_popups.empty();
auto charOrVkey = UNICODE_NULL;
auto commandLineEditingKeys = false;
auto popupKeys = false;
constauto pCommandLineEditingKeys = hasPopup ? nullptr : &commandLineEditingKeys;
constauto pPopupKeys = hasPopup ? &popupKeys : nullptr;
DWORD modifiers = 0;
constauto status = GetChar(_pInputBuffer, &charOrVkey, true, pCommandLineEditingKeys, pPopupKeys, &modifiers);
if (status == CONSOLE_STATUS_WAIT)
{
break;
}
THROW_IF_NTSTATUS_FAILED(status);
if (hasPopup)
{
constauto wch = static_cast<wchar_t>(popupKeys ? 0 : charOrVkey);
constauto vkey = static_cast<uint16_t>(popupKeys ? charOrVkey : 0);
_popupHandleInput(wch, vkey, modifiers);
}
else
{
if (commandLineEditingKeys)
{
_handleVkey(charOrVkey, modifiers);
}
else
{
_handleChar(charOrVkey, modifiers);
}
}
}
}
// Handles character input for _readCharInputLoop() when no popups exist.
voidCOOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers)
{
// All paths in this function modify the buffer.
if (_ctrlWakeupMask != 0 && wch < L'' && (_ctrlWakeupMask & (1 << wch)))
{
// The old implementation (all the way since the 90s) overwrote the character at the current cursor position with the given wch.
// But simultaneously it incremented the buffer length, which would have only worked if it was written at the end of the buffer.
// Press tab past the "f" in the string "foo" and you'd get "f\to " (a trailing whitespace; the initial contents of the buffer back then).
// It's unclear whether the original intention was to write at the end of the buffer at all times or to implement an insert mode.
// I went with insert mode.
//
// The old implementation also failed to clear the end of the prompt if you pressed tab in the middle of it.
// You can reproduce this issue by launching cmd in an old conhost build and writing "<command that doesn't exist> foo",
// moving your cursor to the space past the <command> and pressing tab. Nothing will happen but the "foo" will be inaccessible.
// I've now fixed this behavior by adding an additional Replace() before the _flushBuffer() call that removes the tail end.
//
// It is important that we don't actually print that character out though, as it's only for the calling application to see.
// That's why we flush the contents before the insertion and then ensure that the _flushBuffer() call in Read() exits early.
_replace(_bufferCursor, npos, nullptr, 0);
_redisplay();
_replace(_bufferCursor, 0, &wch, 1);
_dirty = false;
_controlKeyState = modifiers;
_transitionState(State::DoneWithWakeupMask);
return;
}
switch (wch)
{
case UNICODE_CARRIAGERETURN:
{
// NOTE: Don't append newlines to the buffer just yet! See _handlePostCharInputLoop for more information.
_setCursorPosition(npos);
_transitionState(State::DoneWithCarriageReturn);
return;
}
case EXTKEY_ERASE_PREV_WORD: // Ctrl+Backspace
case UNICODE_BACKSPACE:
if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT))
{
constauto cursor = _bufferCursor;
constauto pos = wch == EXTKEY_ERASE_PREV_WORD ? _wordPrev(_buffer, cursor) : TextBuffer::GraphemePrev(_buffer, cursor);
_replace(pos, cursor - pos, nullptr, 0);
return;
}
// If processed mode is disabled, control characters like backspace are treated like any other character.
break;
default:
break;
}
size_tremove = 0;
if (!_insertMode)
{
// TODO GH#15875: If the input grapheme is >1 char, then this will replace >1 grapheme
// --> We should accumulate input text as much as possible and then call _processInput with wstring_view.
constauto cursor = _bufferCursor;
remove = TextBuffer::GraphemeNext(_buffer, cursor) - cursor;
}
_replace(_bufferCursor, remove, &wch, 1);
}
// Handles non-character input for _readCharInputLoop() when no popups exist.
voidCOOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers)
{
constauto ctrlPressed = WI_IsAnyFlagSet(modifiers, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED);
constauto altPressed = WI_IsAnyFlagSet(modifiers, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED);
switch (vkey)
{
case VK_ESCAPE:
if (!_buffer.empty())
{
_replace(0, npos, nullptr, 0);
}
break;
case VK_HOME:
if (_bufferCursor > 0)
{
if (ctrlPressed)
{
_replace(0, _bufferCursor, nullptr, 0);
}
_setCursorPosition(0);
}
break;
case VK_END:
if (_bufferCursor < _buffer.size())
{
if (ctrlPressed)
{
_replace(_bufferCursor, npos, nullptr, 0);
}
_setCursorPosition(npos);
}
break;
case VK_LEFT:
if (_bufferCursor != 0)
{
if (ctrlPressed)
{
_setCursorPosition(_wordPrev(_buffer, _bufferCursor));
}
else
{
_setCursorPosition(TextBuffer::GraphemePrev(_buffer, _bufferCursor));
}
}
break;
case VK_F1:
case VK_RIGHT:
if (_bufferCursor != _buffer.size())
{
if (ctrlPressed && vkey == VK_RIGHT)
{
_setCursorPosition(_wordNext(_buffer, _bufferCursor));
}
else
{
_setCursorPosition(TextBuffer::GraphemeNext(_buffer, _bufferCursor));
}
}
elseif (_history)
{
// Traditionally pressing right at the end of an input line would paste characters from the previous command.
constauto cmd = _history->GetLastCommand();
constauto bufferSize = _buffer.size();
constauto cmdSize = cmd.size();
size_t bufferBeg = 0;
size_t cmdBeg = 0;
// We cannot just check if the cmd is longer than the _buffer, because we want to copy graphemes,
// not characters and there's no correlation between the number of graphemes and their byte length.
while (cmdBeg < cmdSize)
{
constauto cmdEnd = TextBuffer::GraphemeNext(cmd, cmdBeg);
if (bufferBeg >= bufferSize)
{
_replace(npos, 0, cmd.data() + cmdBeg, cmdEnd - cmdBeg);
break;
}
bufferBeg = TextBuffer::GraphemeNext(_buffer, bufferBeg);
cmdBeg = cmdEnd;
}
}
break;
case VK_INSERT:
_insertMode = !_insertMode;
_screenInfo.SetCursorDBMode(_insertMode != ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode());
break;
case VK_DELETE:
if (_bufferCursor < _buffer.size())
{
constauto beg = _bufferCursor;
constauto end = TextBuffer::GraphemeNext(_buffer, beg);
_replace(beg, end - beg, nullptr, 0);
}
break;
case VK_UP:
case VK_F5:
if (_history && !_history->AtFirstCommand())
{
_replace(_history->Retrieve(CommandHistory::SearchDirection::Previous));
}
break;
case VK_DOWN:
if (_history && !_history->AtLastCommand())
{
_replace(_history->Retrieve(CommandHistory::SearchDirection::Next));
}
break;
case VK_PRIOR:
if (_history && !_history->AtFirstCommand())
{
_replace(_history->RetrieveNth(0));
}
break;
case VK_NEXT:
if (_history && !_history->AtLastCommand())
{
_replace(_history->RetrieveNth(INT_MAX));
}
break;
case VK_F2:
if (_history)
{
_popupPush(PopupKind::CopyToChar);
}
break;
case VK_F3:
if (_history)
{
constauto last = _history->GetLastCommand();
if (last.size() > _bufferCursor)
{
constauto count = last.size() - _bufferCursor;
_replace(_bufferCursor, npos, last.data() + _bufferCursor, count);
}
}
break;
case VK_F4:
// Historically the CopyFromChar popup was constrained to only work when a history exists,
// but I don't see why that should be. It doesn't depend on _history at all.
_popupPush(PopupKind::CopyFromChar);
break;
case VK_F6:
// Don't ask me why but F6 is an alias for ^Z.
_handleChar(0x1a, modifiers);
break;
case VK_F7:
if (!ctrlPressed && !altPressed)
{
if (_history && _history->GetNumberOfCommands())
{
_popupPush(PopupKind::CommandList);
}
}
elseif (altPressed)
{
if (_history)
{
_history->Empty();
_history->Flags |= CommandHistory::CLE_ALLOCATED;
}
}
break;
case VK_F8:
if (_history)
{
CommandHistory::Index index = 0;
constauto cursorPos = _bufferCursor;
constauto prefix = std::wstring_view{ _buffer }.substr(0, cursorPos);
if (_history->FindMatchingCommand(prefix, _history->LastDisplayed, index, CommandHistory::MatchOptions::None))
{
_replace(_history->RetrieveNth(index));
_setCursorPosition(cursorPos);
}
}
break;
case VK_F9:
if (_history && _history->GetNumberOfCommands())
{
_popupPush(PopupKind::CommandNumber);
}
break;
case VK_F10:
// Alt+F10 clears the aliases for specifically cmd.exe.
if (altPressed)
{
Alias::s_ClearCmdExeAliases();
}
break;
default:
assert(false); // Unrecognized VK. Fix or don't call this function?
break;
}
}
// Handles any tasks that need to be completed after the read input loop finishes,
// like handling doskey aliases and converting the input to non-UTF16.
voidCOOKED_READ_DATA::_handlePostCharInputLoop(constbool isUnicode, size_t& numBytes, ULONG& controlKeyState)
{
auto writer = _userBuffer;
auto buffer = std::move(_buffer);
std::wstring_view input{ buffer };
size_t lineCount = 1;
if (_state == State::DoneWithCarriageReturn)
{
staticconstexpr std::wstring_view cr{ L"\r" };
staticconstexpr std::wstring_view crlf{ L"\r\n" };
constauto newlineSuffix = WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) ? crlf : cr;
std::wstring alias;
// Here's why we can't easily use _flushBuffer() to handle newlines:
//
// A carriage return (enter key) will increase the _distanceEnd by up to viewport-width many columns,
// since it increases the Y distance between the start and end by 1 (it's a newline after all).
// This will make _flushBuffer() think that the new _buffer is way longer than the old one and so
// _erase() ends up not erasing the tail end of the prompt, even if the new prompt is actually shorter.
//
// If you were to break this (remove this code and then append \r\n in _handleChar())
// you can reproduce the issue easily if you do this:
// * Run cmd.exe
// * Write "echo hello" and press Enter
// * Write "foobar foo bar" (don't press Enter)
// * Press F7, select "echo hello" and press Enter
//
// It'll print "hello" but the previous prompt will say "echo hello bar" because the _distanceEnd
// ended up being well over 14 leading it to believe that "bar" got overwritten during WriteCharsLegacy().
WriteCharsLegacy(_screenInfo, newlineSuffix, nullptr);
if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT))
{
if (_history)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
LOG_IF_FAILED(_history->Add(input, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP)));
}
Tracing::s_TraceCookedRead(_processHandle, input);
alias = Alias::s_MatchAndCopyAlias(input, _exeName, lineCount);
}
if (!alias.empty())
{
buffer = std::move(alias);
}
else
{
buffer.append(newlineSuffix);
}
input = std::wstring_view{ buffer };
// doskey aliases may result in multiple lines of output (for instance `doskey test=echo foo$Techo bar$Techo baz`).
// We need to emit them as multiple cooked reads as well, so that each read completes at a \r\n.
if (lineCount > 1)
{
// ProcessAliases() is supposed to end each line with \r\n. If it doesn't we might as well fail-fast.
constauto firstLineEnd = input.find(UNICODE_LINEFEED) + 1;
input = input.substr(0, std::min(input.size(), firstLineEnd));
}
}
constauto inputSizeBefore = input.size();
_pInputBuffer->Consume(isUnicode, input, writer);
if (lineCount > 1)
{
// This is a continuation of the above identical if condition.
// We've truncated the `input` slice and now we need to restore it.
constauto inputSizeAfter = input.size();
constauto amountConsumed = inputSizeBefore - inputSizeAfter;
input = std::wstring_view{ buffer };
input = input.substr(std::min(input.size(), amountConsumed));
GetInputReadHandleData()->SaveMultilinePendingInput(input);
}
elseif (!input.empty())
{
GetInputReadHandleData()->SavePendingInput(input);
}
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP;
// If we previously called SetCursorDBMode() with true,
// this will ensure that the cursor returns to its normal look.
_screenInfo.SetCursorDBMode(false);
numBytes = _userBuffer.size() - writer.size();
controlKeyState = _controlKeyState;
}
voidCOOKED_READ_DATA::_transitionState(State state) noexcept
{
assert(_state == State::Accumulating);
_state = state;
}
til::point COOKED_READ_DATA::_getViewportCursorPosition() constnoexcept
{
constauto& textBuffer = _screenInfo.GetTextBuffer();
constauto& cursor = textBuffer.GetCursor();
auto cursorPos = cursor.GetPosition();
_screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos);
cursorPos.x = std::max(0, cursorPos.x);
cursorPos.y = std::max(0, cursorPos.y);
return cursorPos;
}
// Some applications initiate a read on stdin and _then_ print the prompt prefix to stdout.
// While that's not correct (because it's a race condition), we can make it significantly
// less bad by delaying the calculation of the origin until we actually need it.
// This turns it from a race between application and terminal into a race between
// application and user, which is much less likely to hit.
til::point COOKED_READ_DATA::_getOriginInViewport() noexcept
{
if (!_originInViewport)
{
_originInViewport.emplace(_getViewportCursorPosition());
}
return *_originInViewport;
}
voidCOOKED_READ_DATA::_replace(size_t offset, size_t remove, constwchar_t* input, size_t count)
{
constauto size = _buffer.size();
offset = std::min(offset, size);
remove = std::min(remove, size - offset);
// Nothing to do. Avoid marking it as dirty.
if (remove == 0 && count == 0)
{
return;
}
_buffer.replace(offset, remove, input, count);
_bufferCursor = offset + count;
_bufferDirtyBeg = std::min(_bufferDirtyBeg, offset);
_dirty = true;
}
voidCOOKED_READ_DATA::_replace(const std::wstring_view& str)
{
_buffer.assign(str);
_bufferCursor = _buffer.size();
_bufferDirtyBeg = 0;
_dirty = true;
}
voidCOOKED_READ_DATA::_setCursorPosition(size_t position) noexcept
{
_bufferCursor = std::min(position, _buffer.size());
_dirty = true;
}
std::wstring_view COOKED_READ_DATA::_slice(size_t from, size_t to) constnoexcept
{
to = std::min(to, _buffer.size());
from = std::min(from, to);
return std::wstring_view{ _buffer.data() + from, to - from };
}
// Draws the contents of _buffer onto the screen.
//
// By using the _dirty flag we avoid redrawing the buffer unless needed.
// This turns the amortized time complexity of _readCharInputLoop() from O(n^2) (n(n+1)/2 redraws) into O(n).
// Without this, pasting text would otherwise quickly turn into "accidentally quadratic" meme material.
//
// NOTE: Don't call _flushBuffer() after appending newlines to the buffer! See _handlePostCharInputLoop for more information.
voidCOOKED_READ_DATA::_redisplay()
{
if (!_dirty || WI_IsFlagClear(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT))
{
return;
}
constauto size = _screenInfo.GetVtPageArea().Dimensions();
auto originInViewport = _getOriginInViewport();
auto originInViewportFinal = originInViewport;
til::point cursorPositionFinal;
til::point pagerPromptEnd;
std::vector<Line> lines;
// FYI: This loop does not loop. It exists because goto is considered evil
// and if MSVC says that then that must be true.
for (;;)
{
cursorPositionFinal = { originInViewport.x, 0 };
// Construct the first line manually so that it starts at the correct horizontal position.
LayoutResult res{ .column = cursorPositionFinal.x };
lines.emplace_back(std::wstring{}, 0, cursorPositionFinal.x, cursorPositionFinal.x);
// Split the buffer into 3 segments, so that we can find the row/column coordinates of
// the cursor within the buffer, as well as the start of the dirty parts of the buffer.
constsize_t offsets[]{
0,
std::min(_bufferDirtyBeg, _bufferCursor),
std::max(_bufferDirtyBeg, _bufferCursor),
npos,
};
for (int i = 0; i < 3; i++)
{
constauto& segment = til::safe_slice_abs(_buffer, offsets[i], offsets[i + 1]);
if (segment.empty())
{
continue;
}
constauto dirty = offsets[i] >= _bufferDirtyBeg;
// Layout the _buffer contents into lines.
for (size_t beg = 0; beg < segment.size();)
{
if (res.column >= size.width)
{
lines.emplace_back();
}
auto& line = lines.back();
res = _layoutLine(line.text, segment, beg, line.columns, size.width);
line.columns = res.column;
if (!dirty)
{
line.dirtyBegOffset = line.text.size();
line.dirtyBegColumn = res.column;
}
beg = res.offset;
}
// If this segment ended at the cursor offset, we got our cursor position in rows/columns.
if (offsets[i + 1] == _bufferCursor)
{
cursorPositionFinal = { res.column, gsl::narrow_cast<til::CoordType>(lines.size() - 1) };
}
}
pagerPromptEnd = { res.column, gsl::narrow_cast<til::CoordType>(lines.size() - 1) };
// If the content got a little shorter than it was before, we need to erase the tail end.
// If the last character on a line got removed, we'll skip this code because `remaining`
// will be negative, and instead we'll erase it later when we append " \r" to the lines.
// If entire lines got removed, then we'll fix this later when comparing against _pagerContentEnd.y.
if (pagerPromptEnd.y <= _pagerPromptEnd.y)
{
constauto endX = _pagerPromptEnd.y == pagerPromptEnd.y ? _pagerPromptEnd.x : size.width;
constauto remaining = endX - pagerPromptEnd.x;
if (remaining > 0)
{
auto& line = lines.back();
// CSI K may be expensive, so use spaces if we can.
if (remaining <= 16)
{
line.text.append(remaining, L'');
line.columns += remaining;
}
else
{
// CSI K doesn't change the cursor position, so we don't modify .columns.
line.text.append(L"\x1b[K");
}
}
}