1 /**
2  * @file win32/console.cpp
3  * @brief Win32 console I/O
4  *
5  * (c) 2013-2018 by Mega Limited, Auckland, New Zealand
6  *
7  * This file is part of the MEGA SDK - Client Access Engine.
8  *
9  * Applications using the MEGA API must present a valid application key
10  * and comply with the the rules set forth in the Terms of Service.
11  *
12  * The MEGA SDK is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15  *
16  * @copyright Simplified (2-clause) BSD License.
17  *
18  * You should have received a copy of the license along with this
19  * program.
20  */
21 
22 #include "mega.h"
23 #include "megaapi.h"
24 #include <windows.h>
25 #include <conio.h>
26 #include <fstream>
27 #include <iomanip>
28 
29 #include <io.h>
30 #include <fcntl.h>
31 #include <algorithm>
32 #include <string>
33 #include <cwctype>
34 
35 namespace mega {
36 
37 using namespace std;
38 
39 #ifdef NO_READLINE
40 template<class T>
clamp(T v,T lo,T hi)41 static T clamp(T v, T lo, T hi)
42 {
43     // todo: switch to c++17 std version when we can
44     if (v < lo)
45     {
46         return lo;
47     }
48     else if (v > hi)
49     {
50         return hi;
51     }
52     else
53     {
54         return v;
55     }
56 }
57 
toUtf8String(const std::wstring & ws,UINT codepage)58 std::string WinConsole::toUtf8String(const std::wstring& ws, UINT codepage)
59 {
60     std::string s;
61     s.resize((ws.size() + 1) * 4);
62     int nchars = WideCharToMultiByte(codepage, 0, ws.data(), int(ws.size()), (LPSTR)s.data(), int(s.size()), NULL, NULL);
63     s.resize(nchars);
64     return s;
65 }
66 
toUtf16String(const std::string & s,UINT codepage)67 std::wstring WinConsole::toUtf16String(const std::string& s, UINT codepage)
68 {
69     std::wstring ws;
70     ws.resize(s.size() + 1);
71     int nwchars = MultiByteToWideChar(codepage, 0, s.data(), int(s.size()), (LPWSTR)ws.data(), int(ws.size()));
72     ws.resize(nwchars);
73     return ws;
74 }
75 
wicmp(wchar_t a,wchar_t b)76 inline static bool wicmp(wchar_t a, wchar_t b)
77 {
78     return(towupper(a) == towupper(b));
79 }
80 
81 struct Utf8Rdbuf : public streambuf
82 {
83     HANDLE h;
84     UINT codepage = CP_UTF8;
85     UINT failover_codepage = CP_UTF8;
86     std::ofstream logfile;
87     WinConsole::logstyle logstyle = WinConsole::no_log;
88     WinConsole* wc;
89 
logmega::Utf8Rdbuf90     bool log(const string& localfile, WinConsole::logstyle ls)
91     {
92         logfile.close();
93         logstyle = WinConsole::no_log;
94 
95         if (localfile.empty() && ls == WinConsole::no_log)
96         {
97             return true;
98         }
99 
100         logfile.open(localfile.c_str(), ios::out | ios::binary | ios::trunc);
101         if (logfile.fail() || !logfile.is_open())
102         {
103             return false;
104         }
105 
106         logstyle = ls;
107         return true;
108     }
109 
Utf8Rdbufmega::Utf8Rdbuf110     Utf8Rdbuf(HANDLE ch, WinConsole* w) : h(ch), wc(w) {}
111 
xsputnmega::Utf8Rdbuf112     streamsize xsputn(const char* s, streamsize n)
113     {
114         DWORD bn = DWORD(_Pnavail()), written = 0;
115         string s8(pbase(), bn);
116         pbump(-int(bn));
117         s8.append(s, size_t(n));
118 
119         if (logstyle == WinConsole::utf8_log)
120         {
121             logfile << s8;
122         }
123 
124         wstring ws = WinConsole::toUtf16String(s8);
125 
126         if (logstyle == WinConsole::utf16_log)
127         {
128             logfile.write((const char*)ws.data(), ws.size() * sizeof(wchar_t));
129         }
130         else if (logstyle == WinConsole::codepage_log)
131         {
132             logfile << WinConsole::toUtf8String(ws, codepage);
133         }
134 
135         if (wc)
136         {
137             wc->retractPrompt();
138         }
139 
140         BOOL b = WriteConsoleW(h, ws.data(), DWORD(ws.size()), &written, NULL);
141         if (!b)
142         {
143             // The font can't display some characters (fails on windows 7 - windows 10 not so much but just in case).
144             // Output those that we can and indicate the others.
145             // If the user selects a suitable font then there should not be any failures.
146             for (unsigned i = 0; i < ws.size(); ++i)
147             {
148                 b = WriteConsoleW(h, ws.data() + i, 1, &written, NULL);
149                 if (!b && failover_codepage != codepage)
150                 {
151                     // for raster fonts, we can have a second go with another code page, translating directly from utf16
152                     if (SetConsoleOutputCP(failover_codepage))
153                     {
154                         b = WriteConsoleW(h, ws.data() + i, 1, &written, NULL);
155                         SetConsoleOutputCP(codepage);
156                     }
157                 }
158                 if (!b)
159                 {
160                     wostringstream wos;
161                     wos << L"<CHAR/" << hex << unsigned short(ws.data()[i]) << L">";
162                     wstring str = wos.str();
163                     WriteConsoleW(h, str.data(), DWORD(str.size()), &written, NULL);
164                 }
165             }
166         }
167 
168         return n;
169     }
170 
overflowmega::Utf8Rdbuf171     int overflow(int c)
172     {
173         char cc = char(c);
174         xsputn(&cc, 1);
175         return c;
176     }
177 
178 };
179 
addInputChar(wchar_t c)180 void ConsoleModel::addInputChar(wchar_t c)
181 {
182     insertPos = clamp<size_t>(insertPos, 0, buffer.size());
183     if (c == 13)
184     {
185         buffer.push_back(c);
186         insertPos = buffer.size();
187         newlinesBuffered = true;
188         consoleNewlineNeeded = true;
189         searchingHistory = false;
190         historySearchString.clear();
191     }
192     else
193     {
194         if (searchingHistory)
195         {
196             historySearchString.push_back(c);
197             updateHistoryMatch(searchingHistoryForward, false);
198         }
199         else
200         {
201             buffer.insert(insertPos, 1, c);
202             insertPos += 1;
203         }
204         redrawInputLineNeeded = true;
205     }
206 #ifdef HAVE_AUTOCOMPLETE
207     autocompleteState.active = false;
208 #endif
209 }
210 
getHistory(int index,int offset)211 void ConsoleModel::getHistory(int index, int offset)
212 {
213     if (inputHistory.empty() && offset == 1)
214     {
215         buffer.clear();
216         newlinesBuffered = false;
217     }
218     else
219     {
220         index = clamp<int>(index, 0, (int)inputHistory.size() - 1) + (enteredHistory ? offset : (offset == -1 ? -1 : 0));
221         if (index < 0 || index >= (int)inputHistory.size())
222         {
223             return;
224         }
225         inputHistoryIndex = index;
226         buffer = inputHistory[inputHistoryIndex];
227         enteredHistory = true;
228         newlinesBuffered = false;
229     }
230     insertPos = buffer.size();
231     redrawInputLineNeeded = true;
232 }
233 
searchHistory(bool forwards)234 void ConsoleModel::searchHistory(bool forwards)
235 {
236     if (!searchingHistory)
237     {
238         searchingHistory = true;
239         searchingHistoryForward = forwards;
240         historySearchString.clear();
241     }
242     else
243     {
244         updateHistoryMatch(forwards, true);
245     }
246     redrawInputLineNeeded = true;
247 }
248 
updateHistoryMatch(bool forwards,bool increment)249 void ConsoleModel::updateHistoryMatch(bool forwards, bool increment)
250 {
251     bool checking = false;
252     for (unsigned i = 0; i < inputHistory.size()*2; ++i)
253     {
254         size_t index = forwards ? inputHistory.size()*2 - i - 1 : i;
255         index %= inputHistory.size();
256         checking = checking || !enteredHistory || index == inputHistoryIndex;
257         if (checking && !(enteredHistory && increment && index == inputHistoryIndex))
258         {
259             auto iter = std::search(inputHistory[index].begin(), inputHistory[index].end(), historySearchString.begin(), historySearchString.end(), wicmp);
260             if (iter != inputHistory[index].end())
261             {
262                 inputHistoryIndex = index;
263                 enteredHistory = true;
264                 buffer = inputHistory[index];
265                 insertPos = buffer.size();
266                 newlinesBuffered = false;
267                 redrawInputLineNeeded = true;
268                 break;
269             }
270         }
271     }
272 }
273 
deleteHistorySearchChars(size_t n)274 void ConsoleModel::deleteHistorySearchChars(size_t n)
275 {
276     if (n == 0)
277     {
278         searchingHistory = false;
279     }
280     else
281     {
282         n = std::min<size_t>(n, historySearchString.size());
283         historySearchString.erase(historySearchString.size() - n, n);
284         updateHistoryMatch(searchingHistoryForward, false);
285     }
286     redrawInputLineNeeded = true;
287 }
288 
redrawInputLine(int p)289 void ConsoleModel::redrawInputLine(int p)
290 {
291     insertPos = clamp<int>(p, 0, (int)buffer.size());
292     redrawInputLineNeeded = true;
293 }
294 
autoComplete(bool forwards,unsigned consoleWidth)295 void ConsoleModel::autoComplete(bool forwards, unsigned consoleWidth)
296 {
297 #ifdef HAVE_AUTOCOMPLETE
298     if (autocompleteSyntax)
299     {
300         if (!autocompleteState.active)
301         {
302             std::string u8line = WinConsole::toUtf8String(buffer);
303             size_t u8InsertPos = WinConsole::toUtf8String(buffer.substr(0, insertPos)).size();
304             autocompleteState = autocomplete::autoComplete(u8line, u8InsertPos, autocompleteSyntax, unixCompletions);
305 
306             if (autocompleteFunction)
307             {
308                 // also get additional app specific options, and merge
309                 std::vector<autocomplete::ACState::Completion> appcompletions = autocompleteFunction(WinConsole::toUtf8String(getInputLineToCursor()));
310                 autocomplete::ACState acs;
311                 acs.words.push_back(autocompleteState.originalWord);
312                 acs.completions.swap(autocompleteState.completions);
313                 for (auto& c : appcompletions)
314                 {
315                     acs.addCompletion(c.s, c.caseInsensitive, c.couldExtend);
316                 }
317                 autocompleteState.completions.swap(acs.completions);
318                 autocompleteState.tidyCompletions();
319             }
320             autocompleteState.active = true;
321         }
322 
323         autocomplete::applyCompletion(autocompleteState, forwards, consoleWidth, redrawInputLineConsoleFeedback);
324         buffer = WinConsole::toUtf16String(autocompleteState.line);
325         newlinesBuffered = false;
326         size_t u16InsertPos = WinConsole::toUtf16String(autocompleteState.line.substr(0, autocompleteState.wordPos.second)).size();
327         insertPos = clamp<size_t>(u16InsertPos, 0, buffer.size());
328         redrawInputLineNeeded = true;
329     }
330 #endif
331 }
332 
isWordBoundary(size_t i,const std::wstring s)333 static bool isWordBoundary(size_t i, const std::wstring s)
334 {
335     return i == 0 || i >= s.size() || isspace(s[i - 1]) && !isspace(s[i + 1]);
336 }
337 
detectWordBoundary(int start,bool forward)338 int ConsoleModel::detectWordBoundary(int start, bool forward)
339 {
340     start = clamp<int>(start, 0, (int)buffer.size());
341     do
342     {
343         start += (forward ? 1 : -1);
344     } while (!isWordBoundary(start, buffer));
345     return start;
346 }
347 
deleteCharRange(int start,int end)348 void ConsoleModel::deleteCharRange(int start, int end)
349 {
350     start = clamp<int>(start, 0, (int)buffer.size());
351     end = clamp<int>(end, 0, (int)buffer.size());
352     if (start < end)
353     {
354         buffer.erase(start, end - start);
355         newlinesBuffered = buffer.find(13) != string::npos;
356         redrawInputLine(start);
357     }
358 }
359 
performLineEditingAction(lineEditAction action,unsigned consoleWidth)360 void ConsoleModel::performLineEditingAction(lineEditAction action, unsigned consoleWidth)
361 {
362 #ifdef HAVE_AUTOCOMPLETE
363     if (action != AutoCompleteForwards && action != AutoCompleteBackwards)
364     {
365         autocompleteState.active = false;
366     }
367 #endif
368     if (action != HistorySearchForward && action != HistorySearchBackward && action != DeleteCharLeft && action != ClearLine)
369     {
370         searchingHistory = false;
371     }
372 
373     int pos = (int)insertPos;
374     int bufSize = (int)buffer.size();
375 
376     switch (action)
377     {
378     case CursorLeft: redrawInputLine(pos - 1); break;
379     case CursorRight: redrawInputLine(pos + 1); break;
380     case CursorStart: redrawInputLine(0);  break;
381     case CursorEnd: redrawInputLine(bufSize); break;
382     case WordLeft: redrawInputLine((int)detectWordBoundary(pos, false));  break;
383     case WordRight: redrawInputLine((int)detectWordBoundary(pos, true));  break;
384     case HistoryUp: getHistory((int)inputHistoryIndex, 1);  break;
385     case HistoryDown: getHistory((int)inputHistoryIndex, -1); break;
386     case HistoryStart: getHistory((int)inputHistory.size() - 1, 0);  break;
387     case HistoryEnd: getHistory(0, 0);  break;
388     case HistorySearchForward: searchHistory(true);  break;
389     case HistorySearchBackward: searchHistory(false);  break;
390     case ClearLine: searchingHistory ? deleteHistorySearchChars(0) : deleteCharRange(0, bufSize);  break;
391     case DeleteCharLeft: searchingHistory ? deleteHistorySearchChars(1) : deleteCharRange(pos - 1, pos);  break;
392     case DeleteCharRight: deleteCharRange(pos, pos + 1); break;
393     case DeleteWordLeft: deleteCharRange(detectWordBoundary(pos, false), pos);  break;
394     case DeleteWordRight: deleteCharRange(pos, detectWordBoundary(pos, true));  break;
395     case AutoCompleteForwards: autoComplete(true, consoleWidth); break;
396     case AutoCompleteBackwards: autoComplete(false, consoleWidth); break;
397     }
398 }
399 
checkForCompletedInputLine(std::wstring & ws)400 bool ConsoleModel::checkForCompletedInputLine(std::wstring& ws)
401 {
402     auto newlinePos = std::find(buffer.begin(), buffer.end(), 13);
403     if (newlinePos != buffer.end())
404     {
405         ws.assign(buffer.begin(), newlinePos);
406         buffer.erase(buffer.begin(), newlinePos + 1);
407         insertPos = 0;
408         newlinesBuffered = buffer.find(13) != string::npos;
409         bool sameAsLastCommand = !inputHistory.empty() && inputHistory[0] == ws;
410         bool sameAsChosenHistory = !inputHistory.empty() &&
411             inputHistoryIndex >= 0 && inputHistoryIndex < inputHistory.size() &&
412             inputHistory[inputHistoryIndex] == ws;
413         if (echoOn && !sameAsLastCommand && !ws.empty())
414         {
415             if (inputHistory.size() + 1 > MaxHistoryEntries)
416             {
417                 inputHistory.pop_back();
418             }
419             inputHistory.push_front(ws);
420             inputHistoryIndex = sameAsChosenHistory ? inputHistoryIndex + 1 : -1;
421         }
422         enteredHistory = false;
423         return true;
424     }
425     newlinesBuffered = false;
426     return false;
427 }
428 
getInputLineToCursor()429 std::wstring ConsoleModel::getInputLineToCursor()
430 {
431     insertPos = clamp<size_t>(insertPos, 0, buffer.size());
432     return buffer.substr(0, insertPos);
433 }
434 #endif
435 
WinConsole()436 WinConsole::WinConsole()
437 {
438 #ifdef NO_READLINE
439     hInput = GetStdHandle(STD_INPUT_HANDLE);
440     hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
441 
442     DWORD dwMode;
443     GetConsoleMode(hInput, &dwMode);
444     SetConsoleMode(hInput, dwMode & ~(ENABLE_MOUSE_INPUT));
445     FlushConsoleInputBuffer(hInput);
446     blockingConsolePeek = false;
447 #endif
448 }
449 
~WinConsole()450 WinConsole::~WinConsole()
451 {
452 #ifdef NO_READLINE
453     if (rdbuf)
454     {
455         std::cout.rdbuf(oldrb1);
456         std::cerr.rdbuf(oldrb2);
457         delete rdbuf;
458     }
459 #endif
460 }
461 
462 #ifdef NO_READLINE
getConsoleFont(COORD & size)463 string WinConsole::getConsoleFont(COORD& size)
464 {
465     CONSOLE_FONT_INFOEX cfi;
466     memset(&cfi, 0, sizeof(cfi));
467     cfi.cbSize = sizeof(cfi);
468     GetCurrentConsoleFontEx(hOutput, FALSE, &cfi);
469 
470     wstring wname = cfi.FaceName;
471     string name = WinConsole::toUtf8String(wname);
472 
473     if (!(cfi.FontFamily & TMPF_TRUETYPE) && (wname.size() < 6 || name.find("?") != string::npos))
474     {
475         // the name is garbled on win 7, try to compensate
476         name = "Terminal";
477     }
478 
479     size = cfi.dwFontSize;
480     return name;
481 }
482 
getShellCodepages(UINT & codepage,UINT & failover_codepage)483 void WinConsole::getShellCodepages(UINT& codepage, UINT& failover_codepage)
484 {
485     if (rdbuf)
486     {
487         codepage = rdbuf->codepage;
488         failover_codepage = rdbuf->failover_codepage;
489     }
490     else
491     {
492         codepage = GetConsoleOutputCP();
493         failover_codepage = codepage;
494     }
495 }
496 
setShellConsole(UINT codepage,UINT failover_codepage)497 bool WinConsole::setShellConsole(UINT codepage, UINT failover_codepage)
498 {
499     // Call this if your console app is taking live input, with the user editing commands on screen, similar to cmd or powershell
500 
501     // Ideally we would work in unicode all the time (with codepage = CP_UTF8).  However in windows 7 for example, with raster
502     // font selected, the o symbol with diacritic (U+00F3) is not output correctly.  So we offer the option to attempt output
503     // a second time in a 'failover' codepage, or to output in a single codepage only.  The user has control with the 'codepage' command.
504 
505     // use cases covered
506     // utf8 output with std::cout (since we already use cout so much and it's compatible with other platforms)
507     // unicode input with windows ReadConsoleInput api
508     // drag and drop filenames from explorer to the console window
509     // copy and paste unicode filenames from 'ls' output into your next command
510     // upload and download unicode/utf-8 filenames to/from Mega
511     // input a unicode/utf8 password without displaying anything
512     // normal cmd window type editing, including autocomplete (with runtime selectable unix style or dos style, default to local platform rules)
513     // the console must have a suitable font selected for the characters to diplay properly
514 
515     BOOL ok =  SetConsoleCP(codepage);
516     ok = ok && SetConsoleOutputCP(codepage);
517     if (!ok)
518     {
519         codepage = CP_UTF8;
520         failover_codepage = GetOEMCP();
521         SetConsoleCP(codepage);
522         SetConsoleOutputCP(codepage);
523     }
524 
525     // skip the historic complexities of output modes etc, our own rdbuf can write direct to console
526     if (!rdbuf)
527     {
528         rdbuf = new Utf8Rdbuf(hOutput, this);
529         oldrb1 = std::cout.rdbuf(rdbuf);
530         oldrb2 = std::cerr.rdbuf(rdbuf);
531     }
532     rdbuf->codepage = codepage;
533     rdbuf->failover_codepage = failover_codepage;
534     return ok;
535 }
536 
537 #ifdef HAVE_AUTOCOMPLETE
setAutocompleteSyntax(autocomplete::ACN a)538 void WinConsole::setAutocompleteSyntax(autocomplete::ACN a)
539 {
540     model.autocompleteSyntax = a;
541 }
542 
setAutocompleteFunction(std::function<vector<autocomplete::ACState::Completion> (string)> f)543 void WinConsole::setAutocompleteFunction(std::function<vector<autocomplete::ACState::Completion>(string)> f)
544 {
545     model.autocompleteFunction = f;
546 }
547 #endif
548 
setAutocompleteStyle(bool unix)549 void WinConsole::setAutocompleteStyle(bool unix)
550 {
551     model.unixCompletions = unix;
552 }
553 
getAutocompleteStyle() const554 bool WinConsole::getAutocompleteStyle() const
555 {
556     return model.unixCompletions;
557 }
558 
inputAvailableHandle()559 HANDLE WinConsole::inputAvailableHandle()
560 {
561     // returns a handle that will be signalled when there is console input to process (ie records available for PeekConsoleInput)
562     // client can wait on this handle with other handles as higher priority
563     return hInput;
564 }
565 
consolePeek()566 bool WinConsole::consolePeek()
567 {
568     return blockingConsolePeek?consolePeekBlocking():consolePeekNonBlocking();
569 }
570 
consolePeekNonBlocking()571 bool WinConsole::consolePeekNonBlocking()
572 {
573     std::cout << std::flush;
574 
575     // Read keypreses up to the first newline (or multiple newlines if
576     bool checkPromptOnce = true;
577     for (;;)
578     {
579         INPUT_RECORD ir;
580         DWORD nRead;
581         BOOL ok = PeekConsoleInput(hInput, &ir, 1, &nRead);  // peek first so we never wait
582         assert(ok);
583         if (!nRead)
584         {
585             break;
586         }
587 
588         bool isCharacterGeneratingKeypress =
589             ir.EventType == 1 && ir.Event.KeyEvent.uChar.UnicodeChar != 0 &&
590             (ir.Event.KeyEvent.bKeyDown ||  // key press
591             (!ir.Event.KeyEvent.bKeyDown && ((ir.Event.KeyEvent.dwControlKeyState & LEFT_ALT_PRESSED) || ir.Event.KeyEvent.wVirtualKeyCode == VK_MENU)));  // key release that emits a unicode char
592 
593         if (isCharacterGeneratingKeypress && (currentPrompt.empty() || model.newlinesBuffered))
594         {
595             break;
596         }
597 
598         ok = ReadConsoleInputW(hInput, &ir, 1, &nRead);  // discard the event record
599         assert(ok);
600         assert(nRead == 1);
601 
602         ConsoleModel::lineEditAction action = interpretLineEditingKeystroke(ir);
603 
604         if ((action != ConsoleModel::nullAction || isCharacterGeneratingKeypress) && checkPromptOnce)
605         {
606             redrawPromptIfLoggingOccurred();
607             checkPromptOnce = false;
608         }
609         if (action != ConsoleModel::nullAction)
610         {
611             CONSOLE_SCREEN_BUFFER_INFO sbi;
612             BOOL ok = GetConsoleScreenBufferInfo(hOutput, &sbi);
613             assert(ok);
614             unsigned consoleWidth = ok ? sbi.dwSize.X : 50;
615 
616             model.performLineEditingAction(action, consoleWidth);
617         }
618         else if (isCharacterGeneratingKeypress)
619         {
620             for (int i = ir.Event.KeyEvent.wRepeatCount; i--; )
621             {
622                 model.addInputChar(ir.Event.KeyEvent.uChar.UnicodeChar);
623             }
624 
625             if (model.newlinesBuffered)
626             {
627                 break;
628             }
629         }
630     }
631     if (model.redrawInputLineNeeded && model.echoOn)
632     {
633 #ifdef HAVE_AUTOCOMPLETE
634         redrawInputLine(&model.redrawInputLineConsoleFeedback);
635 #else
636         redrawInputLine();
637 #endif
638     }
639     if (model.consoleNewlineNeeded)
640     {
641         DWORD written = 0;
642         #ifndef NDEBUG
643         BOOL b =
644         #endif
645         WriteConsoleW(hOutput, L"\n", 1, &written, NULL);
646         assert(b && written == 1);
647     }
648     model.redrawInputLineNeeded = false;
649     model.consoleNewlineNeeded = false;
650     return model.newlinesBuffered;
651 }
652 
consolePeekBlocking()653 bool WinConsole::consolePeekBlocking()
654 {
655     std::cout << std::flush;
656 
657     // Read keypreses up to the first newline (or multiple newlines if
658     bool checkPromptOnce = true;
659     bool isCharacterGeneratingKeypress = false;
660 
661     INPUT_RECORD ir;
662     DWORD nRead;
663     if (!ReadConsoleInputW(hInput, &ir, 1, &nRead))  // discard the event record
664     {
665         return false;
666     }
667 
668     irs.push_back(ir);
669 
670     isCharacterGeneratingKeypress =
671             ir.EventType == 1 && ir.Event.KeyEvent.uChar.UnicodeChar != 0 &&
672             (ir.Event.KeyEvent.bKeyDown ||  // key press
673              (!ir.Event.KeyEvent.bKeyDown && ((ir.Event.KeyEvent.dwControlKeyState & LEFT_ALT_PRESSED) || ir.Event.KeyEvent.wVirtualKeyCode == VK_MENU)));  // key release that emits a unicode char
674 
675     if (!(isCharacterGeneratingKeypress && (currentPrompt.empty() || model.newlinesBuffered)))
676     {
677         while(!irs.empty())
678         {
679             INPUT_RECORD &ir = irs.front();
680             ConsoleModel::lineEditAction action = interpretLineEditingKeystroke(ir);
681 
682             if ((action != ConsoleModel::nullAction || isCharacterGeneratingKeypress) && checkPromptOnce)
683             {
684                 redrawPromptIfLoggingOccurred();
685                 checkPromptOnce = false;
686             }
687             if (action != ConsoleModel::nullAction)
688             {
689                 CONSOLE_SCREEN_BUFFER_INFO sbi;
690                 BOOL ok = GetConsoleScreenBufferInfo(hOutput, &sbi);
691                 assert(ok);
692                 unsigned consoleWidth = ok ? sbi.dwSize.X : 50;
693 
694                 model.performLineEditingAction(action, consoleWidth);
695             }
696             else if (isCharacterGeneratingKeypress)
697             {
698                 for (int i = ir.Event.KeyEvent.wRepeatCount; i--; )
699                 {
700                     model.addInputChar(ir.Event.KeyEvent.uChar.UnicodeChar);
701                 }
702                 if (model.newlinesBuffered)  // todo: address case where multiple newlines were added from this one record (as we may get stuck in wait())
703                 {
704                     irs.pop_front();
705                     break;
706                 }
707             }
708             irs.pop_front();
709         }
710     }
711     if (model.redrawInputLineNeeded && model.echoOn)
712     {
713 #ifdef HAVE_AUTOCOMPLETE
714         redrawInputLine(&model.redrawInputLineConsoleFeedback);
715 #else
716         redrawInputLine();
717 #endif
718     }
719     if (model.consoleNewlineNeeded)
720     {
721         DWORD written = 0;
722         #ifndef NDEBUG
723         BOOL b =
724         #endif
725         WriteConsoleW(hOutput, L"\n", 1, &written, NULL);
726         assert(b && written == 1);
727     }
728     model.redrawInputLineNeeded = false;
729     model.consoleNewlineNeeded = false;
730     return model.newlinesBuffered;
731 }
732 
interpretLineEditingKeystroke(INPUT_RECORD & ir)733 ConsoleModel::lineEditAction WinConsole::interpretLineEditingKeystroke(INPUT_RECORD &ir)
734 {
735     if (ir.EventType == 1 && ir.Event.KeyEvent.bKeyDown)
736     {
737         bool ctrl = ir.Event.KeyEvent.dwControlKeyState & (RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED);
738         bool shift = ir.Event.KeyEvent.dwControlKeyState & SHIFT_PRESSED;
739         switch (ir.Event.KeyEvent.wVirtualKeyCode)
740         {
741         case VK_LEFT: return ctrl ? ConsoleModel::WordLeft : ConsoleModel::CursorLeft;
742         case VK_RIGHT: return ctrl ? ConsoleModel::WordRight : ConsoleModel::CursorRight;
743         case VK_UP: return ConsoleModel::HistoryUp;
744         case VK_DOWN: return ConsoleModel::HistoryDown;
745         case VK_PRIOR: return ConsoleModel::HistoryStart; // pageup
746         case VK_NEXT: return ConsoleModel::HistoryEnd; // pagedown
747         case VK_HOME: return ConsoleModel::CursorStart;
748         case VK_END: return ConsoleModel::CursorEnd;
749         case VK_DELETE: return ConsoleModel::DeleteCharRight;
750         case VK_INSERT: return ConsoleModel::Paste;  // the OS takes care of this; we don't see it
751         case VK_CONTROL: break;
752         case VK_SHIFT: break;
753         case 's':
754         case 'S': return ctrl ? (shift ? ConsoleModel::HistorySearchBackward : ConsoleModel::HistorySearchForward) : ConsoleModel::nullAction;
755         case 'r':
756         case 'R': return ctrl ? (shift ? ConsoleModel::HistorySearchForward : ConsoleModel::HistorySearchBackward) : ConsoleModel::nullAction;
757         default:
758             switch (ir.Event.KeyEvent.uChar.UnicodeChar)
759             {
760             case '\b': return ConsoleModel::DeleteCharLeft;
761             case '\t': return shift ? ConsoleModel::AutoCompleteBackwards : ConsoleModel::AutoCompleteForwards;
762             case VK_ESCAPE: return ConsoleModel::ClearLine;
763             default:
764                 break;
765             }
766             break;
767         }
768     }
769     return ConsoleModel::nullAction;
770 }
771 
772 #ifdef HAVE_AUTOCOMPLETE
redrawInputLine(::mega::autocomplete::CompletionTextOut * autocompleteFeedback=nullptr)773 void WinConsole::redrawInputLine(::mega::autocomplete::CompletionTextOut* autocompleteFeedback = nullptr)
774 #else
775 void WinConsole::redrawInputLine()
776 #endif
777 
778 {
779     CONSOLE_SCREEN_BUFFER_INFO sbi;
780 
781 #ifdef HAVE_AUTOCOMPLETE
782     if (autocompleteFeedback && !autocompleteFeedback->stringgrid.empty())
783     {
784         promptRetracted = true;
785         cout << "\n" << std::flush;
786         for (auto& r : autocompleteFeedback->stringgrid)
787         {
788             int x = 0;
789             for (unsigned c = 0; c < r.size(); ++c)
790             {
791                 cout << r[c] << std::flush;
792                 if (c + 1 == r.size())
793                 {
794                     cout << "\n" << std::flush;
795                 }
796                 else
797                 {
798                     x += autocompleteFeedback->columnwidths[c];
799 
800                     // to make the grid nice in the presence of unicode characters that are sometimes double-width glyphs, we set the X coordinate explicitly
801                     BOOL ok = GetConsoleScreenBufferInfo(hOutput, &sbi);
802                     if (ok && sbi.dwCursorPosition.X < x)
803                     {
804                         sbi.dwCursorPosition.X = short(x);
805                         SetConsoleCursorPosition(hOutput, sbi.dwCursorPosition);
806                     }
807                 }
808             }
809         }
810         autocompleteFeedback->stringgrid.clear();
811         autocompleteFeedback->columnwidths.clear();
812     }
813 #endif
814 
815     BOOL ok = GetConsoleScreenBufferInfo(hOutput, &sbi);
816     assert(ok);
817     if (ok)
818     {
819         std::string sprompt = model.searchingHistory ? ("history-" + std::string(model.searchingHistoryForward ? "F:'" : "R:'") + toUtf8String(model.historySearchString) + "'> ")
820                                                     : currentPrompt;
821         std::wstring wprompt = toUtf16String(sprompt);
822 
823         if (long(wprompt.size() + model.buffer.size() + 1) < sbi.dwSize.X || !model.echoOn)
824         {
825             inputLineOffset = 0;
826         }
827         else
828         {
829             // scroll the line if the cursor reaches the end, or moves back within 15 of the start
830             size_t showleft = 15;
831             if (inputLineOffset + showleft >= model.insertPos)
832             {
833                 inputLineOffset = model.insertPos - std::min<size_t>(showleft, model.insertPos);
834             } else if (wprompt.size() + model.insertPos + 1 >= inputLineOffset + sbi.dwSize.X)
835             {
836                 inputLineOffset = wprompt.size() + model.insertPos + 1 - sbi.dwSize.X;
837             }
838         }
839 
840         size_t width = std::max<size_t>(wprompt.size() + model.buffer.size() + 1 + inputLineOffset, sbi.dwSize.X); // +1 to show character under cursor
841         std::unique_ptr<CHAR_INFO[]> line(new CHAR_INFO[width]);
842 
843         for (size_t i = width; i--; )
844         {
845             line[i].Attributes = sbi.wAttributes;
846             if (i < inputLineOffset)
847             {
848                 line[i].Char.UnicodeChar = ' ';
849             }
850             else if (inputLineOffset && i + 1 == inputLineOffset + wprompt.size())
851             {
852                 line[i].Char.UnicodeChar = '|';
853                 line[i].Attributes |= FOREGROUND_INTENSITY | FOREGROUND_GREEN;
854                 line[i].Attributes &= ~(FOREGROUND_RED | FOREGROUND_BLUE);
855             }
856             else if (i < inputLineOffset + wprompt.size())
857             {
858                 line[i].Char.UnicodeChar = wprompt[i - inputLineOffset];
859                 line[i].Attributes |= FOREGROUND_INTENSITY;
860             }
861             else if (i < wprompt.size() + model.buffer.size() && model.echoOn)
862             {
863                 line[i].Char.UnicodeChar = model.buffer[i - wprompt.size()];
864             }
865             else
866             {
867                 line[i].Char.UnicodeChar = ' ';
868             }
869         }
870 
871         SMALL_RECT screenarea2{ 0, sbi.dwCursorPosition.Y, sbi.dwSize.X, sbi.dwCursorPosition.Y };
872         ok = WriteConsoleOutputW(hOutput, line.get(), COORD{ SHORT(width), 1 }, COORD{ SHORT(inputLineOffset), 0 }, &screenarea2);
873         assert(ok);
874 
875         COORD cpos{ SHORT(wprompt.size() + model.insertPos - inputLineOffset), sbi.dwCursorPosition.Y };
876         ok = SetConsoleCursorPosition(hOutput, cpos);
877         assert(ok);
878 
879         promptRetracted = false;
880     }
881 }
882 
retractPrompt()883 void WinConsole::retractPrompt()
884 {
885     if (currentPrompt.size() && !promptRetracted)
886     {
887         CONSOLE_SCREEN_BUFFER_INFO sbi;
888         BOOL ok = GetConsoleScreenBufferInfo(hOutput, &sbi);
889         assert(ok);
890         //if (0 == memcmp(&knownCursorPos, &sbi.dwCursorPosition, sizeof(COORD)))
891         {
892             size_t width = std::max<size_t>(currentPrompt.size() + model.buffer.size() + 1 + inputLineOffset, sbi.dwSize.X); // +1 to show character under cursor
893             std::unique_ptr<CHAR_INFO[]> line(new CHAR_INFO[width]);
894 
895             for (size_t i = width; i--; )
896             {
897                 line[i].Attributes = sbi.wAttributes;
898                 line[i].Char.UnicodeChar = ' ';
899             }
900 
901             SMALL_RECT screenarea2{ 0, sbi.dwCursorPosition.Y, sbi.dwSize.X, sbi.dwCursorPosition.Y };
902             ok = WriteConsoleOutputW(hOutput, line.get(), COORD{ SHORT(width), 1 }, COORD{ SHORT(inputLineOffset), 0 }, &screenarea2);
903             assert(ok);
904 
905             COORD cpos{ 0, sbi.dwCursorPosition.Y };
906             ok = SetConsoleCursorPosition(hOutput, cpos);
907             assert(ok);
908 
909             promptRetracted = true;
910         }
911     }
912 }
913 
getInputLineToCursor()914 wstring WinConsole::getInputLineToCursor()
915 {
916     return model.getInputLineToCursor();
917 }
918 
consoleGetch(wchar_t & c)919 bool WinConsole::consoleGetch(wchar_t& c)
920 {
921     // todo: remove this function once we don't need to support readline for any version of megacli on windows
922     if (consolePeek())
923     {
924         c = model.buffer.front();
925         model.buffer.erase(0, 1);
926         model.newlinesBuffered = model.buffer.find(13) != string::npos;
927         return true;
928     }
929     return false;
930 }
931 #endif
readpwchar(char * pw_buf,int pw_buf_size,int * pw_buf_pos,char ** line)932 void WinConsole::readpwchar(char* pw_buf, int pw_buf_size, int* pw_buf_pos, char** line)
933 {
934 #ifdef NO_READLINE
935     // todo: remove/stub this function once we don't need to support readline for any version of megacli on windows
936     wchar_t c;
937     if (consoleGetch(c))  // only processes once newline is buffered, so no backspace processing needed
938     {
939         if (c == 13)
940         {
941             *line = _strdup(toUtf8String(wstring(&c, 1)).c_str());
942             memset(pw_buf, 0, pw_buf_size);
943         }
944         else if (*pw_buf_pos + 2 <= pw_buf_size)
945         {
946             *(wchar_t*)(pw_buf + *pw_buf_pos) = c;
947             *pw_buf_pos += 2;
948         }
949     }
950 #else
951 
952     char c;
953     DWORD cread;
954 
955     if (ReadConsole(GetStdHandle(STD_INPUT_HANDLE), &c, 1, &cread, NULL) == 1)
956     {
957         if ((c == 8) && *pw_buf_pos)
958         {
959             (*pw_buf_pos)--;
960         }
961         else if (c == 13)
962         {
963             *line = (char*)malloc(*pw_buf_pos + 1);
964             memcpy(*line, pw_buf, *pw_buf_pos);
965             (*line)[*pw_buf_pos] = 0;
966         }
967         else if (*pw_buf_pos < pw_buf_size)
968         {
969             pw_buf[(*pw_buf_pos)++] = c;
970         }
971     }
972 #endif
973 }
974 
setecho(bool echo)975 void WinConsole::setecho(bool echo)
976 {
977 #ifdef NO_READLINE
978     model.echoOn = echo;
979 #else
980 
981     HANDLE hCon = GetStdHandle(STD_INPUT_HANDLE);
982     DWORD mode;
983 
984     GetConsoleMode(hCon, &mode);
985 
986     if (echo)
987     {
988         mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT;
989     }
990     else
991     {
992         mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT);
993     }
994 
995     SetConsoleMode(hCon, mode);
996 #endif
997 }
998 
999 #ifdef NO_READLINE
redrawPromptIfLoggingOccurred()1000 void WinConsole::redrawPromptIfLoggingOccurred()
1001 {
1002     if (promptRetracted)
1003     {
1004         redrawInputLine();
1005     }
1006 }
1007 
updateInputPrompt(const std::string & newprompt)1008 void WinConsole::updateInputPrompt(const std::string& newprompt)
1009 {
1010     cout << std::flush;
1011     currentPrompt = newprompt;
1012     redrawInputLine();
1013 }
1014 
checkForCompletedInputLine()1015 char* WinConsole::checkForCompletedInputLine()
1016 {
1017     if (rdbuf && rdbuf->logstyle != WinConsole::no_log)
1018     {
1019         rdbuf->logfile << flush;
1020     }
1021     redrawPromptIfLoggingOccurred();
1022     if (consolePeek())
1023     {
1024         std::wstring ws;
1025         if (model.checkForCompletedInputLine(ws))
1026         {
1027 
1028             if (rdbuf && rdbuf->logstyle == WinConsole::utf16_log)
1029             {
1030                 std::wstring wprompt = toUtf16String(currentPrompt);
1031                 rdbuf->logfile.write((const char*)wprompt.data(), wprompt.size() * sizeof(wchar_t));
1032                 rdbuf->logfile.write((const char*)ws.data(), ws.size() * sizeof(wchar_t));
1033                 rdbuf->logfile.write((const char*)(const wchar_t*)L"\n", sizeof(wchar_t));
1034             }
1035 
1036             string u8s = toUtf8String(ws);
1037 
1038             if (rdbuf && rdbuf->logstyle == WinConsole::utf8_log)
1039             {
1040                 rdbuf->logfile << currentPrompt << u8s << "\n";
1041             }
1042             else if (rdbuf && rdbuf->logstyle == WinConsole::codepage_log)
1043             {
1044                 rdbuf->logfile << currentPrompt << toUtf8String(ws, rdbuf->codepage) << "\n";
1045             }
1046 
1047             currentPrompt.clear();
1048 
1049             return _strdup(u8s.c_str());
1050         }
1051     }
1052     return NULL;
1053 }
1054 
clearScreen()1055 void WinConsole::clearScreen()
1056 {
1057     CONSOLE_SCREEN_BUFFER_INFO csbi;
1058     BOOL ok = GetConsoleScreenBufferInfo(hOutput, &csbi);
1059     assert(ok);
1060     if (ok)
1061     {
1062         // Fill the entire buffer with spaces
1063         DWORD count;
1064         ok = FillConsoleOutputCharacter(hOutput, (TCHAR) ' ', csbi.dwSize.X *csbi.dwSize.Y, { 0, 0 }, &count);
1065         assert(ok);
1066 
1067         // Fill the entire buffer with the current colors and attributes
1068         ok = FillConsoleOutputAttribute(hOutput, csbi.wAttributes, csbi.dwSize.X *csbi.dwSize.Y, { 0, 0 }, &count);
1069         assert(ok);
1070     }
1071     ok = SetConsoleCursorPosition(hOutput, { 0, 0 });
1072     assert(ok);
1073     currentPrompt.clear();
1074 }
1075 
outputHistory()1076 void WinConsole::outputHistory()
1077 {
1078     for (size_t i = model.inputHistory.size(); i--; )
1079     {
1080         std::cout << toUtf8String(model.inputHistory[i]) << std::endl;
1081     }
1082 }
1083 
log(const std::string & filename,logstyle logstyle)1084 bool WinConsole::log(const std::string& filename, logstyle logstyle)
1085 {
1086     return rdbuf->log(filename, logstyle);
1087 }
1088 #endif
1089 } // namespace
1090