550 lines
17 KiB
C++
550 lines
17 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "CommandListPopup.hpp"
|
|
#include "stream.h"
|
|
#include "_stream.h"
|
|
#include "cmdline.h"
|
|
#include "misc.h"
|
|
#include "_output.h"
|
|
#include "dbcs.h"
|
|
#include "../types/inc/GlyphWidth.hpp"
|
|
|
|
#include "../interactivity/inc/ServiceLocator.hpp"
|
|
|
|
static constexpr size_t COMMAND_NUMBER_SIZE = 8; // size of command number buffer
|
|
|
|
// Routine Description:
|
|
// - Calculates what the proposed size of the popup should be, based on the commands in the history
|
|
// Arguments:
|
|
// - history - the history to look through to measure command sizes
|
|
// Return Value:
|
|
// - the proposed size of the popup with the history list taken into account
|
|
static COORD calculatePopupSize(const CommandHistory& history)
|
|
{
|
|
// this is the historical size of the popup, so it is now used as a minimum
|
|
const COORD minSize = { 40, 10 };
|
|
|
|
// padding is for the command number listing before a command is printed to the window.
|
|
// ex: |10: echo blah
|
|
// ^^^^ <- these are the cells that are being accounted for by padding
|
|
const size_t padding = 4;
|
|
|
|
// find the widest command history item and use it for the width
|
|
size_t width = minSize.X;
|
|
for (size_t i = 0; i < history.GetNumberOfCommands(); ++i)
|
|
{
|
|
const auto& historyItem = history.GetNth(gsl::narrow<short>(i));
|
|
width = std::max(width, historyItem.size() + padding);
|
|
}
|
|
if (width > SHRT_MAX)
|
|
{
|
|
width = SHRT_MAX;
|
|
}
|
|
|
|
// calculate height, it can range up to 20 rows
|
|
short height = std::clamp(gsl::narrow<short>(history.GetNumberOfCommands()), minSize.Y, 20i16);
|
|
|
|
return { gsl::narrow<short>(width), height };
|
|
}
|
|
|
|
CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history) :
|
|
Popup(screenInfo, calculatePopupSize(history)),
|
|
_history{ history },
|
|
_currentCommand{ std::min(history.LastDisplayed, static_cast<SHORT>(history.GetNumberOfCommands() - 1)) }
|
|
{
|
|
FAIL_FAST_IF(_currentCommand < 0);
|
|
_setBottomIndex();
|
|
}
|
|
|
|
[[nodiscard]] NTSTATUS CommandListPopup::_handlePopupKeys(COOKED_READ_DATA& cookedReadData,
|
|
const wchar_t wch,
|
|
const DWORD modifiers) noexcept
|
|
{
|
|
try
|
|
{
|
|
short Index = 0;
|
|
const bool shiftPressed = WI_IsFlagSet(modifiers, SHIFT_PRESSED);
|
|
switch (wch)
|
|
{
|
|
case VK_F9:
|
|
{
|
|
const HRESULT hr = CommandLine::Instance().StartCommandNumberPopup(cookedReadData);
|
|
if (S_FALSE == hr)
|
|
{
|
|
// If we couldn't make the popup, break and go around to read another input character.
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
return hr;
|
|
}
|
|
}
|
|
case VK_ESCAPE:
|
|
CommandLine::Instance().EndCurrentPopup();
|
|
return CONSOLE_STATUS_WAIT_NO_BLOCK;
|
|
case VK_UP:
|
|
if (shiftPressed)
|
|
{
|
|
return _swapUp(cookedReadData);
|
|
}
|
|
else
|
|
{
|
|
_update(-1);
|
|
}
|
|
break;
|
|
case VK_DOWN:
|
|
if (shiftPressed)
|
|
{
|
|
return _swapDown(cookedReadData);
|
|
}
|
|
else
|
|
{
|
|
_update(1);
|
|
}
|
|
break;
|
|
case VK_END:
|
|
// Move waaay forward, UpdateCommandListPopup() can handle it.
|
|
_update((SHORT)(cookedReadData.History().GetNumberOfCommands()));
|
|
break;
|
|
case VK_HOME:
|
|
// Move waaay back, UpdateCommandListPopup() can handle it.
|
|
_update(-(SHORT)(cookedReadData.History().GetNumberOfCommands()));
|
|
break;
|
|
case VK_PRIOR:
|
|
_update(-(SHORT)Height());
|
|
break;
|
|
case VK_NEXT:
|
|
_update((SHORT)Height());
|
|
break;
|
|
case VK_DELETE:
|
|
return _deleteSelection(cookedReadData);
|
|
case VK_LEFT:
|
|
case VK_RIGHT:
|
|
Index = _currentCommand;
|
|
CommandLine::Instance().EndCurrentPopup();
|
|
SetCurrentCommandLine(cookedReadData, (SHORT)Index);
|
|
return CONSOLE_STATUS_WAIT_NO_BLOCK;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
void CommandListPopup::_setBottomIndex()
|
|
{
|
|
if (_currentCommand < (SHORT)(_history.GetNumberOfCommands() - Height()))
|
|
{
|
|
_bottomIndex = std::max(_currentCommand, gsl::narrow<SHORT>(Height() - 1i16));
|
|
}
|
|
else
|
|
{
|
|
_bottomIndex = (SHORT)(_history.GetNumberOfCommands() - 1);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] NTSTATUS CommandListPopup::_deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept
|
|
{
|
|
try
|
|
{
|
|
auto& history = cookedReadData.History();
|
|
history.Remove(static_cast<short>(_currentCommand));
|
|
_setBottomIndex();
|
|
|
|
if (history.GetNumberOfCommands() == 0)
|
|
{
|
|
// close the popup
|
|
return CONSOLE_STATUS_READ_COMPLETE;
|
|
}
|
|
else if (_currentCommand >= static_cast<short>(history.GetNumberOfCommands()))
|
|
{
|
|
_currentCommand = static_cast<short>(history.GetNumberOfCommands() - 1);
|
|
_bottomIndex = _currentCommand;
|
|
}
|
|
|
|
_drawList();
|
|
}
|
|
CATCH_LOG();
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - moves the selected history item up in the history list
|
|
// Arguments:
|
|
// - cookedReadData - the read wait object to operate upon
|
|
[[nodiscard]] NTSTATUS CommandListPopup::_swapUp(COOKED_READ_DATA& cookedReadData) noexcept
|
|
{
|
|
try
|
|
{
|
|
auto& history = cookedReadData.History();
|
|
|
|
if (history.GetNumberOfCommands() <= 1 || _currentCommand == 0)
|
|
{
|
|
return STATUS_SUCCESS;
|
|
}
|
|
history.Swap(_currentCommand, _currentCommand - 1);
|
|
_update(-1);
|
|
_drawList();
|
|
}
|
|
CATCH_LOG();
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - moves the selected history item down in the history list
|
|
// Arguments:
|
|
// - cookedReadData - the read wait object to operate upon
|
|
[[nodiscard]] NTSTATUS CommandListPopup::_swapDown(COOKED_READ_DATA& cookedReadData) noexcept
|
|
{
|
|
try
|
|
{
|
|
auto& history = cookedReadData.History();
|
|
|
|
if (history.GetNumberOfCommands() <= 1 || _currentCommand == gsl::narrow<short>(history.GetNumberOfCommands()) - 1i16)
|
|
{
|
|
return STATUS_SUCCESS;
|
|
}
|
|
history.Swap(_currentCommand, _currentCommand + 1);
|
|
_update(1);
|
|
_drawList();
|
|
}
|
|
CATCH_LOG();
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
void CommandListPopup::_handleReturn(COOKED_READ_DATA& cookedReadData)
|
|
{
|
|
short Index = 0;
|
|
NTSTATUS Status = STATUS_SUCCESS;
|
|
DWORD LineCount = 1;
|
|
Index = _currentCommand;
|
|
CommandLine::Instance().EndCurrentPopup();
|
|
SetCurrentCommandLine(cookedReadData, (SHORT)Index);
|
|
cookedReadData.ProcessInput(UNICODE_CARRIAGERETURN, 0, Status);
|
|
// complete read
|
|
if (cookedReadData.IsEchoInput())
|
|
{
|
|
// check for alias
|
|
cookedReadData.ProcessAliases(LineCount);
|
|
}
|
|
|
|
Status = STATUS_SUCCESS;
|
|
size_t NumBytes;
|
|
if (cookedReadData.BytesRead() > cookedReadData.UserBufferSize() || LineCount > 1)
|
|
{
|
|
if (LineCount > 1)
|
|
{
|
|
const wchar_t* Tmp;
|
|
for (Tmp = cookedReadData.BufferStartPtr(); *Tmp != UNICODE_LINEFEED; Tmp++)
|
|
{
|
|
FAIL_FAST_IF(!(Tmp < (cookedReadData.BufferStartPtr() + cookedReadData.BytesRead())));
|
|
}
|
|
NumBytes = (Tmp - cookedReadData.BufferStartPtr() + 1) * sizeof(*Tmp);
|
|
}
|
|
else
|
|
{
|
|
NumBytes = cookedReadData.UserBufferSize();
|
|
}
|
|
|
|
// Copy what we can fit into the user buffer
|
|
const size_t bytesWritten = cookedReadData.SavePromptToUserBuffer(NumBytes / sizeof(wchar_t));
|
|
|
|
// Store all of the remaining as pending until the next read operation.
|
|
cookedReadData.SavePendingInput(NumBytes / sizeof(wchar_t), LineCount > 1);
|
|
NumBytes = bytesWritten;
|
|
}
|
|
else
|
|
{
|
|
NumBytes = cookedReadData.BytesRead();
|
|
NumBytes = cookedReadData.SavePromptToUserBuffer(NumBytes / sizeof(wchar_t));
|
|
}
|
|
|
|
cookedReadData.SetReportedByteCount(NumBytes);
|
|
}
|
|
|
|
void CommandListPopup::_cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch)
|
|
{
|
|
short Index = 0;
|
|
if (cookedReadData.History().FindMatchingCommand({ &wch, 1 },
|
|
_currentCommand,
|
|
Index,
|
|
CommandHistory::MatchOptions::JustLooking))
|
|
{
|
|
_update((SHORT)(Index - _currentCommand), true);
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - This routine handles the command list popup. It returns when we're out of input or the user has selected a command line.
|
|
// Return Value:
|
|
// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created
|
|
// - CONSOLE_STATUS_READ_COMPLETE - user hit return
|
|
[[nodiscard]] NTSTATUS CommandListPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept
|
|
{
|
|
NTSTATUS Status = STATUS_SUCCESS;
|
|
|
|
for (;;)
|
|
{
|
|
WCHAR wch = UNICODE_NULL;
|
|
bool popupKeys = false;
|
|
DWORD modifiers = 0;
|
|
|
|
Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch);
|
|
if (!NT_SUCCESS(Status))
|
|
{
|
|
return Status;
|
|
}
|
|
|
|
if (popupKeys)
|
|
{
|
|
Status = _handlePopupKeys(cookedReadData, wch, modifiers);
|
|
if (Status != STATUS_SUCCESS)
|
|
{
|
|
return Status;
|
|
}
|
|
}
|
|
else if (wch == UNICODE_CARRIAGERETURN)
|
|
{
|
|
_handleReturn(cookedReadData);
|
|
return CONSOLE_STATUS_READ_COMPLETE;
|
|
}
|
|
else
|
|
{
|
|
// cycle through commands that start with the letter of the key pressed
|
|
_cycleSelectionToMatchingCommands(cookedReadData, wch);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CommandListPopup::_DrawContent()
|
|
{
|
|
_drawList();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Draws a list of commands for the user to choose from
|
|
void CommandListPopup::_drawList()
|
|
{
|
|
// draw empty popup
|
|
COORD WriteCoord;
|
|
WriteCoord.X = _region.Left + 1i16;
|
|
WriteCoord.Y = _region.Top + 1i16;
|
|
size_t lStringLength = Width();
|
|
for (SHORT i = 0; i < Height(); ++i)
|
|
{
|
|
const OutputCellIterator spaces(UNICODE_SPACE, _attributes, lStringLength);
|
|
const auto result = _screenInfo.Write(spaces, WriteCoord);
|
|
lStringLength = result.GetCellDistance(spaces);
|
|
WriteCoord.Y += 1i16;
|
|
}
|
|
|
|
auto& api = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().api;
|
|
|
|
WriteCoord.Y = _region.Top + 1i16;
|
|
SHORT i = std::max(gsl::narrow<SHORT>(_bottomIndex - Height() + 1), 0i16);
|
|
for (; i <= _bottomIndex; i++)
|
|
{
|
|
CHAR CommandNumber[COMMAND_NUMBER_SIZE];
|
|
// Write command number to screen.
|
|
if (0 != _itoa_s(i, CommandNumber, ARRAYSIZE(CommandNumber), 10))
|
|
{
|
|
return;
|
|
}
|
|
|
|
PCHAR CommandNumberPtr = CommandNumber;
|
|
|
|
size_t CommandNumberLength;
|
|
if (FAILED(StringCchLengthA(CommandNumberPtr, ARRAYSIZE(CommandNumber), &CommandNumberLength)))
|
|
{
|
|
return;
|
|
}
|
|
__assume_bound(CommandNumberLength);
|
|
|
|
if (CommandNumberLength + 1 >= ARRAYSIZE(CommandNumber))
|
|
{
|
|
return;
|
|
}
|
|
|
|
CommandNumber[CommandNumberLength] = ':';
|
|
CommandNumber[CommandNumberLength + 1] = ' ';
|
|
CommandNumberLength += 2;
|
|
if (CommandNumberLength > static_cast<ULONG>(Width()))
|
|
{
|
|
CommandNumberLength = static_cast<ULONG>(Width());
|
|
}
|
|
|
|
WriteCoord.X = _region.Left + 1i16;
|
|
|
|
LOG_IF_FAILED(api.WriteConsoleOutputCharacterAImpl(_screenInfo,
|
|
{ CommandNumberPtr, CommandNumberLength },
|
|
WriteCoord,
|
|
CommandNumberLength));
|
|
|
|
// write command to screen
|
|
auto command = _history.GetNth(i);
|
|
lStringLength = command.size();
|
|
{
|
|
size_t lTmpStringLength = lStringLength;
|
|
LONG lPopupLength = static_cast<LONG>(Width() - CommandNumberLength);
|
|
PCWCHAR lpStr = command.data();
|
|
while (lTmpStringLength--)
|
|
{
|
|
if (IsGlyphFullWidth(*lpStr++))
|
|
{
|
|
lPopupLength -= 2;
|
|
}
|
|
else
|
|
{
|
|
lPopupLength--;
|
|
}
|
|
|
|
if (lPopupLength <= 0)
|
|
{
|
|
lStringLength -= lTmpStringLength;
|
|
if (lPopupLength < 0)
|
|
{
|
|
lStringLength--;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
WriteCoord.X = gsl::narrow<SHORT>(WriteCoord.X + CommandNumberLength);
|
|
size_t used;
|
|
LOG_IF_FAILED(api.WriteConsoleOutputCharacterWImpl(_screenInfo,
|
|
{ command.data(), lStringLength },
|
|
WriteCoord,
|
|
used));
|
|
|
|
// write attributes to screen
|
|
if (i == _currentCommand)
|
|
{
|
|
WriteCoord.X = _region.Left + 1i16;
|
|
// inverted attributes
|
|
lStringLength = Width();
|
|
TextAttribute inverted = _attributes;
|
|
inverted.Invert();
|
|
|
|
const OutputCellIterator it(inverted, lStringLength);
|
|
const auto done = _screenInfo.Write(it, WriteCoord);
|
|
|
|
lStringLength = done.GetCellDistance(it);
|
|
}
|
|
|
|
WriteCoord.Y += 1;
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - For popup lists, will adjust the position of the highlighted item and
|
|
// possibly scroll the list if necessary.
|
|
// Arguments:
|
|
// - originalDelta - The number of lines to move up or down
|
|
// - wrap - Down past the bottom or up past the top should wrap the command list
|
|
void CommandListPopup::_update(const SHORT originalDelta, const bool wrap)
|
|
{
|
|
SHORT delta = originalDelta;
|
|
if (delta == 0)
|
|
{
|
|
return;
|
|
}
|
|
SHORT const Size = Height();
|
|
|
|
SHORT CurCmdNum = _currentCommand;
|
|
SHORT NewCmdNum = CurCmdNum + delta;
|
|
|
|
if (wrap)
|
|
{
|
|
// Modulo the number of commands to "circle" around if we went off the end.
|
|
NewCmdNum %= _history.GetNumberOfCommands();
|
|
}
|
|
else
|
|
{
|
|
if (NewCmdNum >= gsl::narrow<SHORT>(_history.GetNumberOfCommands()))
|
|
{
|
|
NewCmdNum = gsl::narrow<SHORT>(_history.GetNumberOfCommands()) - 1i16;
|
|
}
|
|
else if (NewCmdNum < 0)
|
|
{
|
|
NewCmdNum = 0;
|
|
}
|
|
}
|
|
delta = NewCmdNum - CurCmdNum;
|
|
|
|
bool Scroll = false;
|
|
// determine amount to scroll, if any
|
|
if (NewCmdNum <= _bottomIndex - Size)
|
|
{
|
|
_bottomIndex += delta;
|
|
if (_bottomIndex < Size - 1i16)
|
|
{
|
|
_bottomIndex = Size - 1i16;
|
|
}
|
|
Scroll = true;
|
|
}
|
|
else if (NewCmdNum > _bottomIndex)
|
|
{
|
|
_bottomIndex += delta;
|
|
if (_bottomIndex >= gsl::narrow<SHORT>(_history.GetNumberOfCommands()))
|
|
{
|
|
_bottomIndex = gsl::narrow<SHORT>(_history.GetNumberOfCommands()) - 1i16;
|
|
}
|
|
Scroll = true;
|
|
}
|
|
|
|
// write commands to popup
|
|
if (Scroll)
|
|
{
|
|
_currentCommand = NewCmdNum;
|
|
_drawList();
|
|
}
|
|
else
|
|
{
|
|
_updateHighlight(_currentCommand, NewCmdNum);
|
|
_currentCommand = NewCmdNum;
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Adjusts the highlighted line in a list of commands
|
|
// Arguments:
|
|
// - OldCurrentCommand - The previous command highlighted
|
|
// - NewCurrentCommand - The new command to be highlighted.
|
|
void CommandListPopup::_updateHighlight(const SHORT OldCurrentCommand, const SHORT NewCurrentCommand)
|
|
{
|
|
SHORT TopIndex;
|
|
if (_bottomIndex < Height())
|
|
{
|
|
TopIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
TopIndex = _bottomIndex - Height() + 1i16;
|
|
}
|
|
COORD WriteCoord;
|
|
WriteCoord.X = _region.Left + 1i16;
|
|
size_t lStringLength = Width();
|
|
|
|
WriteCoord.Y = _region.Top + 1i16 + OldCurrentCommand - TopIndex;
|
|
|
|
const OutputCellIterator it(_attributes, lStringLength);
|
|
const auto done = _screenInfo.Write(it, WriteCoord);
|
|
lStringLength = done.GetCellDistance(it);
|
|
|
|
// highlight new command
|
|
WriteCoord.Y = _region.Top + 1i16 + NewCurrentCommand - TopIndex;
|
|
|
|
// inverted attributes
|
|
TextAttribute inverted = _attributes;
|
|
inverted.Invert();
|
|
const OutputCellIterator itAttr(inverted, lStringLength);
|
|
const auto doneAttr = _screenInfo.Write(itAttr, WriteCoord);
|
|
lStringLength = done.GetCellDistance(itAttr);
|
|
}
|