// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "cmdline.h" #include "popup.h" #include "CommandNumberPopup.hpp" #include "CommandListPopup.hpp" #include "CopyFromCharPopup.hpp" #include "CopyToCharPopup.hpp" #include "_output.h" #include "output.h" #include "stream.h" #include "_stream.h" #include "dbcs.h" #include "handle.h" #include "misc.h" #include "../types/inc/convert.hpp" #include "srvinit.h" #include "ApiRoutines.h" #include "../interactivity/inc/ServiceLocator.hpp" #pragma hdrstop using Microsoft::Console::Interactivity::ServiceLocator; // Routine Description: // - This routine validates a string buffer and returns the pointers of where the strings start within the buffer. // Arguments: // - Unicode - Supplies a boolean that is TRUE if the buffer contains Unicode strings, FALSE otherwise. // - Buffer - Supplies the buffer to be validated. // - Size - Supplies the size, in bytes, of the buffer to be validated. // - Count - Supplies the expected number of strings in the buffer. // ... - Supplies a pair of arguments per expected string. The first one is the expected size, in bytes, of the string // and the second one receives a pointer to where the string starts. // Return Value: // - TRUE if the buffer is valid, FALSE otherwise. bool IsValidStringBuffer(_In_ bool Unicode, _In_reads_bytes_(Size) PVOID Buffer, _In_ ULONG Size, _In_ ULONG Count, ...) { va_list Marker; va_start(Marker, Count); while (Count > 0) { ULONG const StringSize = va_arg(Marker, ULONG); PVOID* StringStart = va_arg(Marker, PVOID*); // Make sure the string fits in the supplied buffer and that it is properly aligned. if (StringSize > Size) { break; } if ((Unicode != false) && ((StringSize % sizeof(WCHAR)) != 0)) { break; } *StringStart = Buffer; // Go to the next string. Buffer = RtlOffsetToPointer(Buffer, StringSize); Size -= StringSize; Count -= 1; } va_end(Marker); return Count == 0; } // Routine Description: // - Detects Word delimiters bool IsWordDelim(const wchar_t wch) { // the space character is always a word delimiter. Do not add it to the WordDelimiters global because // that contains the user configurable word delimiters only. if (wch == UNICODE_SPACE) { return true; } const auto& delimiters = ServiceLocator::LocateGlobals().WordDelimiters; return std::find(delimiters.begin(), delimiters.end(), wch) != delimiters.end(); } bool IsWordDelim(const std::wstring_view charData) { if (charData.size() != 1) { return false; } return IsWordDelim(charData.front()); } CommandLine::CommandLine() : _isVisible{ true } { } CommandLine::~CommandLine() { } CommandLine& CommandLine::Instance() { static CommandLine c; return c; } bool CommandLine::IsEditLineEmpty() const { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (!gci.HasPendingCookedRead()) { // If the cooked read data pointer is null, there is no edit line data and therefore it's empty. return true; } else if (0 == gci.CookedReadData().VisibleCharCount()) { // If we had a valid pointer, but there are no visible characters for the edit line, then it's empty. // Someone started editing and back spaced the whole line out so it exists, but has no data. return true; } else { return false; } } void CommandLine::Hide(const bool fUpdateFields) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (!IsEditLineEmpty()) { DeleteCommandLine(gci.CookedReadData(), fUpdateFields); } _isVisible = false; } void CommandLine::Show() { _isVisible = true; CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (!IsEditLineEmpty()) { RedrawCommandLine(gci.CookedReadData()); } } // Routine Description: // - Returns true if the commandline is currently being displayed. This is false // after Hide() is called, and before Show() is called again. // Return Value: // - true if the commandline should be displayed. Does not take into account // the echo state of the input. This is only controlled by calls to Hide/Show bool CommandLine::IsVisible() const noexcept { return _isVisible; } // Routine Description: // - checks for the presence of a popup // Return Value: // - true if popup is present bool CommandLine::HasPopup() const noexcept { return !_popups.empty(); } // Routine Description: // - gets the topmost popup // Arguments: // Return Value: // - ref to the topmost popup Popup& CommandLine::GetPopup() { return *_popups.front(); } // Routine Description: // - stops the current popup void CommandLine::EndCurrentPopup() { if (!_popups.empty()) { _popups.front()->End(); _popups.pop_front(); } } // Routine Description: // - stops all popups void CommandLine::EndAllPopups() { while (!_popups.empty()) { EndCurrentPopup(); } } void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateFields) { size_t CharsToWrite = cookedReadData.VisibleCharCount(); COORD coordOriginalCursor = cookedReadData.OriginalCursorPosition(); const COORD coordBufferSize = cookedReadData.ScreenInfo().GetBufferSize().Dimensions(); // catch the case where the current command has scrolled off the top of the screen. if (coordOriginalCursor.Y < 0) { CharsToWrite += coordBufferSize.X * coordOriginalCursor.Y; CharsToWrite += cookedReadData.OriginalCursorPosition().X; // account for prompt cookedReadData.OriginalCursorPosition().X = 0; cookedReadData.OriginalCursorPosition().Y = 0; coordOriginalCursor.X = 0; coordOriginalCursor.Y = 0; } if (!CheckBisectStringW(cookedReadData.BufferStartPtr(), CharsToWrite, coordBufferSize.X - cookedReadData.OriginalCursorPosition().X)) { CharsToWrite++; } try { cookedReadData.ScreenInfo().Write(OutputCellIterator(UNICODE_SPACE, CharsToWrite), coordOriginalCursor); } CATCH_LOG(); if (fUpdateFields) { cookedReadData.Erase(); } LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cookedReadData.OriginalCursorPosition(), true)); } void RedrawCommandLine(COOKED_READ_DATA& cookedReadData) { if (cookedReadData.IsEchoInput()) { // Draw the command line cookedReadData.OriginalCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); SHORT ScrollY = 0; #pragma prefast(suppress : 28931, "Status is not unused. It's used in debug assertions.") NTSTATUS Status = WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferStartPtr(), cookedReadData.BufferStartPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY); FAIL_FAST_IF_NTSTATUS_FAILED(Status); cookedReadData.OriginalCursorPosition().Y += ScrollY; // Move the cursor back to the right position COORD CursorPosition = cookedReadData.OriginalCursorPosition(); CursorPosition.X += (SHORT)RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint()); if (CheckBisectStringW(cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint(), cookedReadData.ScreenInfo().GetBufferSize().Width() - cookedReadData.OriginalCursorPosition().X)) { CursorPosition.X++; } Status = AdjustCursorPosition(cookedReadData.ScreenInfo(), CursorPosition, TRUE, nullptr); FAIL_FAST_IF_NTSTATUS_FAILED(Status); } } // Routine Description: // - This routine copies the commandline specified by Index into the cooked read buffer void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ SHORT Index) // index, not command number { DeleteCommandLine(cookedReadData, TRUE); FAIL_FAST_IF_FAILED(cookedReadData.History().RetrieveNth(Index, cookedReadData.SpanWholeBuffer(), cookedReadData.BytesRead())); FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); if (cookedReadData.IsEchoInput()) { SHORT ScrollY = 0; FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY)); cookedReadData.OriginalCursorPosition().Y += ScrollY; } size_t const CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); cookedReadData.InsertionPoint() = CharsToWrite; cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); } // Routine Description: // - This routine handles the command list popup. It puts up the popup, then calls ProcessCommandListInput to get and process input. // Return Value: // - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created // - STATUS_SUCCESS - read was fully completed (user hit return) [[nodiscard]] NTSTATUS CommandLine::_startCommandListPopup(COOKED_READ_DATA& cookedReadData) { if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) { try { auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo(), cookedReadData.History())); popup.Draw(); return popup.Process(cookedReadData); } CATCH_RETURN(); } else { return S_FALSE; } } // Routine Description: // - This routine handles the "delete up to this char" popup. It puts up the popup, then calls ProcessCopyFromCharInput to get and process input. // Return Value: // - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created // - STATUS_SUCCESS - read was fully completed (user hit return) [[nodiscard]] NTSTATUS CommandLine::_startCopyFromCharPopup(COOKED_READ_DATA& cookedReadData) { // Delete the current command from cursor position to the // letter specified by the user. The user is prompted via // popup to enter a character. if (cookedReadData.HasHistory()) { try { auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); popup.Draw(); return popup.Process(cookedReadData); } CATCH_RETURN(); } else { return S_FALSE; } } // Routine Description: // - This routine handles the "copy up to this char" popup. It puts up the popup, then calls ProcessCopyToCharInput to get and process input. // Return Value: // - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created // - STATUS_SUCCESS - read was fully completed (user hit return) // - S_FALSE - if we couldn't make a popup because we had no commands [[nodiscard]] NTSTATUS CommandLine::_startCopyToCharPopup(COOKED_READ_DATA& cookedReadData) { // copy the previous command to the current command, up to but // not including the character specified by the user. the user // is prompted via popup to enter a character. if (cookedReadData.HasHistory()) { try { auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); popup.Draw(); return popup.Process(cookedReadData); } CATCH_RETURN(); } else { return S_FALSE; } } // Routine Description: // - This routine handles the "enter command number" popup. It puts up the popup, then calls ProcessCommandNumberInput to get and process input. // Return Value: // - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created // - STATUS_SUCCESS - read was fully completed (user hit return) // - S_FALSE - if we couldn't make a popup because we had no commands or it wouldn't fit. [[nodiscard]] HRESULT CommandLine::StartCommandNumberPopup(COOKED_READ_DATA& cookedReadData) { if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands() && cookedReadData.ScreenInfo().GetBufferSize().Width() >= Popup::MINIMUM_COMMAND_PROMPT_SIZE + 2) { try { auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); popup.Draw(); // Save the original cursor position in case the user cancels out of the dialog cookedReadData.BeforeDialogCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); // Move the cursor into the dialog so the user can type multiple characters for the command number const COORD CursorPosition = popup.GetCursorPosition(); LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(CursorPosition, TRUE)); // Transfer control to the handler routine return popup.Process(cookedReadData); } CATCH_RETURN(); } else { return S_FALSE; } } // Routine Description: // - Process virtual key code and updates the prompt line with the next history element in the direction // specified by wch // Arguments: // - cookedReadData - The cooked read data to operate on // - searchDirection - Direction in history to search // Note: // - May throw exceptions void CommandLine::_processHistoryCycling(COOKED_READ_DATA& cookedReadData, const CommandHistory::SearchDirection searchDirection) { // for doskey compatibility, buffer isn't circular. don't do anything if attempting // to cycle history past the bounds of the history buffer if (!cookedReadData.HasHistory()) { return; } else if (searchDirection == CommandHistory::SearchDirection::Previous && cookedReadData.History().AtFirstCommand()) { return; } else if (searchDirection == CommandHistory::SearchDirection::Next && cookedReadData.History().AtLastCommand()) { return; } DeleteCommandLine(cookedReadData, true); THROW_IF_FAILED(cookedReadData.History().Retrieve(searchDirection, cookedReadData.SpanWholeBuffer(), cookedReadData.BytesRead())); FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); if (cookedReadData.IsEchoInput()) { short ScrollY = 0; FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY)); cookedReadData.OriginalCursorPosition().Y += ScrollY; } const size_t CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); cookedReadData.InsertionPoint() = CharsToWrite; cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); } // Routine Description: // - Sets the text on the prompt to the oldest run command in the cookedReadData's history // Arguments: // - cookedReadData - The cooked read data to operate on // Note: // - May throw exceptions void CommandLine::_setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData) { if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) { DeleteCommandLine(cookedReadData, true); const short commandNumber = 0; THROW_IF_FAILED(cookedReadData.History().RetrieveNth(commandNumber, cookedReadData.SpanWholeBuffer(), cookedReadData.BytesRead())); FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); if (cookedReadData.IsEchoInput()) { short ScrollY = 0; FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY)); cookedReadData.OriginalCursorPosition().Y += ScrollY; } size_t CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); cookedReadData.InsertionPoint() = CharsToWrite; cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); } } // Routine Description: // - Sets the text on the prompt the most recently run command in cookedReadData's history // Arguments: // - cookedReadData - The cooked read data to operate on // Note: // - May throw exceptions void CommandLine::_setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData) { DeleteCommandLine(cookedReadData, true); if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) { const short commandNumber = (SHORT)(cookedReadData.History().GetNumberOfCommands() - 1); THROW_IF_FAILED(cookedReadData.History().RetrieveNth(commandNumber, cookedReadData.SpanWholeBuffer(), cookedReadData.BytesRead())); FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); if (cookedReadData.IsEchoInput()) { short ScrollY = 0; FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY)); cookedReadData.OriginalCursorPosition().Y += ScrollY; } size_t CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); cookedReadData.InsertionPoint() = CharsToWrite; cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); } } // Routine Description: // - Deletes all prompt text to the right of the cursor // Arguments: // - cookedReadData - The cooked read data to operate on void CommandLine::DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noexcept { DeleteCommandLine(cookedReadData, false); cookedReadData.BytesRead() = cookedReadData.InsertionPoint() * sizeof(WCHAR); if (cookedReadData.IsEchoInput()) { FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferStartPtr(), cookedReadData.BufferStartPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, nullptr)); } } // Routine Description: // - Deletes all user input on the prompt to the left of the cursor // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - The new cursor position COORD CommandLine::_deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadData) noexcept { DeleteCommandLine(cookedReadData, false); cookedReadData.BytesRead() -= cookedReadData.InsertionPoint() * sizeof(WCHAR); cookedReadData.InsertionPoint() = 0; memmove(cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BytesRead()); cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr()); if (cookedReadData.IsEchoInput()) { FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferStartPtr(), cookedReadData.BufferStartPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, nullptr)); } return cookedReadData.OriginalCursorPosition(); } // Routine Description: // - Moves the cursor to the end of the prompt text // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - The new cursor position COORD CommandLine::_moveCursorToEndOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept { cookedReadData.InsertionPoint() = cookedReadData.BytesRead() / sizeof(WCHAR); cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + cookedReadData.InsertionPoint()); COORD cursorPosition{ 0, 0 }; cursorPosition.X = (SHORT)(cookedReadData.OriginalCursorPosition().X + cookedReadData.VisibleCharCount()); cursorPosition.Y = cookedReadData.OriginalCursorPosition().Y; const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); if (CheckBisectProcessW(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint(), sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X, cookedReadData.OriginalCursorPosition().X, true)) { cursorPosition.X++; } return cursorPosition; } // Routine Description: // - Moves the cursor to the start of the user input on the prompt // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - The new cursor position COORD CommandLine::_moveCursorToStartOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept { cookedReadData.InsertionPoint() = 0; cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr()); return cookedReadData.OriginalCursorPosition(); } // Routine Description: // - Moves the cursor left by a word // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - New cursor position COORD CommandLine::_moveCursorLeftByWord(COOKED_READ_DATA& cookedReadData) noexcept { PWCHAR LastWord; COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); if (cookedReadData.BufferCurrentPtr() != cookedReadData.BufferStartPtr()) { // A bit better word skipping. LastWord = cookedReadData.BufferCurrentPtr() - 1; if (LastWord != cookedReadData.BufferStartPtr()) { if (*LastWord == L' ') { // Skip spaces, until the non-space character is found. while (--LastWord != cookedReadData.BufferStartPtr()) { FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); if (*LastWord != L' ') { break; } } } if (LastWord != cookedReadData.BufferStartPtr()) { if (IsWordDelim(*LastWord)) { // Skip WORD_DELIMs until space or non WORD_DELIM is found. while (--LastWord != cookedReadData.BufferStartPtr()) { FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); if (*LastWord == L' ' || !IsWordDelim(*LastWord)) { break; } } } else { // Skip the regular words while (--LastWord != cookedReadData.BufferStartPtr()) { FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); if (IsWordDelim(*LastWord)) { break; } } } } FAIL_FAST_IF(!(LastWord >= cookedReadData.BufferStartPtr())); if (LastWord != cookedReadData.BufferStartPtr()) { // LastWord is currently pointing to the last character // of the previous word, unless it backed up to the beginning // of the buffer. // Let's increment LastWord so that it points to the expected // insertion point. ++LastWord; } cookedReadData.SetBufferCurrentPtr(LastWord); } cookedReadData.InsertionPoint() = (ULONG)(cookedReadData.BufferCurrentPtr() - cookedReadData.BufferStartPtr()); cursorPosition = cookedReadData.OriginalCursorPosition(); cursorPosition.X = (SHORT)(cursorPosition.X + RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint())); const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); if (CheckBisectStringW(cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() + 1, sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X)) { cursorPosition.X++; } } return cursorPosition; } // Routine Description: // - Moves cursor left by a glyph // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - New cursor position COORD CommandLine::_moveCursorLeft(COOKED_READ_DATA& cookedReadData) { COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); if (cookedReadData.BufferCurrentPtr() != cookedReadData.BufferStartPtr()) { cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() - 1); cookedReadData.InsertionPoint()--; cursorPosition.X = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition().X; cursorPosition.Y = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition().Y; cursorPosition.X = (SHORT)(cursorPosition.X - RetrieveNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint())); const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); if (CheckBisectProcessW(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() + 2, sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X, cookedReadData.OriginalCursorPosition().X, true)) { if ((cursorPosition.X == -2) || (cursorPosition.X == -1)) { cursorPosition.X--; } } } return cursorPosition; } // Routine Description: // - Moves the cursor to the right by a word // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - The new cursor position COORD CommandLine::_moveCursorRightByWord(COOKED_READ_DATA& cookedReadData) noexcept { COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); if (cookedReadData.InsertionPoint() < (cookedReadData.BytesRead() / sizeof(WCHAR))) { PWCHAR NextWord = cookedReadData.BufferCurrentPtr(); // A bit better word skipping. PWCHAR BufLast = cookedReadData.BufferStartPtr() + cookedReadData.BytesRead() / sizeof(WCHAR); FAIL_FAST_IF(!(NextWord < BufLast)); if (*NextWord == L' ') { // If the current character is space, skip to the next non-space character. while (NextWord < BufLast) { if (*NextWord != L' ') { break; } ++NextWord; } } else { // Skip the body part. bool fStartFromDelim = IsWordDelim(*NextWord); while (++NextWord < BufLast) { if (fStartFromDelim != IsWordDelim(*NextWord)) { break; } } // Skip the space block. if (NextWord < BufLast && *NextWord == L' ') { while (++NextWord < BufLast) { if (*NextWord != L' ') { break; } } } } cookedReadData.SetBufferCurrentPtr(NextWord); cookedReadData.InsertionPoint() = (ULONG)(cookedReadData.BufferCurrentPtr() - cookedReadData.BufferStartPtr()); cursorPosition = cookedReadData.OriginalCursorPosition(); cursorPosition.X = (SHORT)(cursorPosition.X + RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint())); const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); if (CheckBisectStringW(cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() + 1, sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X)) { cursorPosition.X++; } } return cursorPosition; } // Routine Description: // - Moves the cursor to the right by a glyph // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - The new cursor position COORD CommandLine::_moveCursorRight(COOKED_READ_DATA& cookedReadData) noexcept { COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); // If not at the end of the line, move cursor position right. if (cookedReadData.InsertionPoint() < (cookedReadData.BytesRead() / sizeof(WCHAR))) { cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); cursorPosition.X = (SHORT)(cursorPosition.X + RetrieveNumberOfSpaces(cookedReadData.OriginalCursorPosition().X, cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint())); if (CheckBisectProcessW(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() + 2, sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X, cookedReadData.OriginalCursorPosition().X, true)) { if (cursorPosition.X == (sScreenBufferSizeX - 1)) cursorPosition.X++; } cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); cookedReadData.InsertionPoint()++; } // if at the end of the line, copy a character from the same position in the last command else if (cookedReadData.HasHistory()) { size_t NumSpaces; const auto LastCommand = cookedReadData.History().GetLastCommand(); if (!LastCommand.empty() && LastCommand.size() > cookedReadData.InsertionPoint()) { *cookedReadData.BufferCurrentPtr() = LastCommand[cookedReadData.InsertionPoint()]; cookedReadData.BytesRead() += sizeof(WCHAR); cookedReadData.InsertionPoint()++; if (cookedReadData.IsEchoInput()) { short ScrollY = 0; size_t CharsToWrite = sizeof(WCHAR); FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr(), &CharsToWrite, &NumSpaces, cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY)); cookedReadData.OriginalCursorPosition().Y += ScrollY; cookedReadData.VisibleCharCount() += NumSpaces; // update reported cursor position if (ScrollY != 0) { cursorPosition.X = 0; cursorPosition.Y += ScrollY; } else { cursorPosition.X += 1; } } cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); } } return cursorPosition; } // Routine Description: // - Place a ctrl-z in the current command line // Arguments: // - cookedReadData - The cooked read data to operate on void CommandLine::_insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept { size_t NumSpaces = 0; *cookedReadData.BufferCurrentPtr() = (WCHAR)0x1a; // ctrl-z cookedReadData.BytesRead() += sizeof(WCHAR); cookedReadData.InsertionPoint()++; if (cookedReadData.IsEchoInput()) { short ScrollY = 0; size_t CharsToWrite = sizeof(WCHAR); FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr(), &CharsToWrite, &NumSpaces, cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY)); cookedReadData.OriginalCursorPosition().Y += ScrollY; cookedReadData.VisibleCharCount() += NumSpaces; } cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); } // Routine Description: // - Empties the command history for cookedReadData // Arguments: // - cookedReadData - The cooked read data to operate on void CommandLine::_deleteCommandHistory(COOKED_READ_DATA& cookedReadData) noexcept { if (cookedReadData.HasHistory()) { cookedReadData.History().Empty(); cookedReadData.History().Flags |= CommandHistory::CLE_ALLOCATED; } } // Routine Description: // - Copy the remainder of the previous command to the current command. // Arguments: // - cookedReadData - The cooked read data to operate on void CommandLine::_fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cookedReadData) noexcept { if (cookedReadData.HasHistory()) { size_t NumSpaces, cchCount; const auto LastCommand = cookedReadData.History().GetLastCommand(); if (!LastCommand.empty() && LastCommand.size() > cookedReadData.InsertionPoint()) { cchCount = LastCommand.size() - cookedReadData.InsertionPoint(); const auto bufferSpan = cookedReadData.SpanAtPointer(); std::copy_n(LastCommand.cbegin() + cookedReadData.InsertionPoint(), cchCount, bufferSpan.begin()); cookedReadData.InsertionPoint() += cchCount; cchCount *= sizeof(WCHAR); cookedReadData.BytesRead() = std::max(LastCommand.size() * sizeof(wchar_t), cookedReadData.BytesRead()); if (cookedReadData.IsEchoInput()) { short ScrollY = 0; FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr(), &cchCount, &NumSpaces, cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY)); cookedReadData.OriginalCursorPosition().Y += ScrollY; cookedReadData.VisibleCharCount() += NumSpaces; } cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + cchCount / sizeof(WCHAR)); } } } // Routine Description: // - Cycles through the stored commands that start with the characters in the current command. // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - The new cursor position COORD CommandLine::_cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& cookedReadData) { COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); if (cookedReadData.HasHistory()) { SHORT index; if (cookedReadData.History().FindMatchingCommand({ cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() }, cookedReadData.History().LastDisplayed, index, CommandHistory::MatchOptions::None)) { SHORT CurrentPos; // save cursor position CurrentPos = (SHORT)cookedReadData.InsertionPoint(); DeleteCommandLine(cookedReadData, true); THROW_IF_FAILED(cookedReadData.History().RetrieveNth((SHORT)index, cookedReadData.SpanWholeBuffer(), cookedReadData.BytesRead())); FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); if (cookedReadData.IsEchoInput()) { short ScrollY = 0; FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, &ScrollY)); cookedReadData.OriginalCursorPosition().Y += ScrollY; cursorPosition.Y += ScrollY; } // restore cursor position cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CurrentPos); cookedReadData.InsertionPoint() = CurrentPos; FAIL_FAST_IF_NTSTATUS_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cursorPosition, true)); } } return cursorPosition; } // Routine Description: // - Deletes a glyph from the right side of the cursor // Arguments: // - cookedReadData - The cooked read data to operate on // Return Value: // - The new cursor position COORD CommandLine::DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData) noexcept { // save cursor position COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); if (!cookedReadData.AtEol()) { // Delete commandline. // clang-format off #pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "Not sure why prefast is getting confused here") // clang-format on DeleteCommandLine(cookedReadData, false); // Delete char. cookedReadData.BytesRead() -= sizeof(WCHAR); memmove(cookedReadData.BufferCurrentPtr(), cookedReadData.BufferCurrentPtr() + 1, cookedReadData.BytesRead() - (cookedReadData.InsertionPoint() * sizeof(WCHAR))); { PWCHAR buf = (PWCHAR)((PBYTE)cookedReadData.BufferStartPtr() + cookedReadData.BytesRead()); *buf = (WCHAR)' '; } // Write commandline. if (cookedReadData.IsEchoInput()) { FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.BufferStartPtr(), cookedReadData.BufferStartPtr(), &cookedReadData.BytesRead(), &cookedReadData.VisibleCharCount(), cookedReadData.OriginalCursorPosition().X, WC_DESTRUCTIVE_BACKSPACE | WC_KEEP_CURSOR_VISIBLE | WC_PRINTABLE_CONTROL_CHARS, nullptr)); } // restore cursor position const SHORT sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); if (CheckBisectProcessW(cookedReadData.ScreenInfo(), cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() + 1, sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().X, cookedReadData.OriginalCursorPosition().X, true)) { cursorPosition.X++; } } return cursorPosition; } // TODO: [MSFT:4586207] Clean up this mess -- needs helpers. http://osgvsowi/4586207 // Routine Description: // - This routine process command line editing keys. // Return Value: // - CONSOLE_STATUS_WAIT - CommandListPopup ran out of input // - CONSOLE_STATUS_READ_COMPLETE - user hit in CommandListPopup // - STATUS_SUCCESS - everything's cool [[nodiscard]] NTSTATUS CommandLine::ProcessCommandLine(COOKED_READ_DATA& cookedReadData, _In_ WCHAR wch, const DWORD dwKeyState) { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); COORD cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); NTSTATUS Status; const bool altPressed = WI_IsAnyFlagSet(dwKeyState, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); const bool ctrlPressed = WI_IsAnyFlagSet(dwKeyState, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); bool UpdateCursorPosition = false; switch (wch) { case VK_ESCAPE: DeleteCommandLine(cookedReadData, true); break; case VK_DOWN: try { _processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); Status = STATUS_SUCCESS; } catch (...) { Status = wil::ResultFromCaughtException(); } break; case VK_UP: case VK_F5: try { _processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); Status = STATUS_SUCCESS; } catch (...) { Status = wil::ResultFromCaughtException(); } break; case VK_PRIOR: try { _setPromptToOldestCommand(cookedReadData); Status = STATUS_SUCCESS; } catch (...) { Status = wil::ResultFromCaughtException(); } break; case VK_NEXT: try { _setPromptToNewestCommand(cookedReadData); Status = STATUS_SUCCESS; } catch (...) { Status = wil::ResultFromCaughtException(); } break; case VK_END: if (ctrlPressed) { DeletePromptAfterCursor(cookedReadData); } else { cursorPosition = _moveCursorToEndOfPrompt(cookedReadData); UpdateCursorPosition = true; } break; case VK_HOME: if (ctrlPressed) { cursorPosition = _deletePromptBeforeCursor(cookedReadData); UpdateCursorPosition = true; } else { cursorPosition = _moveCursorToStartOfPrompt(cookedReadData); UpdateCursorPosition = true; } break; case VK_LEFT: if (ctrlPressed) { cursorPosition = _moveCursorLeftByWord(cookedReadData); UpdateCursorPosition = true; } else { cursorPosition = _moveCursorLeft(cookedReadData); UpdateCursorPosition = true; } break; case VK_F1: { // we don't need to check for end of buffer here because we've // already done it. cursorPosition = _moveCursorRight(cookedReadData); UpdateCursorPosition = true; break; } case VK_RIGHT: // we don't need to check for end of buffer here because we've // already done it. if (ctrlPressed) { cursorPosition = _moveCursorRightByWord(cookedReadData); UpdateCursorPosition = true; } else { cursorPosition = _moveCursorRight(cookedReadData); UpdateCursorPosition = true; } break; case VK_F2: { Status = _startCopyToCharPopup(cookedReadData); if (S_FALSE == Status) { // We couldn't make the popup, so loop around and read the next character. break; } else { return Status; } } case VK_F3: _fillPromptWithPreviousCommandFragment(cookedReadData); break; case VK_F4: { Status = _startCopyFromCharPopup(cookedReadData); if (S_FALSE == Status) { // We couldn't display a popup. Go around a loop behind. break; } else { return Status; } } case VK_F6: { _insertCtrlZ(cookedReadData); break; } case VK_F7: if (!ctrlPressed && !altPressed) { Status = _startCommandListPopup(cookedReadData); } else if (altPressed) { _deleteCommandHistory(cookedReadData); } break; case VK_F8: try { cursorPosition = _cycleMatchingCommandHistoryToPrompt(cookedReadData); UpdateCursorPosition = true; } catch (...) { Status = wil::ResultFromCaughtException(); } break; case VK_F9: { Status = StartCommandNumberPopup(cookedReadData); if (S_FALSE == Status) { // If we couldn't make the popup, break and go around to read another input character. break; } else { return Status; } } case VK_F10: // Alt+F10 clears the aliases for specifically cmd.exe. if (altPressed) { Alias::s_ClearCmdExeAliases(); } break; case VK_INSERT: cookedReadData.SetInsertMode(!cookedReadData.IsInsertMode()); cookedReadData.ScreenInfo().SetCursorDBMode(cookedReadData.IsInsertMode() != gci.GetInsertMode()); break; case VK_DELETE: cursorPosition = DeleteFromRightOfCursor(cookedReadData); UpdateCursorPosition = true; break; default: FAIL_FAST_HR(E_NOTIMPL); break; } if (UpdateCursorPosition && cookedReadData.IsEchoInput()) { Status = AdjustCursorPosition(cookedReadData.ScreenInfo(), cursorPosition, true, nullptr); FAIL_FAST_IF_NTSTATUS_FAILED(Status); } return STATUS_SUCCESS; }