This PR provides a faster algorithm for converting 8-bit and 24-bit colors into the 4-bit legacy values that are required by the Win32 console APIs. It also fixes areas of the code that were incorrectly using a simple 16-color conversion that didn't handle 8-bit and 24-bit values. The faster conversion algorithm should be an improvement for issues #783 and #3950. One of the main points of this PR was to fix the `ReadConsoleOutputAttribute` API, which was using a simplified legacy color conversion (the original `TextAttribute:GetLegacyAttributes` method), which could only handle values from the 16-color table. RGB values, and colors from the 256-color table, would be mapped to completely nonsensical values. This API has now been updated to use the more correct `Settings::GenerateLegacyAttributes` method. But there were also a couple of other places in the code that were using `GetLegacyAttributes` when they really had no reason to be working with legacy attributes at all. This could result in colors being downgraded to 4-bit values (often badly, as explained above), when the code was already perfectly capable of displaying the full 24-bits. This included the fill colors in the IME composer (in `ConsoleImeInfo`), and the construction of the highlighting colors in the color search/selection handler (`Selection::_HandleColorSelection`). I also got rid of some legacy attribute code in the `Popup` class, which was originally intended to update colors below the popup when the settings changed, but actually caused more problems than it solved. The other major goal of this PR was to improve the performance of the `GenerateLegacyAttributes` method, since the existing implementation could be quite slow when dealing with RGB values. The simple cases are handled much the same as they were before. For an `IsDefault` color, we get the default index from the `Settings::_wFillAttribute` field. For an `IsIndex16` color, the index can just be returned as is. For an `IsRgb` color, the RGB components are compressed down to 8 bits (3 red, 3 green, 2 blue), simply by dropping the least significant bits. This 8-bit value is then used to lookup a representative 16-color value from a hard-coded table. An `IsIndex256` color is also converted with a lookup table, just using the existing 8-bit index. The RGB mapping table was calculated by taking each compressed 8-bit color, and picking a entry from the _Campbell_ palette that best approximated that color. This was done by looking at a range of 24-bit colors that mapped to the 8-bit value, finding the best _Campbell_ match for each of them (using a [CIEDE2000] color difference calculation), and then the most common match became the index that the 8-bit value would map to. The 256-color table was just a simpler version of this process. For each entry in the table, we take the default RGB palette value, and find it's closest match in the _Campbell_ palette. Because these tables are hard-coded, the results won't adjust to changes in the palette. However, they should still produce reasonable results for palettes that follow the standard ANSI color range. And since they're only a very loose approximation of the colors anyway, the exact value really isn't that important. That said, I have tried to make sure that if you take an RGB value for a particular index in a reasonable color scheme, then the legacy color mapped from that value should ideally match the same index. This will never be possible for all color schemes, but I have tweaked a few of the table entries to improve the results for some of the common schemes. One other point worth making regarding the hard-coded tables: even if we wanted to take the active palette into account, that wouldn't actually be possible over a conpty connection, because we can't easily know what color scheme the client application is using. At least this way the results in conhost are guaranteed to be the same as in the Windows Terminal. [CIEDE2000]: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000 ## Validation Steps Performed This code still passes the `TextAttributeTests` that check the basic `GetLegacyAttribute` behaviour and verify the all legacy attributes roundtrip correctly. However, some of the values in the `RgbColorTests` had to be updated, since we're now intentionally returning different values as a result of the changes to the RGB conversion algorithm. I haven't added additional unit tests, but I have done a lot of manual testing to see how well the new algorithm works with a range of colors and a variety of different color schemes. It's not perfect in every situation, but I think it works well enough for the purpose it serves. I've also confirmed that the issues reported in #5940 and #6247 are now fixed by these changes. Closes #5940 Closes #6247
1317 lines
54 KiB
C++
1317 lines
54 KiB
C++
// 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_ECHO,
|
|
&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_ECHO,
|
|
&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<CommandListPopup>(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<CopyFromCharPopup>(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<CopyToCharPopup>(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<CommandNumberPopup>(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_ECHO,
|
|
&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_ECHO,
|
|
&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_ECHO,
|
|
&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_ECHO,
|
|
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_ECHO,
|
|
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_ECHO,
|
|
&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_ECHO,
|
|
&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_ECHO,
|
|
&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_ECHO,
|
|
&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_ECHO,
|
|
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 <enter> 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;
|
|
}
|