mirror of
https://github.com/microsoft/terminal.git
synced 2026-07-08 18:16:28 +00:00
Improve runtime handling for some settings
This commit is contained in:
@@ -4042,10 +4042,11 @@ namespace winrt::TerminalApp::implementation
|
||||
_UpdateTabWidthMode();
|
||||
_CreateNewTabFlyout();
|
||||
|
||||
// Reload the current value of alwaysOnTop from the settings file. This
|
||||
// will let the user hot-reload this setting, but any runtime changes to
|
||||
// the alwaysOnTop setting will be lost.
|
||||
_isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop();
|
||||
// Reload the current value of alwaysOnTop from the settings file, but let
|
||||
// a runtime toggle (ToggleAlwaysOnTop) win so a settings reload doesn't
|
||||
// stomp it (GH#12424). This still lets the user hot-reload the setting when
|
||||
// they haven't overridden it at runtime.
|
||||
_isAlwaysOnTop = _runtimeAlwaysOnTop.value_or(_settings.GlobalSettings().AlwaysOnTop());
|
||||
AlwaysOnTopChanged.raise(*this, nullptr);
|
||||
|
||||
_showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen();
|
||||
@@ -4266,6 +4267,8 @@ namespace winrt::TerminalApp::implementation
|
||||
void TerminalPage::ToggleAlwaysOnTop()
|
||||
{
|
||||
_isAlwaysOnTop = !_isAlwaysOnTop;
|
||||
// Record the runtime override so a settings reload preserves it (GH#12424).
|
||||
_runtimeAlwaysOnTop = _isAlwaysOnTop;
|
||||
AlwaysOnTopChanged.raise(*this, nullptr);
|
||||
}
|
||||
|
||||
|
||||
@@ -253,6 +253,10 @@ namespace winrt::TerminalApp::implementation
|
||||
bool _isFullscreen{ false };
|
||||
bool _isMaximized{ false };
|
||||
bool _isAlwaysOnTop{ false };
|
||||
// Runtime override for always-on-top (toggled via ToggleAlwaysOnTop). When
|
||||
// set, it wins over GlobalSettings().AlwaysOnTop() so a settings reload
|
||||
// doesn't stomp the user's runtime toggle (GH#12424).
|
||||
std::optional<bool> _runtimeAlwaysOnTop{ std::nullopt };
|
||||
bool _showTabsFullscreen{ false };
|
||||
|
||||
std::optional<uint32_t> _loadFromPersistedLayoutIdx{};
|
||||
|
||||
@@ -912,6 +912,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
// - 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)
|
||||
{
|
||||
// Capture any runtime opacity adjustment (e.g. Ctrl+Shift+scroll, the
|
||||
// adjustOpacity action) as a delta over the *old* settings opacity before
|
||||
// _settings is replaced. On reload we re-apply that delta over the new
|
||||
// settings opacity instead of snapping back to the configured value, so a
|
||||
// settings reload doesn't stomp the user's runtime adjustment (GH#12424).
|
||||
// This mirrors the font-size delta below, but is derived from the live
|
||||
// runtime value rather than stored, because opacity is clamped to [0,1] on
|
||||
// every adjustment -- a separately accumulated delta would drift out of
|
||||
// sync with the (clamped) runtime opacity.
|
||||
std::optional<float> runtimeOpacityDelta;
|
||||
if (_runtimeOpacity)
|
||||
{
|
||||
runtimeOpacityDelta = *_runtimeOpacity - _settings.Opacity();
|
||||
}
|
||||
|
||||
_settings = settings;
|
||||
_hasUnfocusedAppearance = static_cast<bool>(newAppearance);
|
||||
_unfocusedAppearance = _hasUnfocusedAppearance ? newAppearance : settings;
|
||||
@@ -922,11 +937,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation
|
||||
_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();
|
||||
// Preserve a runtime opacity adjustment across the reload by re-applying
|
||||
// the captured delta over the new settings opacity. FocusedAppearance() is
|
||||
// _settings, so _runtimeFocusedOpacity shares the same base and resolves to
|
||||
// the same value.
|
||||
if (runtimeOpacityDelta)
|
||||
{
|
||||
const auto resolvedOpacity = std::clamp(_settings.Opacity() + *runtimeOpacityDelta, 0.0f, 1.0f);
|
||||
_runtimeOpacity = resolvedOpacity;
|
||||
_runtimeFocusedOpacity = resolvedOpacity;
|
||||
}
|
||||
else
|
||||
{
|
||||
_runtimeOpacity = std::nullopt;
|
||||
_runtimeFocusedOpacity = std::nullopt;
|
||||
}
|
||||
|
||||
// Manually turn off acrylic if they turn off transparency. Use the resolved
|
||||
// (runtime-preserved) opacity so acrylic follows the runtime adjustment.
|
||||
_runtimeUseAcrylic = Opacity() < 1.0 && _settings.UseAcrylic();
|
||||
|
||||
const auto sizeChanged = _setFontSizeUnderLock(_settings.FontSize() + _accumulatedFontSizeDelta);
|
||||
|
||||
|
||||
@@ -91,6 +91,18 @@ void Terminal::CreateFromSettings(ICoreSettings settings,
|
||||
// - settings: an ICoreSettings with new settings values for us to use.
|
||||
void Terminal::UpdateSettings(ICoreSettings settings)
|
||||
{
|
||||
// Capture any color table entries that were overridden at runtime by VT
|
||||
// sequences (OSC 4/10/11/12) so the color-scheme refresh below doesn't stomp
|
||||
// them on a settings reload (GH#12424). They're re-applied after the new
|
||||
// scheme is saved as the default. Skipped on the very first load, where the
|
||||
// constructor's default fg/bg setup would otherwise look like a runtime
|
||||
// override (the scheme baseline isn't established until the first save below).
|
||||
std::vector<std::pair<size_t, COLORREF>> runtimeColorOverrides;
|
||||
if (_colorSchemeInitialized)
|
||||
{
|
||||
runtimeColorOverrides = GetRenderSettings().GetRuntimeColorTableOverrides();
|
||||
}
|
||||
|
||||
UpdateAppearance(settings);
|
||||
|
||||
_snapOnInput = settings.SnapOnInput();
|
||||
@@ -123,6 +135,15 @@ void Terminal::UpdateSettings(ICoreSettings settings)
|
||||
// Save the changes made above and in UpdateAppearance as the new default render settings.
|
||||
GetRenderSettings().SaveDefaultSettings();
|
||||
|
||||
// Re-apply any runtime VT color overrides captured above on top of the
|
||||
// refreshed scheme baseline, so a settings reload preserves them (GH#12424).
|
||||
// A hard reset (RIS) still restores the new scheme via RestoreDefaultSettings.
|
||||
for (const auto& [index, color] : runtimeColorOverrides)
|
||||
{
|
||||
GetRenderSettings().SetColorTableEntry(index, color);
|
||||
}
|
||||
_colorSchemeInitialized = true;
|
||||
|
||||
if (!_startingTitle)
|
||||
{
|
||||
_startingTitle = settings.StartingTitle();
|
||||
|
||||
@@ -356,6 +356,11 @@ private:
|
||||
std::optional<std::wstring> _startingTitle;
|
||||
std::optional<til::color> _startingTabColor;
|
||||
|
||||
// Tracks whether UpdateSettings has run at least once, so the first load
|
||||
// doesn't mistake the constructor's default fg/bg color setup for runtime VT
|
||||
// color overrides when preserving them across reloads (GH#12424).
|
||||
bool _colorSchemeInitialized{ false };
|
||||
|
||||
std::vector<til::point_span> _searchHighlights;
|
||||
size_t _searchHighlightFocused = 0;
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace ControlUnitTests
|
||||
TEST_METHOD(InstantiateCore);
|
||||
TEST_METHOD(TestInitialize);
|
||||
TEST_METHOD(TestAdjustAcrylic);
|
||||
TEST_METHOD(TestRuntimeOpacitySurvivesSettingsReload);
|
||||
TEST_METHOD(TestReadOnlySurvivesSettingsReload);
|
||||
|
||||
TEST_METHOD(TestFreeAfterClose);
|
||||
|
||||
@@ -206,6 +208,66 @@ namespace ControlUnitTests
|
||||
core->AdjustOpacity(-0.25f);
|
||||
}
|
||||
|
||||
void ControlCoreTests::TestRuntimeOpacitySurvivesSettingsReload()
|
||||
{
|
||||
// GH#12424: a runtime opacity adjustment (Ctrl+Shift+scroll / adjustOpacity)
|
||||
// must survive a settings reload as a delta over the new settings opacity,
|
||||
// rather than snapping back to the configured value.
|
||||
auto [settings, conn] = _createSettingsAndConnection();
|
||||
|
||||
settings->UseAcrylic(true);
|
||||
settings->Opacity(0.5f);
|
||||
|
||||
auto core = createCore(*settings, *conn);
|
||||
VERIFY_IS_NOT_NULL(core);
|
||||
core->Initialize(270, 380, 1.0);
|
||||
VERIFY_IS_TRUE(core->_initializedTerminal);
|
||||
|
||||
Log::Comment(L"Adjust opacity at runtime: 0.5 -> 0.7 (delta +0.2)");
|
||||
core->AdjustOpacity(0.2f);
|
||||
VERIFY_ARE_EQUAL(0.7f, core->Opacity());
|
||||
|
||||
Log::Comment(L"Reload settings with a different opacity (0.4). A reload builds a "
|
||||
L"fresh settings object, so use a separate mock here.");
|
||||
auto reloaded = winrt::make_self<MockControlSettings>();
|
||||
reloaded->UseAcrylic(true);
|
||||
reloaded->Opacity(0.4f);
|
||||
core->UpdateSettings(*reloaded, *reloaded);
|
||||
|
||||
Log::Comment(L"The runtime delta (+0.2) rides on top of the new settings opacity "
|
||||
L"(0.4) -> 0.6, instead of being stomped back to 0.4.");
|
||||
VERIFY_ARE_EQUAL(0.6f, core->Opacity());
|
||||
// Acrylic follows the resolved (runtime-preserved) opacity, which is < 1.0.
|
||||
VERIFY_IS_TRUE(core->UseAcrylic());
|
||||
|
||||
Log::Comment(L"With no runtime adjustment, a reload follows the settings opacity.");
|
||||
auto core2 = createCore(*settings, *conn);
|
||||
core2->Initialize(270, 380, 1.0);
|
||||
VERIFY_ARE_EQUAL(0.5f, core2->Opacity());
|
||||
auto reloaded2 = winrt::make_self<MockControlSettings>();
|
||||
reloaded2->UseAcrylic(true);
|
||||
reloaded2->Opacity(0.3f);
|
||||
core2->UpdateSettings(*reloaded2, *reloaded2);
|
||||
VERIFY_ARE_EQUAL(0.3f, core2->Opacity());
|
||||
}
|
||||
|
||||
void ControlCoreTests::TestReadOnlySurvivesSettingsReload()
|
||||
{
|
||||
// GH#12424: a runtime read-only toggle must not be reset by a settings reload.
|
||||
auto [settings, conn] = _createSettingsAndConnection();
|
||||
auto core = createCore(*settings, *conn);
|
||||
VERIFY_IS_NOT_NULL(core);
|
||||
core->Initialize(270, 380, 1.0);
|
||||
|
||||
VERIFY_IS_FALSE(core->IsInReadOnlyMode());
|
||||
core->SetReadOnlyMode(true);
|
||||
VERIFY_IS_TRUE(core->IsInReadOnlyMode());
|
||||
|
||||
auto reloaded = winrt::make_self<MockControlSettings>();
|
||||
core->UpdateSettings(*reloaded, *reloaded);
|
||||
VERIFY_IS_TRUE(core->IsInReadOnlyMode());
|
||||
}
|
||||
|
||||
void ControlCoreTests::TestFreeAfterClose()
|
||||
{
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace TerminalCoreUnitTests
|
||||
TEST_CLASS(TerminalApiTest);
|
||||
|
||||
TEST_METHOD(SetColorTableEntry);
|
||||
TEST_METHOD(PreserveRuntimeColorAcrossSettingsReload);
|
||||
|
||||
TEST_METHOD(CursorVisibilityViaStateMachine);
|
||||
|
||||
@@ -60,6 +61,47 @@ void TerminalApiTest::SetColorTableEntry()
|
||||
VERIFY_THROWS(term._renderSettings.SetColorTableEntry(512, 100), std::exception);
|
||||
}
|
||||
|
||||
void TerminalApiTest::PreserveRuntimeColorAcrossSettingsReload()
|
||||
{
|
||||
// GH#12424: a color table entry changed at runtime by a VT sequence (e.g.
|
||||
// OSC 4) must survive a settings reload -- the new color scheme updates the
|
||||
// baseline, but the runtime override is preserved. A non-overridden entry
|
||||
// still picks up the new scheme.
|
||||
Terminal term{ Terminal::TestDummyMarker{} };
|
||||
DummyRenderer renderer{ &term };
|
||||
term.Create({ 100, 100 }, 0, renderer);
|
||||
|
||||
const auto makeUniformTable = [](uint8_t v) {
|
||||
std::array<winrt::Microsoft::Terminal::Core::Color, 16> table;
|
||||
table.fill(winrt::Microsoft::Terminal::Core::Color{ v, v, v, 255 });
|
||||
return table;
|
||||
};
|
||||
|
||||
// Initial load with scheme A (all entries 0x111111).
|
||||
auto settingsA = winrt::make_self<MockTermSettings>(100, 100, 100);
|
||||
settingsA->SetColorTable(makeUniformTable(0x11));
|
||||
term.UpdateSettings(*settingsA);
|
||||
|
||||
// Simulate an OSC color override at index 5.
|
||||
const auto runtimeColor = static_cast<COLORREF>(RGB(0xAB, 0xCD, 0xEF));
|
||||
term._renderSettings.SetColorTableEntry(5, runtimeColor);
|
||||
VERIFY_ARE_EQUAL(runtimeColor, term._renderSettings.GetColorTableEntry(5));
|
||||
|
||||
// Reload with scheme B (all entries 0x222222). A reload builds a fresh
|
||||
// settings object, so use a separate mock.
|
||||
auto settingsB = winrt::make_self<MockTermSettings>(100, 100, 100);
|
||||
settingsB->SetColorTable(makeUniformTable(0x22));
|
||||
term.UpdateSettings(*settingsB);
|
||||
|
||||
const auto schemeBColor = static_cast<COLORREF>(til::color{ winrt::Microsoft::Terminal::Core::Color{ 0x22, 0x22, 0x22, 255 } });
|
||||
|
||||
Log::Comment(L"The runtime-overridden entry keeps its value across the reload.");
|
||||
VERIFY_ARE_EQUAL(runtimeColor, term._renderSettings.GetColorTableEntry(5));
|
||||
|
||||
Log::Comment(L"A non-overridden entry picks up the new scheme.");
|
||||
VERIFY_ARE_EQUAL(schemeBColor, term._renderSettings.GetColorTableEntry(3));
|
||||
}
|
||||
|
||||
// Terminal::_WriteBuffer used to enter infinite loops under certain conditions.
|
||||
// This test ensures that Terminal::_WriteBuffer doesn't get stuck when
|
||||
// PrintString() is called with more code units than the buffer width.
|
||||
|
||||
@@ -79,6 +79,31 @@ const std::array<COLORREF, TextColor::TABLE_SIZE>& RenderSettings::GetColorTable
|
||||
return _colorTable;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Returns the color table entries that have diverged from the saved defaults
|
||||
// since the last SaveDefaultSettings() -- i.e. entries that were changed at
|
||||
// runtime, typically by OSC color sequences (OSC 4/10/11/12). A settings
|
||||
// reload uses this to refresh the color-scheme baseline (the default table)
|
||||
// without stomping these runtime overrides (GH#12424).
|
||||
// - The frame colors (tab fg/bg) are intentionally excluded: they're driven by
|
||||
// the separate tab-color settings/runtime path, not treated as VT overrides.
|
||||
std::vector<std::pair<size_t, COLORREF>> RenderSettings::GetRuntimeColorTableOverrides() const
|
||||
{
|
||||
std::vector<std::pair<size_t, COLORREF>> overrides;
|
||||
for (size_t i = 0; i < TextColor::TABLE_SIZE; i++)
|
||||
{
|
||||
if (i == TextColor::FRAME_FOREGROUND || i == TextColor::FRAME_BACKGROUND)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_colorTable.at(i) != _defaultColorTable.at(i))
|
||||
{
|
||||
overrides.emplace_back(i, _colorTable.at(i));
|
||||
}
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
|
||||
// Routine Description:
|
||||
// - Resets the first 16 color table entries with default values.
|
||||
void RenderSettings::ResetColorTable() noexcept
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace Microsoft::Console::Render
|
||||
void SetRenderMode(const Mode mode, const bool enabled) noexcept;
|
||||
bool GetRenderMode(const Mode mode) const noexcept;
|
||||
const std::array<COLORREF, TextColor::TABLE_SIZE>& GetColorTable() const noexcept;
|
||||
std::vector<std::pair<size_t, COLORREF>> GetRuntimeColorTableOverrides() const;
|
||||
void ResetColorTable() noexcept;
|
||||
void SetColorTableEntry(const size_t tableIndex, const COLORREF color);
|
||||
COLORREF GetColorTableEntry(const size_t tableIndex) const;
|
||||
|
||||
Reference in New Issue
Block a user