// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "precomp.h"
#include "readDataCooked.hpp"

#include "alias.h"
#include "history.h"
#include "resource.h"
#include "stream.h"
#include "_stream.h"
#include "../interactivity/inc/ServiceLocator.hpp"

#define COOKED_READ_DEBUG 0

#if COOKED_READ_DEBUG
#include <til/colorbrewer.h>
#endif

using Microsoft::Console::Interactivity::ServiceLocator;
using Microsoft::Console::VirtualTerminal::VtIo;

// Routine Description:
// - Constructs cooked read data class to hold context across key presses while a user is modifying their 'input line'.
// Arguments:
// - pInputBuffer - Buffer that data will be read from.
// - pInputReadHandleData - Context stored across calls from the same input handle to return partial data appropriately.
// - screenInfo - Output buffer that will be used for 'echoing' the line back to the user so they can see/manipulate it
// - UserBufferSize - The byte count of the buffer presented by the client
// - UserBuffer - The buffer that was presented by the client for filling with input data on read conclusion/return from server/host.
// - CtrlWakeupMask - Special client parameter to interrupt editing, end the wait, and return control to the client application
// - initialData - any text data that should be prepopulated into the buffer
// - pClientProcess - Attached process handle object
COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer,
                                   _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData,
                                   SCREEN_INFORMATION& screenInfo,
                                   _In_ size_t UserBufferSize,
                                   _In_ char* UserBuffer,
                                   _In_ ULONG CtrlWakeupMask,
                                   _In_ const std::wstring_view exeName,
                                   _In_ const std::wstring_view initialData,
                                   _In_ ConsoleProcessHandle* const pClientProcess) :
    ReadData(pInputBuffer, pInputReadHandleData),
    _screenInfo{ screenInfo },
    _userBuffer{ UserBuffer, UserBufferSize },
    _exeName{ exeName },
    _processHandle{ pClientProcess },
    _history{ CommandHistory::s_Find(pClientProcess) },
    _ctrlWakeupMask{ CtrlWakeupMask },
    _insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() }
{
#ifndef UNIT_TESTING
    // The screen buffer instance is basically a reference counted HANDLE given out to the user.
    // We need to ensure that it stays alive for the duration of the read.
    // Coincidentally this serves another important purpose: It checks whether we're allowed to read from
    // the given buffer in the first place. If it's missing the FILE_SHARE_READ flag, we can't read from it.
    //
    // GH#16158: It's important that we hold a handle to the main instead of the alt buffer
    // even if this cooked read targets the latter, because alt buffers are fake
    // SCREEN_INFORMATION objects that are owned by the main buffer.
    THROW_IF_FAILED(_screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, _tempHandle));
#endif

    if (!initialData.empty())
    {
        // The console API around `nInitialChars` in `CONSOLE_READCONSOLE_CONTROL` is pretty weird.
        // The way it works is that cmd.exe does a ReadConsole() with a `dwCtrlWakeupMask` that includes \t,
        // so when you press tab it can autocomplete the prompt based on the available file names.
        // The weird part is that it's not us who then prints the autocompletion. It's cmd.exe which calls WriteConsoleW().
        // It then initiates another ReadConsole() where the `nInitialChars` is the amount of chars it wrote via WriteConsoleW().
        //
        // In other words, `nInitialChars` is a "trust me bro, I just wrote that in the buffer" API.
        // This unfortunately means that the API is inherently broken: ReadConsole() visualizes control
        // characters like Ctrl+X as "^X" and WriteConsoleW() doesn't and so the column counts don't match.
        // Solving these issues is technically possible, but it's also quite difficult to do so correctly.
        //
        // But unfortunately (or fortunately) the initial implementation (from the 1990s up to 2023) looked something like that:
        //   cursor = cursor.GetPosition();
        //   cursor.x -= initialData.size();
        //   while (cursor.x < 0)
        //   {
        //       cursor.x += textBuffer.Width();
        //       cursor.y -= 1;
        //   }
        //
        // In other words, it assumed that the number of code units in the initial data corresponds 1:1 to
        // the column count. This meant that the API never supported tabs for instance (nor wide glyphs).
        //
        //
        // The new implementation is a lot more complex to be a little more correct.
        // It replicates part of the _redisplay() logic to layout the text at various
        // starting positions until it finds one that matches the current cursor position.

        const auto cursorPos = _getViewportCursorPosition();
        const auto size = _screenInfo.GetVtPageArea().Dimensions();

        // Guess the initial cursor position based on the string length, assuming that 1 char = 1 column.
        const auto columnRemainder = gsl::narrow_cast<til::CoordType>((initialData.size() % size.width));
        const auto bestGuessColumn = (cursorPos.x - columnRemainder + size.width) % size.width;

        std::wstring line;
        LayoutResult res;
        til::CoordType bestDistance = til::CoordTypeMax;
        til::CoordType bestColumnBegin = 0;
        til::CoordType bestNewlineCount = 0;

        line.reserve(size.width);

        // We're given an "end position" and a string and we need to find its starting position.
        // The problem is that a wide glyph that doesn't fit into the last column of a row gets padded with a whitespace
        // and then written on the next line. Because of this, multiple starting positions can result in the same end
        // position and this prevents us from simply laying out the text backwards from the end position.
        // To solve this, we do a brute force search for the best starting position that ends at the end position.
        // The search is centered around `bestGuessColumn` with offsets 0, 1, -1, 2, -2, 3, -3, ...
        for (til::CoordType i = 0, attempts = 2 * size.width; i <= attempts; i++)
        {
            // Hilarious bit-trickery that no one can read. But it works. Check it out in a debugger.
            // The idea is to use bits 1:31 as the value (i >> 1) and bit 0 (i & 1) as a trigger to bit-flip the value.
            // A bit-flipped positive number is negative, but offset by 1, so we add 1 at the end. Fun!
            const auto offset = ((i >> 1) ^ ((i & 1) - 1)) + 1;
            const auto columnBegin = bestGuessColumn + offset;

            if (columnBegin < 0 || columnBegin >= size.width)
            {
                continue;
            }

            til::CoordType newlineCount = 0;
            res.column = columnBegin;

            for (size_t beg = 0; beg < initialData.size();)
            {
                line.clear();
                res = _layoutLine(line, initialData, beg, res.column, size.width);
                beg = res.offset;

                if (res.column >= size.width)
                {
                    res.column = 0;
                    newlineCount += 1;
                }
            }

            const auto distance = abs(res.column - cursorPos.x);
            if (distance < bestDistance)
            {
                bestDistance = distance;
                bestColumnBegin = columnBegin;
                bestNewlineCount = newlineCount;
            }
            if (distance == 0)
            {
                break;
            }
        }

        auto originInViewport = cursorPos;
        originInViewport.x = bestColumnBegin;
        originInViewport.y = originInViewport.y - bestNewlineCount;

        if (originInViewport.y < 0)
        {
            originInViewport = {};
        }

        // We can't mark the buffer as dirty because this messes up the cursor position for cmd
        // somehow when the prompt is longer than the viewport height. I haven't investigated
        // why that happens, but it works decently well enough that it's not too important.
        _buffer.assign(initialData);
        _bufferDirtyBeg = _buffer.size();
        _bufferCursor = _buffer.size();

        _originInViewport = originInViewport;
        _pagerPromptEnd = cursorPos;
        _pagerHeight = std::min(size.height, bestNewlineCount + 1);
    }
}

const SCREEN_INFORMATION* COOKED_READ_DATA::GetScreenBuffer() const noexcept
{
    return &_screenInfo;
}

// Routine Description:
// - This routine is called to complete a cooked read that blocked in ReadInputBuffer.
// - The context of the read was saved in the CookedReadData structure.
// - This routine is called when events have been written to the input buffer.
// - It is called in the context of the writing thread.
// - It may be called more than once.
// Arguments:
// - TerminationReason - if this routine is called because a ctrl-c or ctrl-break was seen, this argument
//                      contains CtrlC or CtrlBreak. If the owning thread is exiting, it will have ThreadDying. Otherwise, 0.
// - fIsUnicode - Whether to convert the final data to A (using Console Input CP) at the end or treat everything as Unicode (UCS-2)
// - pReplyStatus - The status code to return to the client application that originally called the API (before it was queued to wait)
// - pNumBytes - The number of bytes of data that the server/driver will need to transmit back to the client process
// - pControlKeyState - For certain types of reads, this specifies which modifier keys were held.
// - pOutputData - not used
// Return Value:
// - true if the wait is done and result buffer/status code can be sent back to the client.
// - false if we need to continue to wait until more data is available.
bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason,
                              const bool fIsUnicode,
                              _Out_ NTSTATUS* const pReplyStatus,
                              _Out_ size_t* const pNumBytes,
                              _Out_ DWORD* const pControlKeyState,
                              _Out_ void* const /*pOutputData*/) noexcept
try
{
    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();

    *pNumBytes = 0;
    *pControlKeyState = 0;
    *pReplyStatus = STATUS_SUCCESS;

    // if ctrl-c or ctrl-break was seen, terminate read.
    if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak)))
    {
        *pReplyStatus = STATUS_ALERTED;
        gci.SetCookedReadData(nullptr);
        return true;
    }

    // See if we were called because the thread that owns this wait block is exiting.
    if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
    {
        *pReplyStatus = STATUS_THREAD_IS_TERMINATING;
        gci.SetCookedReadData(nullptr);
        return true;
    }

    // We must see if we were woken up because the handle is being closed. If
    // so, we decrement the read count. If it goes to zero, we wake up the
    // close thread. Otherwise, we wake up any other thread waiting for data.
    if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing))
    {
        *pReplyStatus = STATUS_ALERTED;
        gci.SetCookedReadData(nullptr);
        return true;
    }

    if (Read(fIsUnicode, *pNumBytes, *pControlKeyState))
    {
        gci.SetCookedReadData(nullptr);
        return true;
    }

    return false;
}
NT_CATCH_RETURN()

void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) noexcept
{
    // See the comment in WaitBlock.cpp for more information.
    if (_userBuffer.data() == oldBuffer)
    {
        _userBuffer = { static_cast<char*>(newBuffer), _userBuffer.size() };
    }
}

// Routine Description:
// - Method that actually retrieves a character/input record from the buffer (key press form)
//   and determines the next action based on the various possible cooked read modes.
// - Mode options include the F-keys popup menus, keyboard manipulation of the edit line, etc.
// - This method also does the actual copying of the final manipulated data into the return buffer.
// Arguments:
// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done.
// - numBytes - On in, the number of bytes available in the client
// buffer. On out, the number of bytes consumed in the client buffer.
// - controlKeyState - For some types of reads, this is the modifier key state with the last button press.
bool COOKED_READ_DATA::Read(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState)
{
    controlKeyState = 0;

    _readCharInputLoop();

    // NOTE: Don't call _flushBuffer in a wil::scope_exit/defer.
    // It may throw and throwing during an ongoing exception is a bad idea.
    _redisplay();

    if (_state == State::Accumulating)
    {
        return false;
    }

    _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState);
    return true;
}

// Printing wide glyphs at the end of a row results in a forced line wrap and a padding whitespace to be inserted.
// When the text buffer resizes these padding spaces may vanish and the _distanceCursor and _distanceEnd measurements become inaccurate.
// To fix this, this function is called before a resize and will clear the input line. Afterward, RedrawAfterResize() will restore it.
void COOKED_READ_DATA::EraseBeforeResize()
{
    // If we've already erased the buffer, we don't need to do it again.
    if (_redrawPending)
    {
        return;
    }

    // If we don't have an origin, we've never had user input, and consequently there's nothing to erase.
    if (!_originInViewport)
    {
        return;
    }

    _redrawPending = true;

    // Position the cursor the start of the prompt before reflow.
    // Then, after reflow, we'll be able to ask the buffer where it went (the new origin).
    // This uses the buffer APIs directly, so that we don't emit unnecessary VT into ConPTY's output.
    auto& textBuffer = _screenInfo.GetTextBuffer();
    auto& cursor = textBuffer.GetCursor();
    auto cursorPos = *_originInViewport;
    _screenInfo.GetVtPageArea().ConvertFromOrigin(&cursorPos);
    cursor.SetPosition(cursorPos);
}

// The counter-part to EraseBeforeResize().
void COOKED_READ_DATA::RedrawAfterResize()
{
    if (!_redrawPending)
    {
        return;
    }

    _redrawPending = false;

    // Get the new cursor position after the reflow, since it may have changed.
    if (_originInViewport)
    {
        _originInViewport = _getViewportCursorPosition();
    }

    // Ensure that we don't use any scroll sequences or try to clear previous pager contents.
    // They have all been erased with the CSI J above.
    _pagerHeight = 0;

    // Ensure that the entire buffer content is rewritten after the above CSI J.
    _bufferDirtyBeg = 0;
    _dirty = !_buffer.empty();

    // Let _redisplay() know to inject a CSI J at the start of the output.
    // This ensures we fully erase the previous contents, that are now in disarray.
    _clearPending = true;

    _redisplay();
}

void COOKED_READ_DATA::SetInsertMode(bool insertMode) noexcept
{
    _insertMode = insertMode;
}

bool COOKED_READ_DATA::IsEmpty() const noexcept
{
    return _buffer.empty() && _popups.empty();
}

bool COOKED_READ_DATA::PresentingPopup() const noexcept
{
    return !_popups.empty();
}

til::point_span COOKED_READ_DATA::GetBoundaries() noexcept
{
    const auto viewport = _screenInfo.GetViewport();
    const auto virtualViewport = _screenInfo.GetVtPageArea();

    static constexpr til::point min;
    const til::point max{ viewport.RightInclusive(), viewport.BottomInclusive() };

    // Convert from VT-viewport-relative coordinate space back to the console one.
    auto beg = _getOriginInViewport();
    virtualViewport.ConvertFromOrigin(&beg);

    // Since the pager may be longer than the viewport is tall, we need to clamp the coordinates to still remain within
    // the current viewport (the pager doesn't write outside of the viewport, since that's not supported by VT).
    auto end = _pagerPromptEnd;
    end.y -= _pagerContentTop;
    end = std::clamp(end, min, max);
    end.y += beg.y;

    return { beg, end };
}

// _wordPrev and _wordNext implement the classic Windows word-wise cursor movement algorithm, as traditionally used by
// conhost, notepad, Visual Studio and other "old" applications. If you look closely you can see how they're the exact
// same "skip 1 char, skip x, skip not-x", but since the "x" between them is different (non-words for _wordPrev and
// words for _wordNext) it results in the inconsistent feeling that these have compared to more modern algorithms.
// TODO: GH#15787
size_t COOKED_READ_DATA::_wordPrev(const std::wstring_view& chars, size_t position)
{
    if (position != 0)
    {
        --position;
        while (position != 0 && chars[position] == L' ')
        {
            --position;
        }

        const auto dc = DelimiterClass(chars[position]);
        while (position != 0 && DelimiterClass(chars[position - 1]) == dc)
        {
            --position;
        }
    }
    return position;
}

size_t COOKED_READ_DATA::_wordNext(const std::wstring_view& chars, size_t position)
{
    if (position < chars.size())
    {
        ++position;
        const auto dc = DelimiterClass(chars[position - 1]);
        while (position != chars.size() && dc == DelimiterClass(chars[position]))
        {
            ++position;
        }
        while (position != chars.size() && chars[position] == L' ')
        {
            ++position;
        }
    }
    return position;
}

// Reads text off of the InputBuffer and dispatches it to the current popup or otherwise into the _buffer contents.
void COOKED_READ_DATA::_readCharInputLoop()
{
    while (_state == State::Accumulating)
    {
        const auto hasPopup = !_popups.empty();
        auto charOrVkey = UNICODE_NULL;
        auto commandLineEditingKeys = false;
        auto popupKeys = false;
        const auto pCommandLineEditingKeys = hasPopup ? nullptr : &commandLineEditingKeys;
        const auto pPopupKeys = hasPopup ? &popupKeys : nullptr;
        DWORD modifiers = 0;

        const auto status = GetChar(_pInputBuffer, &charOrVkey, true, pCommandLineEditingKeys, pPopupKeys, &modifiers);
        if (status == CONSOLE_STATUS_WAIT)
        {
            break;
        }
        THROW_IF_NTSTATUS_FAILED(status);

        if (hasPopup)
        {
            const auto wch = static_cast<wchar_t>(popupKeys ? 0 : charOrVkey);
            const auto vkey = static_cast<uint16_t>(popupKeys ? charOrVkey : 0);
            _popupHandleInput(wch, vkey, modifiers);
        }
        else
        {
            if (commandLineEditingKeys)
            {
                _handleVkey(charOrVkey, modifiers);
            }
            else
            {
                _handleChar(charOrVkey, modifiers);
            }
        }
    }
}

// Handles character input for _readCharInputLoop() when no popups exist.
void COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers)
{
    // All paths in this function modify the buffer.

    if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch)))
    {
        // The old implementation (all the way since the 90s) overwrote the character at the current cursor position with the given wch.
        // But simultaneously it incremented the buffer length, which would have only worked if it was written at the end of the buffer.
        // Press tab past the "f" in the string "foo" and you'd get "f\to " (a trailing whitespace; the initial contents of the buffer back then).
        // It's unclear whether the original intention was to write at the end of the buffer at all times or to implement an insert mode.
        // I went with insert mode.
        //
        // The old implementation also failed to clear the end of the prompt if you pressed tab in the middle of it.
        // You can reproduce this issue by launching cmd in an old conhost build and writing "<command that doesn't exist> foo",
        // moving your cursor to the space past the <command> and pressing tab. Nothing will happen but the "foo" will be inaccessible.
        // I've now fixed this behavior by adding an additional Replace() before the _flushBuffer() call that removes the tail end.
        //
        // It is important that we don't actually print that character out though, as it's only for the calling application to see.
        // That's why we flush the contents before the insertion and then ensure that the _flushBuffer() call in Read() exits early.
        _replace(_bufferCursor, npos, nullptr, 0);
        _redisplay();
        _replace(_bufferCursor, 0, &wch, 1);
        _dirty = false;

        _controlKeyState = modifiers;
        _transitionState(State::DoneWithWakeupMask);
        return;
    }

    switch (wch)
    {
    case UNICODE_CARRIAGERETURN:
    {
        // NOTE: Don't append newlines to the buffer just yet! See _handlePostCharInputLoop for more information.
        _setCursorPosition(npos);
        _transitionState(State::DoneWithCarriageReturn);
        return;
    }
    case EXTKEY_ERASE_PREV_WORD: // Ctrl+Backspace
    case UNICODE_BACKSPACE:
        if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT))
        {
            const auto cursor = _bufferCursor;
            const auto pos = wch == EXTKEY_ERASE_PREV_WORD ? _wordPrev(_buffer, cursor) : TextBuffer::GraphemePrev(_buffer, cursor);
            _replace(pos, cursor - pos, nullptr, 0);
            return;
        }
        // If processed mode is disabled, control characters like backspace are treated like any other character.
        break;
    default:
        break;
    }

    size_t remove = 0;
    if (!_insertMode)
    {
        // TODO GH#15875: If the input grapheme is >1 char, then this will replace >1 grapheme
        // --> We should accumulate input text as much as possible and then call _processInput with wstring_view.
        const auto cursor = _bufferCursor;
        remove = TextBuffer::GraphemeNext(_buffer, cursor) - cursor;
    }

    _replace(_bufferCursor, remove, &wch, 1);
}

// Handles non-character input for _readCharInputLoop() when no popups exist.
void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers)
{
    const auto ctrlPressed = WI_IsAnyFlagSet(modifiers, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED);
    const auto altPressed = WI_IsAnyFlagSet(modifiers, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED);

    switch (vkey)
    {
    case VK_ESCAPE:
        if (!_buffer.empty())
        {
            _replace(0, npos, nullptr, 0);
        }
        break;
    case VK_HOME:
        if (_bufferCursor > 0)
        {
            if (ctrlPressed)
            {
                _replace(0, _bufferCursor, nullptr, 0);
            }
            _setCursorPosition(0);
        }
        break;
    case VK_END:
        if (_bufferCursor < _buffer.size())
        {
            if (ctrlPressed)
            {
                _replace(_bufferCursor, npos, nullptr, 0);
            }
            _setCursorPosition(npos);
        }
        break;
    case VK_LEFT:
        if (_bufferCursor != 0)
        {
            if (ctrlPressed)
            {
                _setCursorPosition(_wordPrev(_buffer, _bufferCursor));
            }
            else
            {
                _setCursorPosition(TextBuffer::GraphemePrev(_buffer, _bufferCursor));
            }
        }
        break;
    case VK_F1:
    case VK_RIGHT:
        if (_bufferCursor != _buffer.size())
        {
            if (ctrlPressed && vkey == VK_RIGHT)
            {
                _setCursorPosition(_wordNext(_buffer, _bufferCursor));
            }
            else
            {
                _setCursorPosition(TextBuffer::GraphemeNext(_buffer, _bufferCursor));
            }
        }
        else if (_history)
        {
            // Traditionally pressing right at the end of an input line would paste characters from the previous command.
            const auto cmd = _history->GetLastCommand();
            const auto bufferSize = _buffer.size();
            const auto cmdSize = cmd.size();
            size_t bufferBeg = 0;
            size_t cmdBeg = 0;

            // We cannot just check if the cmd is longer than the _buffer, because we want to copy graphemes,
            // not characters and there's no correlation between the number of graphemes and their byte length.
            while (cmdBeg < cmdSize)
            {
                const auto cmdEnd = TextBuffer::GraphemeNext(cmd, cmdBeg);

                if (bufferBeg >= bufferSize)
                {
                    _replace(npos, 0, cmd.data() + cmdBeg, cmdEnd - cmdBeg);
                    break;
                }

                bufferBeg = TextBuffer::GraphemeNext(_buffer, bufferBeg);
                cmdBeg = cmdEnd;
            }
        }
        break;
    case VK_INSERT:
        _insertMode = !_insertMode;
        _screenInfo.SetCursorDBMode(_insertMode != ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode());
        break;
    case VK_DELETE:
        if (_bufferCursor < _buffer.size())
        {
            const auto beg = _bufferCursor;
            const auto end = TextBuffer::GraphemeNext(_buffer, beg);
            _replace(beg, end - beg, nullptr, 0);
        }
        break;
    case VK_UP:
    case VK_F5:
        if (_history && !_history->AtFirstCommand())
        {
            _replace(_history->Retrieve(CommandHistory::SearchDirection::Previous));
        }
        break;
    case VK_DOWN:
        if (_history && !_history->AtLastCommand())
        {
            _replace(_history->Retrieve(CommandHistory::SearchDirection::Next));
        }
        break;
    case VK_PRIOR:
        if (_history && !_history->AtFirstCommand())
        {
            _replace(_history->RetrieveNth(0));
        }
        break;
    case VK_NEXT:
        if (_history && !_history->AtLastCommand())
        {
            _replace(_history->RetrieveNth(INT_MAX));
        }
        break;
    case VK_F2:
        if (_history)
        {
            _popupPush(PopupKind::CopyToChar);
        }
        break;
    case VK_F3:
        if (_history)
        {
            const auto last = _history->GetLastCommand();
            if (last.size() > _bufferCursor)
            {
                const auto count = last.size() - _bufferCursor;
                _replace(_bufferCursor, npos, last.data() + _bufferCursor, count);
            }
        }
        break;
    case VK_F4:
        // Historically the CopyFromChar popup was constrained to only work when a history exists,
        // but I don't see why that should be. It doesn't depend on _history at all.
        _popupPush(PopupKind::CopyFromChar);
        break;
    case VK_F6:
        // Don't ask me why but F6 is an alias for ^Z.
        _handleChar(0x1a, modifiers);
        break;
    case VK_F7:
        if (!ctrlPressed && !altPressed)
        {
            if (_history && _history->GetNumberOfCommands())
            {
                _popupPush(PopupKind::CommandList);
            }
        }
        else if (altPressed)
        {
            if (_history)
            {
                _history->Empty();
                _history->Flags |= CommandHistory::CLE_ALLOCATED;
            }
        }
        break;
    case VK_F8:
        if (_history)
        {
            CommandHistory::Index index = 0;
            const auto cursorPos = _bufferCursor;
            const auto prefix = std::wstring_view{ _buffer }.substr(0, cursorPos);
            if (_history->FindMatchingCommand(prefix, _history->LastDisplayed, index, CommandHistory::MatchOptions::None))
            {
                _replace(_history->RetrieveNth(index));
                _setCursorPosition(cursorPos);
            }
        }
        break;
    case VK_F9:
        if (_history && _history->GetNumberOfCommands())
        {
            _popupPush(PopupKind::CommandNumber);
        }
        break;
    case VK_F10:
        // Alt+F10 clears the aliases for specifically cmd.exe.
        if (altPressed)
        {
            Alias::s_ClearCmdExeAliases();
        }
        break;
    default:
        assert(false); // Unrecognized VK. Fix or don't call this function?
        break;
    }
}

// Handles any tasks that need to be completed after the read input loop finishes,
// like handling doskey aliases and converting the input to non-UTF16.
void COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState)
{
    auto writer = _userBuffer;
    auto buffer = std::move(_buffer);
    std::wstring_view input{ buffer };
    size_t lineCount = 1;

    if (_state == State::DoneWithCarriageReturn)
    {
        static constexpr std::wstring_view cr{ L"\r" };
        static constexpr std::wstring_view crlf{ L"\r\n" };
        const auto newlineSuffix = WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) ? crlf : cr;
        std::wstring alias;

        // Here's why we can't easily use _flushBuffer() to handle newlines:
        //
        // A carriage return (enter key) will increase the _distanceEnd by up to viewport-width many columns,
        // since it increases the Y distance between the start and end by 1 (it's a newline after all).
        // This will make _flushBuffer() think that the new _buffer is way longer than the old one and so
        // _erase() ends up not erasing the tail end of the prompt, even if the new prompt is actually shorter.
        //
        // If you were to break this (remove this code and then append \r\n in _handleChar())
        // you can reproduce the issue easily if you do this:
        // * Run cmd.exe
        // * Write "echo hello" and press Enter
        // * Write "foobar foo bar" (don't press Enter)
        // * Press F7, select "echo hello" and press Enter
        //
        // It'll print "hello" but the previous prompt will say "echo hello bar" because the _distanceEnd
        // ended up being well over 14 leading it to believe that "bar" got overwritten during WriteCharsLegacy().

        WriteCharsLegacy(_screenInfo, newlineSuffix, nullptr);

        if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT))
        {
            if (_history)
            {
                auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
                LOG_IF_FAILED(_history->Add(input, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP)));
            }

            Tracing::s_TraceCookedRead(_processHandle, input);
            alias = Alias::s_MatchAndCopyAlias(input, _exeName, lineCount);
        }

        if (!alias.empty())
        {
            buffer = std::move(alias);
        }
        else
        {
            buffer.append(newlineSuffix);
        }

        input = std::wstring_view{ buffer };

        // doskey aliases may result in multiple lines of output (for instance `doskey test=echo foo$Techo bar$Techo baz`).
        // We need to emit them as multiple cooked reads as well, so that each read completes at a \r\n.
        if (lineCount > 1)
        {
            // ProcessAliases() is supposed to end each line with \r\n. If it doesn't we might as well fail-fast.
            const auto firstLineEnd = input.find(UNICODE_LINEFEED) + 1;
            input = input.substr(0, std::min(input.size(), firstLineEnd));
        }
    }

    const auto inputSizeBefore = input.size();
    _pInputBuffer->Consume(isUnicode, input, writer);

    if (lineCount > 1)
    {
        // This is a continuation of the above identical if condition.
        // We've truncated the `input` slice and now we need to restore it.
        const auto inputSizeAfter = input.size();
        const auto amountConsumed = inputSizeBefore - inputSizeAfter;
        input = std::wstring_view{ buffer };
        input = input.substr(std::min(input.size(), amountConsumed));
        GetInputReadHandleData()->SaveMultilinePendingInput(input);
    }
    else if (!input.empty())
    {
        GetInputReadHandleData()->SavePendingInput(input);
    }

    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
    gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP;

    // If we previously called SetCursorDBMode() with true,
    // this will ensure that the cursor returns to its normal look.
    _screenInfo.SetCursorDBMode(false);

    numBytes = _userBuffer.size() - writer.size();
    controlKeyState = _controlKeyState;
}

void COOKED_READ_DATA::_transitionState(State state) noexcept
{
    assert(_state == State::Accumulating);
    _state = state;
}

til::point COOKED_READ_DATA::_getViewportCursorPosition() const noexcept
{
    const auto& textBuffer = _screenInfo.GetTextBuffer();
    const auto& cursor = textBuffer.GetCursor();
    auto cursorPos = cursor.GetPosition();

    _screenInfo.GetVtPageArea().ConvertToOrigin(&cursorPos);
    cursorPos.x = std::max(0, cursorPos.x);
    cursorPos.y = std::max(0, cursorPos.y);
    return cursorPos;
}

// Some applications initiate a read on stdin and _then_ print the prompt prefix to stdout.
// While that's not correct (because it's a race condition), we can make it significantly
// less bad by delaying the calculation of the origin until we actually need it.
// This turns it from a race between application and terminal into a race between
// application and user, which is much less likely to hit.
til::point COOKED_READ_DATA::_getOriginInViewport() noexcept
{
    if (!_originInViewport)
    {
        _originInViewport.emplace(_getViewportCursorPosition());
    }
    return *_originInViewport;
}

void COOKED_READ_DATA::_replace(size_t offset, size_t remove, const wchar_t* input, size_t count)
{
    const auto size = _buffer.size();
    offset = std::min(offset, size);
    remove = std::min(remove, size - offset);

    // Nothing to do. Avoid marking it as dirty.
    if (remove == 0 && count == 0)
    {
        return;
    }

    _buffer.replace(offset, remove, input, count);
    _bufferCursor = offset + count;
    _bufferDirtyBeg = std::min(_bufferDirtyBeg, offset);
    _dirty = true;
}

void COOKED_READ_DATA::_replace(const std::wstring_view& str)
{
    _buffer.assign(str);
    _bufferCursor = _buffer.size();
    _bufferDirtyBeg = 0;
    _dirty = true;
}

void COOKED_READ_DATA::_setCursorPosition(size_t position) noexcept
{
    _bufferCursor = std::min(position, _buffer.size());
    _dirty = true;
}

std::wstring_view COOKED_READ_DATA::_slice(size_t from, size_t to) const noexcept
{
    to = std::min(to, _buffer.size());
    from = std::min(from, to);
    return std::wstring_view{ _buffer.data() + from, to - from };
}

// Draws the contents of _buffer onto the screen.
//
// By using the _dirty flag we avoid redrawing the buffer unless needed.
// This turns the amortized time complexity of _readCharInputLoop() from O(n^2) (n(n+1)/2 redraws) into O(n).
// Without this, pasting text would otherwise quickly turn into "accidentally quadratic" meme material.
//
// NOTE: Don't call _flushBuffer() after appending newlines to the buffer! See _handlePostCharInputLoop for more information.
void COOKED_READ_DATA::_redisplay()
{
    if (!_dirty || WI_IsFlagClear(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT))
    {
        return;
    }

    const auto size = _screenInfo.GetVtPageArea().Dimensions();
    auto originInViewport = _getOriginInViewport();
    auto originInViewportFinal = originInViewport;
    til::point cursorPositionFinal;
    til::point pagerPromptEnd;
    std::vector<Line> lines;

    // FYI: This loop does not loop. It exists because goto is considered evil
    // and if MSVC says that then that must be true.
    for (;;)
    {
        cursorPositionFinal = { originInViewport.x, 0 };

        // Construct the first line manually so that it starts at the correct horizontal position.
        LayoutResult res{ .column = cursorPositionFinal.x };
        lines.emplace_back(std::wstring{}, 0, cursorPositionFinal.x, cursorPositionFinal.x);

        // Split the buffer into 3 segments, so that we can find the row/column coordinates of
        // the cursor within the buffer, as well as the start of the dirty parts of the buffer.
        const size_t offsets[]{
            0,
            std::min(_bufferDirtyBeg, _bufferCursor),
            std::max(_bufferDirtyBeg, _bufferCursor),
            npos,
        };

        for (int i = 0; i < 3; i++)
        {
            const auto& segment = til::safe_slice_abs(_buffer, offsets[i], offsets[i + 1]);
            if (segment.empty())
            {
                continue;
            }

            const auto dirty = offsets[i] >= _bufferDirtyBeg;

            // Layout the _buffer contents into lines.
            for (size_t beg = 0; beg < segment.size();)
            {
                if (res.column >= size.width)
                {
                    lines.emplace_back();
                }

                auto& line = lines.back();
                res = _layoutLine(line.text, segment, beg, line.columns, size.width);
                line.columns = res.column;

                if (!dirty)
                {
                    line.dirtyBegOffset = line.text.size();
                    line.dirtyBegColumn = res.column;
                }

                beg = res.offset;
            }

            // If this segment ended at the cursor offset, we got our cursor position in rows/columns.
            if (offsets[i + 1] == _bufferCursor)
            {
                cursorPositionFinal = { res.column, gsl::narrow_cast<til::CoordType>(lines.size() - 1) };
            }
        }

        pagerPromptEnd = { res.column, gsl::narrow_cast<til::CoordType>(lines.size() - 1) };

        // If the content got a little shorter than it was before, we need to erase the tail end.
        // If the last character on a line got removed, we'll skip this code because `remaining`
        // will be negative, and instead we'll erase it later when we append "  \r" to the lines.
        // If entire lines got removed, then we'll fix this later when comparing against _pagerContentEnd.y.
        if (pagerPromptEnd.y <= _pagerPromptEnd.y)
        {
            const auto endX = _pagerPromptEnd.y == pagerPromptEnd.y ? _pagerPromptEnd.x : size.width;
            const auto remaining = endX - pagerPromptEnd.x;

            if (remaining > 0)
            {
                auto& line = lines.back();

                // CSI K may be expensive, so use spaces if we can.
                if (remaining <= 16)
                {
                    line.text.append(remaining, L' ');
                    line.columns += remaining;
                }
                else
                {
                    // CSI K doesn't change the cursor position, so we don't modify .columns.
                    line.text.append(L"\x1b[K");
                }
            }
        }

        // Render the popups, if there are any.
        if (!_popups.empty())
        {
            auto& popup = _popups.front();

            // Ensure that the popup is not considered part of the prompt line. That is, if someone double-clicks
            // to select the last word in the prompt, it should not select the first word in the popup.
            auto& lastLine = lines.back();
            lastLine.text.append(L"\r\n");
            lastLine.columns = size.width;

            switch (popup.kind)
            {
            case PopupKind::CopyToChar:
                _popupDrawPrompt(lines, size.width, ID_CONSOLE_MSGCMDLINEF2, {}, {});
                break;
            case PopupKind::CopyFromChar:
                _popupDrawPrompt(lines, size.width, ID_CONSOLE_MSGCMDLINEF4, {}, {});
                break;
            case PopupKind::CommandNumber:
                _popupDrawPrompt(lines, size.width, ID_CONSOLE_MSGCMDLINEF9, {}, { popup.commandNumber.buffer.data(), CommandNumberMaxInputLength });
                break;
            case PopupKind::CommandList:
                _popupDrawCommandList(lines, size, popup);
                break;
            default:
                assert(false);
            }

            // Put the cursor at the end of the contents. This ensures we scroll all the way down.
            cursorPositionFinal.x = lines.back().columns;
            cursorPositionFinal.y = gsl::narrow_cast<til::CoordType>(lines.size()) - 1;
        }
        // If the cursor is at a delay-wrapped position, wrap it explicitly.
        // This ensures that the cursor is always "after" the insertion position.
        // We don't need to do this when popups are present, because they're not supposed to end in a newline.
        else if (cursorPositionFinal.x >= size.width)
        {
            cursorPositionFinal.x = 0;
            cursorPositionFinal.y++;

            // If the cursor is at the end of the buffer we must always show it after the last character.
            // Since VT uses delayed EOL wrapping, we must write at least 1 more character to force the
            // potential delayed line wrap at the end of the prompt, on the last line.
            // We append an extra line to get the lineCount for scrolling right.
            if (_bufferCursor == _buffer.size())
            {
                auto& line = lines.emplace_back();

                // This mirrors the `if (pagerPromptEnd.y <= _pagerPromptEnd.y)` above. We need to repeat this here,
                // because if we append another line then we also need to repeat the "delete to end" logic.
                // The best way to see this code kick in is if you have a prompt like this:
                //   +----------+
                //   |C:\> foo  | <-- end the line in >=1 spaces
                //   |bar_      | <-- start the line with a word >2 characters
                //   +----------+
                // Then put the cursor at the end (where the "_" is) and press Ctrl+Backspace.
                auto remaining = (_pagerPromptEnd.y - pagerPromptEnd.y) * size.width + _pagerPromptEnd.x - pagerPromptEnd.x;

                // Here we ensure that we force a EOL wrap no matter what. At a minimum this will result in " \r".
                remaining = std::max(1, remaining);

                // CSI K may be expensive, so use spaces if we can.
                if (remaining <= 16)
                {
                    line.text.append(remaining, L' ');
                    line.text.push_back(L'\r');
                }
                else
                {
                    line.text.append(L" \r\x1b[K");
                }
            }
        }

        // Usually we'll be on a "prompt> ..." line and behave like a regular single-line-editor.
        // But once the entire viewport is full of text, we need to behave more like a pager (= scrolling, etc.).
        // This code retries the layout process if needed, because then the cursor starts at origin {0, 0}.
        if (gsl::narrow_cast<til::CoordType>(lines.size()) > size.height && originInViewportFinal.x != 0)
        {
            lines.clear();
            _bufferDirtyBeg = 0;
            originInViewport.x = 0;
            originInViewportFinal = {};
            continue;
        }

        break;
    }

    const auto lineCount = gsl::narrow_cast<til::CoordType>(lines.size());
    const auto pagerHeight = std::min(lineCount, size.height);

    // If the contents of the prompt are longer than the remaining number of lines in the viewport,
    // we need to reduce originInViewportFinal.y towards 0 to account for that. In other words,
    // as the viewport fills itself with text the _originInViewport will slowly move towards 0.
    originInViewportFinal.y = std::min(originInViewportFinal.y, size.height - pagerHeight);

    auto pagerContentTop = _pagerContentTop;
    // If the cursor is above the viewport, we go up...
    pagerContentTop = std::min(pagerContentTop, cursorPositionFinal.y);
    // and if the cursor is below it, we go down.
    pagerContentTop = std::max(pagerContentTop, cursorPositionFinal.y - size.height + 1);
    // The value may be out of bounds, because the above min/max doesn't ensure this on its own.
    pagerContentTop = std::clamp(pagerContentTop, 0, lineCount - pagerHeight);

    // Transform the recorded position from the lines vector coordinate space into VT screen space.
    // Due to the above scrolling of pagerTop, cursorPosition should now always be within the viewport.
    // dirtyBegPosition however could be outside of it.
    cursorPositionFinal.y += originInViewportFinal.y - pagerContentTop;

    std::wstring output;

    if (_clearPending)
    {
        _clearPending = false;
        _appendCUP(output, originInViewport);
        output.append(L"\x1b[J");
    }

    // Backup the attributes (DECSC) and disable the cursor when opening a popup (DECTCEM).
    // Restore the attributes (DECRC) reenable the cursor when closing them (DECTCEM).
    if (const auto popupOpened = !_popups.empty(); _popupOpened != popupOpened)
    {
        wchar_t buf[] =
            // Back/restore cursor position & attributes (commonly supported)
            L"\u001b7"
            // Show/hide cursor (commonly supported)
            "\u001b[?25l"
            // The popup code uses XTPUSHSGR (CSI # {) / XTPOPSGR (CSI # }) to draw the popups in the popup-colors,
            // while properly restoring the previous VT attributes. On terminals that support them, the following
            // won't do anything. On other terminals however, it'll reset the attributes to default.
            // This is important as the first thing the popup drawing code uses CSI K to erase the previous contents
            // and CSI m to reset the attributes on terminals that don't support XTPUSHSGR/XTPOPSGR. In order for
            // the first CSI K to behave as if there had a previous CSI m, we must emit an initial CSI m here.
            // (rarely supported)
            "\x1b[#{\x1b[m\x1b[#}";

        buf[1] = popupOpened ? '7' : '8';
        buf[7] = popupOpened ? 'l' : 'h';

        // When the popup closes we skip the XTPUSHSGR/XTPOPSGR sequence. This is crucial because we
        // use DECRC to restore the cursor position and attributes with a widely supported sequence.
        // If we emitted that XTPUSHSGR/XTPOPSGR sequence it would reset the attributes again.
        const size_t len = popupOpened ? 19 : 8;

        output.append(buf, len);
        _popupOpened = popupOpened;
    }

    // If we have so much text that it doesn't fit into the viewport (origin == {0,0}),
    // then we can scroll the existing contents of the pager and only write what got newly uncovered.
    //
    // The check for origin == {0,0} is important because it ensures that we "own" the entire viewport and
    // that scrolling our contents doesn't scroll away the user's output that may still be in the viewport.
    // (Anything below the origin is assumed to belong to us.)
    if (const auto delta = pagerContentTop - _pagerContentTop; delta != 0 && originInViewport == til::point{})
    {
        const auto deltaAbs = abs(delta);
        til::CoordType beg = 0;
        til::CoordType end = pagerHeight;

        // Let's say the viewport is 10 lines tall. Scenarios:
        // * We had 2 lines (_pagerContentTop == 0, _pagerHeight == 2),
        //   and now it's 11 lines (pagerContentTop == 1, pagerHeight == 11).
        //   --> deltaAbs == 1
        //   --> Scroll ✔️
        // * We had 2 lines (_pagerContentTop == 0, _pagerHeight == 2),
        //   and now it's 12 lines (pagerContentTop == 2, pagerHeight == 12).
        //   --> deltaAbs == 2
        //   --> Scroll ❌
        //
        // The same applies when going from 11/12 lines back to 2. It appears scrolling
        // makes sense if the delta is smaller than the current or previous pagerHeight.
        if (deltaAbs < std::min(_pagerHeight, pagerHeight))
        {
            beg = delta >= 0 ? pagerHeight - deltaAbs : 0;
            end = delta >= 0 ? pagerHeight : deltaAbs;
            const auto cmd = delta >= 0 ? L'S' : L'T';
            fmt::format_to(std::back_inserter(output), FMT_COMPILE(L"\x1b[{}{}"), deltaAbs, cmd);
        }
        else
        {
            // We may not be scrolling with VT, because we're scrolling by more rows than the pagerHeight.
            // Since no one is now clearing the scrolled in rows for us anymore, we need to do it ourselves.
            auto& lastLine = lines.at(pagerHeight - 1 + pagerContentTop);
            if (lastLine.columns < size.width)
            {
                lastLine.text.append(L"\x1b[K");
            }
        }

        // Mark each row that has been uncovered by the scroll as dirty.
        for (auto i = beg; i < end; i++)
        {
            auto& line = lines.at(i + pagerContentTop);
            line.dirtyBegOffset = 0;
            line.dirtyBegColumn = 0;
        }
    }

    bool anyDirty = false;
    for (til::CoordType i = 0; i < pagerHeight; i++)
    {
        const auto& line = lines.at(i + pagerContentTop);
        anyDirty = line.dirtyBegOffset < line.text.size();
        if (anyDirty)
        {
            break;
        }
    }

    til::point writeCursorPosition{ -1, -1 };

    if (anyDirty)
    {
#if COOKED_READ_DEBUG
        static size_t debugColorIndex = 0;
        const auto color = til::colorbrewer::dark2[++debugColorIndex % std::size(til::colorbrewer::dark2)];
        fmt::format_to(std::back_inserter(output), FMT_COMPILE(L"\x1b[48;2;{};{};{}m"), GetRValue(color), GetGValue(color), GetBValue(color));
#endif

        for (til::CoordType i = 0; i < pagerHeight; i++)
        {
            const auto row = std::min(originInViewport.y + i, size.height - 1);

            // If the last write left the cursor at the end of a line, the next write will start at the beginning of the next line.
            // This avoids needless calls to _appendCUP. The reason it's here and not at the end of the loop is similar to how
            // delay-wrapping in VT works: The line wrap only occurs after writing 1 more character than fits on the line.
            if (writeCursorPosition.x >= size.width)
            {
                writeCursorPosition.x = 0;
                writeCursorPosition.y = row;
            }

            const auto& line = lines.at(i + pagerContentTop);

            // Skip lines that aren't marked as dirty.
            // We use dirtyBegColumn instead of dirtyBegOffset to test for dirtiness, because a line that has 1 column
            // of space for layout and was asked to fit a wide glyph will have no text, but still be "dirty".
            // This ensures that we get the initial starting position of the _appendCUP below right.
            if (line.dirtyBegColumn >= size.width)
            {
                continue;
            }

            // Position the cursor wherever the dirty part of the line starts.
            if (const til::point pos{ line.dirtyBegColumn, row }; writeCursorPosition != pos)
            {
                writeCursorPosition = pos;
                _appendCUP(output, pos);
            }

            output.append(line.text, line.dirtyBegOffset);
            writeCursorPosition.x = line.columns;
        }

#if COOKED_READ_DEBUG
        output.append(L"\x1b[m");
#endif
    }

    // Clear any lines that we previously filled and are now empty.
    {
        const auto pagerHeightPrevious = std::min(_pagerHeight, size.height);

        if (pagerHeight < pagerHeightPrevious)
        {
            const auto row = std::min(originInViewport.y + pagerHeight, size.height - 1);
            _appendCUP(output, { 0, row });
            output.append(L"\x1b[K");

            for (til::CoordType i = pagerHeight + 1; i < pagerHeightPrevious; i++)
            {
                output.append(L"\x1b[E\x1b[K");
            }
        }
    }

    _appendCUP(output, cursorPositionFinal);
    WriteCharsVT(_screenInfo, output);

    _originInViewport = originInViewportFinal;
    _pagerPromptEnd = pagerPromptEnd;
    _pagerContentTop = pagerContentTop;
    _pagerHeight = pagerHeight;
    _bufferDirtyBeg = _buffer.size();
    _dirty = false;
}

COOKED_READ_DATA::LayoutResult COOKED_READ_DATA::_layoutLine(std::wstring& output, const std::wstring_view& input, const size_t inputOffset, const til::CoordType columnBegin, const til::CoordType columnLimit) const
{
    const auto& textBuffer = _screenInfo.GetTextBuffer();
    const auto beg = input.data();
    const auto end = beg + input.size();
    auto it = beg + std::min(inputOffset, input.size());
    auto column = std::min(columnBegin, columnLimit);

    output.reserve(output.size() + columnLimit - column);

    while (it != end && column < columnLimit)
    {
        const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return wch < L' '; });
        if (it != nextControlChar)
        {
            std::wstring_view text{ it, nextControlChar };
            til::CoordType cols = 0;
            const auto len = textBuffer.FitTextIntoColumns(text, columnLimit - column, cols);

            output.append(text, 0, len);
            column += cols;
            it += len;

            if (it != nextControlChar)
            {
                // The only reason that not all text could be fit into the line is if the last character was a wide glyph.
                // In that case we want to return the columnLimit, to indicate that the row is full and a line wrap is required,
                // BUT DON'T want to pad the line with a whitespace to actually fill the line to the columnLimit.
                // This is because copying the prompt contents (Ctrl-A, Ctrl-C) should not copy any trailing padding whitespace.
                //
                // Thanks to this lie, the _redisplay() code will not use a CRLF sequence or similar to move to the next line,
                // as it thinks that this row has naturally wrapped. This causes it to print the wide glyph on the preceding line
                // which causes the terminal to insert the padding whitespace for us.
                column = columnLimit;
                break;
            }

            if (column >= columnLimit)
            {
                break;
            }
        }

        const auto nextPlainChar = std::find_if(it, end, [](const auto& wch) { return wch >= L' '; });
        for (; it != nextPlainChar; ++it)
        {
            const auto wch = *it;
            wchar_t buf[8];
            til::CoordType len = 0;

            if (wch == UNICODE_TAB)
            {
                const auto remaining = columnLimit - column;
                len = std::min(8 - (column & 7), remaining);
                std::fill_n(&buf[0], len, L' ');
            }
            else
            {
                buf[0] = L'^';
                buf[1] = wch + L'@';
                len = 2;
            }

            if (column + len > columnLimit)
            {
                // Unlike above with regular text we can't avoid padding the line with whitespace, because a string
                // like "^A" is not a wide glyph, and so we cannot trick the terminal to insert the padding for us.
                output.append(columnLimit - column, L' ');
                column = columnLimit;
                goto outerLoopExit;
            }

            output.append(buf, len);
            column += len;

            if (column >= columnLimit)
            {
                goto outerLoopExit;
            }
        }
    }

outerLoopExit:
    return {
        .offset = static_cast<size_t>(it - beg),
        .column = column,
    };
}

void COOKED_READ_DATA::_appendCUP(std::wstring& output, til::point pos)
{
    fmt::format_to(std::back_inserter(output), FMT_COMPILE(L"\x1b[{};{}H"), pos.y + 1, pos.x + 1);
}

void COOKED_READ_DATA::_appendPopupAttr(std::wstring& output) const
{
    VtIo::FormatAttributes(output, _screenInfo.GetPopupAttributes());
}

void COOKED_READ_DATA::_popupPush(const PopupKind kind)
try
{
    auto& popup = _popups.emplace_back(kind);
    _dirty = true;

    switch (kind)
    {
    case PopupKind::CommandNumber:
        popup.commandNumber.buffer.fill(' ');
        popup.commandNumber.bufferSize = 0;
        break;
    case PopupKind::CommandList:
        popup.commandList.top = -1;
        popup.commandList.height = 10;
        popup.commandList.selected = _history->LastDisplayed;
        break;
    default:
        break;
    }
}
catch (...)
{
    LOG_CAUGHT_EXCEPTION();
    _popupsDone();
}

// Dismisses all current popups at once. Right now we don't need support for just dismissing the topmost popup.
// In fact, there's only a single situation right now where there can be >1 popup:
// Pressing F7 followed by F9 (CommandNumber on top of CommandList).
void COOKED_READ_DATA::_popupsDone()
{
    _popups.clear();
    _dirty = true;
}

void COOKED_READ_DATA::_popupHandleInput(wchar_t wch, uint16_t vkey, DWORD modifiers)
{
    if (_popups.empty())
    {
        assert(false); // Don't call this function.
        return;
    }

    auto& popup = _popups.back();
    switch (popup.kind)
    {
    case PopupKind::CopyToChar:
        _popupHandleCopyToCharInput(popup, wch, vkey, modifiers);
        break;
    case PopupKind::CopyFromChar:
        _popupHandleCopyFromCharInput(popup, wch, vkey, modifiers);
        break;
    case PopupKind::CommandNumber:
        _popupHandleCommandNumberInput(popup, wch, vkey, modifiers);
        break;
    case PopupKind::CommandList:
        _popupHandleCommandListInput(popup, wch, vkey, modifiers);
        break;
    default:
        break;
    }
}

void COOKED_READ_DATA::_popupHandleCopyToCharInput(Popup& /*popup*/, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/)
{
    if (vkey)
    {
        if (vkey == VK_ESCAPE)
        {
            _popupsDone();
        }
    }
    else
    {
        // See PopupKind::CopyToChar for more information about this code.
        const auto cmd = _history->GetLastCommand();
        const auto cursor = _bufferCursor;
        const auto idx = cmd.find(wch, cursor);

        if (idx != decltype(cmd)::npos)
        {
            // When we enter this if condition it's guaranteed that _bufferCursor must be
            // smaller than idx, which in turn implies that it's smaller than cmd.size().
            // As such, calculating length is safe and str.size() == length.
            const auto count = idx - cursor;
            _replace(cursor, count, cmd.data() + cursor, count);
        }

        _popupsDone();
    }
}

void COOKED_READ_DATA::_popupHandleCopyFromCharInput(Popup& /*popup*/, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/)
{
    if (vkey)
    {
        if (vkey == VK_ESCAPE)
        {
            _popupsDone();
        }
    }
    else
    {
        // See PopupKind::CopyFromChar for more information about this code.
        const auto cursor = _bufferCursor;
        auto idx = _buffer.find(wch, cursor);
        idx = std::min(idx, _buffer.size());
        _replace(cursor, idx - cursor, nullptr, 0);
        _popupsDone();
    }
}

void COOKED_READ_DATA::_popupHandleCommandNumberInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/)
{
    if (vkey)
    {
        if (vkey == VK_ESCAPE)
        {
            _popupsDone();
        }
    }
    else
    {
        if (wch == UNICODE_CARRIAGERETURN)
        {
            popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = L'\0';
            _replace(_history->RetrieveNth(std::stoi(popup.commandNumber.buffer.data())));
            _popupsDone();
        }
        else if (wch >= L'0' && wch <= L'9')
        {
            if (popup.commandNumber.bufferSize < CommandNumberMaxInputLength)
            {
                popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = wch;
                _dirty = true;
            }
        }
        else if (wch == UNICODE_BACKSPACE)
        {
            if (popup.commandNumber.bufferSize > 0)
            {
                popup.commandNumber.buffer[--popup.commandNumber.bufferSize] = L' ';
                _dirty = true;
            }
        }
    }
}

void COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD modifiers)
{
    auto& cl = popup.commandList;

    if (wch == UNICODE_CARRIAGERETURN)
    {
        _replace(_history->RetrieveNth(cl.selected));
        _popupsDone();
        _handleChar(UNICODE_CARRIAGERETURN, modifiers);
        return;
    }

    switch (vkey)
    {
    case VK_ESCAPE:
        _popupsDone();
        return;
    case VK_F9:
        _popupPush(PopupKind::CommandNumber);
        return;
    case VK_DELETE:
        _history->Remove(cl.selected);
        if (_history->GetNumberOfCommands() <= 0)
        {
            _popupsDone();
            return;
        }
        break;
    case VK_LEFT:
    case VK_RIGHT:
        _replace(_history->RetrieveNth(cl.selected));
        _popupsDone();
        return;
    case VK_UP:
        if (WI_IsFlagSet(modifiers, SHIFT_PRESSED))
        {
            _history->Swap(cl.selected, cl.selected - 1);
        }
        // _popupDrawCommandList() clamps all values to valid ranges in `cl`.
        cl.selected--;
        break;
    case VK_DOWN:
        if (WI_IsFlagSet(modifiers, SHIFT_PRESSED))
        {
            _history->Swap(cl.selected, cl.selected + 1);
        }
        // _popupDrawCommandList() clamps all values to valid ranges in `cl`.
        cl.selected++;
        break;
    case VK_HOME:
        cl.selected = 0;
        break;
    case VK_END:
        // _popupDrawCommandList() clamps all values to valid ranges in `cl`.
        cl.selected = INT_MAX;
        break;
    case VK_PRIOR:
        // _popupDrawCommandList() clamps all values to valid ranges in `cl`.
        cl.selected -= cl.height;
        break;
    case VK_NEXT:
        // _popupDrawCommandList() clamps all values to valid ranges in `cl`.
        cl.selected += cl.height;
        break;
    default:
        return;
    }

    _dirty = true;
}

void COOKED_READ_DATA::_popupDrawPrompt(std::vector<Line>& lines, const til::CoordType width, const UINT id, const std::wstring_view& prefix, const std::wstring_view& suffix) const
{
    std::wstring str;
    str.append(prefix);
    _LoadString(id, str);
    str.append(suffix);

    std::wstring line;
    line.append(L"\x1b[#{\x1b[K");
    _appendPopupAttr(line);
    const auto res = _layoutLine(line, str, 0, 0, width);
    line.append(L"\x1b[m\x1b[#}");

    lines.emplace_back(std::move(line), 0, 0, res.column);
}

void COOKED_READ_DATA::_popupDrawCommandList(std::vector<Line>& lines, const til::size size, Popup& popup) const
{
    assert(popup.kind == PopupKind::CommandList);

    auto& cl = popup.commandList;
    const auto historySize = _history->GetNumberOfCommands();
    const auto indexWidth = gsl::narrow_cast<til::CoordType>(fmt::formatted_size(FMT_COMPILE(L"{}"), historySize));

    // The popup is half the height of the viewport, but at least 1 and at most 20 lines.
    // Unless of course the history size is less than that. We also reserve 1 additional line
    // of space in case the user presses F9 which will open the "Enter command number:" popup.
    const auto height = std::min(historySize, std::min(size.height / 2 - 1, 20));
    if (height < 1)
    {
        return;
    }

    // cl.selected may be out of bounds after a page up/down, etc., so we need to clamp it.
    cl.selected = std::clamp(cl.selected, 0, historySize - 1);

    // If it hasn't been initialized it yet, center the selected item.
    if (cl.top < 0)
    {
        cl.top = std::max(0, cl.selected - height / 2);
    }

    // If the selection is above the viewport, we go up...
    cl.top = std::min(cl.top, cl.selected);
    // and if the selection is below it, we go down.
    cl.top = std::max(cl.top, cl.selected - height + 1);
    // The value may be out of bounds, because the above min/max doesn't ensure this on its own.
    cl.top = std::clamp(cl.top, 0, historySize - height);

    // We also need to update the height for future page up/down movements.
    cl.height = height;

    // Calculate the position of the █ track in the scrollbar among all the ▒.
    // The position is offset by +1 because at off == 0 we draw the ▲.
    // We add historyMax/2 to round the division result to the nearest value.
    const auto historyMax = historySize - 1;
    const auto trackPositionMax = height - 3;
    const auto trackPosition = historyMax <= 0 ? 0 : 1 + (trackPositionMax * cl.selected + historyMax / 2) / historyMax;
    const auto stackedCommandNumberPopup = _popups.size() == 2 && _popups.back().kind == PopupKind::CommandNumber;

    for (til::CoordType off = 0; off < height; ++off)
    {
        const auto index = cl.top + off;
        const auto str = _history->GetNth(index);
        const auto selected = index == cl.selected && !stackedCommandNumberPopup;

        std::wstring line;
        line.append(L"\x1b[#{\x1b[K");
        _appendPopupAttr(line);

        wchar_t scrollbarChar = L' ';
        if (historySize > height)
        {
            if (off == 0)
            {
                scrollbarChar = L'▴';
            }
            else if (off == height - 1)
            {
                scrollbarChar = L'▾';
            }
            else
            {
                scrollbarChar = off == trackPosition ? L'█' : L'▒';
            }
        }
        line.push_back(scrollbarChar);

        if (selected)
        {
            line.push_back(L'▸');
        }
        else
        {
            line.append(L"\x1b[m\x1b[#} ");
        }

        fmt::format_to(std::back_inserter(line), FMT_COMPILE(L"{:{}}: "), index, indexWidth);

        _layoutLine(line, str, 0, indexWidth + 4, size.width);

        if (selected)
        {
            line.append(L"\x1b[m\x1b[#}");
        }

        line.append(L"\r\n");
        lines.emplace_back(std::move(line), 0, 0, size.width);
    }

    if (stackedCommandNumberPopup)
    {
        const std::wstring_view suffix{ _popups.back().commandNumber.buffer.data(), CommandNumberMaxInputLength };
        _popupDrawPrompt(lines, size.width - 1, ID_CONSOLE_MSGCMDLINEF9, L"╰", suffix);
    }
    else
    {
        // Remove the \r\n we added to the last line, as we don't want to have an empty line at the end.
        auto& lastLine = lines.back();
        lastLine.text.erase(lastLine.text.size() - 2);
    }
}
