Leonard Hecker 5ddf429688 wip
2025-09-02 19:47:21 +02:00

2936 lines
113 KiB
C++

// 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 <LibraryResources.h>
#include <unicode.hpp>
#include <utils.hpp>
#include <WinUser.h>
#include "EventArgs.h"
#include "../../renderer/atlas/AtlasEngine.h"
#include "../../renderer/base/renderer.hpp"
#include "../../renderer/uia/UiaRenderer.hpp"
#include "../../types/inc/CodepointWidthDetector.hpp"
#include "ControlCore.g.cpp"
#include "SelectionColor.g.cpp"
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);
return true;
}();
_settings = winrt::make_self<implementation::ControlSettings>(settings, unfocusedAppearance);
_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.raise(nullptr, nullptr); });
}
UpdateSettings(settings, unfocusedAppearance);
}
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(const Microsoft::Terminal::Control::IKeyBindings& keyBindings)
{
_settings->KeyBindings(keyBindings);
_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
_settings->InitialCols(width);
_settings->InitialRows(height);
_terminal->CreateFromSettings(*_settings, *_renderer);
// 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 til::point 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 = winrt::make_self<implementation::ControlSettings>(settings, newAppearance);
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());
// 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 auto& newAppearance{ focused ? _settings->FocusedAppearance() : _settings->UnfocusedAppearance() };
// Update the terminal core with its new Core settings
_terminal->UpdateAppearance(*newAppearance);
// 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->FocusedAppearance();
}
Control::IControlAppearance ControlCore::UnfocusedAppearance() const
{
return *_settings->UnfocusedAppearance();
}
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 IFontFeatureMap& 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();
}
Core::Point ControlCore::ScrollOffset() const
{
const auto lock = _terminal->LockForReading();
return _terminal->GetScrollOffset().to_core_point();
}
// 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
Core::Point ControlCore::ViewSize() const
{
const auto lock = _terminal->LockForReading();
return _terminal->GetViewport().Dimensions().to_core_point();
}
// 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
Core::Point ControlCore::BufferSize() const
{
const auto lock = _terminal->LockForReading();
return _terminal->GetBufferSize().to_core_point();
}
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(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.ResetOnly)
{
std::vector<til::point_span> oldResults;
til::point_span oldFocused;
if (const auto focused = _terminal->GetSearchHighlightFocused())
{
oldFocused = *focused;
}
if (searchInvalidated)
{
oldResults = _searcher.ExtractResults();
_searcher.Reset(*_terminal.get(), request.Text, flags, !request.GoForward);
_terminal->SetSearchHighlights(_searcher.Results());
}
if (!request.ResetOnly)
{
_searcher.FindNext(!request.GoForward);
}
_terminal->SetSearchHighlightFocused(gsl::narrow<size_t>(std::max<ptrdiff_t>(0, _searcher.CurrentMatch())));
_renderer->TriggerSearchHighlight(oldResults);
if (const auto focused = _terminal->GetSearchHighlightFocused(); focused && *focused != oldFocused)
{
_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();
_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::PersistToPath(const wchar_t* path) const
{
const auto lock = _terminal->LockForReading();
_terminal->SerializeMainBuffer(path);
}
void ControlCore::RestoreFromPath(const wchar_t* path) const
{
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) };
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->GetCursorPosition().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::BlinkAttributeTick()
{
const auto lock = _terminal->LockForWriting();
auto& renderSettings = _terminal->GetRenderSettings();
renderSettings.ToggleBlinkRendition(_renderer.get());
}
void ControlCore::BlinkCursor()
{
const auto lock = _terminal->LockForWriting();
_terminal->BlinkCursor();
}
bool ControlCore::CursorOn() const
{
return _terminal->IsCursorOn();
}
void ControlCore::CursorOn(const bool isCursorOn)
{
const auto lock = _terminal->LockForWriting();
_terminal->SetCursorOn(isCursorOn);
}
void ControlCore::ResumeRendering()
{
// The lock must be held, because it calls into IRenderData which is shared state.
const auto lock = _terminal->LockForWriting();
_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();
}
// 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.
const auto cursorPos{ _terminal->GetCursorPosition() };
// 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 bufferSize = _terminal->GetTextBuffer().GetSize();
auto lastNonSpace = _terminal->GetTextBuffer().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 hstring& hstr)
{
try
{
{
const auto lock = _terminal->LockForWriting();
_terminal->Write(hstr);
}
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;
}
Core::Scheme ControlCore::ColorScheme() const noexcept
{
Core::Scheme s;
// This part is definitely a hack.
//
// This function is usually used by the "Preview Color Scheme"
// functionality in TerminalPage. If we've got an unfocused appearance,
// then we've applied that appearance before this is even getting called
// (because the command palette is open with focus on top of us). If we
// return the _current_ colors now, we'll return out the _unfocused_
// colors. If we do that, and the user dismisses the command palette,
// then the scheme that will get restored is the _unfocused_ one, which
// is not what we want.
//
// So if that's the case, then let's grab the colors from the focused
// appearance as the scheme instead. We'll lose any current runtime
// changes to the color table, but those were already blown away when we
// switched to an unfocused appearance.
//
// IF WE DON'T HAVE AN UNFOCUSED APPEARANCE: then just ask the Terminal
// for its current color table. That way, we can restore those colors
// back.
if (HasUnfocusedAppearance())
{
s.Foreground = _settings->FocusedAppearance()->DefaultForeground();
s.Background = _settings->FocusedAppearance()->DefaultBackground();
s.CursorColor = _settings->FocusedAppearance()->CursorColor();
s.Black = _settings->FocusedAppearance()->GetColorTableEntry(0);
s.Red = _settings->FocusedAppearance()->GetColorTableEntry(1);
s.Green = _settings->FocusedAppearance()->GetColorTableEntry(2);
s.Yellow = _settings->FocusedAppearance()->GetColorTableEntry(3);
s.Blue = _settings->FocusedAppearance()->GetColorTableEntry(4);
s.Purple = _settings->FocusedAppearance()->GetColorTableEntry(5);
s.Cyan = _settings->FocusedAppearance()->GetColorTableEntry(6);
s.White = _settings->FocusedAppearance()->GetColorTableEntry(7);
s.BrightBlack = _settings->FocusedAppearance()->GetColorTableEntry(8);
s.BrightRed = _settings->FocusedAppearance()->GetColorTableEntry(9);
s.BrightGreen = _settings->FocusedAppearance()->GetColorTableEntry(10);
s.BrightYellow = _settings->FocusedAppearance()->GetColorTableEntry(11);
s.BrightBlue = _settings->FocusedAppearance()->GetColorTableEntry(12);
s.BrightPurple = _settings->FocusedAppearance()->GetColorTableEntry(13);
s.BrightCyan = _settings->FocusedAppearance()->GetColorTableEntry(14);
s.BrightWhite = _settings->FocusedAppearance()->GetColorTableEntry(15);
}
else
{
const auto lock = _terminal->LockForReading();
s = _terminal->GetColorScheme();
}
// This might be a tad bit of a hack. This event only gets called by set
// color scheme / preview color scheme, and in that case, we know the
// control _is_ focused.
s.SelectionBackground = _settings->FocusedAppearance()->SelectionBackground();
return s;
}
// Method Description:
// - Apply the given color scheme to this control. We'll take the colors out
// of it and apply them to our focused appearance, and update the terminal
// buffer with the new color table.
// - This is here to support the Set Color Scheme action, and the ability to
// preview schemes in the control.
// Arguments:
// - scheme: the collection of colors to apply.
// Return Value:
// - <none>
void ControlCore::ColorScheme(const Core::Scheme& scheme)
{
_settings->FocusedAppearance()->DefaultForeground(scheme.Foreground);
_settings->FocusedAppearance()->DefaultBackground(scheme.Background);
_settings->FocusedAppearance()->CursorColor(scheme.CursorColor);
_settings->FocusedAppearance()->SelectionBackground(scheme.SelectionBackground);
_settings->FocusedAppearance()->SetColorTableEntry(0, scheme.Black);
_settings->FocusedAppearance()->SetColorTableEntry(1, scheme.Red);
_settings->FocusedAppearance()->SetColorTableEntry(2, scheme.Green);
_settings->FocusedAppearance()->SetColorTableEntry(3, scheme.Yellow);
_settings->FocusedAppearance()->SetColorTableEntry(4, scheme.Blue);
_settings->FocusedAppearance()->SetColorTableEntry(5, scheme.Purple);
_settings->FocusedAppearance()->SetColorTableEntry(6, scheme.Cyan);
_settings->FocusedAppearance()->SetColorTableEntry(7, scheme.White);
_settings->FocusedAppearance()->SetColorTableEntry(8, scheme.BrightBlack);
_settings->FocusedAppearance()->SetColorTableEntry(9, scheme.BrightRed);
_settings->FocusedAppearance()->SetColorTableEntry(10, scheme.BrightGreen);
_settings->FocusedAppearance()->SetColorTableEntry(11, scheme.BrightYellow);
_settings->FocusedAppearance()->SetColorTableEntry(12, scheme.BrightBlue);
_settings->FocusedAppearance()->SetColorTableEntry(13, scheme.BrightPurple);
_settings->FocusedAppearance()->SetColorTableEntry(14, scheme.BrightCyan);
_settings->FocusedAppearance()->SetColorTableEntry(15, scheme.BrightWhite);
const auto lock = _terminal->LockForWriting();
_terminal->ApplyScheme(scheme);
_renderer->TriggerRedrawAll(true);
}
bool ControlCore::HasUnfocusedAppearance() const
{
return _settings->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->LockForReading();
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().Y;
const auto& marks{ _terminal->GetMarkExtents() };
std::optional<::MarkExtents> tgt;
switch (direction)
{
case ScrollToMarkDirection::Last:
{
auto highest = currentOffset;
for (const auto& mark : marks)
{
const auto newY = mark.start.y;
if (newY > highest)
{
tgt = mark;
highest = newY;
}
}
break;
}
case ScrollToMarkDirection::First:
{
auto lowest = currentOffset;
for (const auto& mark : marks)
{
const auto newY = mark.start.y;
if (newY < lowest)
{
tgt = mark;
lowest = newY;
}
}
break;
}
case ScrollToMarkDirection::Next:
{
auto minDistance = til::CoordTypeMax;
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:
{
auto minDistance = til::CoordTypeMax;
for (const auto& mark : marks)
{
const auto delta = currentOffset - mark.start.y;
if (delta > 0 && delta < minDistance)
{
tgt = mark;
minDistance = delta;
}
}
break;
}
}
const auto scrollOffsetX = ScrollOffset().X;
const auto viewHeight = ViewSize().Y;
const auto bufferSize = BufferSize().Y;
// UserScrollViewport, to update the Terminal about where the viewport should be
// then raise a _terminalScrollPositionChanged to inform the control to update the scrollbar.
til::CoordType y = 0;
if (tgt)
{
y = tgt->start.y;
}
else if (direction == ScrollToMarkDirection::Last || direction == ScrollToMarkDirection::Next)
{
y = bufferSize;
}
UserScrollViewport({ scrollOffsetX, y });
_terminalScrollPositionChanged(y, 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);
}
}