Compare commits

...

13 Commits

Author SHA1 Message Date
Carlos Zamora
7dc9bb1558 Convert NewTabMenu 2026-04-21 17:38:06 -07:00
Carlos Zamora
45e93cac42 Convert IMediaResource 2026-04-21 17:27:13 -07:00
Carlos Zamora
9bf994dd20 Convert more settings; ColorSchemeRef added 2026-04-21 16:49:00 -07:00
Carlos Zamora
a7380638a8 Make settings model JSON backed 2026-04-21 14:57:01 -07:00
Carlos Zamora
8f4fdf9751 Normalize commandline in media resource tests (#20118)
## Summary of the Pull Request
Some of the Media Resource tests were failing on my machine. Turns out
that it's because my machine uses `C:\WINDOWS` instead of `C:\Windows`.

This PR fixes those tests by calling `Profile::NormalizeCommandline()`
on a few impacted strings. This also makes them loaded after enabling
filesystem redirection in `TEST_CLASS_SETUP` so that we resolve to the
correct path expected by x86.

Admittedly, I'm not the biggest fan of using that to fix the tests, but
it's better than the alternative which is a case-insensitive string
comparison. Also, the tests are testing fallback behavior, so this
doesn't really impact that. It just makes the tests more consistently
reliable.

This also removes the unused `pingCommandline` variable.

## Validation Steps Performed
 Tests pass
2026-04-15 19:13:41 +00:00
Windows Console Service Bot
33a80191c1 Localization Updates - URL dialog and PathTranslation - 04/11/2026 (#20096)
Co-authored-by: Console Service Bot <consvc@microsoft.com>
2026-04-15 14:09:19 -05:00
Dustin L. Howett
84ae7adec6 OS PR 14890400: fix defects caught with strict compiler switches (#20100)
Related work items: MSFT-53015560

Co-authored-by: Sudhakar Prabhu <sprabhu@microsoft.com>
2026-04-10 11:41:03 -07:00
Dustin L. Howett
a6ebdd3d4a render: check the thread exit condition after every long operation (#20097)
We have received an internal report that teardown on OneCore is still
hanging. It looks like there's a chance that `TriggerTeardown` is called
during `PaintFrame`, which may result in `_threadShouldKeepRunning`
getting set to `false` (TriggerTeardown) and `_redraw` being set to
`false` as well (PaintFrame). The thread will wait forever on `_redraw`
to be signalled, which it never will, because `TriggerTeardown` is
waiting for the thread to exit.

That is:

```
Render Thread      | ConIoSrv Thread
------------------------------------
Check _enabled     |
Wait on _redraw    |
Check _keepRunning | TriggerTeardown
Paint              |   _keepRunning = false
                   |   _redraw = true
    _redraw = false|   Signal _enabled
Paint Completes    |   Wait on thread
Check _enabled     |
Wait on _redraw    |
**DEADLOCK**       | **DEADLOCK**
                   v
```

This may not be an ideal fix, but at least it checks
`_threadShouldKeepRunning` after every "long" operation (waiting and
painting) now.
2026-04-09 16:34:09 -05:00
Dustin L. Howett
031998bfc3 Track the cursor dirty flag in the correct buffer (#20095)
Failure to do so will result in every console API requiring a cursor
update stalling for 500ms because we sent the update to the wrong
buffer.

Closes #20092
2026-04-08 22:08:28 +00:00
Dustin L. Howett
41e08a68bd Path translation setting: use 🡒 instead of -> (#20088)
It looks better, as decided by committee.
2026-04-07 16:38:51 -05:00
Dustin L. Howett
81170aff78 Display a warning dialog for unsafe URLs (#20065)
We are getting a sufficient number of LLM-generated security reports
telling us that Ctrl+click and a tooltip are insufficient protection
from users clicking on links to dangerous things.

This commit displays a warning that prevents users from blindly clicking
on dangerous things.

Dangerous things include:
- any non-http and non-https and non-file URLs
- any file URLs that point to something understandable as a "program"
(so, something which resides in `PATHEXT`.)

In doing this, I learned that `til::ends_with_insensitive_ascii` was
broken.

I also learned that ContentDialogs summoned by any event handler out of
TermControl::Pointer* would lose focus immediately. It turns out that in
the absolute earliest days of Terminal, when we first created the
UserControl that became TermControl, we added our Tapped event handler.

It unconditionally focused the control.

Since `Tapped` is a higher-level event handler than `PointerPressed`, it
was firing after the gesture that opened the content dialog and stealing
focus back.

I'm fairly certain we don't need it.

Refs #7562
2026-04-07 11:23:15 -05:00
Windows Console Service Bot
c90ace8326 Localization Updates - drag drop delimiter - 04/07/2026 03:05:15 (#20042)
Co-authored-by: Console Service Bot <consvc@microsoft.com>
2026-04-07 09:59:50 -05:00
Dustin L. Howett
fd30d00304 Draft-TerminalReleases: add nuget package support, remove asset hashes (#20061) 2026-04-06 17:34:28 -05:00
54 changed files with 2269 additions and 518 deletions

View File

@@ -350,7 +350,7 @@ void ROW::_init() noexcept
std::iota(_charOffsets.begin(), _charOffsets.end(), uint16_t{ 0 });
#endif
#pragma warning(push)
#pragma warning(pop)
}
void ROW::CopyFrom(const ROW& source)

View File

@@ -672,9 +672,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Dieser Linktyp wird derzeit nicht unterstützt:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Abbrechen</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Dieser Link kann zu einem unsicheren Speicherort führen. Links können ihren Computer und Ihre Daten beschädigen. Klicken Sie zum Schutz Des Computers nur auf Links aus vertrauenswürdigen Quellen.</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Trotzdem öffnen</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Einstellungen</value>
</data>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>This link type is currently not supported:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>This link may lead to an unsafe location. Hyperlinks can be harmful to your computer and data. To protect your computer, only click links from trusted sources.</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Open anyway</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Settings</value>
</data>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Este tipo de vínculo no se admite actualmente:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Cancelar</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Este vínculo puede dar lugar a una ubicación no segura. Los hipervínculos pueden ser perjudiciales para el equipo y los datos. Para proteger el equipo, haga clic solo en vínculos de orígenes de confianza.</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Abrir de todas formas</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Configuración</value>
</data>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Ce type de lien nest actuellement pas pris en charge :</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Annuler</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Ce lien peut entraîner un emplacement non sécurisé. Les liens hypertexte peuvent endommager votre ordinateur et vos données. Pour protéger votre ordinateur, cliquez uniquement sur des liens provenant de sources fiables.</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Ouvrir quand même</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Paramètres</value>
</data>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Questo tipo di collegamento non è al momento supportato:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Annulla</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Questo collegamento potrebbe causare un percorso non sicuro. I collegamenti ipertestuali possono essere dannosi per il computer e i dati. Per proteggere il computer, fare clic solo su collegamenti da origini attendibili.</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Apri comunque</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Impostazioni</value>
</data>

View File

@@ -670,9 +670,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>このリンクの種類は現在サポートされていません:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>キャンセル</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>このリンクは安全でない可能性があります。ハイパーリンクは、コンピューターやデータに問題を起こす可能性があります。コンピューターを保護するには、信頼できるソースからのリンクのみをクリックしてください。</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>開く</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>設定</value>
</data>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>이 링크 형식은 현재 지원되지 않습니다.</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>취소</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>이 링크로 인해 안전하지 않은 위치가 발생할 수 있습니다. 하이퍼링크는 컴퓨터와 데이터를 손상시킬 수 있습니다. 컴퓨터를 보호하려면 신뢰할 수 있는 원본의 링크만 클릭하세요.</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>열기</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>설정</value>
</data>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Não há suporte para este tipo de link no momento:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Cancelar</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Este link pode levar a um local não seguro. Hiperlinks podem ser prejudiciais ao computador e aos dados. Para proteger o computador, clique somente em links de fontes confiáveis.</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Abrir mesmo assim</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Configurações</value>
</data>

View File

@@ -669,8 +669,14 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Ťђïś łϊηќ ŧурē ιş çũґѓзⁿτľÿ ñστ şΰρρоŕŧĕđ: !!! !!! !!! !!! </value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<value>Сąñс℮ł !</value>
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Çдπсёľ !</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Ŧђīś ℓîŋќ мαў ľêãδ τб áń úʼnšàƒé ℓоćάŧίоñ. Ĥўрзŗℓĭŋķѕ çâⁿ ъέ ђąřмƒúļ τό ўôця ċómφύŧèґ аňδ ðáťǻ. Ţб ρгøťėçŧ ўòύг ςömφùţĕŕ, ŏŋľỳ čℓΐςķ łίŋκѕ ƒřöм ťŗμѕŧєđ śόυяčêś. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!!</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Őρέй ǻпŷŵãγ !!!</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Śëţťĩпğś !!</value>

View File

@@ -669,8 +669,14 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Ťђïś łϊηќ ŧурē ιş çũґѓзⁿτľÿ ñστ şΰρρоŕŧĕđ: !!! !!! !!! !!! </value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<value>Сąñс℮ł !</value>
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Çдπсёľ !</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Ŧђīś ℓîŋќ мαў ľêãδ τб áń úʼnšàƒé ℓоćάŧίоñ. Ĥўрзŗℓĭŋķѕ çâⁿ ъέ ђąřмƒúļ τό ўôця ċómφύŧèґ аňδ ðáťǻ. Ţб ρгøťėçŧ ўòύг ςömφùţĕŕ, ŏŋľỳ čℓΐςķ łίŋκѕ ƒřöм ťŗμѕŧєđ śόυяčêś. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!!</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Őρέй ǻпŷŵãγ !!!</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Śëţťĩпğś !!</value>

View File

@@ -669,8 +669,14 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Ťђïś łϊηќ ŧурē ιş çũґѓзⁿτľÿ ñστ şΰρρоŕŧĕđ: !!! !!! !!! !!! </value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<value>Сąñс℮ł !</value>
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Çдπсёľ !</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Ŧђīś ℓîŋќ мαў ľêãδ τб áń úʼnšàƒé ℓоćάŧίоñ. Ĥўрзŗℓĭŋķѕ çâⁿ ъέ ђąřмƒúļ τό ўôця ċómφύŧèґ аňδ ðáťǻ. Ţб ρгøťėçŧ ўòύг ςömφùţĕŕ, ŏŋľỳ čℓΐςķ łίŋκѕ ƒřöм ťŗμѕŧєđ śόυяčêś. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!!</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Őρέй ǻпŷŵãγ !!!</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Śëţťĩпğś !!</value>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>Этот тип связи в настоящее время не поддерживается:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>Отмена</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>Эта ссылка может привести к небезопасному расположению. Гиперссылки могут нанести вред компьютеру и данным. Чтобы защитить компьютер, щелкните ссылки только из надежных источников.</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>Все равно открыть</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>Параметры</value>
</data>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>当前不支持此链接类型:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>取消</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>此链接可能会导致不安全的位置。超链接可能对你的计算机和数据有害。若要保护你的计算机,请仅单击来自受信任源的链接。</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>仍然打开</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>设置</value>
</data>

View File

@@ -669,9 +669,15 @@
<data name="UnsupportedSchemeText" xml:space="preserve">
<value>目前不支援此連結類型:</value>
</data>
<data name="CouldNotOpenUriDialog.PrimaryButtonText" xml:space="preserve">
<data name="UriErrorDialog.CloseButtonText" xml:space="preserve">
<value>取消</value>
</data>
<data name="UnsafeUrlConfirmText" xml:space="preserve">
<value>此連結可能通往不安全地點。超連結可能對你的電腦和資料造成傷害。為了保護你的電腦,只點擊來自可信來源的連結。</value>
</data>
<data name="UnsafeUrlConfirmAllowAction" xml:space="preserve">
<value>一律開啟</value>
</data>
<data name="SettingsTab" xml:space="preserve">
<value>設定</value>
</data>

View File

@@ -3085,18 +3085,42 @@ namespace winrt::TerminalApp::implementation
}
CATCH_LOG();
void TerminalPage::_OpenHyperlinkHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs)
safe_void_coroutine TerminalPage::_OpenHyperlinkHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs)
{
try
{
auto parsed = winrt::Windows::Foundation::Uri(eventArgs.Uri());
auto uriString{ eventArgs.Uri() };
auto parsed = winrt::Windows::Foundation::Uri(uriString);
if (_IsUriSupported(parsed))
{
ShellExecute(nullptr, L"open", eventArgs.Uri().c_str(), nullptr, nullptr, SW_SHOWNORMAL);
bool shouldLaunch{ _IsUriConsideredSomewhatSafe(parsed) };
if (!shouldLaunch)
{
if (auto presenter{ _dialogPresenter.get() })
{
// FindName needs to be called first to actually load the xaml object
auto unopenedUriDialog = FindName(L"UriErrorDialog").try_as<WUX::Controls::ContentDialog>();
// Insert the reason and the URI
unopenedUriDialog.SecondaryButtonText(RS_(L"UnsafeUrlConfirmAllowAction"));
CouldNotOpenUriReason().Text(RS_(L"UnsafeUrlConfirmText"));
UnopenedUri().Text(uriString);
// Show the dialog
auto result = co_await presenter.ShowDialog(unopenedUriDialog);
shouldLaunch = result == ContentDialogResult::Secondary;
}
}
if (shouldLaunch)
{
ShellExecuteW(nullptr, L"open", uriString.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
}
}
else
{
_ShowCouldNotOpenDialog(RS_(L"UnsupportedSchemeText"), eventArgs.Uri());
_ShowCouldNotOpenDialog(RS_(L"UnsupportedSchemeText"), uriString);
}
}
catch (...)
@@ -3116,9 +3140,10 @@ namespace winrt::TerminalApp::implementation
if (auto presenter{ _dialogPresenter.get() })
{
// FindName needs to be called first to actually load the xaml object
auto unopenedUriDialog = FindName(L"CouldNotOpenUriDialog").try_as<WUX::Controls::ContentDialog>();
auto unopenedUriDialog = FindName(L"UriErrorDialog").try_as<WUX::Controls::ContentDialog>();
// Insert the reason and the URI
unopenedUriDialog.SecondaryButtonText({});
CouldNotOpenUriReason().Text(reason);
UnopenedUri().Text(uri);
@@ -3171,6 +3196,30 @@ namespace winrt::TerminalApp::implementation
return true;
}
bool TerminalPage::_IsUriConsideredSomewhatSafe(const winrt::Windows::Foundation::Uri& parsedUri)
{
if (parsedUri.SchemeName() == L"http" || parsedUri.SchemeName() == L"https")
{
return true;
}
if (parsedUri.SchemeName() == L"file")
{
static const auto pathext{ wil::TryGetEnvironmentVariableW<std::wstring>(L"PATHEXT") };
const auto filename = parsedUri.Path();
for (const auto& e : til::split_iterator{ std::wstring_view{ pathext }, L';' })
{
if (til::ends_with_insensitive_ascii(filename, e))
{
return false;
}
}
return true;
}
return false;
}
// Important! Don't take this eventArgs by reference, we need to extend the
// lifetime of it to the other side of the co_await!
safe_void_coroutine TerminalPage::_ControlNoticeRaisedHandler(const IInspectable /*sender*/,

View File

@@ -422,8 +422,9 @@ namespace winrt::TerminalApp::implementation
safe_void_coroutine _PasteFromClipboardHandler(const IInspectable sender,
const Microsoft::Terminal::Control::PasteFromClipboardEventArgs eventArgs);
void _OpenHyperlinkHandler(const IInspectable sender, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs);
bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri);
safe_void_coroutine _OpenHyperlinkHandler(const IInspectable sender, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs);
static bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri);
static bool _IsUriConsideredSomewhatSafe(const winrt::Windows::Foundation::Uri& parsedUri);
void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri);
bool _CopyText(bool dismissSelection, bool singleLine, bool withControlSequences, Microsoft::Terminal::Control::CopyFormat formats);

View File

@@ -144,17 +144,17 @@
</TextBlock>
</ContentDialog>
<ContentDialog x:Name="CouldNotOpenUriDialog"
x:Uid="CouldNotOpenUriDialog"
<ContentDialog x:Name="UriErrorDialog"
x:Uid="UriErrorDialog"
Grid.Row="2"
x:Load="False"
DefaultButton="Primary">
DefaultButton="Close">
<TextBlock IsTextSelectionEnabled="True"
TextWrapping="WrapWholeWords">
<TextBlock.ContextFlyout>
<mtu:TextMenuFlyout />
</TextBlock.ContextFlyout>
<Run x:Name="CouldNotOpenUriReason" /> <LineBreak />
<Run x:Name="CouldNotOpenUriReason" /> <LineBreak /> <LineBreak />
<Run x:Name="UnopenedUri"
FontFamily="Cascadia Mono" />
</TextBlock>

View File

@@ -1935,8 +1935,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// - args: event data
void TermControl::_TappedHandler(const IInspectable& /*sender*/, const TappedRoutedEventArgs& e)
{
Focus(FocusState::Pointer);
if (e.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch)
{
// Normally TSF would be responsible for showing the touch keyboard, but it's buggy for us:

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>Tippen, um Symbole zu filtern</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Trennzeichen per Drag &amp;amp; Drop</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Dieser Text wird zwischen den Pfaden mehrerer in das Terminal gezogener Dateien eingefügt.</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2610,19 +2610,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>Escriba para filtrar iconos</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Delimitador de arrastrar y colocar</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Este texto se insertará entre las rutas de acceso de varios archivos colocados en el terminal.</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C :\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C :\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>Taper pour filtrer les icônes</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Glisser-déplacer le délimiteur</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Ce texte sera inséré entre les chemins daccès de plusieurs fichiers déposés dans le terminal.</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>Digita per filtrare icone</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Trascina e rilascia il delimitatore</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Questo testo verrà inserito tra i percorsi di più file trascinati nel terminale.</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>入力してアイコンをフィルター処理します</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>ドラッグ アンド ドロップ区切り記号</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>このテキストは、ターミナルにドロップされた複数のファイルのパスの間に挿入されます。</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL(C:\ -&gt; /mnt/c)</value>
<value>WSL(C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin(C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin(C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2(C:\ -&gt; /c)</value>
<value>MSYS2(C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW(C:\ -&gt; C:/)</value>
<value>MinGW(C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>입력하여 아이콘 필터링</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>끌어서 놓기 구분 기호</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>이 텍스트는 터미널에 놓인 여러 파일의 경로 사이에 삽입됩니다.</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>Digite para filtrar ícones</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Delimitador de Arrastar e soltar</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Este texto será inserido entre os caminhos de vários arquivos descartados no terminal.</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2610,19 +2610,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c) !!! !!!</value>
<value>WSL (C:\ 🡒 /mnt/c) !!! !!!</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c) !!! !!! !!</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c) !!! !!! !!</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c) !!! !!</value>
<value>MSYS2 (C:\ 🡒 /c) !!! !!</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/) !!! !!</value>
<value>MinGW (C:\ 🡒 C:/) !!! !!</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2748,4 +2748,12 @@
<value>Тÿρě ţθ ƒíŀŧēŗ īçōйš !!! !!!</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
</root>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Đґâġ ąńð δŗορ ďèŀιмïţ℮я !!! !!! </value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Тĥїś ťэхť ẃĭŀł вё îⁿŝέŗŧеď вēťщ℮ěπ τĥę ρªτħѕ óƒ мџĺţīрℓé ƒĭļèś đяǿρрεδ ιйţθ ţħê ţèřмĭлªŀ. !!! !!! !!! !!! !!! !!! !!! !!! !!!</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2610,19 +2610,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c) !!! !!!</value>
<value>WSL (C:\ 🡒 /mnt/c) !!! !!!</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c) !!! !!! !!</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c) !!! !!! !!</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c) !!! !!</value>
<value>MSYS2 (C:\ 🡒 /c) !!! !!</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/) !!! !!</value>
<value>MinGW (C:\ 🡒 C:/) !!! !!</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2748,4 +2748,12 @@
<value>Тÿρě ţθ ƒíŀŧēŗ īçōйš !!! !!!</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
</root>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Đґâġ ąńð δŗορ ďèŀιмïţ℮я !!! !!! </value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Тĥїś ťэхť ẃĭŀł вё îⁿŝέŗŧеď вēťщ℮ěπ τĥę ρªτħѕ óƒ мџĺţīрℓé ƒĭļèś đяǿρрεδ ιйţθ ţħê ţèřмĭлªŀ. !!! !!! !!! !!! !!! !!! !!! !!! !!!</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2610,19 +2610,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c) !!! !!!</value>
<value>WSL (C:\ 🡒 /mnt/c) !!! !!!</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c) !!! !!! !!</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c) !!! !!! !!</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c) !!! !!</value>
<value>MSYS2 (C:\ 🡒 /c) !!! !!</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/) !!! !!</value>
<value>MinGW (C:\ 🡒 C:/) !!! !!</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2748,4 +2748,12 @@
<value>Тÿρě ţθ ƒíŀŧēŗ īçōйš !!! !!!</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
</root>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Đґâġ ąńð δŗορ ďèŀιмïţ℮я !!! !!! </value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Тĥїś ťэхť ẃĭŀł вё îⁿŝέŗŧеď вēťщ℮ěπ τĥę ρªτħѕ óƒ мџĺţīрℓé ƒĭļèś đяǿρрεδ ιйţθ ţħê ţèřмĭлªŀ. !!! !!! !!! !!! !!! !!! !!! !!! !!!</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>Введите текст для фильтрации значков</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>Перетащите разделитель</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>Этот текст будет вставлен между путями нескольких файлов, перетащенных в терминал.</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>键入以筛选图标</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>拖放分隔符</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>此文本将在放置到终端的多个文件的路径之间插入。</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -2606,19 +2606,19 @@
<comment>An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleWsl.Content" xml:space="preserve">
<value>WSL (C:\ -&gt; /mnt/c)</value>
<value>WSL (C:\ 🡒 /mnt/c)</value>
<comment>{Locked="WSL","C:\","/mnt/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleCygwin.Content" xml:space="preserve">
<value>Cygwin (C:\ -&gt; /cygdrive/c)</value>
<value>Cygwin (C:\ 🡒 /cygdrive/c)</value>
<comment>{Locked="Cygwin","C:\","/cygdrive/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMsys2.Content" xml:space="preserve">
<value>MSYS2 (C:\ -&gt; /c)</value>
<value>MSYS2 (C:\ 🡒 /c)</value>
<comment>{Locked="MSYS2","C:\","/c"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_PathTranslationStyleMinGW.Content" xml:space="preserve">
<value>MinGW (C:\ -&gt; C:/)</value>
<value>MinGW (C:\ 🡒 C:/)</value>
<comment>{Locked="MinGW","C:\","C:/"} An option to choose from for the "path translation" setting.</comment>
</data>
<data name="Profile_Delete_Orphaned.Header" xml:space="preserve">
@@ -2744,4 +2744,12 @@
<value>輸入以篩選圖示</value>
<comment>Placeholder text for a text box to filter and select an icon.</comment>
</data>
<data name="Profile_DragDropDelimiter.Header" xml:space="preserve">
<value>拖放分隔符號</value>
<comment>Header for a control to set the delimiter used when dragging multiple files into the terminal.</comment>
</data>
<data name="Profile_DragDropDelimiter.HelpText" xml:space="preserve">
<value>這些文字會插入多個丟入終端機的檔案路徑之間。</value>
<comment>A description for what the "drag drop delimiter" setting does.</comment>
</data>
</root>

View File

@@ -31,19 +31,18 @@ AppearanceConfig::AppearanceConfig(winrt::weak_ref<Model::Profile> sourceProfile
winrt::com_ptr<AppearanceConfig> AppearanceConfig::CopyAppearance(const AppearanceConfig* source, winrt::weak_ref<Model::Profile> sourceProfile)
{
auto appearance{ winrt::make_self<AppearanceConfig>(std::move(sourceProfile)) };
appearance->_Foreground = source->_Foreground;
appearance->_Background = source->_Background;
appearance->_SelectionBackground = source->_SelectionBackground;
appearance->_CursorColor = source->_CursorColor;
appearance->_Opacity = source->_Opacity;
appearance->_DarkColorSchemeName = source->_DarkColorSchemeName;
appearance->_LightColorSchemeName = source->_LightColorSchemeName;
appearance->_json = source->_json;
#define APPEARANCE_SETTINGS_COPY(type, name, jsonKey, ...) \
appearance->_##name = source->_##name;
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_COPY)
#undef APPEARANCE_SETTINGS_COPY
// JSON-backed settings (Foreground, Background, SelectionBackground, CursorColor,
// Opacity, DarkColorSchemeName, LightColorSchemeName, MTSM settings) all live in
// _json, which is already deep-copied above.
// Complex/mutable settings — backing fields for resolution lifecycle.
// _json (copied above) is the source of truth; backing fields hold resolved runtime state.
appearance->_PixelShaderPath = source->_PixelShaderPath;
appearance->_PixelShaderImagePath = source->_PixelShaderImagePath;
appearance->_BackgroundImagePath = source->_BackgroundImagePath;
return appearance;
}
@@ -52,33 +51,126 @@ Json::Value AppearanceConfig::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, ForegroundKey, _Foreground);
JsonUtils::SetValueForKey(json, BackgroundKey, _Background);
JsonUtils::SetValueForKey(json, SelectionBackgroundKey, _SelectionBackground);
JsonUtils::SetValueForKey(json, CursorColorKey, _CursorColor);
JsonUtils::SetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter<float, IntAsFloatPercentConversionTrait>{});
if (HasDarkColorSchemeName() || HasLightColorSchemeName())
// Nullable color settings: key presence matters (explicit null is valid)
for (const auto& key : { ForegroundKey, BackgroundKey, SelectionBackgroundKey, CursorColorKey })
{
// check if the setting is coming from the UI, if so grab the ColorSchemeName until the settings UI is fixed.
if (_LightColorSchemeName != _DarkColorSchemeName)
{
JsonUtils::SetValueForKey(json["colorScheme"], "dark", _DarkColorSchemeName);
JsonUtils::SetValueForKey(json["colorScheme"], "light", _LightColorSchemeName);
}
else
{
JsonUtils::SetValueForKey(json, "colorScheme", _DarkColorSchemeName);
}
JsonUtils::CopyKeyIfPresent(_json, json, key);
}
// Opacity: copy from _json (may be int or float — preserves original form)
JsonUtils::CopyKeyIfPresent(_json, json, OpacityKey);
// ColorScheme: ConversionTrait<ColorSchemeReference> handles string<->object form.
// _json already has the correct serialized form from SetValueForKey.
JsonUtils::CopyKeyIfPresent(_json, json, ColorSchemeKey);
// MTSM appearance settings: copy from _json (the source of truth)
#define APPEARANCE_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
JsonUtils::CopyKeyIfPresent(_json, json, jsonKey);
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_TO_JSON)
#undef APPEARANCE_SETTINGS_TO_JSON
// Complex/mutable settings — read from _json (source of truth), not backing fields
JsonUtils::CopyKeyIfPresent(_json, json, "experimental.pixelShaderPath");
JsonUtils::CopyKeyIfPresent(_json, json, "experimental.pixelShaderImagePath");
JsonUtils::CopyKeyIfPresent(_json, json, "backgroundImage");
return json;
}
bool AppearanceConfig::HasSetting(AppearanceSettingKey key) const
{
switch (key)
{
#define _APPEARANCE_HAS_SETTING(type, name, jsonKey, ...) \
case AppearanceSettingKey::name: \
return Has##name();
MTSM_APPEARANCE_SETTINGS(_APPEARANCE_HAS_SETTING)
#undef _APPEARANCE_HAS_SETTING
case AppearanceSettingKey::_Foreground:
return HasForeground();
case AppearanceSettingKey::_Background:
return HasBackground();
case AppearanceSettingKey::_SelectionBackground:
return HasSelectionBackground();
case AppearanceSettingKey::_CursorColor:
return HasCursorColor();
case AppearanceSettingKey::_Opacity:
return HasOpacity();
case AppearanceSettingKey::_DarkColorSchemeName:
return HasDarkColorSchemeName();
case AppearanceSettingKey::_LightColorSchemeName:
return HasLightColorSchemeName();
case AppearanceSettingKey::_PixelShaderPath:
return HasPixelShaderPath();
case AppearanceSettingKey::_PixelShaderImagePath:
return HasPixelShaderImagePath();
case AppearanceSettingKey::_BackgroundImagePath:
return HasBackgroundImagePath();
default:
return false;
}
}
void AppearanceConfig::ClearSetting(AppearanceSettingKey key)
{
switch (key)
{
#define _APPEARANCE_CLEAR_SETTING(type, name, jsonKey, ...) \
case AppearanceSettingKey::name: \
Clear##name(); \
break;
MTSM_APPEARANCE_SETTINGS(_APPEARANCE_CLEAR_SETTING)
#undef _APPEARANCE_CLEAR_SETTING
case AppearanceSettingKey::_Foreground:
ClearForeground();
break;
case AppearanceSettingKey::_Background:
ClearBackground();
break;
case AppearanceSettingKey::_SelectionBackground:
ClearSelectionBackground();
break;
case AppearanceSettingKey::_CursorColor:
ClearCursorColor();
break;
case AppearanceSettingKey::_Opacity:
ClearOpacity();
break;
case AppearanceSettingKey::_DarkColorSchemeName:
ClearDarkColorSchemeName();
break;
case AppearanceSettingKey::_LightColorSchemeName:
ClearLightColorSchemeName();
break;
case AppearanceSettingKey::_PixelShaderPath:
ClearPixelShaderPath();
break;
case AppearanceSettingKey::_PixelShaderImagePath:
ClearPixelShaderImagePath();
break;
case AppearanceSettingKey::_BackgroundImagePath:
ClearBackgroundImagePath();
break;
default:
break;
}
}
std::vector<AppearanceSettingKey> AppearanceConfig::CurrentSettings() const
{
std::vector<AppearanceSettingKey> result;
for (auto i = 0; i < static_cast<int>(AppearanceSettingKey::SETTINGS_SIZE); i++)
{
const auto key = static_cast<AppearanceSettingKey>(i);
if (HasSetting(key))
{
result.push_back(key);
}
}
return result;
}
// Method Description:
// - Layer values from the given json object on top of the existing properties
// of this object. For any keys we're expecting to be able to parse in the
@@ -92,45 +184,50 @@ Json::Value AppearanceConfig::ToJson() const
// - json: an object which should be a partial serialization of an AppearanceConfig object.
void AppearanceConfig::LayerJson(const Json::Value& json)
{
JsonUtils::GetValueForKey(json, ForegroundKey, _Foreground);
_logSettingIfSet(ForegroundKey, _Foreground.has_value());
// Merge incoming JSON keys into stored _json (key-wise, not replacement).
// AppearanceConfig receives the full profile JSON; we store all keys and
// read only appearance-relevant ones from it.
JsonUtils::MergeJsonKeys(json, _json);
JsonUtils::GetValueForKey(json, BackgroundKey, _Background);
_logSettingIfSet(BackgroundKey, _Background.has_value());
// Nullable color settings are now JSON-backed. Log which were set.
_logSettingIfSet(ForegroundKey, HasForeground());
_logSettingIfSet(BackgroundKey, HasBackground());
_logSettingIfSet(SelectionBackgroundKey, HasSelectionBackground());
_logSettingIfSet(CursorColorKey, HasCursorColor());
JsonUtils::GetValueForKey(json, SelectionBackgroundKey, _SelectionBackground);
_logSettingIfSet(SelectionBackgroundKey, _SelectionBackground.has_value());
JsonUtils::GetValueForKey(json, CursorColorKey, _CursorColor);
_logSettingIfSet(CursorColorKey, _CursorColor.has_value());
JsonUtils::GetValueForKey(json, LegacyAcrylicTransparencyKey, _Opacity);
JsonUtils::GetValueForKey(json, OpacityKey, _Opacity, JsonUtils::OptionalConverter<float, IntAsFloatPercentConversionTrait>{});
_logSettingIfSet(OpacityKey, _Opacity.has_value());
if (json["colorScheme"].isString())
// Normalize legacy opacity key into canonical _json key
if (json.isMember(JsonKey(LegacyAcrylicTransparencyKey)))
{
// to make the UI happy, set ColorSchemeName.
JsonUtils::GetValueForKey(json, ColorSchemeKey, _DarkColorSchemeName);
_LightColorSchemeName = _DarkColorSchemeName;
_logSettingSet(ColorSchemeKey);
_json[JsonKey(OpacityKey)] = json[JsonKey(LegacyAcrylicTransparencyKey)];
}
else if (json["colorScheme"].isObject())
// Normalize integer percent to float (e.g. 50 → 0.5)
if (_json.isMember(JsonKey(OpacityKey)) && _json[JsonKey(OpacityKey)].isInt())
{
// to make the UI happy, set ColorSchemeName to whatever the dark value is.
JsonUtils::GetValueForKey(json["colorScheme"], "dark", _DarkColorSchemeName);
JsonUtils::GetValueForKey(json["colorScheme"], "light", _LightColorSchemeName);
_logSettingSet("colorScheme.dark");
_logSettingSet("colorScheme.light");
_json[JsonKey(OpacityKey)] = _json[JsonKey(OpacityKey)].asInt() / 100.0f;
}
_logSettingIfSet(OpacityKey, HasOpacity());
// ColorScheme: ConversionTrait<ColorSchemeReference> handles string↔object normalization.
// The raw JSON is stored in _json; the trait interprets it on read.
_logSettingIfSet(ColorSchemeKey, HasColorSchemeRef());
// MTSM settings are now JSON-backed (no backing fields).
// Values are already in _json from the merge step above.
// We only need to log which settings were set in this layer.
#define APPEARANCE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
_logSettingIfSet(jsonKey, json.isMember(jsonKey) && !json[jsonKey].isNull());
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_LAYER_JSON)
#undef APPEARANCE_SETTINGS_LAYER_JSON
// Complex/mutable settings — backing fields populated from _json for resolution lifecycle.
// _json is the source of truth for serialization; backing fields are for resolution lifecycle.
JsonUtils::GetValueForKey(json, "experimental.pixelShaderPath", _PixelShaderPath);
_logSettingIfSet("experimental.pixelShaderPath", _PixelShaderPath.has_value());
JsonUtils::GetValueForKey(json, "experimental.pixelShaderImagePath", _PixelShaderImagePath);
_logSettingIfSet("experimental.pixelShaderImagePath", _PixelShaderImagePath.has_value());
JsonUtils::GetValueForKey(json, "backgroundImage", _BackgroundImagePath);
_logSettingIfSet("backgroundImage", _BackgroundImagePath.has_value());
}
winrt::Microsoft::Terminal::Settings::Model::Profile AppearanceConfig::SourceProfile()

View File

@@ -18,11 +18,80 @@ Author(s):
#include "AppearanceConfig.g.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "IInheritable.h"
#include "MTSMSettings.h"
#include "MediaResourceSupport.h"
#include <DefaultSettings.h>
// ColorSchemeReference: a pair of color scheme names for dark and light themes.
// Represents the polymorphic "colorScheme" JSON key, which can be either:
// "colorScheme": "Campbell" → { dark: "Campbell", light: "Campbell" }
// "colorScheme": { "dark": "X", "light": "Y" } → { dark: "X", light: "Y" }
struct ColorSchemeReference
{
winrt::hstring dark{ L"Campbell" };
winrt::hstring light{ L"Campbell" };
static ColorSchemeReference Default() { return {}; }
bool operator==(const ColorSchemeReference&) const = default;
};
namespace Microsoft::Terminal::Settings::Model::JsonUtils
{
template<>
struct ConversionTrait<ColorSchemeReference>
{
ColorSchemeReference FromJson(const Json::Value& json)
{
ColorSchemeReference result;
if (json.isString())
{
// Simple form: both dark and light use the same scheme
const auto name = winrt::hstring{ til::u8u16(json.asString()) };
result.dark = name;
result.light = name;
}
else if (json.isObject())
{
// Structured form: { "dark": "...", "light": "..." }
if (json.isMember("dark"))
{
result.dark = winrt::hstring{ til::u8u16(json["dark"].asString()) };
}
if (json.isMember("light"))
{
result.light = winrt::hstring{ til::u8u16(json["light"].asString()) };
}
}
return result;
}
bool CanConvert(const Json::Value& json) const
{
return json.isString() || json.isObject();
}
Json::Value ToJson(const ColorSchemeReference& val)
{
// Collapse to string when dark == light
if (val.dark == val.light)
{
return til::u16u8(val.dark);
}
Json::Value obj{ Json::ValueType::objectValue };
obj["dark"] = til::u16u8(val.dark);
obj["light"] = til::u16u8(val.light);
return obj;
}
std::string TypeDescription() const
{
return "color scheme name (string or {dark, light})";
}
};
}
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
struct AppearanceConfig : AppearanceConfigT<AppearanceConfig, IMediaResourceContainer>, IInheritable<AppearanceConfig>
@@ -38,22 +107,65 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void ResolveMediaResources(const Model::MediaResourceResolver& resolver);
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Foreground, nullptr);
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Background, nullptr);
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, SelectionBackground, nullptr);
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, CursorColor, nullptr);
INHERITABLE_SETTING(Model::IAppearanceConfig, float, Opacity, 1.0f);
// Generic setting access via SettingKey
bool HasSetting(AppearanceSettingKey key) const;
void ClearSetting(AppearanceSettingKey key);
std::vector<AppearanceSettingKey> CurrentSettings() const;
INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, DarkColorSchemeName, L"Campbell");
INHERITABLE_SETTING(Model::IAppearanceConfig, hstring, LightColorSchemeName, L"Campbell");
// Nullable color settings (JSON-backed)
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Foreground, "foreground", nullptr)
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, Background, "background", nullptr)
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, SelectionBackground, "selectionBackground", nullptr)
INHERITABLE_NULLABLE_SETTING(Model::IAppearanceConfig, Microsoft::Terminal::Core::Color, CursorColor, "cursorColor", nullptr)
// Opacity: JSON-backed with normalization (int percent → float in LayerJson)
INHERITABLE_SETTING(Model::IAppearanceConfig, float, Opacity, "opacity", 1.0f)
// ColorScheme: JSON-backed via INHERITABLE_SETTING with ColorSchemeReference struct.
// The polymorphic JSON key ("colorScheme" as string or object) is handled by
// ConversionTrait<ColorSchemeReference>. The macro provides Has/Clear/OverrideSource.
// Thin wrappers expose the IDL-required DarkColorSchemeName / LightColorSchemeName.
INHERITABLE_SETTING(Model::IAppearanceConfig, ColorSchemeReference, ColorSchemeRef, "colorScheme", ColorSchemeReference::Default())
public:
hstring DarkColorSchemeName() const { return ColorSchemeRef().dark; }
void DarkColorSchemeName(const hstring& value)
{
auto ref = ColorSchemeRef();
ref.dark = value;
ColorSchemeRef(ref);
}
bool HasDarkColorSchemeName() const { return HasColorSchemeRef(); }
void ClearDarkColorSchemeName() { ClearColorSchemeRef(); }
Model::IAppearanceConfig DarkColorSchemeNameOverrideSource() { return ColorSchemeRefOverrideSource(); }
hstring LightColorSchemeName() const { return ColorSchemeRef().light; }
void LightColorSchemeName(const hstring& value)
{
auto ref = ColorSchemeRef();
ref.light = value;
ColorSchemeRef(ref);
}
bool HasLightColorSchemeName() const { return HasColorSchemeRef(); }
void ClearLightColorSchemeName() { ClearColorSchemeRef(); }
Model::IAppearanceConfig LightColorSchemeNameOverrideSource() { return ColorSchemeRefOverrideSource(); }
#define APPEARANCE_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
INHERITABLE_SETTING(Model::IAppearanceConfig, type, name, ##__VA_ARGS__)
INHERITABLE_SETTING(Model::IAppearanceConfig, type, name, jsonKey, ##__VA_ARGS__)
MTSM_APPEARANCE_SETTINGS(APPEARANCE_SETTINGS_INITIALIZE)
#undef APPEARANCE_SETTINGS_INITIALIZE
// IMediaResource settings with backing fields for resolution lifecycle.
// See IInheritable.h for the INHERITABLE_MEDIA_RESOURCE_SETTING macro definition.
INHERITABLE_MEDIA_RESOURCE_SETTING(Model::IAppearanceConfig, PixelShaderPath, "experimental.pixelShaderPath", implementation::MediaResource::Empty())
INHERITABLE_MEDIA_RESOURCE_SETTING(Model::IAppearanceConfig, PixelShaderImagePath, "experimental.pixelShaderImagePath", implementation::MediaResource::Empty())
INHERITABLE_MEDIA_RESOURCE_SETTING(Model::IAppearanceConfig, BackgroundImagePath, "backgroundImage", implementation::MediaResource::Empty())
private:
winrt::weak_ref<Profile> _sourceProfile;
// Raw JSON for this layer (appearance-relevant keys only).
Json::Value _json{ Json::ValueType::objectValue };
std::set<std::string> _changeLog;
void _logSettingSet(const std::string_view& setting);

View File

@@ -12,6 +12,8 @@ using namespace Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
static constexpr std::string_view FontInfoKey{ "font" };
static constexpr std::string_view FontAxesKey{ "axes" };
static constexpr std::string_view FontFeaturesKey{ "features" };
static constexpr std::string_view LegacyFontFaceKey{ "fontFace" };
static constexpr std::string_view LegacyFontSizeKey{ "fontSize" };
static constexpr std::string_view LegacyFontWeightKey{ "fontWeight" };
@@ -25,29 +27,11 @@ winrt::com_ptr<FontConfig> FontConfig::CopyFontInfo(const FontConfig* source, wi
{
auto fontInfo{ winrt::make_self<FontConfig>(std::move(sourceProfile)) };
#define FONT_SETTINGS_COPY(type, name, jsonKey, ...) \
fontInfo->_##name = source->_##name;
MTSM_FONT_SETTINGS(FONT_SETTINGS_COPY)
#undef FONT_SETTINGS_COPY
fontInfo->_json = source->_json;
// We cannot simply copy the font axes and features with `fontInfo->_FontAxes = source->_FontAxes;`
// since that'll just create a reference; we have to manually copy the values.
static constexpr auto cloneFontMap = [](const IFontFeatureMap& map) {
std::map<winrt::hstring, float> fontAxes;
for (const auto& [k, v] : map)
{
fontAxes.emplace(k, v);
}
return winrt::single_threaded_map(std::move(fontAxes));
};
if (source->_FontAxes)
{
fontInfo->_FontAxes = cloneFontMap(*source->_FontAxes);
}
if (source->_FontFeatures)
{
fontInfo->_FontFeatures = cloneFontMap(*source->_FontFeatures);
}
// FontAxes and FontFeatures are now JSON-backed — handled by _json copy above.
// No manual deep-copy needed; the JSON-backed getter returns a fresh deserialized
// collection each time.
return fontInfo;
}
@@ -56,14 +40,58 @@ Json::Value FontConfig::ToJson() const
{
Json::Value json{ Json::ValueType::objectValue };
// MTSM font settings: copy from _json (the source of truth)
#define FONT_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
JsonUtils::CopyKeyIfPresent(_json, json, jsonKey);
MTSM_FONT_SETTINGS(FONT_SETTINGS_TO_JSON)
#undef FONT_SETTINGS_TO_JSON
return json;
}
bool FontConfig::HasSetting(FontSettingKey key) const
{
switch (key)
{
#define _FONT_HAS_SETTING(type, name, jsonKey, ...) \
case FontSettingKey::name: \
return Has##name();
MTSM_FONT_SETTINGS(_FONT_HAS_SETTING)
#undef _FONT_HAS_SETTING
default:
return false;
}
}
void FontConfig::ClearSetting(FontSettingKey key)
{
switch (key)
{
#define _FONT_CLEAR_SETTING(type, name, jsonKey, ...) \
case FontSettingKey::name: \
Clear##name(); \
break;
MTSM_FONT_SETTINGS(_FONT_CLEAR_SETTING)
#undef _FONT_CLEAR_SETTING
default:
break;
}
}
std::vector<FontSettingKey> FontConfig::CurrentSettings() const
{
std::vector<FontSettingKey> result;
for (auto i = 0; i < static_cast<int>(FontSettingKey::SETTINGS_SIZE); i++)
{
const auto key = static_cast<FontSettingKey>(i);
if (HasSetting(key))
{
result.push_back(key);
}
}
return result;
}
// Method Description:
// - Layer values from the given json object on top of the existing properties
// of this object. For any keys we're expecting to be able to parse in the
@@ -83,25 +111,38 @@ void FontConfig::LayerJson(const Json::Value& json)
{
// A font object is defined, use that
const auto fontInfoJson = json[JsonKey(FontInfoKey)];
#define FONT_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(fontInfoJson, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
// Merge the font sub-object into stored _json (font-object shape).
JsonUtils::MergeJsonKeys(fontInfoJson, _json);
// MTSM font settings are now JSON-backed (including FontAxes/FontFeatures).
// Values are already in _json. We only need to log which settings were set.
#define FONT_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
_logSettingIfSet(jsonKey, fontInfoJson.isMember(jsonKey) && !fontInfoJson[jsonKey].isNull());
MTSM_FONT_SETTINGS(FONT_SETTINGS_LAYER_JSON)
#undef FONT_SETTINGS_LAYER_JSON
}
else
{
// No font object is defined
// No font object is defined — normalize legacy flat keys into font-object shape.
if (json.isMember(JsonKey(LegacyFontFaceKey)))
{
_json["face"] = json[JsonKey(LegacyFontFaceKey)];
}
if (json.isMember(JsonKey(LegacyFontSizeKey)))
{
_json["size"] = json[JsonKey(LegacyFontSizeKey)];
}
if (json.isMember(JsonKey(LegacyFontWeightKey)))
{
_json["weight"] = json[JsonKey(LegacyFontWeightKey)];
}
// Log settings as if they were a part of the font object
JsonUtils::GetValueForKey(json, LegacyFontFaceKey, _FontFace);
_logSettingIfSet("face", _FontFace.has_value());
JsonUtils::GetValueForKey(json, LegacyFontSizeKey, _FontSize);
_logSettingIfSet("size", _FontSize.has_value());
JsonUtils::GetValueForKey(json, LegacyFontWeightKey, _FontWeight);
_logSettingIfSet("weight", _FontWeight.has_value());
_logSettingIfSet("face", json.isMember(JsonKey(LegacyFontFaceKey)));
_logSettingIfSet("size", json.isMember(JsonKey(LegacyFontSizeKey)));
_logSettingIfSet("weight", json.isMember(JsonKey(LegacyFontWeightKey)));
}
}
@@ -112,16 +153,16 @@ winrt::Microsoft::Terminal::Settings::Model::Profile FontConfig::SourceProfile()
void FontConfig::_logSettingSet(const std::string_view& setting)
{
if (setting == "axes" && _FontAxes.has_value())
if (setting == FontAxesKey && HasFontAxes())
{
for (const auto& [mapKey, _] : _FontAxes.value())
for (const auto& [mapKey, _] : FontAxes())
{
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, til::u16u8(mapKey)));
}
}
else if (setting == "features" && _FontFeatures.has_value())
else if (setting == FontFeaturesKey && HasFontFeatures())
{
for (const auto& [mapKey, _] : _FontFeatures.value())
for (const auto& [mapKey, _] : FontFeatures())
{
_changeLog.emplace(fmt::format(FMT_COMPILE("{}.{}"), setting, til::u16u8(mapKey)));
}

View File

@@ -19,6 +19,7 @@ Author(s):
#include "pch.h"
#include "FontConfig.g.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "MTSMSettings.h"
#include "IInheritable.h"
#include <DefaultSettings.h>
@@ -39,13 +40,22 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Model::Profile SourceProfile();
// Generic setting access via SettingKey
bool HasSetting(FontSettingKey key) const;
void ClearSetting(FontSettingKey key);
std::vector<FontSettingKey> CurrentSettings() const;
#define FONT_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
INHERITABLE_SETTING(Model::FontConfig, type, name, ##__VA_ARGS__)
INHERITABLE_SETTING(Model::FontConfig, type, name, jsonKey, ##__VA_ARGS__)
MTSM_FONT_SETTINGS(FONT_SETTINGS_INITIALIZE)
#undef FONT_SETTINGS_INITIALIZE
private:
winrt::weak_ref<Profile> _sourceProfile;
// Raw JSON for this layer (font sub-object shape).
Json::Value _json{ Json::ValueType::objectValue };
std::set<std::string> _changeLog;
void _logSettingSet(const std::string_view& setting);

View File

@@ -59,16 +59,13 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
{
auto globals{ winrt::make_self<GlobalAppSettings>() };
globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile;
globals->_defaultProfile = _defaultProfile;
globals->_actionMap = _actionMap->Copy();
globals->_keybindingsWarnings = _keybindingsWarnings;
globals->_json = _json;
#define GLOBAL_SETTINGS_COPY(type, name, jsonKey, ...) \
globals->_##name = _##name;
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_COPY)
#undef GLOBAL_SETTINGS_COPY
// JSON-backed settings (UnparsedDefaultProfile, MTSM settings) all live in _json,
// which is already deep-copied above. No per-setting copy needed.
if (_colorSchemes)
{
@@ -86,6 +83,9 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
globals->_themes.Insert(kv.Key(), *themeImpl->Copy());
}
}
// Complex/mutable settings — backing fields for mutation lifecycle.
// _json (copied above) is the source of truth; backing fields hold runtime state.
if (_NewTabMenu)
{
globals->_NewTabMenu = winrt::single_threaded_vector<Model::NewTabMenuEntry>();
@@ -94,14 +94,6 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::Copy() const
globals->_NewTabMenu->Append(get_self<NewTabMenuEntry>(entry)->Copy());
}
}
if (_DisabledProfileSources)
{
globals->_DisabledProfileSources = winrt::single_threaded_vector<hstring>();
for (const auto& src : *_DisabledProfileSources)
{
globals->_DisabledProfileSources->Append(src);
}
}
for (const auto& parent : _parents)
{
@@ -120,7 +112,7 @@ winrt::Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::Microso
void GlobalAppSettings::DefaultProfile(const winrt::guid& defaultProfile) noexcept
{
_defaultProfile = defaultProfile;
_UnparsedDefaultProfile = Utils::GuidToString(defaultProfile);
UnparsedDefaultProfile(hstring{ Utils::GuidToString(defaultProfile) });
}
winrt::guid GlobalAppSettings::DefaultProfile() const
@@ -150,25 +142,46 @@ winrt::com_ptr<GlobalAppSettings> GlobalAppSettings::FromJson(const Json::Value&
void GlobalAppSettings::LayerJson(const Json::Value& json, const OriginTag origin)
{
JsonUtils::GetValueForKey(json, DefaultProfileKey, _UnparsedDefaultProfile);
// Merge incoming JSON keys into stored _json (key-wise, not replacement).
JsonUtils::MergeJsonKeys(json, _json);
// UnparsedDefaultProfile is now JSON-backed (in _json["defaultProfile"]).
// No backing field to populate.
// GH#8076 - when adding enum values to this key, we also changed it from
// "useTabSwitcher" to "tabSwitcherMode". Continue supporting
// "useTabSwitcher", but prefer "tabSwitcherMode"
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyUseTabSwitcherModeKey, _TabSwitcherMode) || _fixupsAppliedDuringLoad;
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyInputServiceWarningKey, _InputServiceWarning) || _fixupsAppliedDuringLoad;
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyWarnAboutLargePasteKey, _WarnAboutLargePaste) || _fixupsAppliedDuringLoad;
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyWarnAboutMultiLinePasteKey, _WarnAboutMultiLinePaste) || _fixupsAppliedDuringLoad;
_fixupsAppliedDuringLoad = JsonUtils::GetValueForKey(json, LegacyConfirmCloseAllTabsKey, _ConfirmCloseAllTabs) || _fixupsAppliedDuringLoad;
// Normalize legacy keys into canonical _json keys for JSON-backed getters.
static constexpr std::pair<std::string_view, std::string_view> legacyKeyMappings[] = {
{ LegacyUseTabSwitcherModeKey, "tabSwitcherMode" },
{ LegacyInputServiceWarningKey, "warning.inputService" },
{ LegacyWarnAboutLargePasteKey, "warning.largePaste" },
{ LegacyWarnAboutMultiLinePasteKey, "warning.multiLinePaste" },
{ LegacyConfirmCloseAllTabsKey, "warning.confirmCloseAllTabs" },
};
for (const auto& [legacyKey, canonicalKey] : legacyKeyMappings)
{
if (json.isMember(JsonKey(legacyKey)))
{
_fixupsAppliedDuringLoad = true;
_json[JsonKey(canonicalKey)] = json[JsonKey(legacyKey)];
}
}
// MTSM settings are now JSON-backed (no backing fields).
// Values are already in _json from the merge step above.
// We only need to log which settings were set in this layer.
#define GLOBAL_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
_logSettingIfSet(jsonKey, json.isMember(jsonKey) && !json[jsonKey].isNull());
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_LAYER_JSON)
#undef GLOBAL_SETTINGS_LAYER_JSON
// Complex/mutable settings — backing fields populated from _json for runtime use.
// _json is the source of truth for serialization; backing fields are for mutation lifecycle.
JsonUtils::GetValueForKey(json, "newTabMenu", _NewTabMenu);
_logSettingIfSet("newTabMenu", _NewTabMenu.has_value());
// GH#11975 We only want to allow sensible values and prevent crashes, so we are clamping those values
// We only want to assign if the value did change through clamping,
// otherwise we could end up setting defaults that get persisted
@@ -304,47 +317,105 @@ Json::Value GlobalAppSettings::ToJson()
// These experimental options should be removed from the settings file if they're at their default value.
// This prevents them from sticking around forever, even if the user was just experimenting with them.
// One could consider this a workaround for the settings UI right now not having a "reset to default" button for these.
if (_GraphicsAPI == Control::GraphicsAPI::Automatic)
if (HasGraphicsAPI() && GraphicsAPI() == Control::GraphicsAPI::Automatic)
{
_GraphicsAPI.reset();
ClearGraphicsAPI();
}
if (_TextMeasurement == Control::TextMeasurement::Graphemes)
if (HasTextMeasurement() && TextMeasurement() == Control::TextMeasurement::Graphemes)
{
_TextMeasurement.reset();
ClearTextMeasurement();
}
if (_AmbiguousWidth == Control::AmbiguousWidth::Narrow)
if (HasAmbiguousWidth() && AmbiguousWidth() == Control::AmbiguousWidth::Narrow)
{
_AmbiguousWidth.reset();
ClearAmbiguousWidth();
}
if (_DefaultInputScope == Control::DefaultInputScope::Default)
if (HasDefaultInputScope() && DefaultInputScope() == Control::DefaultInputScope::Default)
{
_DefaultInputScope.reset();
ClearDefaultInputScope();
}
if (_DisablePartialInvalidation == false)
if (HasDisablePartialInvalidation() && DisablePartialInvalidation() == false)
{
_DisablePartialInvalidation.reset();
ClearDisablePartialInvalidation();
}
if (_SoftwareRendering == false)
if (HasSoftwareRendering() && SoftwareRendering() == false)
{
_SoftwareRendering.reset();
ClearSoftwareRendering();
}
Json::Value json{ Json::ValueType::objectValue };
JsonUtils::SetValueForKey(json, DefaultProfileKey, _UnparsedDefaultProfile);
// DefaultProfile: copy from _json
JsonUtils::CopyKeyIfPresent(_json, json, DefaultProfileKey);
// MTSM global settings: copy from _json (the source of truth)
#define GLOBAL_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
JsonUtils::CopyKeyIfPresent(_json, json, jsonKey);
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_TO_JSON)
#undef GLOBAL_SETTINGS_TO_JSON
// Complex/mutable settings — read from _json (source of truth), not backing fields
JsonUtils::CopyKeyIfPresent(_json, json, "newTabMenu");
json[JsonKey(ActionsKey)] = _actionMap->ToJson();
json[JsonKey(KeybindingsKey)] = _actionMap->KeyBindingsToJson();
return json;
}
bool GlobalAppSettings::HasSetting(GlobalSettingKey key) const
{
switch (key)
{
#define _GLOBAL_HAS_SETTING(type, name, jsonKey, ...) \
case GlobalSettingKey::name: \
return Has##name();
MTSM_GLOBAL_SETTINGS(_GLOBAL_HAS_SETTING)
#undef _GLOBAL_HAS_SETTING
case GlobalSettingKey::_UnparsedDefaultProfile:
return HasUnparsedDefaultProfile();
case GlobalSettingKey::_NewTabMenu:
return HasNewTabMenu();
default:
return false;
}
}
void GlobalAppSettings::ClearSetting(GlobalSettingKey key)
{
switch (key)
{
#define _GLOBAL_CLEAR_SETTING(type, name, jsonKey, ...) \
case GlobalSettingKey::name: \
Clear##name(); \
break;
MTSM_GLOBAL_SETTINGS(_GLOBAL_CLEAR_SETTING)
#undef _GLOBAL_CLEAR_SETTING
case GlobalSettingKey::_UnparsedDefaultProfile:
ClearUnparsedDefaultProfile();
break;
case GlobalSettingKey::_NewTabMenu:
ClearNewTabMenu();
break;
default:
break;
}
}
std::vector<GlobalSettingKey> GlobalAppSettings::CurrentSettings() const
{
std::vector<GlobalSettingKey> result;
for (auto i = 0; i < static_cast<int>(GlobalSettingKey::SETTINGS_SIZE); i++)
{
const auto key = static_cast<GlobalSettingKey>(i);
if (HasSetting(key))
{
result.push_back(key);
}
}
return result;
}
bool GlobalAppSettings::FixupsAppliedDuringLoad()
{
return _fixupsAppliedDuringLoad || _actionMap->FixupsAppliedDuringLoad();
@@ -394,9 +465,9 @@ bool GlobalAppSettings::ShouldUsePersistedLayout() const
void GlobalAppSettings::ResolveMediaResources(const Model::MediaResourceResolver& resolver)
{
_actionMap->ResolveMediaResourcesWithBasePath(SourceBasePath, resolver);
if (_NewTabMenu)
if (HasNewTabMenu())
{
for (const auto& entry : *_NewTabMenu)
for (const auto& entry : NewTabMenu())
{
if (const auto resolvable{ entry.try_as<IPathlessMediaResourceContainer>() })
{
@@ -414,11 +485,12 @@ void GlobalAppSettings::_logSettingSet(const std::string_view& setting)
{
if (setting == "theme")
{
if (_Theme.has_value())
if (HasTheme())
{
const auto theme = Theme();
// ThemePair always has a Dark/Light value,
// so we need to check if they were explicitly set
if (_Theme->DarkName() == _Theme->LightName())
if (theme.DarkName() == theme.LightName())
{
_changeLog.emplace(setting);
}
@@ -431,9 +503,9 @@ void GlobalAppSettings::_logSettingSet(const std::string_view& setting)
}
else if (setting == "newTabMenu")
{
if (_NewTabMenu.has_value())
if (HasNewTabMenu())
{
for (const auto& entry : *_NewTabMenu)
for (const auto& entry : NewTabMenu())
{
std::string entryType;
switch (entry.Type())
@@ -476,7 +548,7 @@ void GlobalAppSettings::UpdateCommandID(const Model::Command& cmd, winrt::hstrin
_actionMap->UpdateCommandID(cmd, newID);
// newID might have been empty when this function was called, if so actionMap would have generated a new ID, use that
newID = cmd.ID();
if (_NewTabMenu)
if (HasNewTabMenu())
{
// Recursive lambda function to look through all the new tab menu entries and update IDs accordingly
std::function<void(const Model::NewTabMenuEntry&)> recursiveEntryIdUpdate;
@@ -503,7 +575,7 @@ void GlobalAppSettings::UpdateCommandID(const Model::Command& cmd, winrt::hstrin
}
};
for (const auto& entry : *_NewTabMenu)
for (const auto& entry : NewTabMenu())
{
recursiveEntryIdUpdate(entry);
}
@@ -515,8 +587,8 @@ void GlobalAppSettings::_logSettingIfSet(const std::string_view& setting, const
if (isSet)
{
// Exclude some false positives from userDefaults.json
const bool settingCopyFormattingToDefault = til::equals_insensitive_ascii(setting, "copyFormatting") && _CopyFormatting.has_value() && _CopyFormatting.value() == static_cast<Control::CopyFormat>(0);
const bool settingNTMToDefault = til::equals_insensitive_ascii(setting, "newTabMenu") && _NewTabMenu.has_value() && _NewTabMenu->Size() == 1 && _NewTabMenu->GetAt(0).Type() == NewTabMenuEntryType::RemainingProfiles;
const bool settingCopyFormattingToDefault = til::equals_insensitive_ascii(setting, "copyFormatting") && HasCopyFormatting() && CopyFormatting() == static_cast<Control::CopyFormat>(0);
const bool settingNTMToDefault = til::equals_insensitive_ascii(setting, "newTabMenu") && HasNewTabMenu() && NewTabMenu().Size() == 1 && NewTabMenu().GetAt(0).Type() == NewTabMenuEntryType::RemainingProfiles;
if (!settingCopyFormattingToDefault && !settingNTMToDefault)
{
_logSettingSet(setting);

View File

@@ -18,6 +18,7 @@ Author(s):
#include "GlobalAppSettings.g.h"
#include "IInheritable.h"
#include "MTSMSettings.h"
#include "TerminalSettingsSerializationHelpers.h"
#include "ActionMap.h"
#include "Command.h"
@@ -56,6 +57,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
Json::Value ToJson();
bool FixupsAppliedDuringLoad();
// Generic setting access via SettingKey
bool HasSetting(GlobalSettingKey key) const;
void ClearSetting(GlobalSettingKey key);
std::vector<GlobalSettingKey> CurrentSettings() const;
const std::vector<SettingsLoadWarnings>& KeybindingsWarnings() const;
// This DefaultProfile() setter is called by CascadiaSettings,
@@ -80,13 +86,28 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
winrt::hstring SourceBasePath;
INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, L"");
INHERITABLE_SETTING(Model::GlobalAppSettings, hstring, UnparsedDefaultProfile, "defaultProfile", L"");
#define GLOBAL_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
INHERITABLE_SETTING_WITH_LOGGING(Model::GlobalAppSettings, type, name, jsonKey, ##__VA_ARGS__)
MTSM_GLOBAL_SETTINGS(GLOBAL_SETTINGS_INITIALIZE)
#undef GLOBAL_SETTINGS_INITIALIZE
// NewTabMenu: mutable with backing field for editor in-place mutation.
// _json is the source of truth for serialization. Setter dual-writes to both.
_BASE_INHERITABLE_MUTABLE_SETTING(Model::GlobalAppSettings, std::optional<winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>>, NewTabMenu, winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} }))
public:
winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry> NewTabMenu() const
{
const auto val{ _getNewTabMenuImpl() };
return val ? *val : winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} });
}
void NewTabMenu(const winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>& value)
{
_NewTabMenu = value;
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(_json, "newTabMenu", value);
}
private:
#ifdef NDEBUG
static constexpr bool debugFeaturesDefault{ false };
@@ -94,6 +115,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
static constexpr bool debugFeaturesDefault{ true };
#endif
// Raw JSON for this layer. Populated by LayerJson(), will become the
// source of truth for settings once the JSON-backed refactor is complete.
Json::Value _json{ Json::ValueType::objectValue };
winrt::guid _defaultProfile{};
bool _fixupsAppliedDuringLoad{ false };
bool _legacyReloadEnvironmentVariables{ true };

View File

@@ -14,6 +14,8 @@ Author(s):
--*/
#pragma once
#include "JsonUtils.h"
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
template<typename T>
@@ -79,189 +81,554 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
using NullableSetting = std::optional<std::optional<T>>;
}
// Shared implementation between the INHERITABLE_SETTING and INHERITABLE_NULLABLE_SETTING macros.
// See below for more details.
#define _BASE_INHERITABLE_SETTING(projectedType, storageType, name, ...) \
public: \
/* Returns true if the user explicitly set the value, false otherwise*/ \
bool Has##name() const \
{ \
return _##name.has_value(); \
} \
\
projectedType name##OverrideSource() \
{ \
/*user set value was not set*/ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return *source; \
} \
} \
\
/*no source was found*/ \
return nullptr; \
} \
\
/* Clear the user set value */ \
void Clear##name() \
{ \
_##name = std::nullopt; \
} \
\
private: \
storageType _##name{ std::nullopt }; \
\
storageType _get##name##Impl() const \
{ \
/*return user set value*/ \
if (_##name) \
{ \
return _##name; \
} \
\
/*user set value was not set*/ \
/*iterate through parents to find a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto val{ parent->_get##name##Impl() }) \
{ \
return val; \
} \
} \
\
/*no value was found*/ \
return std::nullopt; \
} \
\
auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \
{ \
/*we have a value*/ \
if (_##name) \
{ \
return get_strong(); \
} \
\
/*user set value was not set*/ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return source; \
} \
} \
\
/*no value was found*/ \
return nullptr; \
} \
\
auto _get##name##OverrideSourceAndValueImpl() \
->std::pair<decltype(get_strong()), storageType> \
{ \
/*we have a value*/ \
if (_##name) \
{ \
return { get_strong(), _##name }; \
} \
\
/*user set value was not set*/ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return { source, source->_##name }; \
} \
} \
\
/*no value was found*/ \
return { nullptr, std::nullopt }; \
// =============================================================================
// Inheritable setting macros
// =============================================================================
//
// These macros implement the standard functions for inheritable settings:
// - Has<NAME>(): checks if the user explicitly set a value for this setting
// - <NAME>OverrideSource(): returns the object that provides the resolved value
// - Getter(): returns the resolved value (this layer --> parent --> default)
// - Setter(): sets the value
// - Clear<NAME>(): clears the user set value (reverts to inherited/default)
//
// There are two storage strategies:
//
// 1. JSON-backed (preferred): All state lives in _json (a Json::Value member).
// Use these for any setting with a ConversionTrait. The owning class must
// declare a `Json::Value _json{ Json::ValueType::objectValue };` member.
//
// 2. Mutable backing-field (exception): State lives in a std::optional<T> member.
// Use these only for types that callers mutate in place after retrieval, such
// as IMediaResource (resolved via ResolveMediaResources), IVector, and IMap.
// A JSON-backed getter would deserialize a fresh copy on each call, losing
// those in-place mutations.
//
// When adding a new setting, use the JSON-backed macros unless the type requires
// in-place mutation. Existing backing-field settings should be migrated to
// JSON-backed as their mutation patterns are resolved.
//
// Available macros (JSON-backed):
// INHERITABLE_SETTING(projectedType, type, name, jsonKey, ...)
// INHERITABLE_SETTING_WITH_LOGGING(projectedType, type, name, jsonKey, ...)
// INHERITABLE_NULLABLE_SETTING(projectedType, type, name, jsonKey, ...)
// _BASE_INHERITABLE_SETTING(projectedType, type, name, jsonKey, ...)
//
// Available macros (mutable backing-field):
// INHERITABLE_MUTABLE_SETTING(projectedType, type, name, ...)
// INHERITABLE_NULLABLE_MUTABLE_SETTING(projectedType, type, name, ...)
//
// =============================================================================
// =============================================================================
// JSON-backed inheritable settings
// =============================================================================
// No std::optional<T> backing field — _json is the source of truth.
// Getters deserialize from _json via ConversionTrait on each call.
// Setters serialize to _json via SetValueForKey.
// Has/Clear check/modify _json directly.
// Shared base for JSON-backed inheritable settings.
// Provides Has, Clear, OverrideSource, and the private implementation helpers.
// Does NOT provide the public getter/setter — use INHERITABLE_SETTING or
// INHERITABLE_SETTING_WITH_LOGGING which add those on top of this base.
#define _BASE_INHERITABLE_SETTING(projectedType, type, name, jsonKey, ...) \
public: \
/* Returns true if the user explicitly set the value, false otherwise */ \
bool Has##name() const \
{ \
return _json.isMember(jsonKey) && !_json[jsonKey].isNull(); \
} \
\
/* Returns the object that provides the resolved value */ \
projectedType name##OverrideSource() \
{ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return *source; \
} \
} \
\
/*no source was found*/ \
return nullptr; \
} \
\
/* Clear the user set value */ \
void Clear##name() \
{ \
_json.removeMember(JsonKey(jsonKey)); \
} \
\
private: \
/* Read value from this layer's _json only (no parent walk) */ \
std::optional<type> _get##name##FromThisLayer() const \
{ \
if (Has##name()) \
{ \
type val{ __VA_ARGS__ }; \
::Microsoft::Terminal::Settings::Model::JsonUtils::GetValueForKey( \
_json, jsonKey, val); \
return val; \
} \
return std::nullopt; \
} \
\
/* Resolve the value: this layer --> parents --> nullopt */ \
std::optional<type> _get##name##Impl() const \
{ \
/*return value from this layer*/ \
if (auto val{ _get##name##FromThisLayer() }) \
{ \
return val; \
} \
\
/*iterate through parents to find a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto val{ parent->_get##name##Impl() }) \
{ \
return val; \
} \
} \
\
/*no value was found*/ \
return std::nullopt; \
} \
\
/* Find the object that provides the resolved value */ \
auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \
{ \
/*we have a value*/ \
if (Has##name()) \
{ \
return get_strong(); \
} \
\
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return source; \
} \
} \
\
/*no value was found*/ \
return nullptr; \
} \
\
/* Find the override source and the value it provides */ \
auto _get##name##OverrideSourceAndValueImpl() \
->std::pair<decltype(get_strong()), std::optional<type>> \
{ \
/*we have a value*/ \
if (Has##name()) \
{ \
return { get_strong(), _get##name##FromThisLayer() }; \
} \
\
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return { source, source->_get##name##FromThisLayer() }; \
} \
} \
\
/*no value was found*/ \
return { nullptr, std::nullopt }; \
}
// Use INHERITABLE_SETTING and INHERITABLE_NULLABLE_SETTING to implement
// standard functions for inheritable settings. This is similar to the WINRT_PROPERTY macro,
// except...
// - Has<NAME>(): checks if the user explicitly set a value for this setting
// - <NAME>OverrideSource(): return the object that provides the resolved value
// - Getter(): return the resolved value
// - Setter(): set the value directly
// - Clear(): clear the user set value
// - the setting is saved as an optional, where nullopt means
// that we must inherit the value from our parent
#define INHERITABLE_SETTING(projectedType, type, name, ...) \
_BASE_INHERITABLE_SETTING(projectedType, std::optional<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: user set value --> inherited value --> system set value */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const type& value) \
{ \
_##name = value; \
// JSON-backed inheritable setting.
// Getter returns the resolved value: this layer --> inherited --> default.
// Setter writes the value to _json.
#define INHERITABLE_SETTING(projectedType, type, name, jsonKey, ...) \
_BASE_INHERITABLE_SETTING(projectedType, type, name, jsonKey, __VA_ARGS__) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value in _json */ \
void name(const type& value) \
{ \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey( \
_json, jsonKey, value); \
}
#define INHERITABLE_SETTING_WITH_LOGGING(projectedType, type, name, jsonKey, ...) \
_BASE_INHERITABLE_SETTING(projectedType, std::optional<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: user set value --> inherited value --> system set value */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const type& value) \
{ \
if (!_##name.has_value() || _##name.value() != value) \
{ \
_logSettingSet(jsonKey); \
} \
_##name = value; \
// JSON-backed inheritable setting with change logging.
// Same as INHERITABLE_SETTING, but the setter also calls _logSettingSet(jsonKey)
// when the value actually changes (for settings change telemetry).
#define INHERITABLE_SETTING_WITH_LOGGING(projectedType, type, name, jsonKey, ...) \
_BASE_INHERITABLE_SETTING(projectedType, type, name, jsonKey, __VA_ARGS__) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value, log the change, and write to _json */ \
void name(const type& value) \
{ \
const auto existingVal{ _get##name##FromThisLayer() }; \
if (!existingVal.has_value() || existingVal.value() != value) \
{ \
_logSettingSet(jsonKey); \
} \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey( \
_json, jsonKey, value); \
}
// This macro is similar to the one above, but is reserved for optional settings
// like Profile.Foreground (where null is interpreted
// as an acceptable value, rather than "inherit")
// "type" is exposed as an IReference
#define INHERITABLE_NULLABLE_SETTING(projectedType, type, name, ...) \
_BASE_INHERITABLE_SETTING(projectedType, NullableSetting<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: user set value --> inherited value --> system set value */ \
winrt::Windows::Foundation::IReference<type> name() const \
{ \
const auto val{ _get##name##Impl() }; \
if (val) \
{ \
if (*val) \
{ \
return **val; \
} \
return nullptr; \
} \
return winrt::Windows::Foundation::IReference<type>{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const winrt::Windows::Foundation::IReference<type>& value) \
{ \
if (value) /*set value is different*/ \
{ \
_##name = std::optional<type>{ value.Value() }; \
} \
else \
{ \
/* note we're setting the _inner_ value */ \
_##name = std::optional<type>{ std::nullopt }; \
} \
// JSON-backed nullable inheritable setting.
// For settings where null is a valid explicit value (e.g., Foreground color).
// Key absent → inherit from parent
// Key present + null → explicitly "no value" (getter returns nullptr)
// Key present + value → has a value
// Getter returns IReference<T>.
#define INHERITABLE_NULLABLE_SETTING(projectedType, type, name, jsonKey, ...) \
public: \
/* Key presence means explicitly set (even if value is null) */ \
bool Has##name() const \
{ \
return _json.isMember(jsonKey); \
} \
\
/* Returns the object that provides the resolved value */ \
projectedType name##OverrideSource() \
{ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return *source; \
} \
} \
return nullptr; \
} \
\
/* Clear the user set value (remove key entirely → inherit from parent) */ \
void Clear##name() \
{ \
_json.removeMember(JsonKey(jsonKey)); \
} \
\
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
winrt::Windows::Foundation::IReference<type> name() const \
{ \
const auto val{ _get##name##Impl() }; \
if (val) \
{ \
if (*val) \
{ \
return **val; \
} \
return nullptr; \
} \
return winrt::Windows::Foundation::IReference<type>{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value in _json */ \
void name(const winrt::Windows::Foundation::IReference<type>& value) \
{ \
if (value) \
{ \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey( \
_json, jsonKey, value.Value()); \
} \
else \
{ \
/* explicitly set to null (not the same as clearing) */ \
_json[JsonKey(jsonKey)] = Json::nullValue; \
} \
} \
\
private: \
/* Read nullable value from this layer's _json only (no parent walk) */ \
/* Returns: nullopt = not set. optional{nullopt} = explicitly null. */ \
/* optional{optional{val}} = has value. */ \
NullableSetting<type> _get##name##FromThisLayer() const \
{ \
if (_json.isMember(jsonKey)) \
{ \
if (_json[jsonKey].isNull()) \
{ \
return std::optional<type>{ std::nullopt }; \
} \
type val{}; \
::Microsoft::Terminal::Settings::Model::JsonUtils::GetValueForKey( \
_json, jsonKey, val); \
return std::optional<type>{ val }; \
} \
return std::nullopt; \
} \
\
/* Resolve the nullable value: this layer --> parents --> nullopt */ \
NullableSetting<type> _get##name##Impl() const \
{ \
if (auto val{ _get##name##FromThisLayer() }) \
{ \
return val; \
} \
for (const auto& parent : _parents) \
{ \
if (auto val{ parent->_get##name##Impl() }) \
{ \
return val; \
} \
} \
return std::nullopt; \
} \
\
/* Find the object that provides the resolved value */ \
auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \
{ \
if (Has##name()) \
{ \
return get_strong(); \
} \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return source; \
} \
} \
return nullptr; \
} \
\
/* Find the override source and the value it provides */ \
auto _get##name##OverrideSourceAndValueImpl() \
->std::pair<decltype(get_strong()), NullableSetting<type>> \
{ \
if (Has##name()) \
{ \
return { get_strong(), _get##name##FromThisLayer() }; \
} \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return { source, source->_get##name##FromThisLayer() }; \
} \
} \
return { nullptr, std::nullopt }; \
}
// =============================================================================
// Mutable backing-field inheritable settings
// =============================================================================
// These use a std::optional<T> backing field instead of _json.
//
// Use these ONLY for types that callers mutate in place after retrieval.
// A JSON-backed getter would deserialize a fresh copy on each call, so
// in-place mutations (e.g. ResolveMediaResources) would be lost.
//
// Current uses:
// Profile: Icon, EnvironmentVariables, BellSound, UnfocusedAppearance
// GlobalAppSettings: DisabledProfileSources, NewTabMenu
// AppearanceConfig: PixelShaderPath, PixelShaderImagePath, BackgroundImagePath,
// DarkColorSchemeName, LightColorSchemeName
// FontConfig: FontAxes, FontFeatures
// Shared base for backing-field inheritable settings.
// Provides Has, Clear, OverrideSource, and the private implementation helpers.
#define _BASE_INHERITABLE_MUTABLE_SETTING(projectedType, storageType, name, ...) \
public: \
/* Returns true if the user explicitly set the value, false otherwise */ \
bool Has##name() const \
{ \
return _##name.has_value(); \
} \
\
/* Returns the object that provides the resolved value */ \
projectedType name##OverrideSource() \
{ \
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return *source; \
} \
} \
\
/*no source was found*/ \
return nullptr; \
} \
\
/* Clear the user set value */ \
void Clear##name() \
{ \
_##name = std::nullopt; \
} \
\
private: \
storageType _##name{ std::nullopt }; \
\
/* Resolve the value: this layer --> parents --> nullopt */ \
storageType _get##name##Impl() const \
{ \
/*return user set value*/ \
if (_##name) \
{ \
return _##name; \
} \
\
/*iterate through parents to find a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto val{ parent->_get##name##Impl() }) \
{ \
return val; \
} \
} \
\
/*no value was found*/ \
return std::nullopt; \
} \
\
/* Find the object that provides the resolved value */ \
auto _get##name##OverrideSourceImpl()->decltype(get_strong()) \
{ \
/*we have a value*/ \
if (_##name) \
{ \
return get_strong(); \
} \
\
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return source; \
} \
} \
\
/*no value was found*/ \
return nullptr; \
} \
\
/* Find the override source and the value it provides */ \
auto _get##name##OverrideSourceAndValueImpl() \
->std::pair<decltype(get_strong()), storageType> \
{ \
/*we have a value*/ \
if (_##name) \
{ \
return { get_strong(), _##name }; \
} \
\
/*iterate through parents to find one with a value*/ \
for (const auto& parent : _parents) \
{ \
if (auto source{ parent->_get##name##OverrideSourceImpl() }) \
{ \
return { source, source->_##name }; \
} \
} \
\
/*no value was found*/ \
return { nullptr, std::nullopt }; \
}
// Mutable backing-field setting.
// Getter returns the resolved value: this layer --> inherited --> default.
// Setter writes to the std::optional<T> backing field.
#define INHERITABLE_MUTABLE_SETTING(projectedType, type, name, ...) \
_BASE_INHERITABLE_MUTABLE_SETTING(projectedType, std::optional<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
type name() const \
{ \
const auto val{ _get##name##Impl() }; \
return val ? *val : type{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const type& value) \
{ \
_##name = value; \
}
// Mutable nullable backing-field setting.
// Same as INHERITABLE_MUTABLE_SETTING, but for optional settings where null is
// a valid explicit value. Getter returns IReference<T>.
#define INHERITABLE_NULLABLE_MUTABLE_SETTING(projectedType, type, name, ...) \
_BASE_INHERITABLE_MUTABLE_SETTING(projectedType, NullableSetting<type>, name, ...) \
public: \
/* Returns the resolved value for this setting */ \
/* fallback: this layer --> inherited value --> default */ \
winrt::Windows::Foundation::IReference<type> name() const \
{ \
const auto val{ _get##name##Impl() }; \
if (val) \
{ \
if (*val) \
{ \
return **val; \
} \
return nullptr; \
} \
return winrt::Windows::Foundation::IReference<type>{ __VA_ARGS__ }; \
} \
\
/* Overwrite the user set value */ \
void name(const winrt::Windows::Foundation::IReference<type>& value) \
{ \
if (value) \
{ \
_##name = std::optional<type>{ value.Value() }; \
} \
else \
{ \
/* explicitly set to null (not the same as clearing) */ \
_##name = std::optional<type>{ std::nullopt }; \
} \
}
// =============================================================================
// IMediaResource settings with backing fields for resolution lifecycle.
// =============================================================================
//
// Use for settings of type IMediaResource that need in-place resolution
// (e.g., Icon, PixelShaderPath, BackgroundImagePath). The backing field
// holds the runtime-resolved object. _json is the source of truth for
// serialization. The setter dual-writes to both.
//
// Parameters:
// projectedType - the WinRT projected type (e.g., Model::Profile)
// name - the setting name (e.g., Icon)
// jsonKey - the JSON key (e.g., "icon")
// ... - default value (e.g., implementation::MediaResource::Empty())
//
#define INHERITABLE_MEDIA_RESOURCE_SETTING(projectedType, name, jsonKey, ...) \
_BASE_INHERITABLE_MUTABLE_SETTING(projectedType, std::optional<IMediaResource>, name, \
implementation::MediaResource::Empty()) \
public: \
/* Returns the resolved value: this layer --> inherited --> default */ \
IMediaResource name() const \
{ \
const auto v{ _get##name##Impl() }; \
return v ? *v : IMediaResource{ __VA_ARGS__ }; \
} \
\
/* Dual-write: backing field (for resolution) + _json (for serialization) */ \
void name(const IMediaResource& value) \
{ \
_##name = value; \
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(_json, jsonKey, value); \
}

View File

@@ -250,6 +250,28 @@ namespace Microsoft::Terminal::Settings::Model::JsonUtils
SetValueForKey(json, key, target, ConversionTrait<std::decay_t<T>>{});
}
// CopyKeyIfPresent: copies a key's raw Json::Value from source to target
// if it exists in source. No type conversion — just raw JSON copy.
// Used in ToJson() to copy JSON-backed settings from _json to output.
inline void CopyKeyIfPresent(const Json::Value& source, Json::Value& target, const std::string_view key)
{
if (source.isMember(JsonKey(key)))
{
target[JsonKey(key)] = source[JsonKey(key)];
}
}
// MergeJsonKeys: copies all keys from source into target (key-wise merge).
// Existing keys in target are overwritten by source values.
// Used in LayerJson() to merge incoming JSON into stored _json.
inline void MergeJsonKeys(const Json::Value& source, Json::Value& target)
{
for (const auto& key : source.getMemberNames())
{
target[key] = source[key];
}
}
template<>
struct ConversionTrait<std::string>
{

View File

@@ -8,6 +8,7 @@ Module Name:
Abstract:
- Contains most of the settings within Terminal Settings Model (global, profile, font, appearance)
- To add a new setting to any one of those classes, simply add it to the respective list below, following the macro format
- Also defines SettingKey enums for generic setting access
Author(s):
- Pankaj Bhojwani - October 2021
@@ -62,16 +63,15 @@ Author(s):
X(Model::WindowingMode, WindowingBehavior, "windowingBehavior", Model::WindowingMode::UseNew) \
X(bool, MinimizeToNotificationArea, "minimizeToNotificationArea", false) \
X(bool, AlwaysShowNotificationIcon, "alwaysShowNotificationIcon", false) \
X(winrt::Windows::Foundation::Collections::IVector<winrt::hstring>, DisabledProfileSources, "disabledProfileSources", nullptr) \
X(bool, ShowAdminShield, "showAdminShield", true) \
X(bool, TrimPaste, "trimPaste", true) \
X(bool, EnableColorSelection, "experimental.enableColorSelection", false) \
X(bool, EnableShellCompletionMenu, "experimental.enableShellCompletionMenu", false) \
X(bool, EnableUnfocusedAcrylic, "compatibility.enableUnfocusedAcrylic", true) \
X(winrt::Windows::Foundation::Collections::IVector<Model::NewTabMenuEntry>, NewTabMenu, "newTabMenu", winrt::single_threaded_vector<Model::NewTabMenuEntry>({ Model::RemainingProfilesEntry{} })) \
X(bool, AllowHeadless, "compatibility.allowHeadless", false) \
X(hstring, SearchWebDefaultQueryUrl, "searchWebDefaultQueryUrl", L"https://www.bing.com/search?q=%22%s%22") \
X(bool, ShowTabsFullscreen, "showTabsFullscreen", false)
X(bool, ShowTabsFullscreen, "showTabsFullscreen", false) \
X(winrt::Windows::Foundation::Collections::IVector<winrt::hstring>, DisabledProfileSources, "disabledProfileSources", nullptr)
// Also add these settings to:
// * Profile.idl
@@ -88,15 +88,12 @@ Author(s):
X(Microsoft::Terminal::Control::ScrollbarState, ScrollState, "scrollbarState", Microsoft::Terminal::Control::ScrollbarState::Visible) \
X(Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, "antialiasingMode", Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale) \
X(hstring, StartingDirectory, "startingDirectory") \
X(IMediaResource, Icon, "icon", implementation::MediaResource::FromString(L"\uE756")) \
X(bool, SuppressApplicationTitle, "suppressApplicationTitle", false) \
X(guid, ConnectionType, "connectionType") \
X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic) \
X(hstring, TabTitle, "tabTitle") \
X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible) \
X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr) \
X(bool, RightClickContextMenu, "rightClickContextMenu", false) \
X(Windows::Foundation::Collections::IVector<IMediaResource>, BellSound, "bellSound", nullptr) \
X(bool, Elevate, "elevate", false) \
X(bool, AutoMarkPrompts, "autoMarkPrompts", true) \
X(bool, ShowMarks, "showMarksOnScrollbar", false) \
@@ -109,7 +106,8 @@ Author(s):
X(bool, AllowVtClipboardWrite, "compatibility.allowOSC52", true) \
X(bool, AllowKeypadMode, "compatibility.allowDECNKM", false) \
X(hstring, DragDropDelimiter, "dragDropDelimiter", L" ") \
X(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, "pathTranslationStyle", Microsoft::Terminal::Control::PathTranslationStyle::None)
X(Microsoft::Terminal::Control::PathTranslationStyle, PathTranslationStyle, "pathTranslationStyle", Microsoft::Terminal::Control::PathTranslationStyle::None) \
X(IEnvironmentVariableMap, EnvironmentVariables, "environment", nullptr)
// Intentionally omitted Profile settings:
// * Name
@@ -124,12 +122,12 @@ Author(s):
X(hstring, FontFace, "face", DEFAULT_FONT_FACE) \
X(float, FontSize, "size", DEFAULT_FONT_SIZE) \
X(winrt::Windows::UI::Text::FontWeight, FontWeight, "weight", DEFAULT_FONT_WEIGHT) \
X(IFontAxesMap, FontAxes, "axes") \
X(IFontFeatureMap, FontFeatures, "features") \
X(bool, EnableBuiltinGlyphs, "builtinGlyphs", true) \
X(bool, EnableColorGlyphs, "colorGlyphs", true) \
X(winrt::hstring, CellWidth, "cellWidth") \
X(winrt::hstring, CellHeight, "cellHeight")
X(winrt::hstring, CellHeight, "cellHeight") \
X(IFontAxesMap, FontAxes, "axes") \
X(IFontFeatureMap, FontFeatures, "features")
#define MTSM_APPEARANCE_SETTINGS(X) \
X(Core::CursorStyle, CursorShape, "cursorShape", Core::CursorStyle::Bar) \
@@ -137,10 +135,7 @@ Author(s):
X(float, BackgroundImageOpacity, "backgroundImageOpacity", 1.0f) \
X(winrt::Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode, "backgroundImageStretchMode", winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill) \
X(bool, RetroTerminalEffect, "experimental.retroTerminalEffect", false) \
X(IMediaResource, PixelShaderPath, "experimental.pixelShaderPath", implementation::MediaResource::Empty()) \
X(IMediaResource, PixelShaderImagePath, "experimental.pixelShaderImagePath", implementation::MediaResource::Empty()) \
X(ConvergedAlignment, BackgroundImageAlignment, "backgroundImageAlignment", ConvergedAlignment::Horizontal_Center | ConvergedAlignment::Vertical_Center) \
X(IMediaResource, BackgroundImagePath, "backgroundImage", implementation::MediaResource::Empty()) \
X(Model::IntenseStyle, IntenseTextStyle, "intenseTextStyle", Model::IntenseStyle::Bright) \
X(Core::AdjustTextMode, AdjustIndistinguishableColors, "adjustIndistinguishableColors", Core::AdjustTextMode::Automatic) \
X(bool, UseAcrylic, "useAcrylic", false)
@@ -174,3 +169,163 @@ Author(s):
X(winrt::Microsoft::Terminal::Settings::Model::ThemeColor, UnfocusedBackground, "unfocusedBackground", nullptr) \
X(winrt::Microsoft::Terminal::Settings::Model::IconStyle, IconStyle, "iconStyle", winrt::Microsoft::Terminal::Settings::Model::IconStyle::Default) \
X(winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility, ShowCloseButton, "showCloseButton", winrt::Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility::Always)
// SettingKey enums: provide a generic way to reference settings by key.
// Generated from the MTSM macros above. Also includes special-cased settings
// that are not part of the macro lists (prefixed with _ to avoid name collisions).
// SETTINGS_SIZE is a sentinel value that must remain last in each enum.
namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
#define _MTSM_ENUM_VALUE(type, name, jsonKey, ...) name,
enum class ProfileSettingKey : int
{
MTSM_PROFILE_SETTINGS(_MTSM_ENUM_VALUE)
// Special-cased profile settings (not in MTSM_PROFILE_SETTINGS due to
// custom JSON parsing, but included here for completeness)
_Name,
_Guid,
_Source,
_Hidden,
_Padding,
_TabColor,
// Complex/mutable settings with backing fields
_Icon,
_BellSound,
SETTINGS_SIZE
};
enum class GlobalSettingKey : int
{
MTSM_GLOBAL_SETTINGS(_MTSM_ENUM_VALUE)
// Special-cased global settings
_UnparsedDefaultProfile,
// Complex/mutable settings with backing fields
_NewTabMenu,
SETTINGS_SIZE
};
enum class FontSettingKey : int
{
MTSM_FONT_SETTINGS(_MTSM_ENUM_VALUE)
SETTINGS_SIZE
};
enum class AppearanceSettingKey : int
{
MTSM_APPEARANCE_SETTINGS(_MTSM_ENUM_VALUE)
// Special-cased appearance settings
_Foreground,
_Background,
_SelectionBackground,
_CursorColor,
_Opacity,
_DarkColorSchemeName,
_LightColorSchemeName,
// Complex/mutable settings with backing fields
_PixelShaderPath,
_PixelShaderImagePath,
_BackgroundImagePath,
SETTINGS_SIZE
};
#undef _MTSM_ENUM_VALUE
// JSON key lookup: returns the JSON key string for a given SettingKey.
// Generated from the same macros to maintain a single source of truth.
constexpr std::string_view JsonKeyForSetting(ProfileSettingKey key)
{
#define _MTSM_KEY_CASE(type, name, jsonKey, ...) \
case ProfileSettingKey::name: \
return jsonKey;
switch (key)
{
MTSM_PROFILE_SETTINGS(_MTSM_KEY_CASE)
case ProfileSettingKey::_Name:
return "name";
case ProfileSettingKey::_Guid:
return "guid";
case ProfileSettingKey::_Source:
return "source";
case ProfileSettingKey::_Hidden:
return "hidden";
case ProfileSettingKey::_Padding:
return "padding";
case ProfileSettingKey::_TabColor:
return "tabColor";
case ProfileSettingKey::_Icon:
return "icon";
case ProfileSettingKey::_BellSound:
return "bellSound";
default:
return {};
}
#undef _MTSM_KEY_CASE
}
constexpr std::string_view JsonKeyForSetting(GlobalSettingKey key)
{
#define _MTSM_KEY_CASE(type, name, jsonKey, ...) \
case GlobalSettingKey::name: \
return jsonKey;
switch (key)
{
MTSM_GLOBAL_SETTINGS(_MTSM_KEY_CASE)
case GlobalSettingKey::_UnparsedDefaultProfile:
return "defaultProfile";
case GlobalSettingKey::_NewTabMenu:
return "newTabMenu";
default:
return {};
}
#undef _MTSM_KEY_CASE
}
constexpr std::string_view JsonKeyForSetting(FontSettingKey key)
{
#define _MTSM_KEY_CASE(type, name, jsonKey, ...) \
case FontSettingKey::name: \
return jsonKey;
switch (key)
{
MTSM_FONT_SETTINGS(_MTSM_KEY_CASE)
default:
return {};
}
#undef _MTSM_KEY_CASE
}
constexpr std::string_view JsonKeyForSetting(AppearanceSettingKey key)
{
#define _MTSM_KEY_CASE(type, name, jsonKey, ...) \
case AppearanceSettingKey::name: \
return jsonKey;
switch (key)
{
MTSM_APPEARANCE_SETTINGS(_MTSM_KEY_CASE)
case AppearanceSettingKey::_Foreground:
return "foreground";
case AppearanceSettingKey::_Background:
return "background";
case AppearanceSettingKey::_SelectionBackground:
return "selectionBackground";
case AppearanceSettingKey::_CursorColor:
return "cursorColor";
case AppearanceSettingKey::_Opacity:
return "opacity";
case AppearanceSettingKey::_DarkColorSchemeName:
return "colorScheme";
case AppearanceSettingKey::_LightColorSchemeName:
return "colorScheme";
case AppearanceSettingKey::_PixelShaderPath:
return "experimental.pixelShaderPath";
case AppearanceSettingKey::_PixelShaderImagePath:
return "experimental.pixelShaderImagePath";
case AppearanceSettingKey::_BackgroundImagePath:
return "backgroundImage";
default:
return {};
}
#undef _MTSM_KEY_CASE
}
}

View File

@@ -37,9 +37,9 @@ static constexpr std::string_view LegacyAutoMarkPromptsKey{ "experimental.autoMa
static constexpr std::string_view LegacyShowMarksKey{ "experimental.showMarksOnScrollbar" };
static constexpr std::string_view LegacyRightClickContextMenuKey{ "experimental.rightClickContextMenu" };
Profile::Profile(guid guid) noexcept :
_Guid(guid)
Profile::Profile(guid guid)
{
Guid(guid);
}
void Profile::CreateUnfocusedAppearance()
@@ -105,25 +105,23 @@ winrt::com_ptr<Profile> Profile::CopySettings() const
profile->_Deleted = _Deleted;
profile->_Orphaned = _Orphaned;
profile->_Updates = _Updates;
profile->_Guid = _Guid;
profile->_Name = _Name;
profile->_Source = _Source;
profile->_Hidden = _Hidden;
profile->_TabColor = _TabColor;
profile->_Padding = _Padding;
profile->_Origin = _Origin;
profile->_FontInfo = *fontInfo;
profile->_DefaultAppearance = *defaultAppearance;
profile->_json = _json;
#define PROFILE_SETTINGS_COPY(type, name, jsonKey, ...) \
profile->_##name = _##name;
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_COPY)
#undef PROFILE_SETTINGS_COPY
// JSON-backed settings (Name, Source, Hidden, Guid, Padding, TabColor, MTSM settings)
// all live in _json, which is already deep-copied above. No per-setting copy needed.
// Complex/mutable settings — backing fields for resolution lifecycle.
// _json (copied above) is the source of truth; backing fields hold resolved runtime state.
profile->_Icon = _Icon;
// BellSound is an IVector<>, so we need to make a new vector pointing at the same objects
if (_BellSound)
{
// BellSound is an IVector<>, so we need to make a new vector pointing at the same objects
profile->_BellSound = winrt::single_threaded_vector(wil::to_vector(*_BellSound));
}
@@ -169,6 +167,10 @@ winrt::com_ptr<winrt::Microsoft::Terminal::Settings::Model::implementation::Prof
// <none>
void Profile::LayerJson(const Json::Value& json)
{
// Merge incoming JSON keys into stored _json (key-wise, not replacement).
// This preserves keys from earlier LayerJson calls that aren't in the new JSON.
JsonUtils::MergeJsonKeys(json, _json);
// Appearance Settings
auto defaultAppearanceImpl = winrt::get_self<implementation::AppearanceConfig>(_DefaultAppearance);
defaultAppearanceImpl->LayerJson(json);
@@ -177,37 +179,53 @@ void Profile::LayerJson(const Json::Value& json)
auto fontInfoImpl = winrt::get_self<implementation::FontConfig>(_FontInfo);
fontInfoImpl->LayerJson(json);
// Profile-specific Settings
JsonUtils::GetValueForKey(json, NameKey, _Name);
// Profile-specific settings: now JSON-backed. Values are already in _json
// from the merge step above. Only need to handle non-JSON fields and logging.
JsonUtils::GetValueForKey(json, UpdatesKey, _Updates);
JsonUtils::GetValueForKey(json, GuidKey, _Guid);
// Normalize Padding: allow integer → string coercion (e.g. "padding": 8 → "8")
if (_json.isMember(JsonKey(PaddingKey)) && _json[JsonKey(PaddingKey)].isInt())
{
_json[JsonKey(PaddingKey)] = std::to_string(_json[JsonKey(PaddingKey)].asInt());
}
// Make sure Source is before Hidden! We use that to exclude false positives from the settings logger!
JsonUtils::GetValueForKey(json, SourceKey, _Source);
JsonUtils::GetValueForKey(json, HiddenKey, _Hidden);
_logSettingIfSet(HiddenKey, _Hidden.has_value());
// Padding was never specified as an integer, but it was a common working mistake.
// Allow it to be permissive.
JsonUtils::GetValueForKey(json, PaddingKey, _Padding, JsonUtils::OptionalConverter<hstring, JsonUtils::PermissiveStringConverter<std::wstring>>{});
_logSettingIfSet(PaddingKey, _Padding.has_value());
JsonUtils::GetValueForKey(json, TabColorKey, _TabColor);
_logSettingIfSet(TabColorKey, _TabColor.has_value());
_logSettingIfSet(HiddenKey, HasHidden());
_logSettingIfSet(PaddingKey, HasPadding());
_logSettingIfSet(TabColorKey, HasTabColor());
// Try to load some legacy keys, to migrate them.
// Done _before_ the MTSM_PROFILE_SETTINGS, which have the updated keys.
JsonUtils::GetValueForKey(json, LegacyShowMarksKey, _ShowMarks);
JsonUtils::GetValueForKey(json, LegacyAutoMarkPromptsKey, _AutoMarkPrompts);
JsonUtils::GetValueForKey(json, LegacyRightClickContextMenuKey, _RightClickContextMenu);
// Normalize legacy keys into canonical _json keys so JSON-backed getters find them.
// Done _before_ the MTSM_PROFILE_SETTINGS logging, which have the updated keys.
static constexpr std::pair<std::string_view, std::string_view> legacyKeyMappings[] = {
{ LegacyShowMarksKey, "showMarksOnScrollbar" },
{ LegacyAutoMarkPromptsKey, "autoMarkPrompts" },
{ LegacyRightClickContextMenuKey, "rightClickContextMenu" },
};
for (const auto& [legacyKey, canonicalKey] : legacyKeyMappings)
{
if (json.isMember(JsonKey(legacyKey)))
{
_json[JsonKey(canonicalKey)] = json[JsonKey(legacyKey)];
}
}
// MTSM settings are now JSON-backed (no backing fields).
// Values are already in _json from the merge step above.
// We only need to log which settings were set in this layer.
#define PROFILE_SETTINGS_LAYER_JSON(type, name, jsonKey, ...) \
JsonUtils::GetValueForKey(json, jsonKey, _##name); \
_logSettingIfSet(jsonKey, _##name.has_value());
_logSettingIfSet(jsonKey, json.isMember(jsonKey) && !json[jsonKey].isNull());
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_LAYER_JSON)
#undef PROFILE_SETTINGS_LAYER_JSON
// Complex/mutable settings — backing fields populated from _json for runtime resolution.
// _json is the source of truth for serialization; backing fields are for resolution lifecycle.
JsonUtils::GetValueForKey(json, "icon", _Icon);
_logSettingIfSet("icon", _Icon.has_value());
JsonUtils::GetValueForKey(json, "bellSound", _BellSound);
_logSettingIfSet("bellSound", _BellSound.has_value());
if (json.isMember(JsonKey(UnfocusedAppearanceKey)))
{
auto unfocusedAppearance{ winrt::make_self<implementation::AppearanceConfig>(weak_ref<Model::Profile>(*this)) };
@@ -332,25 +350,44 @@ Json::Value Profile::ToJson() const
// GH #9962:
// If the settings.json was missing, when we load the dynamic profiles, they are completely empty.
// This caused us to serialize empty profiles "{}" on accident.
const auto writeBasicSettings{ !Source().empty() };
const auto writeBasicSettings{ HasSource() };
// Profile-specific Settings
JsonUtils::SetValueForKey(json, NameKey, writeBasicSettings ? Name() : _Name);
JsonUtils::SetValueForKey(json, GuidKey, writeBasicSettings ? Guid() : _Guid);
JsonUtils::SetValueForKey(json, HiddenKey, writeBasicSettings ? Hidden() : _Hidden);
JsonUtils::SetValueForKey(json, SourceKey, writeBasicSettings ? Source() : _Source);
// Profile-specific Settings (JSON-backed)
if (writeBasicSettings)
{
// Dynamic profiles: write resolved values so defaults are materialized
JsonUtils::SetValueForKey(json, NameKey, Name());
JsonUtils::SetValueForKey(json, GuidKey, Guid());
JsonUtils::SetValueForKey(json, HiddenKey, Hidden());
JsonUtils::SetValueForKey(json, SourceKey, Source());
}
else
{
// User-defined profiles: copy local layer from _json
for (const auto& key : { NameKey, GuidKey, HiddenKey, SourceKey })
{
JsonUtils::CopyKeyIfPresent(_json, json, key);
}
}
// PermissiveStringConverter is unnecessary for serialization
JsonUtils::SetValueForKey(json, PaddingKey, _Padding);
// Padding: copy from _json
JsonUtils::CopyKeyIfPresent(_json, json, PaddingKey);
JsonUtils::SetValueForKey(json, TabColorKey, _TabColor);
// TabColor: nullable — key presence matters (explicit null is valid)
JsonUtils::CopyKeyIfPresent(_json, json, TabColorKey);
// MTSM profile settings: copy from _json (the source of truth)
#define PROFILE_SETTINGS_TO_JSON(type, name, jsonKey, ...) \
JsonUtils::SetValueForKey(json, jsonKey, _##name);
JsonUtils::CopyKeyIfPresent(_json, json, jsonKey);
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_TO_JSON)
#undef PROFILE_SETTINGS_TO_JSON
// Complex/mutable settings with backing fields (runtime resolution)
// Read from _json (source of truth), not from backing fields
JsonUtils::CopyKeyIfPresent(_json, json, "icon");
JsonUtils::CopyKeyIfPresent(_json, json, "bellSound");
if (auto fontJSON = winrt::get_self<FontConfig>(_FontInfo)->ToJson(); !fontJSON.empty())
{
json[JsonKey(FontInfoKey)] = std::move(fontJSON);
@@ -364,6 +401,89 @@ Json::Value Profile::ToJson() const
return json;
}
bool Profile::HasSetting(ProfileSettingKey key) const
{
switch (key)
{
#define _PROFILE_HAS_SETTING(type, name, jsonKey, ...) \
case ProfileSettingKey::name: \
return Has##name();
MTSM_PROFILE_SETTINGS(_PROFILE_HAS_SETTING)
#undef _PROFILE_HAS_SETTING
case ProfileSettingKey::_Name:
return HasName();
case ProfileSettingKey::_Guid:
return HasGuid();
case ProfileSettingKey::_Source:
return HasSource();
case ProfileSettingKey::_Hidden:
return HasHidden();
case ProfileSettingKey::_Padding:
return HasPadding();
case ProfileSettingKey::_TabColor:
return HasTabColor();
case ProfileSettingKey::_Icon:
return HasIcon();
case ProfileSettingKey::_BellSound:
return HasBellSound();
default:
return false;
}
}
void Profile::ClearSetting(ProfileSettingKey key)
{
switch (key)
{
#define _PROFILE_CLEAR_SETTING(type, name, jsonKey, ...) \
case ProfileSettingKey::name: \
Clear##name(); \
break;
MTSM_PROFILE_SETTINGS(_PROFILE_CLEAR_SETTING)
#undef _PROFILE_CLEAR_SETTING
case ProfileSettingKey::_Name:
ClearName();
break;
case ProfileSettingKey::_Guid:
ClearGuid();
break;
case ProfileSettingKey::_Source:
ClearSource();
break;
case ProfileSettingKey::_Hidden:
ClearHidden();
break;
case ProfileSettingKey::_Padding:
ClearPadding();
break;
case ProfileSettingKey::_TabColor:
ClearTabColor();
break;
case ProfileSettingKey::_Icon:
ClearIcon();
break;
case ProfileSettingKey::_BellSound:
ClearBellSound();
break;
default:
break;
}
}
std::vector<ProfileSettingKey> Profile::CurrentSettings() const
{
std::vector<ProfileSettingKey> result;
for (auto i = 0; i < static_cast<int>(ProfileSettingKey::SETTINGS_SIZE); i++)
{
const auto key = static_cast<ProfileSettingKey>(i);
if (HasSetting(key))
{
result.push_back(key);
}
}
return result;
}
// Given a commandLine like the following:
// * "C:\WINDOWS\System32\cmd.exe"
// * "pwsh -WorkingDirectory ~"
@@ -502,13 +622,13 @@ void Profile::_logSettingIfSet(const std::string_view& setting, const bool isSet
// Exclude some false positives from userDefaults.json
// NOTE: we can't use the OriginTag here because it hasn't been set yet!
const bool isWinPow = _Guid.has_value() && *_Guid == DEFAULT_WINDOWS_POWERSHELL_GUID; //_Name.has_value() && til::equals_insensitive_ascii(*_Name, L"Windows PowerShell");
const bool isCmd = _Guid.has_value() && *_Guid == DEFAULT_COMMAND_PROMPT_GUID; //_Name.has_value() && til::equals_insensitive_ascii(*_Name, L"Command Prompt");
const bool isACS = _Name.has_value() && til::equals_insensitive_ascii(*_Name, L"Azure Cloud Shell");
const bool isWTDynamicProfile = _Source.has_value() && til::starts_with(*_Source, L"Windows.Terminal");
const bool settingHiddenToFalse = til::equals_insensitive_ascii(setting, HiddenKey) && _Hidden.has_value() && _Hidden == false;
const bool settingCommandlineToWinPow = til::equals_insensitive_ascii(setting, "commandline") && _Commandline.has_value() && til::equals_insensitive_ascii(*_Commandline, L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
const bool settingCommandlineToCmd = til::equals_insensitive_ascii(setting, "commandline") && _Commandline.has_value() && til::equals_insensitive_ascii(*_Commandline, L"%SystemRoot%\\System32\\cmd.exe");
const bool isWinPow = HasGuid() && Guid() == DEFAULT_WINDOWS_POWERSHELL_GUID;
const bool isCmd = HasGuid() && Guid() == DEFAULT_COMMAND_PROMPT_GUID;
const bool isACS = HasName() && til::equals_insensitive_ascii(Name(), L"Azure Cloud Shell");
const bool isWTDynamicProfile = HasSource() && til::starts_with(Source(), L"Windows.Terminal");
const bool settingHiddenToFalse = til::equals_insensitive_ascii(setting, HiddenKey) && HasHidden() && Hidden() == false;
const bool settingCommandlineToWinPow = til::equals_insensitive_ascii(setting, "commandline") && HasCommandline() && til::equals_insensitive_ascii(Commandline(), L"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
const bool settingCommandlineToCmd = til::equals_insensitive_ascii(setting, "commandline") && HasCommandline() && til::equals_insensitive_ascii(Commandline(), L"%SystemRoot%\\System32\\cmd.exe");
// clang-format off
if (!(isWinPow && (settingHiddenToFalse || settingCommandlineToWinPow))
&& !(isCmd && (settingHiddenToFalse || settingCommandlineToCmd))
@@ -541,7 +661,7 @@ void Profile::ResolveMediaResources(const Model::MediaResourceResolver& resolver
auto newIcon{ MediaResource::FromString(icon->Path()) };
const std::wstring cmdline{ NormalizeCommandLine(iconSource->Commandline().c_str()) };
newIcon.Resolve(cmdline.c_str() /* c_str: give hstring a chance to find the null terminator */);
iconSource->_Icon = std::move(newIcon);
iconSource->Icon(std::move(newIcon));
}
}

View File

@@ -49,6 +49,7 @@ Author(s):
#include "MTSMSettings.h"
#include "JsonUtils.h"
#include "TerminalSettingsSerializationHelpers.h"
#include <DefaultSettings.h>
#include "MediaResourceSupport.h"
#include "AppearanceConfig.h"
@@ -80,7 +81,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
{
public:
Profile() noexcept = default;
Profile(guid guid) noexcept;
// Not noexcept: Guid(guid) writes to _json, which allocates.
// The old backing-field constructor was noexcept (POD copy into std::optional),
// but JSON-backed storage requires heap allocation.
Profile(guid guid);
void CreateUnfocusedAppearance();
void DeleteUnfocusedAppearance();
@@ -98,6 +102,11 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
void LayerJson(const Json::Value& json);
Json::Value ToJson() const;
// Generic setting access via SettingKey
bool HasSetting(ProfileSettingKey key) const;
void ClearSetting(ProfileSettingKey key);
std::vector<ProfileSettingKey> CurrentSettings() const;
hstring EvaluatedStartingDirectory() const;
Model::IAppearanceConfig DefaultAppearance();
@@ -122,16 +131,29 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
WINRT_PROPERTY(OriginTag, Origin, OriginTag::None);
WINRT_PROPERTY(guid, Updates);
// Nullable/optional settings
INHERITABLE_NULLABLE_SETTING(Model::Profile, Microsoft::Terminal::Core::Color, TabColor, nullptr);
INHERITABLE_SETTING(Model::Profile, Model::IAppearanceConfig, UnfocusedAppearance, nullptr);
// Nullable/optional settings (JSON-backed)
INHERITABLE_NULLABLE_SETTING(Model::Profile, Microsoft::Terminal::Core::Color, TabColor, "tabColor", nullptr)
INHERITABLE_MUTABLE_SETTING(Model::Profile, Model::IAppearanceConfig, UnfocusedAppearance, nullptr);
// Settings that cannot be put in the macro because of how they are handled in ToJson/LayerJson
INHERITABLE_SETTING(Model::Profile, hstring, Name, L"Default");
INHERITABLE_SETTING(Model::Profile, hstring, Source);
INHERITABLE_SETTING(Model::Profile, bool, Hidden, false);
INHERITABLE_SETTING(Model::Profile, guid, Guid, _GenerateGuidForProfile(Name(), Source()));
INHERITABLE_SETTING(Model::Profile, hstring, Padding, DEFAULT_PADDING);
// Settings that are JSON-backed but need custom handling in ToJson/LayerJson
INHERITABLE_SETTING(Model::Profile, hstring, Name, "name", L"Default")
INHERITABLE_SETTING(Model::Profile, hstring, Source, "source")
INHERITABLE_SETTING(Model::Profile, bool, Hidden, "hidden", false)
INHERITABLE_SETTING(Model::Profile, hstring, Padding, "padding", DEFAULT_PADDING)
// Guid: hand-written JSON-backed (dynamic default)
_BASE_INHERITABLE_SETTING(Model::Profile, guid, Guid, "guid")
public:
guid Guid() const
{
const auto val{ _getGuidImpl() };
return val ? *val : _GenerateGuidForProfile(Name(), Source());
}
void Guid(const guid& value)
{
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(
_json, "guid", value);
}
winrt::hstring SourceBasePath;
@@ -141,10 +163,33 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_INITIALIZE)
#undef PROFILE_SETTINGS_INITIALIZE
// IMediaResource settings with backing fields for resolution lifecycle.
// _json is the source of truth for serialization. Setters dual-write to both
// the backing field (for runtime resolution) and _json (for auto-save).
INHERITABLE_MEDIA_RESOURCE_SETTING(Model::Profile, Icon, "icon", implementation::MediaResource::FromString(L"\uE756"))
// BellSound: IVector<IMediaResource> with backing field + dual-write setter.
_BASE_INHERITABLE_MUTABLE_SETTING(Model::Profile, std::optional<Windows::Foundation::Collections::IVector<IMediaResource>>, BellSound, nullptr)
public:
Windows::Foundation::Collections::IVector<IMediaResource> BellSound() const
{
const auto val{ _getBellSoundImpl() };
return val ? *val : nullptr;
}
void BellSound(const Windows::Foundation::Collections::IVector<IMediaResource>& value)
{
_BellSound = value;
::Microsoft::Terminal::Settings::Model::JsonUtils::SetValueForKey(_json, "bellSound", value);
}
private:
Model::IAppearanceConfig _DefaultAppearance{ winrt::make<AppearanceConfig>(weak_ref<Model::Profile>(*this)) };
Model::FontConfig _FontInfo{ winrt::make<FontConfig>(weak_ref<Model::Profile>(*this)) };
// Raw JSON for this layer. Populated by LayerJson(), will become the
// source of truth for settings once the JSON-backed refactor is complete.
Json::Value _json{ Json::ValueType::objectValue };
std::set<std::string> _changeLog;
static std::wstring EvaluateStartingDirectory(const std::wstring& directory);

View File

@@ -105,9 +105,10 @@ namespace SettingsModelUnitTests
TEST_METHOD(RealResolverUrlCases);
TEST_METHOD(RealResolverUNCCases);
static constexpr std::wstring_view pingCommandline{ LR"(C:\Windows\System32\PING.EXE)" }; // Normalized by Profile (this is the casing that Windows stores on disk)
static constexpr std::wstring_view overrideCommandline{ LR"(C:\Windows\System32\cscript.exe)" };
static constexpr std::wstring_view cmdCommandline{ LR"(C:\Windows\System32\cmd.exe)" }; // The default commandline for a profile
// These are normalized by NormalizeCommandLine, which resolves to the on-disk casing.
// They are used in test cases where media paths fall back to profile command lines.
static inline std::wstring overrideCommandline;
static inline std::wstring cmdCommandline;
static constexpr std::wstring_view fragmentBasePath1{ LR"(C:\Windows\Media)" };
private:
@@ -218,6 +219,9 @@ namespace SettingsModelUnitTests
// Some of our tests use paths under system32. Just don't redirect them.
Wow64DisableWow64FsRedirection(&redirectionFlag);
#endif
// Normalize these AFTER the call above so that we get the correctly redirected paths.
overrideCommandline = implementation::Profile::NormalizeCommandLine(LR"(C:\Windows\System32\cscript.exe)");
cmdCommandline = implementation::Profile::NormalizeCommandLine(LR"(C:\Windows\System32\cmd.exe)");
return true;
}

View File

@@ -31,6 +31,14 @@ namespace SettingsModelUnitTests
TEST_METHOD(TestGenGuidsForProfiles);
TEST_METHOD(TestCorrectOldDefaultShellPaths);
TEST_METHOD(ProfileDefaultsProhibitedSettings);
TEST_METHOD(JsonSyncOnSetAndClear);
TEST_METHOD(SettingKeyEnumAndJsonKeyLookup);
TEST_METHOD(GenericHasAndClearMatchTypedAPIs);
TEST_METHOD(CurrentSettingsReturnsCorrectKeys);
TEST_METHOD(SpecialCasedSettingsJsonBacked);
TEST_METHOD(NullableSettingJsonBacked);
TEST_METHOD(ColorSchemeJsonBacked);
};
void ProfileTests::ProfileGeneratesGuid()
@@ -532,4 +540,424 @@ namespace SettingsModelUnitTests
VERIFY_ARE_NOT_EQUAL(L"Default Profile Source", allProfiles.GetAt(2).Source());
VERIFY_ARE_NOT_EQUAL(L"foo.exe", allProfiles.GetAt(2).Commandline());
}
void ProfileTests::JsonSyncOnSetAndClear()
{
// Verify that setting a value via the typed setter updates the internal
// _json, and clearing it removes the key from _json.
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
// Verify initial value
VERIFY_ARE_EQUAL(1000, profile.HistorySize());
VERIFY_IS_TRUE(profile.HasHistorySize());
// Modify setting; _json should be updated
profile.HistorySize(5000);
VERIFY_ARE_EQUAL(5000, profile.HistorySize());
// Verify ToJson reflects the change
const auto json = profileImpl->ToJson();
VERIFY_ARE_EQUAL(5000, json["historySize"].asInt());
// Clear setting; should fall back to default
profile.ClearHistorySize();
VERIFY_IS_FALSE(profile.HasHistorySize());
// Should now inherit or use default (9001 is the DEFAULT_HISTORY_SIZE)
VERIFY_ARE_EQUAL(DEFAULT_HISTORY_SIZE, profile.HistorySize());
// ToJson should no longer have historySize
const auto json2 = profileImpl->ToJson();
VERIFY_IS_FALSE(json2.isMember("historySize"));
}
void ProfileTests::SettingKeyEnumAndJsonKeyLookup()
{
// Verify that the SettingKey enums map to correct JSON keys.
using namespace implementation;
// Spot-check a few profile setting keys
VERIFY_ARE_EQUAL(std::string_view{ "historySize" }, JsonKeyForSetting(ProfileSettingKey::HistorySize));
VERIFY_ARE_EQUAL(std::string_view{ "snapOnInput" }, JsonKeyForSetting(ProfileSettingKey::SnapOnInput));
VERIFY_ARE_EQUAL(std::string_view{ "commandline" }, JsonKeyForSetting(ProfileSettingKey::Commandline));
VERIFY_ARE_EQUAL(std::string_view{ "tabTitle" }, JsonKeyForSetting(ProfileSettingKey::TabTitle));
// Special-cased settings
VERIFY_ARE_EQUAL(std::string_view{ "name" }, JsonKeyForSetting(ProfileSettingKey::_Name));
VERIFY_ARE_EQUAL(std::string_view{ "guid" }, JsonKeyForSetting(ProfileSettingKey::_Guid));
VERIFY_ARE_EQUAL(std::string_view{ "hidden" }, JsonKeyForSetting(ProfileSettingKey::_Hidden));
VERIFY_ARE_EQUAL(std::string_view{ "padding" }, JsonKeyForSetting(ProfileSettingKey::_Padding));
VERIFY_ARE_EQUAL(std::string_view{ "tabColor" }, JsonKeyForSetting(ProfileSettingKey::_TabColor));
// Global setting keys
VERIFY_ARE_EQUAL(std::string_view{ "initialRows" }, JsonKeyForSetting(GlobalSettingKey::InitialRows));
VERIFY_ARE_EQUAL(std::string_view{ "alwaysOnTop" }, JsonKeyForSetting(GlobalSettingKey::AlwaysOnTop));
// Font setting keys
VERIFY_ARE_EQUAL(std::string_view{ "face" }, JsonKeyForSetting(FontSettingKey::FontFace));
VERIFY_ARE_EQUAL(std::string_view{ "size" }, JsonKeyForSetting(FontSettingKey::FontSize));
// SETTINGS_SIZE should be a valid (but large) enum value
VERIFY_IS_TRUE(static_cast<int>(ProfileSettingKey::SETTINGS_SIZE) > 0);
VERIFY_IS_TRUE(static_cast<int>(GlobalSettingKey::SETTINGS_SIZE) > 0);
VERIFY_IS_TRUE(static_cast<int>(FontSettingKey::SETTINGS_SIZE) > 0);
VERIFY_IS_TRUE(static_cast<int>(AppearanceSettingKey::SETTINGS_SIZE) > 0);
}
void ProfileTests::GenericHasAndClearMatchTypedAPIs()
{
// Verify that HasSetting(key) and ClearSetting(key) match the
// typed HasXxx() and ClearXxx() methods.
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000,
"snapOnInput": false
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
// HasSetting should match HasXxx for set values
VERIFY_ARE_EQUAL(profile.HasHistorySize(), profileImpl->HasSetting(implementation::ProfileSettingKey::HistorySize));
VERIFY_ARE_EQUAL(profile.HasSnapOnInput(), profileImpl->HasSetting(implementation::ProfileSettingKey::SnapOnInput));
// HasSetting should match HasXxx for unset values
VERIFY_ARE_EQUAL(profile.HasTabTitle(), profileImpl->HasSetting(implementation::ProfileSettingKey::TabTitle));
VERIFY_IS_FALSE(profileImpl->HasSetting(implementation::ProfileSettingKey::TabTitle));
// ClearSetting should behave like ClearXxx
profileImpl->ClearSetting(implementation::ProfileSettingKey::HistorySize);
VERIFY_IS_FALSE(profile.HasHistorySize());
VERIFY_IS_FALSE(profileImpl->HasSetting(implementation::ProfileSettingKey::HistorySize));
}
void ProfileTests::CurrentSettingsReturnsCorrectKeys()
{
// Verify that CurrentSettings() returns the keys that are explicitly
// set at the current layer.
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"historySize": 1000,
"snapOnInput": false,
"tabTitle": "MyTab"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto currentKeys = profileImpl->CurrentSettings();
// historySize, snapOnInput, and tabTitle should be in the list
auto hasHistorySize = false;
auto hasSnapOnInput = false;
auto hasTabTitle = false;
for (const auto& key : currentKeys)
{
if (key == implementation::ProfileSettingKey::HistorySize)
{
hasHistorySize = true;
}
if (key == implementation::ProfileSettingKey::SnapOnInput)
{
hasSnapOnInput = true;
}
if (key == implementation::ProfileSettingKey::TabTitle)
{
hasTabTitle = true;
}
}
VERIFY_IS_TRUE(hasHistorySize, L"historySize should be in CurrentSettings");
VERIFY_IS_TRUE(hasSnapOnInput, L"snapOnInput should be in CurrentSettings");
VERIFY_IS_TRUE(hasTabTitle, L"tabTitle should be in CurrentSettings");
// Clear one and verify it's removed
profileImpl->ClearSetting(implementation::ProfileSettingKey::TabTitle);
const auto updatedKeys = profileImpl->CurrentSettings();
auto hasTabTitleAfterClear = false;
for (const auto& key : updatedKeys)
{
if (key == implementation::ProfileSettingKey::TabTitle)
{
hasTabTitleAfterClear = true;
}
}
VERIFY_IS_FALSE(hasTabTitleAfterClear, L"tabTitle should NOT be in CurrentSettings after clear");
}
void ProfileTests::SpecialCasedSettingsJsonBacked()
{
// Verify that special-cased settings (Name, Guid, Hidden, Padding)
// are now JSON-backed: setters write to _json, Has/Clear work correctly.
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "TestProfile",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"hidden": true,
"padding": "12"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
// Verify initial values loaded from JSON
VERIFY_ARE_EQUAL(L"TestProfile", profile.Name());
VERIFY_IS_TRUE(profile.HasName());
VERIFY_IS_TRUE(profile.Hidden());
VERIFY_IS_TRUE(profile.HasHidden());
VERIFY_ARE_EQUAL(L"12", profile.Padding());
VERIFY_IS_TRUE(profile.HasPadding());
// Modify Name via setter — should update _json
profile.Name(L"NewName");
VERIFY_ARE_EQUAL(L"NewName", profile.Name());
const auto json1 = profileImpl->ToJson();
VERIFY_ARE_EQUAL(L"NewName", til::u8u16(json1["name"].asString()));
// Clear Name — should fall back to default
profile.ClearName();
VERIFY_IS_FALSE(profile.HasName());
VERIFY_ARE_EQUAL(L"Default", profile.Name());
// Modify Hidden via setter
profile.Hidden(false);
VERIFY_ARE_EQUAL(false, profile.Hidden());
VERIFY_IS_TRUE(profile.HasHidden());
// Clear Hidden — should fall back to default (false)
profile.ClearHidden();
VERIFY_IS_FALSE(profile.HasHidden());
VERIFY_ARE_EQUAL(false, profile.Hidden());
// Modify Padding — should write to _json
profile.Padding(L"24");
VERIFY_ARE_EQUAL(L"24", profile.Padding());
// Test Guid setter
static constexpr winrt::guid testGuid{ 0x11111111, 0x2222, 0x3333, { 0x44, 0x44, 0x55, 0x55, 0x66, 0x66, 0x77, 0x77 } };
profile.Guid(testGuid);
VERIFY_ARE_EQUAL(testGuid, profile.Guid());
}
void ProfileTests::NullableSettingJsonBacked()
{
// Verify that nullable settings (TabColor) are JSON-backed.
// Null is a valid explicit value meaning "no color".
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"tabColor": "#FF0000"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
// Verify initial value
VERIFY_IS_TRUE(profile.HasTabColor());
VERIFY_IS_NOT_NULL(profile.TabColor());
// Set to null explicitly (means "no color")
profile.TabColor(nullptr);
VERIFY_IS_TRUE(profile.HasTabColor()); // still "set" — explicitly null
VERIFY_IS_NULL(profile.TabColor());
// Clear — should inherit from parent (which has no tabColor)
profile.ClearTabColor();
VERIFY_IS_FALSE(profile.HasTabColor());
// ToJson should not have tabColor after clearing
const auto json = profileImpl->ToJson();
VERIFY_IS_FALSE(json.isMember("tabColor"));
}
void ProfileTests::ColorSchemeJsonBacked()
{
// Verify that DarkColorSchemeName/LightColorSchemeName are JSON-backed,
// including round-trip through LayerJson/ToJson, parent inheritance,
// and the polymorphic colorScheme key (string vs object forms).
// Case 1: string form input — sets both dark and light to the same scheme
{
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"colorScheme": "One Half Dark"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto appearance = profile.DefaultAppearance();
const auto appearanceImpl = winrt::get_self<implementation::AppearanceConfig>(appearance);
// Both dark and light should be set to the same value
VERIFY_ARE_EQUAL(L"One Half Dark", appearance.DarkColorSchemeName());
VERIFY_ARE_EQUAL(L"One Half Dark", appearance.LightColorSchemeName());
VERIFY_IS_TRUE(appearanceImpl->HasDarkColorSchemeName());
VERIFY_IS_TRUE(appearanceImpl->HasLightColorSchemeName());
// ToJson should collapse to a string since dark == light
const auto json = profileImpl->ToJson();
VERIFY_IS_TRUE(json.isMember("colorScheme"));
VERIFY_IS_TRUE(json["colorScheme"].isString());
VERIFY_ARE_EQUAL("One Half Dark", json["colorScheme"].asString());
}
// Case 2: object form input — dark and light are different
{
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"colorScheme": {
"dark": "One Half Dark",
"light": "One Half Light"
}
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto appearance = profile.DefaultAppearance();
const auto appearanceImpl = winrt::get_self<implementation::AppearanceConfig>(appearance);
// Each should return its own value
VERIFY_ARE_EQUAL(L"One Half Dark", appearance.DarkColorSchemeName());
VERIFY_ARE_EQUAL(L"One Half Light", appearance.LightColorSchemeName());
// ToJson should produce an object since dark != light
const auto json = profileImpl->ToJson();
VERIFY_IS_TRUE(json.isMember("colorScheme"));
VERIFY_IS_TRUE(json["colorScheme"].isObject());
VERIFY_ARE_EQUAL("One Half Dark", json["colorScheme"]["dark"].asString());
VERIFY_ARE_EQUAL("One Half Light", json["colorScheme"]["light"].asString());
}
// Case 3: setter updates — setting dark should preserve light, and vice versa
{
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"colorScheme": "Campbell"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto appearance = profile.DefaultAppearance();
const auto appearanceImpl = winrt::get_self<implementation::AppearanceConfig>(appearance);
// Change dark only — light should stay "Campbell"
appearance.DarkColorSchemeName(L"Tango Dark");
VERIFY_ARE_EQUAL(L"Tango Dark", appearance.DarkColorSchemeName());
VERIFY_ARE_EQUAL(L"Campbell", appearance.LightColorSchemeName());
// ToJson should produce an object since dark != light
const auto json1 = profileImpl->ToJson();
VERIFY_IS_TRUE(json1["colorScheme"].isObject());
VERIFY_ARE_EQUAL("Tango Dark", json1["colorScheme"]["dark"].asString());
VERIFY_ARE_EQUAL("Campbell", json1["colorScheme"]["light"].asString());
// Change light to match dark — should collapse to string on ToJson
appearance.LightColorSchemeName(L"Tango Dark");
VERIFY_ARE_EQUAL(L"Tango Dark", appearance.LightColorSchemeName());
const auto json2 = profileImpl->ToJson();
VERIFY_IS_TRUE(json2["colorScheme"].isString());
VERIFY_ARE_EQUAL("Tango Dark", json2["colorScheme"].asString());
}
// Case 4: clear — removes colorScheme entirely, falls back to default "Campbell"
{
static constexpr std::string_view settingsJson{ R"({
"profiles": {
"list": [
{
"name": "profile0",
"guid": "{6239a42c-0000-49a3-80bd-e8fdd045185c}",
"colorScheme": "One Half Dark"
}
]
}
})" };
const auto settings = winrt::make_self<implementation::CascadiaSettings>(settingsJson);
const auto profile = settings->AllProfiles().GetAt(0);
const auto profileImpl = winrt::get_self<implementation::Profile>(profile);
const auto appearance = profile.DefaultAppearance();
const auto appearanceImpl = winrt::get_self<implementation::AppearanceConfig>(appearance);
VERIFY_IS_TRUE(appearanceImpl->HasDarkColorSchemeName());
// Clear removes colorScheme from this layer
appearanceImpl->ClearDarkColorSchemeName();
VERIFY_IS_FALSE(appearanceImpl->HasDarkColorSchemeName());
VERIFY_IS_FALSE(appearanceImpl->HasLightColorSchemeName());
// Falls back to default "Campbell"
VERIFY_ARE_EQUAL(L"Campbell", appearance.DarkColorSchemeName());
VERIFY_ARE_EQUAL(L"Campbell", appearance.LightColorSchemeName());
// ToJson should not have colorScheme
const auto json = profileImpl->ToJson();
VERIFY_IS_FALSE(json.isMember("colorScheme"));
}
}
}

View File

@@ -67,7 +67,7 @@ void FontTests::TestCurrentFontAPIsInvalid()
}
else
{
hConsoleOutput = (HANDLE)dwConsoleOutput;
hConsoleOutput = ULongToHandle(dwConsoleOutput);
}
if (strOperation == L"Get")
@@ -107,7 +107,7 @@ void FontTests::TestGetFontSizeInvalid()
// Need to make sure that last error is cleared so that we can verify that lasterror was set by GetConsoleFontSize
SetLastError(0);
auto coordFontSize = OneCoreDelay::GetConsoleFontSize((HANDLE)dwConsoleOutput, 0);
auto coordFontSize = OneCoreDelay::GetConsoleFontSize(ULongToHandle(dwConsoleOutput), 0);
VERIFY_ARE_EQUAL(coordFontSize, c_coordZero, L"Ensure (0,0) coord returned to indicate failure");
VERIFY_ARE_EQUAL(GetLastError(), (DWORD)ERROR_INVALID_HANDLE, L"Ensure last error was set appropriately");
}

View File

@@ -36,7 +36,7 @@ void ConhostInternalGetSet::UnknownSequence() noexcept
// us from using a more conservative solution (e.g. always fetching the cursor position).
if (gci.IsInVtIoMode())
{
gci.GetActiveOutputBuffer().SetConptyCursorPositionMayBeWrong();
gci.GetActiveOutputBuffer().GetActiveBuffer().SetConptyCursorPositionMayBeWrong();
}
}

View File

@@ -241,7 +241,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
constexpr bool ends_with_insensitive_ascii(const std::basic_string_view<T, Traits>& str, const std::basic_string_view<T, Traits>& suffix) noexcept
{
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
return str.size() >= suffix.size() && equals_insensitive_ascii<>({ str.data() - suffix.size(), suffix.size() }, suffix);
return str.size() >= suffix.size() && equals_insensitive_ascii<>({ str.data() + str.size() - suffix.size(), suffix.size() }, suffix);
}
constexpr bool ends_with_insensitive_ascii(const std::string_view& str, const std::string_view& prefix) noexcept
@@ -251,7 +251,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
constexpr bool ends_with_insensitive_ascii(const std::wstring_view& str, const std::wstring_view& prefix) noexcept
{
return ends_with<>(str, prefix);
return ends_with_insensitive_ascii<>(str, prefix);
}
template<typename T, typename Traits>

View File

@@ -6,8 +6,3 @@
TARGETNAME = ConInteractivityOneCoreLib
TARGETTYPE = LIBRARY
# VSTS 14847240: Locally suppress individual -Wv:17 compiler warnings.
# For more information, visit https://osgwiki.com/wiki/Windows_C%2B%2B_Toolset_Status.
USER_C_FLAGS=$(USER_C_FLAGS) /wd4302 # 'conversion': truncation from 'type1' to 'type2'
USER_C_FLAGS=$(USER_C_FLAGS) /wd4311 # 'variable': pointer truncation from 'type 1' to 'type 2'

View File

@@ -119,12 +119,15 @@ DWORD WINAPI Renderer::s_renderThread(void* param) noexcept
DWORD Renderer::_renderThread() noexcept
{
while (true)
while (_threadKeepRunning.load(std::memory_order_relaxed))
{
_enable.wait();
_waitUntilCanRender();
_waitUntilTimerOrRedraw();
// We just completed what could have been a long wait;
// eagerly check again to prevent rendering if we don't
// need to.
if (!_threadKeepRunning.load(std::memory_order_relaxed))
{
break;

View File

@@ -595,9 +595,12 @@ constexpr T saturate(auto val)
RETURN_IF_FAILED(pObjectHandle->GetScreenBuffer(GENERIC_READ, &pObj));
// See ConptyCursorPositionMayBeWrong() for details.
if (pObj->ConptyCursorPositionMayBeWrong())
auto& activeBuffer = pObj->GetActiveBuffer();
// GetConsoleScreenBufferInfoExImpl uses GetActiveBuffer internally, but
// under the console lock.
if (activeBuffer.ConptyCursorPositionMayBeWrong())
{
pObj->WaitForConptyCursorPositionToBeSynchronized();
activeBuffer.WaitForConptyCursorPositionToBeSynchronized();
}
m->_pApiRoutines->GetConsoleScreenBufferInfoExImpl(*pObj, ex);

View File

@@ -166,7 +166,7 @@ void InteractDispatch::MoveCursor(const VTInt row, const VTInt col)
// Unblock any callers inside SCREEN_INFORMATION::WaitForConptyCursorPositionToBeSynchronized().
// The cursor position has now been updated to the terminal's.
info.ResetConptyCursorPositionMayBeWrong();
info.GetActiveBuffer().ResetConptyCursorPositionMayBeWrong();
}
// Routine Description:

View File

@@ -31,6 +31,7 @@ Enum AssetType {
ApplicationBundle
PreinstallKit
GroupPolicy
NugetPackage
Zip
}
@@ -87,6 +88,9 @@ Class Asset {
} ElseIf (".zip" -eq $local:ext -and $local:filename -like 'GroupPolicy*') {
$this.Type = [AssetType]::GroupPolicy
$this.Architecture = "all"
} ElseIf (".nupkg" -eq $local:ext) {
$this.Type = [AssetType]::NugetPackage
$this.Architecture = "all"
} ElseIf (".zip" -eq $local:ext) {
$this.Type = [AssetType]::Zip
} ElseIf (".msixbundle" -eq $local:ext) {
@@ -108,6 +112,13 @@ Class Asset {
Write-Verbose "Parsing AppxManifest.xml"
$local:Manifest = [xml](Get-Content (Join-Path $local:directory AppxManifest.xml))
$this.ParseManifest($local:Manifest)
} ElseIf ($this.Type -Eq [AssetType]::NugetPackage) {
Write-Verbose "Cracking nuget package $($local:filename)"
& $script:tar -x -f $local:bundlePath -C $local:directory *.nuspec
$local:nuspec = Get-ChildItem $local:directory *.nuspec -Recurse | Select-Object -First 1
Write-Verbose "Parsing $($local:nuspec.Name)"
$local:Manifest = [xml](Get-Content $local:nuspec)
$this.ParseNuspec($local:Manifest)
} Else {
If ($this.Type -Ne [AssetType]::GroupPolicy) {
& $script:tar -x -f $this.Path -C $local:directory --strip-components=1 '*/wt.exe'
@@ -135,6 +146,12 @@ Class Asset {
$this.Version = $Manifest.Package.Identity.Version
}
[void]ParseNuspec([xml]$Nuspec) {
$this.Name = $Nuspec.package.metadata.id
$this.Version = ($Nuspec.package.metadata.version -split '-')[0]
$this.ExpandedVersion = $Nuspec.package.metadata.version
}
[void]ParseFilename([string]$filename) {
$parts = [IO.Path]::GetFileNameWithoutExtension($filename).Split("_")
$this.Name = $parts[0]
@@ -160,6 +177,9 @@ Class Asset {
GroupPolicy {
"{0}_{1}.zip" -f ($this.Name, $this.Version)
}
NugetPackage {
"{0}.{1}.nupkg" -f ($this.Name, $this.ExpandedVersion)
}
Default {
Throw "Unknown type $($_.Type)"
}
@@ -253,15 +273,11 @@ Function New-ReleaseBody([Release]$Release) {
If (-Not [String]::IsNullOrEmpty($zipAssetVersion)) {
$body += "_Binary files inside the unpackaged distribution archive bear the version number ``$zipAssetVersion``._`n`n"
}
$body += "### Asset Hashes`n`n";
ForEach($a in $Release.Assets) {
$body += "- {0}`n - SHA256 ``{1}```n" -f ($a.IdealFilename(), (Get-FileHash $a.Path -Algorithm SHA256 | Select-Object -Expand Hash))
}
Return $body
}
# Collect Assets from $Directory, figure out what those assets are
$Assets = Get-ChildItem $Directory -Recurse -Include *.msixbundle, *.zip | ForEach-Object {
$Assets = Get-ChildItem $Directory -Recurse -Include *.msixbundle, *.zip, *.nupkg -Exclude *.Wpf.*,*.symbols.nupkg | ForEach-Object {
[Asset]::CreateFromFile($_.FullName)
}