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

#include "pch.h"
#include "ControlCore.h"

// MidiAudio
#include <mmeapi.h>
#include <dsound.h>

#include <DefaultSettings.h>
#include <unicode.hpp>

#include "EventArgs.h"
#include "../../renderer/atlas/AtlasEngine.h"
#include "../../renderer/base/renderer.hpp"
#include "../../renderer/uia/UiaRenderer.hpp"
#include "../../types/inc/CodepointWidthDetector.hpp"
#include "../../types/inc/utils.hpp"

#include "ControlCore.g.cpp"
#include "SelectionColor.g.cpp"

using namespace ::Microsoft::Console;
using namespace ::Microsoft::Console::Types;
using namespace ::Microsoft::Console::VirtualTerminal;
using namespace ::Microsoft::Terminal::Core;
using namespace winrt::Windows::Graphics::Display;
using namespace winrt::Windows::System;
using namespace winrt::Windows::ApplicationModel::DataTransfer;

namespace winrt::Microsoft::Terminal::Control::implementation
{
    static winrt::Microsoft::Terminal::Core::OptionalColor OptionalFromColor(const til::color& c) noexcept
    {
        Core::OptionalColor result;
        result.Color = c;
        result.HasValue = true;
        return result;
    }

    static ::Microsoft::Console::Render::Atlas::GraphicsAPI parseGraphicsAPI(GraphicsAPI api) noexcept
    {
        using GA = ::Microsoft::Console::Render::Atlas::GraphicsAPI;
        switch (api)
        {
        case GraphicsAPI::Direct2D:
            return GA::Direct2D;
        case GraphicsAPI::Direct3D11:
            return GA::Direct3D11;
        default:
            return GA::Automatic;
        }
    }

    TextColor SelectionColor::AsTextColor() const noexcept
    {
        if (IsIndex16())
        {
            return { Color().r, false };
        }
        else
        {
            return { static_cast<COLORREF>(Color()) };
        }
    }

    ControlCore::ControlCore(Control::IControlSettings settings,
                             Control::IControlAppearance unfocusedAppearance,
                             TerminalConnection::ITerminalConnection connection) :
        _desiredFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, DEFAULT_FONT_SIZE, CP_UTF8 },
        _actualFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false }
    {
        static const auto textMeasurementInit = [&]() {
            TextMeasurementMode mode = TextMeasurementMode::Graphemes;
            switch (settings.TextMeasurement())
            {
            case TextMeasurement::Wcswidth:
                mode = TextMeasurementMode::Wcswidth;
                break;
            case TextMeasurement::Console:
                mode = TextMeasurementMode::Console;
                break;
            default:
                break;
            }
            CodepointWidthDetector::Singleton().Reset(mode);

            if (settings.AmbiguousWidth() == AmbiguousWidth::Wide)
            {
                CodepointWidthDetector::Singleton().SetAmbiguousWidth(2);
            }

            return true;
        }();

        _settings = settings;
        _hasUnfocusedAppearance = static_cast<bool>(unfocusedAppearance);
        _unfocusedAppearance = _hasUnfocusedAppearance ? unfocusedAppearance : settings;
        _terminal = std::make_shared<::Microsoft::Terminal::Core::Terminal>();
        const auto lock = _terminal->LockForWriting();

        _setupDispatcherAndCallbacks();

        Connection(connection);

        _terminal->SetWriteInputCallback([this](std::wstring_view wstr) {
            _pendingResponses.append(wstr);
        });

        // GH#8969: pre-seed working directory to prevent potential races
        _terminal->SetWorkingDirectory(_settings.StartingDirectory());

        _terminal->SetCopyToClipboardCallback([this](wil::zwstring_view wstr) {
            WriteToClipboard.raise(*this, winrt::make<WriteToClipboardEventArgs>(winrt::hstring{ std::wstring_view{ wstr } }, std::string{}, std::string{}));
        });

        auto pfnWarningBell = [this] { _terminalWarningBell(); };
        _terminal->SetWarningBellCallback(pfnWarningBell);

        auto pfnTitleChanged = [this](auto&& PH1) { _terminalTitleChanged(std::forward<decltype(PH1)>(PH1)); };
        _terminal->SetTitleChangedCallback(pfnTitleChanged);

        auto pfnScrollPositionChanged = [this](auto&& PH1, auto&& PH2, auto&& PH3) { _terminalScrollPositionChanged(std::forward<decltype(PH1)>(PH1), std::forward<decltype(PH2)>(PH2), std::forward<decltype(PH3)>(PH3)); };
        _terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged);

        auto pfnTerminalTaskbarProgressChanged = [this] { _terminalTaskbarProgressChanged(); };
        _terminal->TaskbarProgressChangedCallback(pfnTerminalTaskbarProgressChanged);

        auto pfnShowWindowChanged = [this](auto&& PH1) { _terminalShowWindowChanged(std::forward<decltype(PH1)>(PH1)); };
        _terminal->SetShowWindowCallback(pfnShowWindowChanged);

        auto pfnPlayMidiNote = [this](auto&& PH1, auto&& PH2, auto&& PH3) { _terminalPlayMidiNote(std::forward<decltype(PH1)>(PH1), std::forward<decltype(PH2)>(PH2), std::forward<decltype(PH3)>(PH3)); };
        _terminal->SetPlayMidiNoteCallback(pfnPlayMidiNote);

        auto pfnCompletionsChanged = [=](auto&& menuJson, auto&& replaceLength) { _terminalCompletionsChanged(menuJson, replaceLength); };
        _terminal->CompletionsChangedCallback(pfnCompletionsChanged);

        auto pfnSearchMissingCommand = [this](auto&& PH1, auto&& PH2) { _terminalSearchMissingCommand(std::forward<decltype(PH1)>(PH1), std::forward<decltype(PH2)>(PH2)); };
        _terminal->SetSearchMissingCommandCallback(pfnSearchMissingCommand);

        auto pfnClearQuickFix = [this] { ClearQuickFix(); };
        _terminal->SetClearQuickFixCallback(pfnClearQuickFix);

        auto pfnWindowSizeChanged = [this](auto&& PH1, auto&& PH2) { _terminalWindowSizeChanged(std::forward<decltype(PH1)>(PH1), std::forward<decltype(PH2)>(PH2)); };
        _terminal->SetWindowSizeChangedCallback(pfnWindowSizeChanged);

        // MSFT 33353327: Initialize the renderer in the ctor instead of Initialize().
        // We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go.
        // If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach
        // the UIA Engine to the renderer. This prevents us from signaling changes to the cursor or buffer.
        {
            // Now create the renderer and initialize the render thread.
            auto& renderSettings = _terminal->GetRenderSettings();
            _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(renderSettings, _terminal.get());

            _renderer->SetBackgroundColorChangedCallback([this]() { _rendererBackgroundColorChanged(); });
            _renderer->SetFrameColorChangedCallback([this]() { _rendererTabColorChanged(); });
            _renderer->SetRendererEnteredErrorStateCallback([this]() { _rendererEnteredErrorState(); });
        }

        UpdateSettings(settings, unfocusedAppearance);
    }

    void ControlCore::_rendererEnteredErrorState()
    {
        // The first time the renderer fails out (after all of its own retries), switch it to D2D and WARP
        // and force it to try again. If it _still_ fails, we can let it halt.
        if (_renderFailures++ == 0)
        {
            const auto lock = _terminal->LockForWriting();
            _renderEngine->SetGraphicsAPI(parseGraphicsAPI(GraphicsAPI::Direct2D));
            _renderEngine->SetSoftwareRendering(true);
            _renderer->EnablePainting();
            return;
        }
        RendererEnteredErrorState.raise(nullptr, nullptr);
    }

    void ControlCore::_setupDispatcherAndCallbacks()
    {
        // Get our dispatcher. If we're hosted in-proc with XAML, this will get
        // us the same dispatcher as TermControl::Dispatcher(). If we're out of
        // proc, this'll return null. We'll need to instead make a new
        // DispatcherQueue (on a new thread), so we can use that for throttled
        // functions.
        _dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread();
        if (!_dispatcher)
        {
            auto controller{ winrt::Windows::System::DispatcherQueueController::CreateOnDedicatedThread() };
            _dispatcher = controller.DispatcherQueue();
        }

        const auto shared = _shared.lock();
        // Raises an OutputIdle event once there hasn't been any output for at least 100ms.
        // It also updates all regex patterns in the viewport.
        //
        // NOTE: Calling UpdatePatternLocations from a background
        // thread is a workaround for us to hit GH#12607 less often.
        shared->outputIdle = std::make_unique<til::throttled_func<>>(
            til::throttled_func_options{
                .delay = std::chrono::milliseconds{ 100 },
                .debounce = true,
                .trailing = true,
            },
            [this, weakThis = get_weak(), dispatcher = _dispatcher]() {
                dispatcher.TryEnqueue(DispatcherQueuePriority::Normal, [weakThis]() {
                    if (const auto self = weakThis.get(); self && !self->_IsClosing())
                    {
                        self->OutputIdle.raise(*self, nullptr);
                    }
                });

                // We can't use a `weak_ptr` to `_terminal` here, because it takes significant
                // dependency on the lifetime of `this` (primarily on our `_renderer`).
                // and a `weak_ptr` would allow it to outlive `this`.
                // Theoretically `debounced_func_trailing` should call `WaitForThreadpoolTimerCallbacks()`
                // with cancel=true on destruction, which should ensure that our use of `this` here is safe.
                const auto lock = _terminal->LockForWriting();
                _terminal->UpdatePatternsUnderLock();
            });

        // If you rapidly show/hide Windows Terminal, something about GotFocus()/LostFocus() gets broken.
        // We'll then receive easily 10+ such calls from WinUI the next time the application is shown.
        shared->focusChanged = std::make_unique<til::throttled_func<bool>>(
            til::throttled_func_options{
                .delay = std::chrono::milliseconds{ 25 },
                .debounce = true,
                .trailing = true,
            },
            [this](const bool focused) {
                // Theoretically `debounced_func_trailing` should call `WaitForThreadpoolTimerCallbacks()`
                // with cancel=true on destruction, which should ensure that our use of `this` here is safe.
                _focusChanged(focused);
            });

        // Scrollbar updates are also expensive (XAML), so we'll throttle them as well.
        shared->updateScrollBar = std::make_shared<ThrottledFunc<Control::ScrollPositionChangedArgs>>(
            _dispatcher,
            til::throttled_func_options{
                .delay = std::chrono::milliseconds{ 8 },
                .trailing = true,
            },
            [weakThis = get_weak()](const auto& update) {
                if (auto core{ weakThis.get() }; core && !core->_IsClosing())
                {
                    core->ScrollPositionChanged.raise(*core, update);
                }
            });
    }

    // Safely disconnects event handlers from the connection and closes it. This is necessary because
    // WinRT event revokers don't prevent pending calls from proceeding (thread-safe but not race-free).
    void ControlCore::_closeConnection()
    {
        _connectionOutputEventRevoker.revoke();
        _connectionStateChangedRevoker.revoke();

        // One of the tasks for `ITerminalConnection::Close()` is to block until all pending
        // callback calls have completed. This solves the race-condition issue mentioned above.
        if (_connection)
        {
            _connection.Close();
            _connection = nullptr;
        }
    }

    ControlCore::~ControlCore()
    {
        Close();

        // See notes about the _renderer member in the header file.
        _renderer->TriggerTeardown();
    }

    void ControlCore::Detach()
    {
        // Disable the renderer, so that it doesn't try to start any new frames
        // for our engines while we're not attached to anything.
        _renderer->TriggerTeardown();

        // Clear out any throttled funcs that we had wired up to run on this UI
        // thread. These will be recreated in _setupDispatcherAndCallbacks, when
        // we're re-attached to a new control (on a possibly new UI thread).
        const auto shared = _shared.lock();
        shared->outputIdle.reset();
        shared->updateScrollBar.reset();
    }

    void ControlCore::AttachToNewControl()
    {
        _setupDispatcherAndCallbacks();
        const auto actualNewSize = _actualFont.GetSize();
        // Bubble this up, so our new control knows how big we want the font.
        FontSizeChanged.raise(*this, winrt::make<FontSizeChangedArgs>(actualNewSize.width, actualNewSize.height));

        // The renderer will be re-enabled in Initialize

        Attached.raise(*this, nullptr);
    }

    TerminalConnection::ITerminalConnection ControlCore::Connection()
    {
        return _connection;
    }

    // Method Description:
    // - Setup our event handlers for this connection. If we've currently got a
    //   connection, then this'll revoke the existing connection's handlers.
    // - This will not call Start on the incoming connection. The caller should do that.
    // - If the caller doesn't want the old connection to be closed, then they
    //   should grab a reference to it before calling this (so that it doesn't
    //   destruct, and close) during this call.
    void ControlCore::Connection(const TerminalConnection::ITerminalConnection& newConnection)
    {
        auto oldState = ConnectionState(); // rely on ControlCore's automatic null handling
        // revoke ALL old handlers immediately

        _closeConnection();

        _connection = newConnection;
        if (_connection)
        {
            // Subscribe to the connection's disconnected event and call our connection closed handlers.
            _connectionStateChangedRevoker = newConnection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*v*/) {
                ConnectionStateChanged.raise(*this, nullptr);
            });

            // Get our current size in rows/cols, and hook them up to
            // this connection too.
            {
                const auto lock = _terminal->LockForReading();
                const auto vp = _terminal->GetViewport();
                const auto width = vp.Width();
                const auto height = vp.Height();

                newConnection.Resize(height, width);
            }
            // Window owner too.
            if (auto conpty{ newConnection.try_as<TerminalConnection::ConptyConnection>() })
            {
                conpty.ReparentWindow(_owningHwnd);
            }

            // This event is explicitly revoked in the destructor: does not need weak_ref
            _connectionOutputEventRevoker = _connection.TerminalOutput(winrt::auto_revoke, { this, &ControlCore::_connectionOutputHandler });
        }

        // Fire off a connection state changed notification, to let our hosting
        // app know that we're in a different state now.
        if (oldState != ConnectionState())
        { // rely on the null handling again
            // send the notification
            ConnectionStateChanged.raise(*this, nullptr);
        }
    }

    bool ControlCore::Initialize(const float actualWidth,
                                 const float actualHeight,
                                 const float compositionScale)
    {
        assert(_settings);

        _panelWidth = actualWidth;
        _panelHeight = actualHeight;
        _compositionScale = compositionScale;

        { // scope for terminalLock
            const auto lock = _terminal->LockForWriting();

            if (_initializedTerminal.load(std::memory_order_relaxed))
            {
                return false;
            }

            const auto windowWidth = actualWidth * compositionScale;
            const auto windowHeight = actualHeight * compositionScale;

            if (windowWidth == 0 || windowHeight == 0)
            {
                return false;
            }

            _renderEngine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
            _renderer->AddRenderEngine(_renderEngine.get());

            // Hook up the warnings callback as early as possible so that we catch everything.
            _renderEngine->SetWarningCallback([this](HRESULT hr, wil::zwstring_view parameter) {
                _rendererWarning(hr, parameter);
            });

            // Initialize our font with the renderer
            // We don't have to care about DPI. We'll get a change message immediately if it's not 96
            // and react accordingly.
            _updateFont();

            const til::size windowSize{ til::math::rounding, windowWidth, windowHeight };

            // First set up the dx engine with the window size in pixels.
            // Then, using the font, get the number of characters that can fit.
            // Resize our terminal connection to match that size, and initialize the terminal with that size.
            const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
            LOG_IF_FAILED(_renderEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));

            const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
            const auto width = vp.Width();
            const auto height = vp.Height();

            if (_connection)
            {
                _connection.Resize(height, width);
            }

            if (_owningHwnd != 0)
            {
                if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
                {
                    conpty.ReparentWindow(_owningHwnd);
                }
            }

            // Override the default width and height to match the size of the swapChainPanel
            const til::size viewportSize{ Utils::ClampToShortMax(width, 1),
                                          Utils::ClampToShortMax(height, 1) };

            // TODO:MSFT:20642297 - Support infinite scrollback here, if HistorySize is -1
            _terminal->Create(viewportSize, Utils::ClampToShortMax(_settings.HistorySize(), 0), *_renderer);
            _terminal->UpdateSettings(_settings);

            // Tell the render engine to notify us when the swap chain changes.
            // We do this after we initially set the swapchain so as to avoid
            // unnecessary callbacks (and locking problems)
            _renderEngine->SetCallback([this](HANDLE handle) {
                _renderEngineSwapChainChanged(handle);
            });

            _renderEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect());
            _renderEngine->SetPixelShaderPath(_settings.PixelShaderPath());
            _renderEngine->SetPixelShaderImagePath(_settings.PixelShaderImagePath());
            _renderEngine->SetGraphicsAPI(parseGraphicsAPI(_settings.GraphicsAPI()));
            _renderEngine->SetDisablePartialInvalidation(_settings.DisablePartialInvalidation());
            _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering());

            _updateAntiAliasingMode();

            // GH#5098: Inform the engine of the opacity of the default text background.
            // GH#11315: Always do this, even if they don't have acrylic on.
            _renderEngine->EnableTransparentBackground(_isBackgroundTransparent());

            _initializedTerminal.store(true, std::memory_order_relaxed);
        } // scope for TerminalLock

        return true;
    }

    // Method Description:
    // - Tell the renderer to start painting.
    // - !! IMPORTANT !! Make sure that we've attached our swap chain to an
    //   actual target before calling this.
    // Arguments:
    // - <none>
    // Return Value:
    // - <none>
    void ControlCore::EnablePainting()
    {
        if (_initializedTerminal.load(std::memory_order_relaxed))
        {
            // The lock must be held, because it calls into IRenderData which is shared state.
            const auto lock = _terminal->LockForWriting();
            _renderer->EnablePainting();
        }
    }

    // Method Description:
    // - Writes the given sequence as input to the active terminal connection.
    // - This method has been overloaded to allow zero-copy winrt::param::hstring optimizations.
    // Arguments:
    // - wstr: the string of characters to write to the terminal connection.
    // Return Value:
    // - <none>
    void ControlCore::_sendInputToConnection(std::wstring_view wstr)
    {
        if (_connection)
        {
            _connection.WriteInput(winrt_wstring_to_array_view(wstr));
        }
    }

    // Method Description:
    // - Writes the given sequence as input to the active terminal connection,
    // Arguments:
    // - wstr: the string of characters to write to the terminal connection.
    // Return Value:
    // - <none>
    void ControlCore::SendInput(const std::wstring_view wstr)
    {
        if (wstr.empty())
        {
            return;
        }

        // The connection may call functions like WriteFile() which may block indefinitely.
        // It's important we don't hold any mutexes across such calls.
        _terminal->_assertUnlocked();

        if (_isReadOnly)
        {
            _raiseReadOnlyWarning();
        }
        else
        {
            _sendInputToConnection(wstr);
        }
    }

    bool ControlCore::SendCharEvent(const wchar_t ch,
                                    const WORD scanCode,
                                    const ::Microsoft::Terminal::Core::ControlKeyStates modifiers)
    {
        const wchar_t CtrlD = 0x4;
        const wchar_t Enter = '\r';

        if (_connection && _connection.State() >= winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Closed)
        {
            if (ch == CtrlD)
            {
                CloseTerminalRequested.raise(*this, nullptr);
                return true;
            }

            if (ch == Enter)
            {
                // Ask the hosting application to give us a new connection.
                RestartTerminalRequested.raise(*this, nullptr);
                return true;
            }
        }

        if (ch == L'\x3') // Ctrl+C or Ctrl+Break
        {
            _handleControlC();
        }

        TerminalInput::OutputType out;
        {
            const auto lock = _terminal->LockForReading();
            out = _terminal->SendCharEvent(ch, scanCode, modifiers);
        }
        if (out)
        {
            SendInput(*out);
            return true;
        }
        return false;
    }

    void ControlCore::_handleControlC()
    {
        if (!_midiAudioSkipTimer)
        {
            _midiAudioSkipTimer = _dispatcher.CreateTimer();
            _midiAudioSkipTimer.Interval(std::chrono::seconds(1));
            _midiAudioSkipTimer.IsRepeating(false);
            _midiAudioSkipTimer.Tick([weakSelf = get_weak()](auto&&, auto&&) {
                if (const auto self = weakSelf.get())
                {
                    self->_midiAudio.EndSkip();
                }
            });
        }

        _midiAudio.BeginSkip();
        _midiAudioSkipTimer.Start();
    }

    bool ControlCore::_shouldTryUpdateSelection(const WORD vkey)
    {
        // GH#6423 - don't update selection if the key that was pressed was a
        // modifier key. We'll wait for a real keystroke to dismiss the
        // GH #7395 - don't update selection when taking PrintScreen
        // selection.
        return _terminal->IsSelectionActive() && ::Microsoft::Terminal::Core::Terminal::IsInputKey(vkey);
    }

    bool ControlCore::TryMarkModeKeybinding(const WORD vkey,
                                            const ::Microsoft::Terminal::Core::ControlKeyStates mods)
    {
        auto lock = _terminal->LockForWriting();

        if (_shouldTryUpdateSelection(vkey) && _terminal->SelectionMode() == ::Terminal::SelectionInteractionMode::Mark)
        {
            if (vkey == 'A' && !mods.IsAltPressed() && !mods.IsShiftPressed() && mods.IsCtrlPressed())
            {
                // Ctrl + A --> Select all
                _terminal->SelectAll();
                _updateSelectionUI();
                return true;
            }
            else if (vkey == VK_TAB && !mods.IsAltPressed() && !mods.IsCtrlPressed() && _settings.DetectURLs())
            {
                // [Shift +] Tab --> next/previous hyperlink
                const auto direction = mods.IsShiftPressed() ? ::Terminal::SearchDirection::Backward : ::Terminal::SearchDirection::Forward;
                _terminal->SelectHyperlink(direction);
                _updateSelectionUI();
                return true;
            }
            else if (vkey == VK_RETURN && mods.IsCtrlPressed() && !mods.IsAltPressed() && !mods.IsShiftPressed())
            {
                // Ctrl + Enter --> Open URL
                if (const auto uri = _terminal->GetHyperlinkAtBufferPosition(_terminal->GetSelectionAnchor()); !uri.empty())
                {
                    lock.unlock();
                    OpenHyperlink.raise(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ uri }));
                }
                else
                {
                    const auto selectedText = _terminal->GetTextBuffer().GetPlainText(_terminal->GetSelectionAnchor(), _terminal->GetSelectionEnd());
                    lock.unlock();
                    OpenHyperlink.raise(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ selectedText }));
                }
                return true;
            }
            else if (vkey == VK_RETURN && !mods.IsCtrlPressed() && !mods.IsAltPressed())
            {
                // [Shift +] Enter --> copy text
                CopySelectionToClipboard(mods.IsShiftPressed(), false, _settings.CopyFormatting());
                _terminal->ClearSelection();
                _updateSelectionUI();
                return true;
            }
            else if (vkey == VK_ESCAPE)
            {
                _terminal->ClearSelection();
                _updateSelectionUI();
                return true;
            }
            else if (const auto updateSlnParams{ _terminal->ConvertKeyEventToUpdateSelectionParams(mods, vkey) })
            {
                // try to update the selection
                _terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, mods);
                _updateSelectionUI();
                return true;
            }
        }
        return false;
    }

    // Method Description:
    // - Send this particular key event to the terminal.
    //   See Terminal::SendKeyEvent for more information.
    // - Clears the current selection.
    // - Makes the cursor briefly visible during typing.
    // Arguments:
    // - vkey: The vkey of the key pressed.
    // - scanCode: The scan code of the key pressed.
    // - modifiers: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
    // - keyDown: If true, the key was pressed; otherwise, the key was released.
    bool ControlCore::TrySendKeyEvent(const WORD vkey,
                                      const WORD scanCode,
                                      const ControlKeyStates modifiers,
                                      const bool keyDown)
    {
        if (!vkey)
        {
            return true;
        }

        TerminalInput::OutputType out;
        {
            const auto lock = _terminal->LockForWriting();

            // Update the selection, if it's present
            // GH#8522, GH#3758 - Only modify the selection on key _down_. If we
            // modify on key up, then there's chance that we'll immediately dismiss
            // a selection created by an action bound to a keydown.
            if (_shouldTryUpdateSelection(vkey) && keyDown)
            {
                // try to update the selection
                if (const auto updateSlnParams{ _terminal->ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) })
                {
                    _terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, modifiers);
                    _updateSelectionUI();
                    return true;
                }

                // GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination.
                if (!modifiers.IsWinPressed())
                {
                    _terminal->ClearSelection();
                    _updateSelectionUI();
                }

                // When there is a selection active, escape should clear it and NOT flow through
                // to the terminal. With any other keypress, it should clear the selection AND
                // flow through to the terminal.
                if (vkey == VK_ESCAPE)
                {
                    return true;
                }
            }

            // If the terminal translated the key, mark the event as handled.
            // This will prevent the system from trying to get the character out
            // of it and sending us a CharacterReceived event.
            out = _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown);
        }
        if (out)
        {
            SendInput(*out);
            return true;
        }
        return false;
    }

    bool ControlCore::SendMouseEvent(const til::point viewportPos,
                                     const unsigned int uiButton,
                                     const ControlKeyStates states,
                                     const short wheelDelta,
                                     const TerminalInput::MouseButtonState state)
    {
        TerminalInput::OutputType out;
        {
            const auto lock = _terminal->LockForReading();
            out = _terminal->SendMouseEvent(viewportPos, uiButton, states, wheelDelta, state);
        }
        if (out)
        {
            SendInput(*out);
            return true;
        }
        return false;
    }

    void ControlCore::UserScrollViewport(const int viewTop)
    {
        {
            // This is a scroll event that wasn't initiated by the terminal
            //      itself - it was initiated by the mouse wheel, or the scrollbar.
            const auto lock = _terminal->LockForWriting();
            _terminal->UserScrollViewport(viewTop);
        }

        const auto shared = _shared.lock_shared();
        if (shared->outputIdle)
        {
            (*shared->outputIdle)();
        }
    }

    void ControlCore::AdjustOpacity(const float adjustment)
    {
        if (adjustment == 0)
        {
            return;
        }

        _setOpacity(Opacity() + adjustment);
    }

    // Method Description:
    // - Updates the opacity of the terminal
    // Arguments:
    // - opacity: The new opacity to set.
    // - focused (default == true): Whether the window is focused or unfocused.
    // Return Value:
    // - <none>
    void ControlCore::_setOpacity(const float opacity, bool focused)
    {
        const auto newOpacity = std::clamp(opacity, 0.0f, 1.0f);

        if (newOpacity == Opacity())
        {
            return;
        }

        // Update our runtime opacity value
        _runtimeOpacity = newOpacity;

        //Stores the focused runtime opacity separately from unfocused opacity
        //to transition smoothly between the two.
        _runtimeFocusedOpacity = focused ? newOpacity : _runtimeFocusedOpacity;

        // Manually turn off acrylic if they turn off transparency.
        _runtimeUseAcrylic = newOpacity < 1.0f && _settings.UseAcrylic();

        // Update the renderer as well. It might need to fall back from
        // cleartype -> grayscale if the BG is transparent / acrylic.
        if (_renderEngine)
        {
            const auto lock = _terminal->LockForWriting();
            _renderEngine->EnableTransparentBackground(_isBackgroundTransparent());
            _renderer->NotifyPaintFrame();
        }

        auto eventArgs = winrt::make_self<TransparencyChangedEventArgs>(newOpacity);
        TransparencyChanged.raise(*this, *eventArgs);
    }

    void ControlCore::ToggleShaderEffects()
    {
        const auto path = _settings.PixelShaderPath();
        const auto lock = _terminal->LockForWriting();
        // Originally, this action could be used to enable the retro effects
        // even when they're set to `false` in the settings. If the user didn't
        // specify a custom pixel shader, manually enable the legacy retro
        // effect first. This will ensure that a toggle off->on will still work,
        // even if they currently have retro effect off.
        if (path.empty())
        {
            _renderEngine->SetRetroTerminalEffect(!_renderEngine->GetRetroTerminalEffect());
        }
        else
        {
            _renderEngine->SetPixelShaderPath(_renderEngine->GetPixelShaderPath().empty() ? std::wstring_view{ path } : std::wstring_view{});
        }
        // Always redraw after toggling effects. This way even if the control
        // does not have focus it will update immediately.
        _renderer->TriggerRedrawAll();
    }

    // Method description:
    // - Updates last hovered cell, renders / removes rendering of hyper-link if required
    // Arguments:
    // - terminalPosition: The terminal position of the pointer
    void ControlCore::SetHoveredCell(Core::Point pos)
    {
        _updateHoveredCell(std::optional<til::point>{ pos });
    }
    void ControlCore::ClearHoveredCell()
    {
        _updateHoveredCell(std::nullopt);
    }

    void ControlCore::_updateHoveredCell(const std::optional<til::point> terminalPosition)
    {
        if (terminalPosition == _lastHoveredCell)
        {
            return;
        }

        // GH#9618 - lock while we're reading from the terminal, and if we need
        // to update something, then lock again to write the terminal.

        _lastHoveredCell = terminalPosition;
        uint16_t newId{ 0u };
        // we can't use auto here because we're pre-declaring newInterval.
        decltype(_terminal->GetHyperlinkIntervalFromViewportPosition({})) newInterval{ std::nullopt };
        if (terminalPosition.has_value())
        {
            const auto lock = _terminal->LockForReading();
            newId = _terminal->GetHyperlinkIdAtViewportPosition(*terminalPosition);
            newInterval = _terminal->GetHyperlinkIntervalFromViewportPosition(*terminalPosition);
        }

        // If the hyperlink ID changed or the interval changed, trigger a redraw all
        // (so this will happen both when we move onto a link and when we move off a link)
        if (newId != _lastHoveredId ||
            (newInterval != _lastHoveredInterval))
        {
            // Introduce scope for lock - we don't want to raise the
            // HoveredHyperlinkChanged event under lock, because then handlers
            // wouldn't be able to ask us about the hyperlink text/position
            // without deadlocking us.
            {
                const auto lock = _terminal->LockForWriting();

                _lastHoveredId = newId;
                _lastHoveredInterval = newInterval;
                _renderer->UpdateHyperlinkHoveredId(newId);
                _renderer->UpdateLastHoveredInterval(newInterval);
                _renderer->TriggerRedrawAll();
            }

            HoveredHyperlinkChanged.raise(*this, nullptr);
        }
    }

    winrt::hstring ControlCore::GetHyperlink(const Core::Point pos) const
    {
        const auto lock = _terminal->LockForReading();
        return winrt::hstring{ _terminal->GetHyperlinkAtViewportPosition(til::point{ pos }) };
    }

    winrt::hstring ControlCore::HoveredUriText() const
    {
        if (_lastHoveredCell.has_value())
        {
            const auto lock = _terminal->LockForReading();
            auto uri{ _terminal->GetHyperlinkAtViewportPosition(*_lastHoveredCell) };
            uri.resize(std::min<size_t>(1024u, uri.size())); // Truncate for display
            return winrt::hstring{ uri };
        }
        return {};
    }

    Windows::Foundation::IReference<Core::Point> ControlCore::HoveredCell() const
    {
        return _lastHoveredCell.has_value() ? Windows::Foundation::IReference<Core::Point>{ _lastHoveredCell.value().to_core_point() } : nullptr;
    }

    // Method Description:
    // - Updates the settings of the current terminal.
    // - INVARIANT: This method can only be called if the caller DOES NOT HAVE writing lock on the terminal.
    void ControlCore::UpdateSettings(const IControlSettings& settings, const IControlAppearance& newAppearance)
    {
        _settings = settings;
        _hasUnfocusedAppearance = static_cast<bool>(newAppearance);
        _unfocusedAppearance = _hasUnfocusedAppearance ? newAppearance : settings;

        const auto lock = _terminal->LockForWriting();

        _builtinGlyphs = _settings.EnableBuiltinGlyphs();
        _colorGlyphs = _settings.EnableColorGlyphs();
        _cellWidth = CSSLengthPercentage::FromString(_settings.CellWidth().c_str());
        _cellHeight = CSSLengthPercentage::FromString(_settings.CellHeight().c_str());
        _runtimeOpacity = std::nullopt;
        _runtimeFocusedOpacity = std::nullopt;

        // Manually turn off acrylic if they turn off transparency.
        _runtimeUseAcrylic = _settings.Opacity() < 1.0 && _settings.UseAcrylic();

        const auto sizeChanged = _setFontSizeUnderLock(_settings.FontSize());

        // Update the terminal core with its new Core settings
        _terminal->UpdateSettings(_settings);

        if (!_initializedTerminal.load(std::memory_order_relaxed))
        {
            // If we haven't initialized, there's no point in continuing.
            // Initialization will handle the renderer settings.
            return;
        }

        _renderEngine->SetGraphicsAPI(parseGraphicsAPI(_settings.GraphicsAPI()));
        _renderEngine->SetDisablePartialInvalidation(_settings.DisablePartialInvalidation());
        _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering());
        // Inform the renderer of our opacity
        _renderEngine->EnableTransparentBackground(_isBackgroundTransparent());
        _renderFailures = 0; // We may have changed the engine; reset the failure counter.

        // Trigger a redraw to repaint the window background and tab colors.
        _renderer->TriggerRedrawAll(true, true);

        _updateAntiAliasingMode();

        if (sizeChanged)
        {
            _refreshSizeUnderLock();
        }
    }

    // Method Description:
    // - Updates the appearance of the current terminal.
    // - INVARIANT: This method can only be called if the caller DOES NOT HAVE writing lock on the terminal.
    void ControlCore::ApplyAppearance(const bool focused)
    {
        const auto lock = _terminal->LockForWriting();
        const IControlAppearance newAppearance{ focused ? _settings : _unfocusedAppearance };
        // Update the terminal core with its new Core settings
        _terminal->UpdateAppearance(newAppearance);
        if ((focused || !_hasUnfocusedAppearance) && _focusedColorSchemeOverride)
        {
            _terminal->UpdateColorScheme(_focusedColorSchemeOverride);
        }

        // Update AtlasEngine settings under the lock
        if (_renderEngine)
        {
            // Update AtlasEngine settings under the lock
            _renderEngine->SetRetroTerminalEffect(newAppearance.RetroTerminalEffect());
            _renderEngine->SetPixelShaderPath(newAppearance.PixelShaderPath());
            _renderEngine->SetPixelShaderImagePath(newAppearance.PixelShaderImagePath());

            // Incase EnableUnfocusedAcrylic is disabled and Focused Acrylic is set to true,
            // the terminal should ignore the unfocused opacity from settings.
            // The Focused Opacity from settings should be ignored if overridden at runtime.
            const auto useFocusedRuntimeOpacity = focused || (!_settings.EnableUnfocusedAcrylic() && UseAcrylic());
            const auto newOpacity = useFocusedRuntimeOpacity ? FocusedOpacity() : newAppearance.Opacity();
            _setOpacity(newOpacity, focused);

            // No need to update Acrylic if UnfocusedAcrylic is disabled
            if (_settings.EnableUnfocusedAcrylic())
            {
                // Manually turn off acrylic if they turn off transparency.
                _runtimeUseAcrylic = Opacity() < 1.0 && newAppearance.UseAcrylic();
            }

            // Update the renderer as well. It might need to fall back from
            // cleartype -> grayscale if the BG is transparent / acrylic.
            _renderEngine->EnableTransparentBackground(_isBackgroundTransparent());
            _renderer->NotifyPaintFrame();

            auto eventArgs = winrt::make_self<TransparencyChangedEventArgs>(Opacity());
            TransparencyChanged.raise(*this, *eventArgs);

            _renderer->TriggerRedrawAll(true, true);
        }
    }

    void ControlCore::SetHighContrastMode(const bool enabled)
    {
        _terminal->SetHighContrastMode(enabled);
    }

    Control::IControlSettings ControlCore::Settings()
    {
        return _settings;
    }

    Control::IControlAppearance ControlCore::FocusedAppearance() const
    {
        return _settings;
    }

    Control::IControlAppearance ControlCore::UnfocusedAppearance() const
    {
        return _unfocusedAppearance;
    }

    void ControlCore::ApplyPreviewColorScheme(const Core::ICoreScheme& scheme)
    {
        const auto lock = _terminal->LockForReading();
        auto& renderSettings = _terminal->GetRenderSettings();
        if (!_stashedColorScheme)
        {
            _stashedColorScheme = std::make_unique_for_overwrite<StashedColorScheme>();
            *_stashedColorScheme = {
                .scheme = renderSettings.GetColorTable(),
                .foregroundAlias = renderSettings.GetColorAliasIndex(ColorAlias::DefaultForeground),
                .backgroundAlias = renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground),
            };
        }
        _terminal->UpdateColorScheme(scheme);
        _renderer->TriggerRedrawAll(true);
    }

    void ControlCore::ResetPreviewColorScheme()
    {
        if (_stashedColorScheme)
        {
            const auto lock = _terminal->LockForWriting();
            auto& renderSettings = _terminal->GetRenderSettings();
            decltype(auto) stashedScheme{ *_stashedColorScheme.get() };
            for (size_t i = 0; i < TextColor::TABLE_SIZE; ++i)
            {
                renderSettings.SetColorTableEntry(i, til::at(stashedScheme.scheme, i));
            }
            renderSettings.SetColorAliasIndex(ColorAlias::DefaultForeground, stashedScheme.foregroundAlias);
            renderSettings.SetColorAliasIndex(ColorAlias::DefaultBackground, stashedScheme.backgroundAlias);
            _renderer->TriggerRedrawAll(true);
        }
        _stashedColorScheme.reset();
    }

    void ControlCore::SetOverrideColorScheme(const Core::ICoreScheme& scheme)
    {
        const auto lock = _terminal->LockForWriting();
        _focusedColorSchemeOverride = scheme;

        _terminal->UpdateColorScheme(scheme ? scheme : _settings.as<Core::ICoreScheme>());
        _renderer->TriggerRedrawAll(true);
    }

    void ControlCore::_updateAntiAliasingMode()
    {
        D2D1_TEXT_ANTIALIAS_MODE mode;
        // Update AtlasEngine's AntialiasingMode
        switch (_settings.AntialiasingMode())
        {
        case TextAntialiasingMode::Cleartype:
            mode = D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE;
            break;
        case TextAntialiasingMode::Aliased:
            mode = D2D1_TEXT_ANTIALIAS_MODE_ALIASED;
            break;
        default:
            mode = D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE;
            break;
        }

        _renderEngine->SetAntialiasingMode(mode);
    }

    // Method Description:
    // - Update the font with the renderer. This will be called either when the
    //      font changes or the DPI changes, as DPI changes will necessitate a
    //      font change. This method will *not* change the buffer/viewport size
    //      to account for the new glyph dimensions. Callers should make sure to
    //      appropriately call _doResizeUnderLock after this method is called.
    // - The write lock should be held when calling this method.
    // Arguments:
    // <none>
    void ControlCore::_updateFont()
    {
        const auto newDpi = static_cast<int>(lrint(_compositionScale * USER_DEFAULT_SCREEN_DPI));

        _terminal->SetFontInfo(_actualFont);

        if (_renderEngine)
        {
            static constexpr auto cloneMap = [](const auto& map) {
                std::unordered_map<std::wstring_view, float> clone;
                if (map)
                {
                    clone.reserve(map.Size());
                    for (const auto& [tag, param] : map)
                    {
                        clone.emplace(tag, param);
                    }
                }
                return clone;
            };

            const auto fontFeatures = _settings.FontFeatures();
            const auto fontAxes = _settings.FontAxes();
            const auto featureMap = cloneMap(fontFeatures);
            const auto axesMap = cloneMap(fontAxes);

            // TODO: MSFT:20895307 If the font doesn't exist, this doesn't
            //      actually fail. We need a way to gracefully fallback.
            LOG_IF_FAILED(_renderEngine->UpdateDpi(newDpi));
            LOG_IF_FAILED(_renderEngine->UpdateFont(_desiredFont, _actualFont, featureMap, axesMap));
        }

        const auto actualNewSize = _actualFont.GetSize();
        FontSizeChanged.raise(*this, winrt::make<FontSizeChangedArgs>(actualNewSize.width, actualNewSize.height));
    }

    // Method Description:
    // - Set the font size of the terminal control.
    // Arguments:
    // - fontSize: The size of the font.
    // Return Value:
    // - Returns true if you need to call _refreshSizeUnderLock().
    bool ControlCore::_setFontSizeUnderLock(float fontSize)
    {
        // Make sure we have a non-zero font size
        const auto newSize = std::max(fontSize, 1.0f);
        const auto fontFace = _settings.FontFace();
        const auto fontWeight = _settings.FontWeight();
        _desiredFont = { fontFace, 0, fontWeight.Weight, newSize, CP_UTF8 };
        _actualFont = { fontFace, 0, fontWeight.Weight, _desiredFont.GetEngineSize(), CP_UTF8, false };

        _desiredFont.SetEnableBuiltinGlyphs(_builtinGlyphs);
        _desiredFont.SetEnableColorGlyphs(_colorGlyphs);
        _desiredFont.SetCellSize(_cellWidth, _cellHeight);

        const auto before = _actualFont.GetSize();
        _updateFont();
        const auto after = _actualFont.GetSize();
        return before != after;
    }

    // Method Description:
    // - Reset the font size of the terminal to its default size.
    // Arguments:
    // - none
    void ControlCore::ResetFontSize()
    {
        const auto lock = _terminal->LockForWriting();

        if (_setFontSizeUnderLock(_settings.FontSize()))
        {
            _refreshSizeUnderLock();
        }
    }

    // Method Description:
    // - Adjust the font size of the terminal control.
    // Arguments:
    // - fontSizeDelta: The amount to increase or decrease the font size by.
    void ControlCore::AdjustFontSize(float fontSizeDelta)
    {
        const auto lock = _terminal->LockForWriting();

        if (_setFontSizeUnderLock(_desiredFont.GetFontSize() + fontSizeDelta))
        {
            _refreshSizeUnderLock();
        }
    }

    // Method Description:
    // - Process a resize event that was initiated by the user. This can either
    //   be due to the user resizing the window (causing the swapchain to
    //   resize) or due to the DPI changing (causing us to need to resize the
    //   buffer to match)
    // - Note that a DPI change will also trigger a font size change, and will
    //   call into here.
    // - The write lock should be held when calling this method, we might be
    //   changing the buffer size in _refreshSizeUnderLock.
    // Arguments:
    // - <none>
    // Return Value:
    // - <none>
    void ControlCore::_refreshSizeUnderLock()
    {
        if (_IsClosing())
        {
            return;
        }

        auto cx = gsl::narrow_cast<til::CoordType>(lrint(_panelWidth * _compositionScale));
        auto cy = gsl::narrow_cast<til::CoordType>(lrint(_panelHeight * _compositionScale));

        // Don't actually resize so small that a single character wouldn't fit
        // in either dimension. The buffer really doesn't like being size 0.
        cx = std::max(cx, _actualFont.GetSize().width);
        cy = std::max(cy, _actualFont.GetSize().height);

        // Convert our new dimensions to characters
        const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, { cx, cy });
        const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);

        _terminal->ClearSelection();

        // Tell the dx engine that our window is now the new size.
        THROW_IF_FAILED(_renderEngine->SetWindowSize({ cx, cy }));

        // Invalidate everything
        _renderer->TriggerRedrawAll();

        // If this function succeeds with S_FALSE, then the terminal didn't
        // actually change size. No need to notify the connection of this no-op.
        const auto hr = _terminal->UserResize({ vp.Width(), vp.Height() });
        if (FAILED(hr) || hr == S_FALSE)
        {
            return;
        }

        if (_connection)
        {
            _connection.Resize(vp.Height(), vp.Width());
        }

        // TermControl will call Search() once the OutputIdle even fires after 100ms.
        // Until then we need to hide the now-stale search results from the renderer.
        ClearSearch();
        const auto shared = _shared.lock_shared();
        if (shared->outputIdle)
        {
            (*shared->outputIdle)();
        }
    }

    void ControlCore::SizeChanged(const float width,
                                  const float height)
    {
        SizeOrScaleChanged(width, height, _compositionScale);
    }

    void ControlCore::ScaleChanged(const float scale)
    {
        if (!_renderEngine)
        {
            return;
        }
        SizeOrScaleChanged(_panelWidth, _panelHeight, scale);
    }

    void ControlCore::SizeOrScaleChanged(const float width,
                                         const float height,
                                         const float scale)
    {
        const auto scaleChanged = _compositionScale != scale;
        // _refreshSizeUnderLock redraws the entire terminal.
        // Don't call it if we don't have to.
        if (_panelWidth == width && _panelHeight == height && !scaleChanged)
        {
            return;
        }

        _panelWidth = width;
        _panelHeight = height;
        _compositionScale = scale;

        const auto lock = _terminal->LockForWriting();
        if (scaleChanged)
        {
            // _updateFont relies on the new _compositionScale set above
            _updateFont();
        }
        _refreshSizeUnderLock();
    }

    void ControlCore::SetSelectionAnchor(const til::point position)
    {
        const auto lock = _terminal->LockForWriting();
        _terminal->SetSelectionAnchor(position);
    }

    // Method Description:
    // - Retrieves selection metadata from Terminal necessary to draw the
    //    selection markers.
    // - Since all of this needs to be done under lock, it is more performant
    //    to throw it all in a struct and pass it along.
    Control::SelectionData ControlCore::SelectionInfo() const
    {
        const auto lock = _terminal->LockForReading();
        Control::SelectionData info;

        const auto start{ _terminal->SelectionStartForRendering() };
        info.StartPos = { start.x, start.y };

        const auto end{ _terminal->SelectionEndForRendering() };
        info.EndPos = { end.x, end.y };

        info.Endpoint = static_cast<SelectionEndpointTarget>(_terminal->SelectionEndpointTarget());

        const auto bufferSize{ _terminal->GetTextBuffer().GetSize() };
        info.StartAtLeftBoundary = _terminal->GetSelectionAnchor().x == bufferSize.Left();
        info.EndAtRightBoundary = _terminal->GetSelectionEnd().x == bufferSize.RightExclusive();
        return info;
    }

    // Method Description:
    // - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging.
    // Arguments:
    // - position: the point in terminal coordinates (in cells, not pixels)
    void ControlCore::SetEndSelectionPoint(const til::point position)
    {
        const auto lock = _terminal->LockForWriting();

        if (!_terminal->IsSelectionActive())
        {
            return;
        }

        // clamp the converted position to be within the viewport bounds
        // x: allow range of [0, RightExclusive]
        // GH #18106: right exclusive needed for selection to support exclusive end
        til::point terminalPosition{
            std::clamp(position.x, 0, _terminal->GetViewport().Width()),
            std::clamp(position.y, 0, _terminal->GetViewport().Height() - 1)
        };

        // save location (for rendering) + render
        _terminal->SetSelectionEnd(terminalPosition);
        _updateSelectionUI();
    }

    // Method Description:
    // - Given a copy-able selection, get the selected text from the buffer and send it to the
    //     Windows Clipboard (CascadiaWin32:main.cpp).
    // Arguments:
    // - singleLine: collapse all of the text to one line
    // - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection
    // - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr
    //             if we should defer which formats are copied to the global setting
    bool ControlCore::CopySelectionToClipboard(bool singleLine,
                                               bool withControlSequences,
                                               const CopyFormat formats)
    {
        ::Microsoft::Terminal::Core::Terminal::TextCopyData payload;
        {
            const auto lock = _terminal->LockForWriting();

            // no selection --> nothing to copy
            if (!_terminal->IsSelectionActive())
            {
                return false;
            }

            const auto copyHtml = WI_IsFlagSet(formats, CopyFormat::HTML);
            const auto copyRtf = WI_IsFlagSet(formats, CopyFormat::RTF);

            // extract text from buffer
            // RetrieveSelectedTextFromBuffer will lock while it's reading
            payload = _terminal->RetrieveSelectedTextFromBuffer(singleLine, withControlSequences, copyHtml, copyRtf);
        }

        WriteToClipboard.raise(
            *this,
            winrt::make<WriteToClipboardEventArgs>(
                winrt::hstring{ payload.plainText },
                std::move(payload.html),
                std::move(payload.rtf)));
        return true;
    }

    void ControlCore::SelectAll()
    {
        const auto lock = _terminal->LockForWriting();
        _terminal->SelectAll();
        _updateSelectionUI();
    }

    void ControlCore::ClearSelection()
    {
        const auto lock = _terminal->LockForWriting();
        _terminal->ClearSelection();
        _updateSelectionUI();
    }

    bool ControlCore::ToggleBlockSelection()
    {
        const auto lock = _terminal->LockForWriting();
        if (_terminal->IsSelectionActive())
        {
            _terminal->SetBlockSelection(!_terminal->IsBlockSelection());
            _renderer->TriggerSelection();
            // do not update the selection markers!
            // if we were showing them, keep it that way.
            // otherwise, continue to not show them
            return true;
        }
        return false;
    }

    void ControlCore::ToggleMarkMode()
    {
        const auto lock = _terminal->LockForWriting();
        _terminal->ToggleMarkMode();
        _updateSelectionUI();
    }

    Control::SelectionInteractionMode ControlCore::SelectionMode() const
    {
        return static_cast<Control::SelectionInteractionMode>(_terminal->SelectionMode());
    }

    bool ControlCore::SwitchSelectionEndpoint()
    {
        const auto lock = _terminal->LockForWriting();
        if (_terminal->IsSelectionActive())
        {
            _terminal->SwitchSelectionEndpoint();
            _updateSelectionUI();
            return true;
        }
        return false;
    }

    bool ControlCore::ExpandSelectionToWord()
    {
        const auto lock = _terminal->LockForWriting();
        if (_terminal->IsSelectionActive())
        {
            _terminal->ExpandSelectionToWord();
            _updateSelectionUI();
            return true;
        }
        return false;
    }

    // Method Description:
    // - Pre-process text pasted (presumably from the clipboard)
    //   before sending it over the terminal's connection.
    void ControlCore::PasteText(const winrt::hstring& hstr)
    {
        using namespace ::Microsoft::Console::Utils;

        auto filtered = FilterStringForPaste(hstr, CarriageReturnNewline | ControlCodes);
        if (BracketedPasteEnabled())
        {
            filtered.insert(0, L"\x1b[200~");
            filtered.append(L"\x1b[201~");
        }

        // It's important to not hold the terminal lock while calling this function as sending the data may take a long time.
        SendInput(filtered);

        const auto lock = _terminal->LockForWriting();
        _terminal->ClearSelection();
        _updateSelectionUI();
        _terminal->TrySnapOnInput();
    }

    FontInfo ControlCore::GetFont() const
    {
        return _actualFont;
    }

    winrt::Windows::Foundation::Size ControlCore::FontSize() const noexcept
    {
        const auto fontSize = _actualFont.GetSize();
        return {
            static_cast<float>(fontSize.width),
            static_cast<float>(fontSize.height)
        };
    }

    uint16_t ControlCore::FontWeight() const noexcept
    {
        return static_cast<uint16_t>(_actualFont.GetWeight());
    }

    winrt::Windows::Foundation::Size ControlCore::FontSizeInDips() const
    {
        const auto fontSize = _actualFont.GetSize();
        const auto scale = 1.0f / _compositionScale;
        return {
            fontSize.width * scale,
            fontSize.height * scale,
        };
    }

    TerminalConnection::ConnectionState ControlCore::ConnectionState() const
    {
        return _connection ? _connection.State() : TerminalConnection::ConnectionState::Closed;
    }

    hstring ControlCore::Title()
    {
        const auto lock = _terminal->LockForReading();
        return hstring{ _terminal->GetConsoleTitle() };
    }

    hstring ControlCore::WorkingDirectory() const
    {
        const auto lock = _terminal->LockForReading();
        return hstring{ _terminal->GetWorkingDirectory() };
    }

    bool ControlCore::BracketedPasteEnabled() const noexcept
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->IsXtermBracketedPasteModeEnabled();
    }

    Windows::Foundation::IReference<winrt::Windows::UI::Color> ControlCore::TabColor() noexcept
    {
        const auto lock = _terminal->LockForReading();
        auto coreColor = _terminal->GetTabColor();
        return coreColor.has_value() ? Windows::Foundation::IReference<winrt::Windows::UI::Color>{ static_cast<winrt::Windows::UI::Color>(coreColor.value()) } :
                                       nullptr;
    }

    til::color ControlCore::ForegroundColor() const
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultForeground);
    }

    til::color ControlCore::BackgroundColor() const
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->GetRenderSettings().GetColorAlias(ColorAlias::DefaultBackground);
    }

    // Method Description:
    // - Gets the internal taskbar state value
    // Return Value:
    // - The taskbar state of this control
    const size_t ControlCore::TaskbarState() const noexcept
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->GetTaskbarState();
    }

    // Method Description:
    // - Gets the internal taskbar progress value
    // Return Value:
    // - The taskbar progress of this control
    const size_t ControlCore::TaskbarProgress() const noexcept
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->GetTaskbarProgress();
    }

    int ControlCore::ScrollOffset()
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->GetScrollOffset();
    }

    // Function Description:
    // - Gets the height of the terminal in lines of text. This is just the
    //   height of the viewport.
    // Return Value:
    // - The height of the terminal in lines of text
    int ControlCore::ViewHeight() const
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->GetViewport().Height();
    }

    // Function Description:
    // - Gets the height of the terminal in lines of text. This includes the
    //   history AND the viewport.
    // Return Value:
    // - The height of the terminal in lines of text
    int ControlCore::BufferHeight() const
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->GetBufferHeight();
    }

    void ControlCore::_terminalWarningBell()
    {
        // Since this can only ever be triggered by output from the connection,
        // then the Terminal already has the write lock when calling this
        // callback.
        WarningBell.raise(*this, nullptr);
    }

    // Method Description:
    // - Called for the Terminal's TitleChanged callback. This will re-raise
    //   a new winrt TypedEvent that can be listened to.
    // - The listeners to this event will re-query the control for the current
    //   value of Title().
    // Arguments:
    // - wstr: the new title of this terminal.
    // Return Value:
    // - <none>
    void ControlCore::_terminalTitleChanged(std::wstring_view wstr)
    {
        // Since this can only ever be triggered by output from the connection,
        // then the Terminal already has the write lock when calling this
        // callback.
        TitleChanged.raise(*this, winrt::make<TitleChangedEventArgs>(winrt::hstring{ wstr }));
    }

    // Method Description:
    // - Update the position and size of the scrollbar to match the given
    //      viewport top, viewport height, and buffer size.
    //   Additionally fires a ScrollPositionChanged event for anyone who's
    //      registered an event handler for us.
    // Arguments:
    // - viewTop: the top of the visible viewport, in rows. 0 indicates the top
    //      of the buffer.
    // - viewHeight: the height of the viewport in rows.
    // - bufferSize: the length of the buffer, in rows
    void ControlCore::_terminalScrollPositionChanged(const int viewTop,
                                                     const int viewHeight,
                                                     const int bufferSize)
    {
        if (!_initializedTerminal.load(std::memory_order_relaxed))
        {
            return;
        }

        // Start the throttled update of our scrollbar.
        auto update{ winrt::make<ScrollPositionChangedArgs>(viewTop,
                                                            viewHeight,
                                                            bufferSize) };

        if (_inUnitTests) [[unlikely]]
        {
            ScrollPositionChanged.raise(*this, update);
        }
        else
        {
            const auto shared = _shared.lock_shared();
            if (shared->updateScrollBar)
            {
                shared->updateScrollBar->Run(update);
            }
        }
    }

    void ControlCore::_terminalTaskbarProgressChanged()
    {
        TaskbarProgressChanged.raise(*this, nullptr);
    }

    void ControlCore::_terminalShowWindowChanged(bool showOrHide)
    {
        auto showWindow = winrt::make_self<implementation::ShowWindowArgs>(showOrHide);
        ShowWindowChanged.raise(*this, *showWindow);
    }

    // Method Description:
    // - Plays a single MIDI note, blocking for the duration.
    // Arguments:
    // - noteNumber - The MIDI note number to be played (0 - 127).
    // - velocity - The force with which the note should be played (0 - 127).
    // - duration - How long the note should be sustained (in microseconds).
    void ControlCore::_terminalPlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
    {
        // The UI thread might try to acquire the console lock from time to time.
        // --> Unlock it, so the UI doesn't hang while we're busy.
        const auto suspension = _terminal->SuspendLock();
        // This call will block for the duration, unless shutdown early.
        _midiAudio.PlayNote(reinterpret_cast<HWND>(_owningHwnd), noteNumber, velocity, std::chrono::duration_cast<std::chrono::milliseconds>(duration));
    }

    void ControlCore::_terminalWindowSizeChanged(int32_t width, int32_t height)
    {
        auto size = winrt::make<implementation::WindowSizeChangedEventArgs>(width, height);
        WindowSizeChanged.raise(*this, size);
    }

    void ControlCore::_terminalSearchMissingCommand(std::wstring_view missingCommand, const til::CoordType& bufferRow)
    {
        SearchMissingCommand.raise(*this, make<implementation::SearchMissingCommandEventArgs>(hstring{ missingCommand }, bufferRow));
    }

    void ControlCore::OpenCWD()
    {
        const auto workingDirectory = WorkingDirectory();
        ShellExecute(nullptr, nullptr, L"explorer", workingDirectory.c_str(), nullptr, SW_SHOW);
    }

    void ControlCore::ClearQuickFix()
    {
        _cachedQuickFixes = nullptr;
        RefreshQuickFixUI.raise(*this, nullptr);
    }

    bool ControlCore::HasSelection() const
    {
        const auto lock = _terminal->LockForReading();
        return _terminal->IsSelectionActive();
    }

    // Method Description:
    // - Checks if the currently active selection spans multiple lines
    // Return Value:
    // - true if selection is multi-line
    bool ControlCore::HasMultiLineSelection() const
    {
        const auto lock = _terminal->LockForReading();
        assert(_terminal->IsSelectionActive()); // should only be called when selection is active
        return _terminal->GetSelectionAnchor().y != _terminal->GetSelectionEnd().y;
    }

    bool ControlCore::CopyOnSelect() const
    {
        return _settings.CopyOnSelect();
    }

    winrt::hstring ControlCore::SelectedText(bool trimTrailingWhitespace) const
    {
        // RetrieveSelectedTextFromBuffer will lock while it's reading
        const auto lock = _terminal->LockForReading();
        const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(!trimTrailingWhitespace) };
        return winrt::hstring{ internalResult.plainText };
    }

    ::Microsoft::Console::Render::IRenderData* ControlCore::GetRenderData() const
    {
        return _terminal.get();
    }

    // Method Description:
    // - Search text in text buffer. This is triggered if the user click
    //   search button or press enter.
    // Arguments:
    // - text: the text to search
    // - goForward: boolean that represents if the current search direction is forward
    // - caseSensitive: boolean that represents if the current search is case-sensitive
    // - resetOnly: If true, only Reset() will be called, if anything. FindNext() will never be called.
    // Return Value:
    // - <none>
    SearchResults ControlCore::Search(const SearchRequest& request)
    {
        const auto lock = _terminal->LockForWriting();

        SearchFlag flags{};
        WI_SetFlagIf(flags, SearchFlag::CaseInsensitive, !request.CaseSensitive);
        WI_SetFlagIf(flags, SearchFlag::RegularExpression, request.RegularExpression);
        const auto searchInvalidated = _searcher.IsStale(*_terminal.get(), request.Text, flags);

        if (searchInvalidated || request.ExecuteSearch)
        {
            std::vector<til::point_span> oldResults;

            if (searchInvalidated)
            {
                oldResults = _searcher.ExtractResults();
                _searcher.Reset(*_terminal.get(), request.Text, flags, !request.GoForward);
                _terminal->SetSearchHighlights(_searcher.Results());
            }

            if (request.ExecuteSearch)
            {
                _searcher.FindNext(!request.GoForward);
            }

            _terminal->SetSearchHighlightFocused(gsl::narrow<size_t>(std::max<ptrdiff_t>(0, _searcher.CurrentMatch())));
            _renderer->TriggerSearchHighlight(oldResults);
        }

        if (request.ScrollIntoView)
        {
            _terminal->ScrollToSearchHighlight(request.ScrollOffset);
        }

        int32_t totalMatches = 0;
        int32_t currentMatch = 0;
        if (const auto idx = _searcher.CurrentMatch(); idx >= 0)
        {
            totalMatches = gsl::narrow<int32_t>(_searcher.Results().size());
            currentMatch = gsl::narrow<int32_t>(idx);
        }

        return {
            .TotalMatches = totalMatches,
            .CurrentMatch = currentMatch,
            .SearchInvalidated = searchInvalidated,
            .SearchRegexInvalid = !_searcher.IsOk(),
        };
    }

    const std::vector<til::point_span>& ControlCore::SearchResultRows() const noexcept
    {
        return _searcher.Results();
    }

    void ControlCore::ClearSearch()
    {
        const auto lock = _terminal->LockForWriting();

        // GH #19358: select the focused search result before clearing search
        if (const auto focusedSearchResult = _terminal->GetSearchHighlightFocused())
        {
            // search results are buffer-relative, whereas the selection functions expect viewport-relative coordinates
            const auto scrollOffset{ _terminal->GetScrollOffset() };
            const auto startPos = til::point{ focusedSearchResult->start.x, focusedSearchResult->start.y - scrollOffset };
            const auto endPos = til::point{ focusedSearchResult->end.x, focusedSearchResult->end.y - scrollOffset };

            _terminal->SetSelectionAnchor(startPos);
            _terminal->SetSelectionEnd(endPos);
            _renderer->TriggerSelection();
        }

        _terminal->SetSearchHighlights({});
        _terminal->SetSearchHighlightFocused(0);
        _renderer->TriggerSearchHighlight(_searcher.Results());
        _searcher = {};
    }

    void ControlCore::Close()
    {
        if (!_IsClosing())
        {
            _closing = true;

            // Ensure Close() doesn't hang, waiting for MidiAudio to finish playing an hour long song.
            _midiAudio.BeginSkip();
        }

        _closeConnection();
    }

    void ControlCore::PersistTo(HANDLE handle) const
    {
        const auto lock = _terminal->LockForReading();
        _terminal->SerializeMainBuffer(handle);
    }

    void ControlCore::RestoreFromPath(const wchar_t* path) const
    {
        wil::unique_handle file{ CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr) };

        // This block of code exists temporarily to fix buffer dumps that were
        // previously persisted as "buffer_" but really should be named "elevated_".
        // If loading the properly named file fails, retry with the old name.
        if (!file)
        {
            static constexpr std::wstring_view needle{ L"\\elevated_" };

            // Check if the path contains "\elevated_", indicating that we're in an elevated session.
            const std::wstring_view pathView{ path };
            const auto idx = pathView.find(needle);

            if (idx != std::wstring_view::npos)
            {
                // If so, try to open the file with "\buffer_" instead, which is what we previously used.
                std::wstring altPath{ pathView };
                altPath.replace(idx, needle.size(), L"\\buffer_");

                file.reset(CreateFileW(altPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr));

                // If the alternate file is found, move it to the correct location.
                if (file)
                {
                    LOG_IF_WIN32_BOOL_FALSE(MoveFileW(altPath.c_str(), path));
                }
            }
        }

        if (!file)
        {
            return;
        }

        FILETIME lastWriteTime;
        FILETIME localFileTime;
        SYSTEMTIME lastWriteSystemTime;

        // Get the last write time in UTC
        if (!GetFileTime(file.get(), nullptr, nullptr, &lastWriteTime))
        {
            return;
        }

        // Convert UTC FILETIME to local FILETIME
        if (!FileTimeToLocalFileTime(&lastWriteTime, &localFileTime))
        {
            return;
        }

        // Convert local FILETIME to SYSTEMTIME
        if (!FileTimeToSystemTime(&localFileTime, &lastWriteSystemTime))
        {
            return;
        }

        wchar_t dateBuf[256];
        const auto dateLen = GetDateFormatEx(nullptr, 0, &lastWriteSystemTime, nullptr, &dateBuf[0], ARRAYSIZE(dateBuf), nullptr);
        wchar_t timeBuf[256];
        const auto timeLen = GetTimeFormatEx(nullptr, 0, &lastWriteSystemTime, nullptr, &timeBuf[0], ARRAYSIZE(timeBuf));

        std::wstring message;
        if (dateLen > 0 && timeLen > 0)
        {
            const auto msg = RS_(L"SessionRestoreMessage");
            const std::wstring_view date{ &dateBuf[0], gsl::narrow_cast<size_t>(dateLen) };
            const std::wstring_view time{ &timeBuf[0], gsl::narrow_cast<size_t>(timeLen) };
            // This escape sequence string
            // * sets the color to white on a bright black background ("\x1b[100;37m")
            // * prints "  [Restored <date> <time>]  <spaces until end of line>  "
            // * resets the color ("\x1b[m")
            // * newlines
            message = fmt::format(FMT_COMPILE(L"\x1b[100;37m  [{} {} {}]\x1b[K\x1b[m\r\n"), msg, date, time);
        }

        wchar_t buffer[32 * 1024];
        DWORD read = 0;

        // Ensure the text file starts with a UTF-16 BOM.
        if (!ReadFile(file.get(), &buffer[0], 2, &read, nullptr) || read < 2 || buffer[0] != L'\uFEFF')
        {
            return;
        }

        for (;;)
        {
            if (!ReadFile(file.get(), &buffer[0], sizeof(buffer), &read, nullptr))
            {
                break;
            }

            const auto lock = _terminal->LockForWriting();
            _terminal->Write({ &buffer[0], read / 2 });

            if (read < sizeof(buffer))
            {
                // Normally the cursor should already be at the start of the line, but let's be absolutely sure it is.
                if (_terminal->GetTextBuffer().GetCursor().GetPosition().x != 0)
                {
                    _terminal->Write(L"\r\n");
                }
                _terminal->Write(message);
                break;
            }
        }
    }

    void ControlCore::_rendererWarning(const HRESULT hr, wil::zwstring_view parameter)
    {
        RendererWarning.raise(*this, winrt::make<RendererWarningArgs>(hr, winrt::hstring{ parameter }));
    }

    safe_void_coroutine ControlCore::_renderEngineSwapChainChanged(const HANDLE sourceHandle)
    {
        // `sourceHandle` is a weak ref to a HANDLE that's ultimately owned by the
        // render engine's own unique_handle. We'll add another ref to it here.
        // This will make sure that we always have a valid HANDLE to give to
        // callers of our own SwapChainHandle method, even if the renderer is
        // currently in the process of discarding this value and creating a new
        // one. Callers should have already set up the SwapChainChanged
        // callback, so this all works out.

        winrt::handle duplicatedHandle;
        const auto processHandle = GetCurrentProcess();
        THROW_IF_WIN32_BOOL_FALSE(DuplicateHandle(processHandle, sourceHandle, processHandle, duplicatedHandle.put(), 0, FALSE, DUPLICATE_SAME_ACCESS));

        const auto weakThis{ get_weak() };

        // Concurrent read of _dispatcher is safe, because Detach() calls TriggerTeardown()
        // which blocks until this call returns. _dispatcher will only be changed afterwards.
        co_await wil::resume_foreground(_dispatcher);

        if (auto core{ weakThis.get() })
        {
            // `this` is safe to use now

            _lastSwapChainHandle = std::move(duplicatedHandle);
            // Now bubble the event up to the control.
            SwapChainChanged.raise(*this, winrt::box_value<uint64_t>(reinterpret_cast<uint64_t>(_lastSwapChainHandle.get())));
        }
    }

    void ControlCore::_rendererBackgroundColorChanged()
    {
        BackgroundColorChanged.raise(*this, nullptr);
    }

    void ControlCore::_rendererTabColorChanged()
    {
        TabColorChanged.raise(*this, nullptr);
    }

    void ControlCore::ResumeRendering()
    {
        // The lock must be held, because it calls into IRenderData which is shared state.
        const auto lock = _terminal->LockForWriting();
        _renderFailures = 0;
        _renderer->EnablePainting();
    }

    bool ControlCore::IsVtMouseModeEnabled() const
    {
        const auto lock = _terminal->LockForWriting();
        return _terminal->IsTrackingMouseInput();
    }
    bool ControlCore::ShouldSendAlternateScroll(const unsigned int uiButton,
                                                const int32_t delta) const
    {
        const auto lock = _terminal->LockForWriting();
        return _terminal->ShouldSendAlternateScroll(uiButton, delta);
    }

    Core::Point ControlCore::CursorPosition() const
    {
        // If we haven't been initialized yet, then fake it.
        if (!_initializedTerminal.load(std::memory_order_relaxed))
        {
            return { 0, 0 };
        }

        const auto lock = _terminal->LockForReading();
        return _terminal->GetViewportRelativeCursorPosition().to_core_point();
    }

    bool ControlCore::ForceCursorVisible() const noexcept
    {
        return _forceCursorVisible;
    }

    void ControlCore::ForceCursorVisible(bool force)
    {
        const auto lock = _terminal->LockForWriting();
        _renderer->AllowCursorVisibility(Render::InhibitionSource::Host, _terminal->IsFocused() || force);
        _forceCursorVisible = force;
    }

    // This one's really pushing the boundary of what counts as "encapsulation".
    // It really belongs in the "Interactivity" layer, which doesn't yet exist.
    // There's so many accesses to the selection in the Core though, that I just
    // put this here. The Control shouldn't be futzing that much with the
    // selection itself.
    void ControlCore::LeftClickOnTerminal(const til::point terminalPosition,
                                          const int numberOfClicks,
                                          const bool altEnabled,
                                          const bool shiftEnabled,
                                          const bool isOnOriginalPosition,
                                          bool& selectionNeedsToBeCopied)
    {
        const auto lock = _terminal->LockForWriting();
        // handle ALT key
        _terminal->SetBlockSelection(altEnabled);

        auto mode = ::Terminal::SelectionExpansion::Char;
        if (numberOfClicks == 1)
        {
            mode = ::Terminal::SelectionExpansion::Char;
        }
        else if (numberOfClicks == 2)
        {
            mode = ::Terminal::SelectionExpansion::Word;
        }
        else if (numberOfClicks == 3)
        {
            mode = ::Terminal::SelectionExpansion::Line;
        }

        // Update the selection appropriately

        // We reset the active selection if one of the conditions apply:
        // - shift is not held
        // - GH#9384: the position is the same as of the first click starting
        //   the selection (we need to reset selection on double-click or
        //   triple-click, so it captures the word or the line, rather than
        //   extending the selection)
        if (_terminal->IsSelectionActive() && (!shiftEnabled || isOnOriginalPosition))
        {
            // Reset the selection
            _terminal->ClearSelection();
            selectionNeedsToBeCopied = false; // there's no selection, so there's nothing to update
        }

        if (shiftEnabled && _terminal->IsSelectionActive())
        {
            // If shift is pressed and there is a selection we extend it using
            // the selection mode (expand the "end" selection point)
            _terminal->SetSelectionEnd(terminalPosition, mode);
            selectionNeedsToBeCopied = true;
        }
        else if (mode != ::Terminal::SelectionExpansion::Char || shiftEnabled)
        {
            // If we are handling a double / triple-click or shift+single click
            // we establish selection using the selected mode
            // (expand both "start" and "end" selection points)
            _terminal->MultiClickSelection(terminalPosition, mode);
            selectionNeedsToBeCopied = true;
        }
        else if (_settings.RepositionCursorWithMouse() && !selectionNeedsToBeCopied) // Don't reposition cursor if this is part of a selection operation
        {
            _repositionCursorWithMouse(terminalPosition);
        }
        _updateSelectionUI();
    }

    void ControlCore::_repositionCursorWithMouse(const til::point terminalPosition)
    {
        // If we're handling a single left click, without shift pressed, and
        // outside mouse mode, AND the user has RepositionCursorWithMouse turned
        // on, let's try to move the cursor.
        //
        // We'll only move the cursor if the user has clicked after the last
        // mark, if there is one. That means the user also needs to set up
        // shell integration to enable this feature.
        //
        // As noted in GH #8573, there's plenty of edge cases with this
        // approach, but it's good enough to bring value to 90% of use cases.

        // Does the current buffer line have a mark on it?
        const auto& marks{ _terminal->GetMarkExtents() };
        if (!marks.empty())
        {
            const auto& last{ marks.back() };
            const auto [start, end] = last.GetExtent();
            const auto& buffer = _terminal->GetTextBuffer();
            const auto cursorPos = buffer.GetCursor().GetPosition();
            const auto bufferSize = buffer.GetSize();
            auto lastNonSpace = buffer.GetLastNonSpaceCharacter();
            bufferSize.IncrementInBounds(lastNonSpace, true);

            // If the user clicked off to the right side of the prompt, we
            // want to send keystrokes to the last character in the prompt +1.
            //
            // We don't want to send too many here. In CMD, if the user's
            // last command is longer than what they've currently typed, and
            // they press right arrow at the end of the prompt, COOKED_READ
            // will fill in characters from the previous command.
            //
            // By only sending keypresses to the end of the command + 1, we
            // should leave the cursor at the very end of the prompt,
            // without adding any characters from a previous command.

            // terminalPosition is viewport-relative.
            const auto bufferPos = _terminal->GetViewport().Origin() + terminalPosition;
            if (bufferPos.y > lastNonSpace.y)
            {
                // Clicked under the prompt. Bail.
                return;
            }

            // Limit the click to 1 past the last character on the last line.
            const auto clampedClick = std::min(bufferPos, lastNonSpace);

            if (clampedClick >= last.end)
            {
                // Get the distance between the cursor and the click, in cells.

                // First, make sure to iterate from the first point to the
                // second. The user may have clicked _earlier_ in the
                // buffer!
                auto goRight = clampedClick > cursorPos;
                const auto startPoint = goRight ? cursorPos : clampedClick;
                const auto endPoint = goRight ? clampedClick : cursorPos;

                const auto delta = _terminal->GetTextBuffer().GetCellDistance(startPoint, endPoint);
                const WORD key = goRight ? VK_RIGHT : VK_LEFT;

                std::wstring buffer;
                const auto append = [&](TerminalInput::OutputType&& out) {
                    if (out)
                    {
                        buffer.append(std::move(*out));
                    }
                };

                // Send an up and a down once per cell. This won't
                // accurately handle wide characters, or continuation
                // prompts, or cases where a single escape character in the
                // command (e.g. ^[) takes up two cells.
                for (size_t i = 0u; i < delta; i++)
                {
                    append(_terminal->SendKeyEvent(key, 0, {}, true));
                    append(_terminal->SendKeyEvent(key, 0, {}, false));
                }

                {
                    // Sending input requires that we're unlocked, because
                    // writing the input pipe may block indefinitely.
                    const auto suspension = _terminal->SuspendLock();
                    SendInput(buffer);
                }
            }
        }
    }

    // Method Description:
    // - Updates the renderer's representation of the selection as well as the selection marker overlay in TermControl
    void ControlCore::_updateSelectionUI()
    {
        _renderer->TriggerSelection();
        // only show the markers if we're doing a keyboard selection or in mark mode
        const bool showMarkers{ _terminal->SelectionMode() >= ::Microsoft::Terminal::Core::Terminal::SelectionInteractionMode::Keyboard };
        UpdateSelectionMarkers.raise(*this, winrt::make<implementation::UpdateSelectionMarkersEventArgs>(!showMarkers));
    }

    void ControlCore::AttachUiaEngine(::Microsoft::Console::Render::UiaEngine* const pEngine)
    {
        // _renderer will always exist since it's introduced in the ctor
        const auto lock = _terminal->LockForWriting();
        _renderer->AddRenderEngine(pEngine);
    }
    void ControlCore::DetachUiaEngine(::Microsoft::Console::Render::UiaEngine* const pEngine)
    {
        const auto lock = _terminal->LockForWriting();
        _renderer->RemoveRenderEngine(pEngine);
    }

    bool ControlCore::IsInReadOnlyMode() const
    {
        return _isReadOnly;
    }

    void ControlCore::ToggleReadOnlyMode()
    {
        _isReadOnly = !_isReadOnly;
    }

    void ControlCore::SetReadOnlyMode(const bool readOnlyState)
    {
        _isReadOnly = readOnlyState;
    }

    void ControlCore::_raiseReadOnlyWarning()
    {
        auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Info, RS_(L"TermControlReadOnly"));
        RaiseNotice.raise(*this, std::move(noticeArgs));
    }
    void ControlCore::_connectionOutputHandler(const winrt::array_view<const char16_t> str)
    {
        try
        {
            {
                const auto lock = _terminal->LockForWriting();
                _terminal->Write(winrt_array_to_wstring_view(str));
            }

            if (!_pendingResponses.empty())
            {
                _sendInputToConnection(_pendingResponses);
                _pendingResponses.clear();
            }

            // Start the throttled update of where our hyperlinks are.
            const auto shared = _shared.lock_shared();
            if (shared->outputIdle)
            {
                (*shared->outputIdle)();
            }
        }
        catch (...)
        {
            // We're expecting to receive an exception here if the terminal
            // is closed while we're blocked playing a MIDI note.
        }
    }

    ::Microsoft::Console::Render::Renderer* ControlCore::GetRenderer() const noexcept
    {
        return _renderer.get();
    }

    uint64_t ControlCore::SwapChainHandle() const
    {
        // This is only ever called by TermControl::AttachContent, which occurs
        // when we're taking an existing core and moving it to a new control.
        // Otherwise, we only ever use the value from the SwapChainChanged
        // event.
        return reinterpret_cast<uint64_t>(_lastSwapChainHandle.get());
    }

    // Method Description:
    // - Clear the contents of the buffer. The region cleared is given by
    //   clearType:
    //   * Screen: Clear only the contents of the visible viewport, leaving the
    //     cursor row at the top of the viewport.
    //   * Scrollback: Clear the contents of the scrollback.
    //   * All: Do both - clear the visible viewport and the scrollback, leaving
    //     only the cursor row at the top of the viewport.
    // Arguments:
    // - clearType: The type of clear to perform.
    // Return Value:
    // - <none>
    void ControlCore::ClearBuffer(Control::ClearBufferType clearType)
    {
        {
            const auto lock = _terminal->LockForWriting();
            // In absolute buffer coordinates, including the scrollback (= Y is offset by the scrollback height).
            const auto viewport = _terminal->GetViewport();
            // The absolute cursor coordinate.
            const auto cursor = _terminal->GetViewportRelativeCursorPosition();

            // GH#18732: Users want the row the cursor is on to be preserved across clears.
            std::wstring sequence;

            if (clearType == ClearBufferType::Scrollback || clearType == ClearBufferType::All)
            {
                sequence.append(L"\x1b[3J");
            }

            if (clearType == ClearBufferType::Screen || clearType == ClearBufferType::All)
            {
                // Erase any viewport contents below (but not including) the cursor row.
                if (viewport.Height() - cursor.y > 1)
                {
                    fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[{};1H\x1b[J"), cursor.y + 2);
                }

                // Erase any viewport contents above (but not including) the cursor row.
                if (cursor.y > 0)
                {
                    // An SU sequence would be simpler than this DL sequence,
                    // but SU isn't well standardized between terminals.
                    // Generally speaking, it's best avoiding it.
                    fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[H\x1b[{}M"), cursor.y);
                }

                fmt::format_to(std::back_inserter(sequence), FMT_COMPILE(L"\x1b[1;{}H"), cursor.x + 1);
            }

            _terminal->Write(sequence);
        }

        if (clearType == Control::ClearBufferType::Screen || clearType == Control::ClearBufferType::All)
        {
            if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
            {
                // Since the clearing of ConPTY occurs asynchronously, this call can result weird issues,
                // where a console application still sees contents that we've already deleted, etc.
                conpty.ClearBuffer(true);
            }
        }
    }

    hstring ControlCore::ReadEntireBuffer() const
    {
        const auto lock = _terminal->LockForWriting();

        const auto& textBuffer = _terminal->GetTextBuffer();

        std::wstring str;
        const auto lastRow = textBuffer.GetLastNonSpaceCharacter().y;
        for (auto rowIndex = 0; rowIndex <= lastRow; rowIndex++)
        {
            const auto& row = textBuffer.GetRowByOffset(rowIndex);
            const auto rowText = row.GetText();
            const auto strEnd = rowText.find_last_not_of(UNICODE_SPACE);
            if (strEnd != decltype(rowText)::npos)
            {
                str.append(rowText.substr(0, strEnd + 1));
            }

            if (!row.WasWrapForced())
            {
                str.append(L"\r\n");
            }
        }

        return hstring{ str };
    }

    // Get all of our recent commands. This will only really work if the user has enabled shell integration.
    Control::CommandHistoryContext ControlCore::CommandHistory() const
    {
        const auto lock = _terminal->LockForWriting();
        const auto& textBuffer = _terminal->GetTextBuffer();

        std::vector<winrt::hstring> commands;
        const auto bufferCommands{ textBuffer.Commands() };

        auto trimToHstring = [](const auto& s) -> winrt::hstring {
            const auto strEnd = s.find_last_not_of(UNICODE_SPACE);
            if (strEnd != std::string::npos)
            {
                const auto trimmed = s.substr(0, strEnd + 1);
                return winrt::hstring{ trimmed };
            }
            return {};
        };

        const auto currentCommand = _terminal->CurrentCommand();
        const auto trimmedCurrentCommand = trimToHstring(currentCommand);

        for (const auto& commandInBuffer : bufferCommands)
        {
            if (const auto hstr{ trimToHstring(commandInBuffer) };
                (!hstr.empty() && hstr != trimmedCurrentCommand))
            {
                commands.push_back(hstr);
            }
        }

        // If the very last thing in the list of recent commands, is exactly the
        // same as the current command, then let's not include it in the
        // history. It's literally the thing the user has typed, RIGHT now.
        // (also account for the fact that the cursor may be in the middle of a commandline)
        if (!commands.empty() &&
            !trimmedCurrentCommand.empty() &&
            std::wstring_view{ commands.back() }.substr(0, trimmedCurrentCommand.size()) == trimmedCurrentCommand)
        {
            commands.pop_back();
        }

        auto context = winrt::make_self<CommandHistoryContext>(std::move(commands));
        context->CurrentCommandline(trimmedCurrentCommand);
        context->QuickFixes(_cachedQuickFixes);
        return *context;
    }

    winrt::hstring ControlCore::CurrentWorkingDirectory() const
    {
        return winrt::hstring{ _terminal->GetWorkingDirectory() };
    }

    bool ControlCore::QuickFixesAvailable() const noexcept
    {
        return _cachedQuickFixes && _cachedQuickFixes.Size() > 0;
    }

    void ControlCore::UpdateQuickFixes(const Windows::Foundation::Collections::IVector<hstring>& quickFixes)
    {
        _cachedQuickFixes = quickFixes;
    }

    bool ControlCore::HasUnfocusedAppearance() const
    {
        return _hasUnfocusedAppearance;
    }

    void ControlCore::AdjustOpacity(const float opacityAdjust, const bool relative)
    {
        if (relative)
        {
            AdjustOpacity(opacityAdjust);
        }
        else
        {
            _setOpacity(opacityAdjust);
        }
    }

    // Method Description:
    // - Notifies the attached PTY that the window has changed visibility state
    // - NOTE: Most VT commands are generated in `TerminalDispatch` and sent to this
    //         class as the target for transmission. But since this message isn't
    //         coming in via VT parsing (and rather from a window state transition)
    //         we generate and send it here.
    // Arguments:
    // - visible: True for visible; false for not visible.
    // Return Value:
    // - <none>
    void ControlCore::WindowVisibilityChanged(const bool showOrHide)
    {
        if (_initializedTerminal.load(std::memory_order_relaxed))
        {
            // show is true, hide is false
            if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
            {
                conpty.ShowHide(showOrHide);
            }
        }
    }

    // Method Description:
    // - When the control gains focus, it needs to tell ConPTY about this.
    //   Usually, these sequences are reserved for applications that
    //   specifically request SET_FOCUS_EVENT_MOUSE, ?1004h. ConPTY uses this
    //   sequence REGARDLESS to communicate if the control was focused or not.
    // - Even if a client application disables this mode, the Terminal & conpty
    //   should always request this from the hosting terminal (and just ignore
    //   internally to ConPTY).
    // - Full support for this sequence is tracked in GH#11682.
    // - This is related to work done for GH#2988.
    void ControlCore::GotFocus()
    {
        const auto shared = _shared.lock_shared();
        if (shared->focusChanged)
        {
            (*shared->focusChanged)(true);
        }
    }

    // See GotFocus.
    void ControlCore::LostFocus()
    {
        const auto shared = _shared.lock_shared();
        if (shared->focusChanged)
        {
            (*shared->focusChanged)(false);
        }
    }

    void ControlCore::_focusChanged(bool focused)
    {
        TerminalInput::OutputType out;
        {
            const auto lock = _terminal->LockForWriting();
            _renderer->AllowCursorVisibility(Render::InhibitionSource::Host, focused || _forceCursorVisible);
            out = _terminal->FocusChanged(focused);
        }
        if (out && !out->empty())
        {
            _sendInputToConnection(*out);
        }
    }

    bool ControlCore::_isBackgroundTransparent()
    {
        // If we're:
        // * Not fully opaque
        // * rendering on top of an image
        //
        // then the renderer should not render "default background" text with a
        // fully opaque background. Doing that would cover up our nice
        // transparency, or our acrylic, or our image.
        return Opacity() < 1.0f || !_settings.BackgroundImage().empty() || _settings.UseBackgroundImageForWindow();
    }

    uint64_t ControlCore::OwningHwnd()
    {
        return _owningHwnd;
    }

    void ControlCore::OwningHwnd(uint64_t owner)
    {
        if (owner != _owningHwnd && _connection)
        {
            if (auto conpty{ _connection.try_as<TerminalConnection::ConptyConnection>() })
            {
                conpty.ReparentWindow(owner);
            }
        }
        _owningHwnd = owner;
    }

    // This one is fairly hot! it gets called every time we redraw the scrollbar
    // marks, which is frequently. Fortunately, we don't need to bother with
    // collecting up the actual extents of the marks in here - we just need the
    // rows they start on.
    Windows::Foundation::Collections::IVector<Control::ScrollMark> ControlCore::ScrollMarks() const
    {
        const auto lock = _terminal->LockForReading();
        const auto& markRows = _terminal->GetMarkRows();
        std::vector<Control::ScrollMark> v;

        v.reserve(markRows.size());

        for (const auto& mark : markRows)
        {
            v.emplace_back(
                mark.row,
                OptionalFromColor(_terminal->GetColorForMark(mark.data)));
        }

        return winrt::single_threaded_vector(std::move(v));
    }

    void ControlCore::AddMark(const Control::ScrollMark& mark)
    {
        const auto lock = _terminal->LockForReading();
        ::ScrollbarData m{};

        if (mark.Color.HasValue)
        {
            m.color = til::color{ mark.Color.Color };
        }
        const auto row = (_terminal->IsSelectionActive()) ?
                             _terminal->GetSelectionAnchor().y :
                             _terminal->GetTextBuffer().GetCursor().GetPosition().y;

        _terminal->AddMarkFromUI(m, row);
    }

    void ControlCore::ClearMark()
    {
        const auto lock = _terminal->LockForWriting();
        _terminal->ClearMark();
    }

    void ControlCore::ClearAllMarks()
    {
        const auto lock = _terminal->LockForWriting();
        _terminal->ClearAllMarks();
    }

    void ControlCore::ScrollToMark(const Control::ScrollToMarkDirection& direction)
    {
        const auto lock = _terminal->LockForWriting();
        const auto currentOffset = ScrollOffset();
        const auto& marks{ _terminal->GetMarkExtents() };

        std::optional<::MarkExtents> tgt;

        switch (direction)
        {
        case ScrollToMarkDirection::Last:
        {
            int highest = currentOffset;
            for (const auto& mark : marks)
            {
                const auto newY = mark.start.y;
                if (newY > highest)
                {
                    tgt = mark;
                    highest = newY;
                }
            }
            break;
        }
        case ScrollToMarkDirection::First:
        {
            int lowest = currentOffset;
            for (const auto& mark : marks)
            {
                const auto newY = mark.start.y;
                if (newY < lowest)
                {
                    tgt = mark;
                    lowest = newY;
                }
            }
            break;
        }
        case ScrollToMarkDirection::Next:
        {
            int minDistance = INT_MAX;
            for (const auto& mark : marks)
            {
                const auto delta = mark.start.y - currentOffset;
                if (delta > 0 && delta < minDistance)
                {
                    tgt = mark;
                    minDistance = delta;
                }
            }
            break;
        }
        case ScrollToMarkDirection::Previous:
        default:
        {
            int minDistance = INT_MAX;
            for (const auto& mark : marks)
            {
                const auto delta = currentOffset - mark.start.y;
                if (delta > 0 && delta < minDistance)
                {
                    tgt = mark;
                    minDistance = delta;
                }
            }
            break;
        }
        }

        const auto viewHeight = ViewHeight();
        const auto bufferSize = BufferHeight();

        // UserScrollViewport, to update the Terminal about where the viewport should be
        // then raise a _terminalScrollPositionChanged to inform the control to update the scrollbar.
        if (tgt.has_value())
        {
            UserScrollViewport(tgt->start.y);
            _terminalScrollPositionChanged(tgt->start.y, viewHeight, bufferSize);
        }
        else
        {
            if (direction == ScrollToMarkDirection::Last || direction == ScrollToMarkDirection::Next)
            {
                UserScrollViewport(BufferHeight());
                _terminalScrollPositionChanged(BufferHeight(), viewHeight, bufferSize);
            }
            else if (direction == ScrollToMarkDirection::First || direction == ScrollToMarkDirection::Previous)
            {
                UserScrollViewport(0);
                _terminalScrollPositionChanged(0, viewHeight, bufferSize);
            }
        }
    }

    void ControlCore::_terminalCompletionsChanged(std::wstring_view menuJson, unsigned int replaceLength)
    {
        CompletionsChanged.raise(*this, winrt::make<CompletionsChangedEventArgs>(winrt::hstring{ menuJson }, replaceLength));
    }

    // Select the region of text between [s.start, s.end), in buffer space
    void ControlCore::_selectSpan(til::point_span s)
    {
        // s.end is an _exclusive_ point. We need an inclusive one.
        const auto bufferSize{ _terminal->GetTextBuffer().GetSize() };
        til::point inclusiveEnd = s.end;
        bufferSize.DecrementInBounds(inclusiveEnd);

        _terminal->SelectNewRegion(s.start, inclusiveEnd);
    }

    void ControlCore::SelectCommand(const bool goUp)
    {
        const auto lock = _terminal->LockForWriting();

        const til::point start = _terminal->IsSelectionActive() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) :
                                                                  _terminal->GetTextBuffer().GetCursor().GetPosition();

        std::optional<::MarkExtents> nearest{ std::nullopt };
        const auto& marks{ _terminal->GetMarkExtents() };

        // Early return so we don't have to check for the validity of `nearest` below after the loop exits.
        if (marks.empty())
        {
            return;
        }

        static constexpr til::point worst{ til::CoordTypeMax, til::CoordTypeMax };
        til::point bestDistance{ worst };

        for (const auto& m : marks)
        {
            if (!m.HasCommand())
            {
                continue;
            }

            const auto distance = goUp ? start - m.end : m.end - start;
            if ((distance > til::point{ 0, 0 }) && distance < bestDistance)
            {
                nearest = m;
                bestDistance = distance;
            }
        }

        if (nearest.has_value())
        {
            const auto start = nearest->end;
            auto end = *nearest->commandEnd;
            _selectSpan(til::point_span{ start, end });
        }
    }

    void ControlCore::SelectOutput(const bool goUp)
    {
        const auto lock = _terminal->LockForWriting();

        const til::point start = _terminal->IsSelectionActive() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) :
                                                                  _terminal->GetTextBuffer().GetCursor().GetPosition();

        std::optional<::MarkExtents> nearest{ std::nullopt };
        const auto& marks{ _terminal->GetMarkExtents() };

        static constexpr til::point worst{ til::CoordTypeMax, til::CoordTypeMax };
        til::point bestDistance{ worst };

        for (const auto& m : marks)
        {
            if (!m.HasOutput())
            {
                continue;
            }

            const auto distance = goUp ? start - *m.commandEnd : *m.commandEnd - start;
            if ((distance > til::point{ 0, 0 }) && distance < bestDistance)
            {
                nearest = m;
                bestDistance = distance;
            }
        }

        if (nearest.has_value())
        {
            const auto start = *nearest->commandEnd;
            auto end = *nearest->outputEnd;
            _selectSpan(til::point_span{ start, end });
        }
    }

    void ControlCore::ColorSelection(const Control::SelectionColor& fg, const Control::SelectionColor& bg, Core::MatchMode matchMode)
    {
        const auto lock = _terminal->LockForWriting();

        if (_terminal->IsSelectionActive())
        {
            const auto pForeground = winrt::get_self<implementation::SelectionColor>(fg);
            const auto pBackground = winrt::get_self<implementation::SelectionColor>(bg);

            TextColor foregroundAsTextColor;
            TextColor backgroundAsTextColor;

            if (pForeground)
            {
                foregroundAsTextColor = pForeground->AsTextColor();
            }

            if (pBackground)
            {
                backgroundAsTextColor = pBackground->AsTextColor();
            }

            TextAttribute attr;
            attr.SetForeground(foregroundAsTextColor);
            attr.SetBackground(backgroundAsTextColor);

            _terminal->ColorSelection(attr, matchMode);
            _terminal->ClearSelection();
            if (matchMode != Core::MatchMode::None)
            {
                // ClearSelection will invalidate the selection area... but if we are
                // coloring other matches, then we need to make sure those get redrawn,
                // too.
                _renderer->TriggerRedrawAll();
                _updateSelectionUI();
            }
        }
    }

    void ControlCore::AnchorContextMenu(const til::point viewportRelativeCharacterPosition)
    {
        // viewportRelativeCharacterPosition is relative to the current
        // viewport, so adjust for that:
        const auto lock = _terminal->LockForReading();
        _contextMenuBufferPosition = _terminal->GetViewport().Origin() + viewportRelativeCharacterPosition;
    }

    void ControlCore::_contextMenuSelectMark(
        const til::point& pos,
        bool (*filter)(const ::MarkExtents&),
        til::point_span (*getSpan)(const ::MarkExtents&))
    {
        const auto lock = _terminal->LockForWriting();

        // Do nothing if the caller didn't give us a way to get the span to select for this mark.
        if (!getSpan)
        {
            return;
        }
        const auto& marks{ _terminal->GetMarkExtents() };

        for (auto&& m : marks)
        {
            // If the caller gave us a way to filter marks, check that now.
            // This can be used to filter to only marks that have a command, or output.
            if (filter && filter(m))
            {
                continue;
            }
            // If they clicked _anywhere_ in the mark...
            const auto [markStart, markEnd] = m.GetExtent();
            if (markStart <= pos &&
                markEnd >= pos)
            {
                // ... select the part of the mark the caller told us about.
                _selectSpan(getSpan(m));
                // And quick bail
                return;
            }
        }
    }

    void ControlCore::ContextMenuSelectCommand()
    {
        _contextMenuSelectMark(
            _contextMenuBufferPosition,
            [](const ::MarkExtents& m) -> bool { return !m.HasCommand(); },
            [](const ::MarkExtents& m) { return til::point_span{ m.end, *m.commandEnd }; });
    }
    void ControlCore::ContextMenuSelectOutput()
    {
        _contextMenuSelectMark(
            _contextMenuBufferPosition,
            [](const ::MarkExtents& m) -> bool { return !m.HasOutput(); },
            [](const ::MarkExtents& m) { return til::point_span{ *m.commandEnd, *m.outputEnd }; });
    }

    bool ControlCore::_clickedOnMark(
        const til::point& pos,
        bool (*filter)(const ::MarkExtents&))
    {
        const auto lock = _terminal->LockForWriting();

        // Don't show this if the click was on the selection
        if (_terminal->IsSelectionActive() &&
            _terminal->GetSelectionAnchor() <= pos &&
            _terminal->GetSelectionEnd() >= pos)
        {
            return false;
        }

        // DO show this if the click was on a mark with a command
        const auto& marks{ _terminal->GetMarkExtents() };
        for (auto&& m : marks)
        {
            if (filter && filter(m))
            {
                continue;
            }

            const auto [start, end] = m.GetExtent();
            if (start <= pos &&
                end >= pos)
            {
                return true;
            }
        }

        // Didn't click on a mark with a command - don't show.
        return false;
    }

    // Method Description:
    // * Don't show this if the click was on the _current_ selection
    // * Don't show this if the click wasn't on a mark with at least a command
    // * Otherwise yea, show it.
    bool ControlCore::ShouldShowSelectCommand()
    {
        // Relies on the anchor set in AnchorContextMenu
        return _clickedOnMark(_contextMenuBufferPosition,
                              [](const ::MarkExtents& m) -> bool { return !m.HasCommand(); });
    }

    // Method Description:
    // * Same as ShouldShowSelectCommand, but with the mark needing output
    bool ControlCore::ShouldShowSelectOutput()
    {
        // Relies on the anchor set in AnchorContextMenu
        return _clickedOnMark(_contextMenuBufferPosition,
                              [](const ::MarkExtents& m) -> bool { return !m.HasOutput(); });
    }

    void ControlCore::PreviewInput(std::wstring_view input)
    {
        _terminal->PreviewText(input);
    }
}
