// 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(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(history.GetNumberOfCommands()), minSize.Y, 20i16); return { gsl::narrow(width), height }; } CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history) : Popup(screenInfo, calculatePopupSize(history)), _history{ history }, _currentCommand{ std::min(history.LastDisplayed, static_cast(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(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(_currentCommand)); _setBottomIndex(); if (history.GetNumberOfCommands() == 0) { // close the popup return CONSOLE_STATUS_READ_COMPLETE; } else if (_currentCommand >= static_cast(history.GetNumberOfCommands())) { _currentCommand = static_cast(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(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(_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(Width())) { CommandNumberLength = static_cast(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(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(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(_history.GetNumberOfCommands())) { NewCmdNum = gsl::narrow(_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(_history.GetNumberOfCommands())) { _bottomIndex = gsl::narrow(_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); }