terminal/src/host/CommandListPopup.cpp

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);
}