Repository: microsoft/terminal (language: cpp)

Issue:
Automatically populate icons for the associated profiles
* Your Windows build number: 
Microsoft Windows [Version 10.0.18362.86]

* What you're doing and what's happening: 
Opening the terminal for the first time does not automatically select icons for the profiles

* What's wrong / what should be happening instead: 
Icons should be automatically placed in the profile for use in the flyout, tab list, and jump list



Discussion / hints:
This is something that I've wanted to do, but just haven't got the chance to yet.

This was previously MSFT: 21097504
Here are some notes I had before:

https://stackoverflow.com/a/1520223
https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-shgetfileinfoa
https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/ns-shellapi-_shfileinfoa

We can get the HICON from an exe. Theoretically, there should be a way to turn that icon into a bitmap, and a way to load that bitmap as an image that UWP XAML can use.

That Icon could be used in the new tab button's flyout.

this _WON'T_ work for WSL distros - they're Reparse points, so their exe's don't have ico's associated with them, the app does. 
> so their exe's don't have ico's associated with them, the app does

Did you check the EXE files in WSL Appx packages?
@Biswa96 It's possible that'll work. You'd probably have to navigate through the reparse point to find the real exe when you encounter one. It's certainly possible to do, though I don't know how to.
This is highly related to #1504
You know, in retrospect, automatically guessing the icon from the commandline is probably a bad idea. Something like `pwsh -nologo` and now we've got to try and find the actual executable in the commandline - not something I want us to be in the business of. 

#1504 is the better solution here - allowing the user to manually declare that they'd like to use an exe for the icon. It's less "magic", but it's more robust. So we'll just focus on that aspect of this request for now. 
So, I'll provide the counteropinion on this one. I think it's the right thing to do.

Because we can be the default terminal on Windows, we will want to derive icons from any `LNK` files¹ and `EXE` files that were used to spawn the console. Building on that: we may want to port _any information we can_ from those files to create ephemeral profiles -- color schemes, cursor styles, etc. -- so that users who switch won't be quite so jarred that we've forgotten all their settings.

¹which allow you to specify a custom icon, and a bunch of internal developers do just that; I have to assume it's common practice outside Microsoft as well :smile:

Huh, I think this got re-iterated in #7552 - I think we're gonna make it so any profile that doesn't have an icon explicitly set will automatically try to use the exe as the icon

Fix patch:
diff --git a/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp b/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp
index 0ae0bcc164e..4dbc7f88db8 100644
--- a/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp
+++ b/src/cascadia/LocalTests_SettingsModel/TerminalSettingsTests.cpp
@@ -173,13 +173,13 @@ namespace SettingsModelLocalTests
         {
             const auto commandLine = file2.native() + LR"( -foo "bar1 bar2" -baz)"s;
             const auto expected = file2.native() + L"\0-foo\0bar1 bar2\0-baz"s;
-            const auto actual = implementation::CascadiaSettings::NormalizeCommandLine(commandLine.c_str());
+            const auto actual = implementation::Profile::NormalizeCommandLine(commandLine.c_str());
             VERIFY_ARE_EQUAL(expected, actual);
         }
         {
             const auto commandLine = L"C:\\";
             const auto expected = L"C:\\";
-            const auto actual = implementation::CascadiaSettings::NormalizeCommandLine(commandLine);
+            const auto actual = implementation::Profile::NormalizeCommandLine(commandLine);
             VERIFY_ARE_EQUAL(expected, actual);
         }
     }
diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp
index 1717c05b8b7..78a1ead3acd 100644
--- a/src/cascadia/TerminalApp/TabManagement.cpp
+++ b/src/cascadia/TerminalApp/TabManagement.cpp
@@ -174,11 +174,12 @@ namespace winrt::TerminalApp::implementation
         // Set this tab's icon to the icon from the user's profile
         if (const auto profile{ newTabImpl->GetFocusedProfile() })
         {
-            if (!profile.Icon().empty())
+            const auto& icon = profile.EvaluatedIcon();
+            if (!icon.empty())
             {
                 const auto theme = _settings.GlobalSettings().CurrentTheme();
                 const auto iconStyle = (theme && theme.Tab()) ? theme.Tab().IconStyle() : IconStyle::Default;
-                newTabImpl->UpdateIcon(profile.Icon(), iconStyle);
+                newTabImpl->UpdateIcon(icon, iconStyle);
             }
         }
 
@@ -245,7 +246,7 @@ namespace winrt::TerminalApp::implementation
         {
             const auto theme = _settings.GlobalSettings().CurrentTheme();
             const auto iconStyle = (theme && theme.Tab()) ? theme.Tab().IconStyle() : IconStyle::Default;
-            tab.UpdateIcon(profile.Icon(), iconStyle);
+            tab.UpdateIcon(profile.EvaluatedIcon(), iconStyle);
         }
     }
 
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index 26a80fa7457..568a4d03f7e 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -1033,9 +1033,10 @@ namespace winrt::TerminalApp::implementation
 
         // If there's an icon set for this profile, set it as the icon for
         // this flyout item
-        if (!profile.Icon().empty())
+        const auto& iconPath = profile.EvaluatedIcon();
+        if (!iconPath.empty())
         {
-            const auto icon = _CreateNewTabFlyoutIcon(profile.Icon());
+            const auto icon = _CreateNewTabFlyoutIcon(iconPath);
             profileMenuItem.Icon(icon);
         }
 
diff --git a/src/cascadia/TerminalSettingsEditor/MainPage.cpp b/src/cascadia/TerminalSettingsEditor/MainPage.cpp
index 797c207ef88..b85b0ad5d11 100644
--- a/src/cascadia/TerminalSettingsEditor/MainPage.cpp
+++ b/src/cascadia/TerminalSettingsEditor/MainPage.cpp
@@ -602,7 +602,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         MUX::Controls::NavigationViewItem profileNavItem;
         profileNavItem.Content(box_value(profile.Name()));
         profileNavItem.Tag(box_value<Editor::ProfileViewModel>(profile));
-        profileNavItem.Icon(IconPathConverter::IconWUX(profile.Icon()));
+        profileNavItem.Icon(IconPathConverter::IconWUX(profile.EvaluatedIcon()));
 
         // Update the menu item when the icon/name changes
         auto weakMenuItem{ make_weak(profileNavItem) };
diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp
index 5f31f6be579..5f7517068b4 100644
--- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp
+++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.cpp
@@ -26,6 +26,8 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
     Windows::Foundation::Collections::IObservableVector<Editor::Font> ProfileViewModel::_MonospaceFontList{ nullptr };
     Windows::Foundation::Collections::IObservableVector<Editor::Font> ProfileViewModel::_FontList{ nullptr };
 
+    static constexpr std::wstring_view HideIconValue{ L"none" };
+
     ProfileViewModel::ProfileViewModel(const Model::Profile& profile, const Model::CascadiaSettings& appSettings) :
         _profile{ profile },
         _defaultAppearanceViewModel{ winrt::make<implementation::AppearanceViewModel>(profile.DefaultAppearance().try_as<AppearanceConfig>()) },
@@ -69,6 +71,10 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
             {
                 _NotifyChanges(L"CurrentScrollState");
             }
+            else if (viewModelProperty == L"Icon")
+            {
+                _NotifyChanges(L"HideIcon");
+            }
         });
 
         // Do the same for the starting directory
@@ -348,6 +354,26 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         }
     }
 
+    bool ProfileViewModel::HideIcon()
+    {
+        return Icon() == HideIconValue;
+    }
+    void ProfileViewModel::HideIcon(const bool hide)
+    {
+        if (hide)
+        {
+            // Stash the current value of Icon. If the user
+            // checks and un-checks the "Hide Icon" checkbox, we want
+            // the path that we display in the text box to remain unchanged.
+            _lastIcon = Icon();
+            Icon(HideIconValue);
+        }
+        else
+        {
+            Icon(_lastIcon);
+        }
+    }
+
     bool ProfileViewModel::IsBellStyleFlagSet(const uint32_t flag)
     {
         return (WI_EnumValue(BellStyle()) & flag) == flag;
diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h
index 44df681665f..7770c1b2f3a 100644
--- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h
+++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.h
@@ -58,11 +58,20 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
             Padding(to_hstring(value));
         }
 
+        winrt::hstring EvaluatedIcon() const
+        {
+            return _profile.EvaluatedIcon();
+        }
+
         // starting directory
         bool UseParentProcessDirectory();
         void UseParentProcessDirectory(const bool useParent);
         bool UseCustomStartingDirectory();
 
+        // icon
+        bool HideIcon();
+        void HideIcon(const bool hide);
+
         // general profile knowledge
         winrt::guid OriginalProfileGuid() const noexcept;
         bool CanDeleteProfile() const;
@@ -119,6 +128,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
         winrt::guid _originalProfileGuid{};
         winrt::hstring _lastBgImagePath;
         winrt::hstring _lastStartingDirectoryPath;
+        winrt::hstring _lastIcon;
         Editor::AppearanceViewModel _defaultAppearanceViewModel;
 
         static Windows::Foundation::Collections::IObservableVector<Editor::Font> _MonospaceFontList;
diff --git a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl
index 5020ecb7945..f9f618c8b80 100644
--- a/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl
+++ b/src/cascadia/TerminalSettingsEditor/ProfileViewModel.idl
@@ -67,6 +67,7 @@ namespace Microsoft.Terminal.Settings.Editor
         ProfileSubPage CurrentPage;
         Boolean UseParentProcessDirectory;
         Boolean UseCustomStartingDirectory { get; };
+        Boolean HideIcon;
         AppearanceViewModel DefaultAppearance { get; };
         Guid OriginalProfileGuid { get; };
         Boolean HasUnfocusedAppearance { get; };
@@ -75,6 +76,8 @@ namespace Microsoft.Terminal.Settings.Editor
         AppearanceViewModel UnfocusedAppearance { get; };
         Boolean VtPassthroughAvailable { get; };
 
+        String EvaluatedIcon { get; };
+
         void CreateUnfocusedAppearance();
         void DeleteUnfocusedAppearance();
 
diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Base.xaml b/src/cascadia/TerminalSettingsEditor/Profiles_Base.xaml
index 11885a52fe3..28f2901261f 100644
--- a/src/cascadia/TerminalSettingsEditor/Profiles_Base.xaml
+++ b/src/cascadia/TerminalSettingsEditor/Profiles_Base.xaml
@@ -108,13 +108,20 @@
                 <StackPanel>
                     <TextBox x:Uid="Profile_IconBox"
                              FontFamily="Segoe UI, Segoe Fluent Icons, Segoe MDL2 Assets"
+                             IsEnabled="{x:Bind local:Converters.InvertBoolean(Profile.HideIcon), Mode=OneWay}"
                              IsSpellCheckEnabled="False"
                              Style="{StaticResource TextBoxSettingStyle}"
-                             Text="{x:Bind Profile.Icon, Mode=TwoWay}" />
+                             Text="{x:Bind Profile.Icon, Mode=TwoWay}"
+                             Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(Profile.HideIcon), Mode=OneWay}" />
                     <Button x:Uid="Profile_IconBrowse"
                             Margin="0,10,0,0"
                             Click="Icon_Click"
-                            Style="{StaticResource BrowseButtonStyle}" />
+                            Style="{StaticResource BrowseButtonStyle}"
+                            Visibility="{x:Bind local:Converters.InvertedBooleanToVisibility(Profile.HideIcon), Mode=OneWay}" />
+                    <CheckBox x:Name="HideIconCheckbox"
+                              x:Uid="Profile_HideIconCheckbox"
+                              Margin="0,5,0,0"
+                              IsChecked="{x:Bind Profile.HideIcon, Mode=TwoWay}" />
                 </StackPanel>
             </local:SettingContainer>
 
diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
index d0e6fe634a9..9e10277faec 100644
--- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw
@@ -1034,6 +1034,14 @@
     <value>If enabled, this profile will spawn in the directory from which Terminal was launched.</value>
     <comment>A description for what the supplementary "use parent process directory" setting does. Presented near "Profile_StartingDirectoryUseParentCheckbox".</comment>
   </data>
+  <data name="Profile_HideIconCheckbox.Content" xml:space="preserve">
+    <value>Hide icon</value>
+    <comment>A supplementary setting to the "icon" setting.</comment>
+  </data>
+  <data name="Profile_HideIconCheckbox.[using:Windows.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
+    <value>If enabled, this profile will have no icon.</value>
+    <comment>A description for what the supplementary "Hide icon" setting does. Presented near "Profile_HideIconCheckbox".</comment>
+  </data>
   <data name="Profile_SuppressApplicationTitle.Header" xml:space="preserve">
     <value>Suppress title changes</value>
     <comment>Header for a control to toggle changes in the app title.</comment>
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
index 851e667ac9a..ad9ac6df600 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.cpp
@@ -299,6 +299,7 @@ Model::Profile CascadiaSettings::DuplicateProfile(const Model::Profile& source)
     // These aren't in MTSM_PROFILE_SETTINGS because they're special
     DUPLICATE_SETTING_MACRO(TabColor);
     DUPLICATE_SETTING_MACRO(Padding);
+    DUPLICATE_SETTING_MACRO(Icon);
 
     {
         const auto font = source.FontInfo();
@@ -515,8 +516,12 @@ void CascadiaSettings::_validateMediaResources()
             }
         }
 
-        // Anything longer than 2 wchar_t's _isn't_ an emoji or symbol,
-        // so treat it as an invalid path.
+        // Anything longer than 2 wchar_t's _isn't_ an emoji or symbol, so treat
+        // it as an invalid path.
+        //
+        // Explicitly just use the Icon here, not the EvaluatedIcon. We don't
+        // want to blow up if we fell back to the commandline and the
+        // commandline _isn't an icon_.
         if (const auto icon = profile.Icon(); icon.size() > 2)
         {
             const auto iconPath{ wil::ExpandEnvironmentStringsW<std::wstring>(icon.c_str()) };
@@ -662,7 +667,7 @@ Model::Profile CascadiaSettings::_getProfileForCommandLine(const winrt::hstring&
 
             try
             {
-                _commandLinesCache.emplace_back(NormalizeCommandLine(cmd.c_str()), profile);
+                _commandLinesCache.emplace_back(Profile::NormalizeCommandLine(cmd.c_str()), profile);
             }
             CATCH_LOG()
         }
@@ -681,7 +686,7 @@ Model::Profile CascadiaSettings::_getProfileForCommandLine(const winrt::hstring&
 
     try
     {
-        const auto needle = NormalizeCommandLine(commandLine.c_str());
+        const auto needle = Profile::NormalizeCommandLine(commandLine.c_str());
 
         // til::starts_with(string, prefix) will always return false if prefix.size() > string.size().
         // --> Using binary search we can safely skip all items in _commandLinesCache where .first.size() > needle.size().
@@ -710,129 +715,6 @@ Model::Profile CascadiaSettings::_getProfileForCommandLine(const winrt::hstring&
     return nullptr;
 }
 
-// Given a commandLine like the following:
-// * "C:\WINDOWS\System32\cmd.exe"
-// * "pwsh -WorkingDirectory ~"
-// * "C:\Program Files\PowerShell\7\pwsh.exe"
-// * "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~"
-//
-// This function returns:
-// * "C:\Windows\System32\cmd.exe"
-// * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
-// * "C:\Program Files\PowerShell\7\pwsh.exe"
-// * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
-//
-// The resulting strings are then used for comparisons in _getProfileForCommandLine().
-// For instance a resulting string of
-//   "C:\Program Files\PowerShell\7\pwsh.exe"
-// is considered a compatible profile with
-//   "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~"
-// as it shares the same (normalized) prefix.
-std::wstring CascadiaSettings::NormalizeCommandLine(LPCWSTR commandLine)
-{
-    // Turn "%SystemRoot%\System32\cmd.exe" into "C:\WINDOWS\System32\cmd.exe".
-    // We do this early, as environment variables might occur anywhere in the commandLine.
-    std::wstring normalized;
-    THROW_IF_FAILED(wil::ExpandEnvironmentStringsW(commandLine, normalized));
-
-    // One of the most important things this function does is to strip quotes.
-    // That way the commandLine "foo.exe -bar" and "\"foo.exe\" \"-bar\"" appear identical.
-    // We'll abuse CommandLineToArgvW for that as it's close to what CreateProcessW uses.
-    auto argc = 0;
-    wil::unique_hlocal_ptr<PWSTR[]> argv{ CommandLineToArgvW(normalized.c_str(), &argc) };
-    THROW_LAST_ERROR_IF(!argc);
-
-    // The index of the first argument in argv for our executable in argv[0].
-    // Given {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"} this will be 1.
-    auto startOfArguments = 1;
-
-    // The given commandLine should start with an executable name or path.
-    // For instance given the following argv arrays:
-    // * {"C:\WINDOWS\System32\cmd.exe"}
-    // * {"pwsh", "-WorkingDirectory", "~"}
-    // * {"C:\Program", "Files\PowerShell\7\pwsh.exe"}
-    //               ^^^^
-    //   Notice how there used to be a space in the path, which was split by ExpandEnvironmentStringsW().
-    //   CreateProcessW() supports such atrocities, so we got to do the same.
-    // * {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"}
-    //
-    // This loop tries to resolve relative paths, as well as executable names in %PATH%
-    // into absolute paths and normalizes them. The results for the above would be:
-    // * "C:\Windows\System32\cmd.exe"
-    // * "C:\Program Files\PowerShell\7\pwsh.exe"
-    // * "C:\Program Files\PowerShell\7\pwsh.exe"
-    // * "C:\Program Files\PowerShell\7\pwsh.exe"
-    for (;;)
-    {
-        // CreateProcessW uses RtlGetExePath to get the lpPath for SearchPathW.
-        // The difference between the behavior of SearchPathW if lpPath is nullptr and what RtlGetExePath returns
-        // seems to be mostly whether SafeProcessSearchMode is respected and the support for relative paths.
-        // Windows Terminal makes the use of relative paths rather impractical which is why we simply dropped the call to RtlGetExePath.
-        const auto status = wil::SearchPathW(nullptr, argv[0], L".exe", normalized);
-
-        if (status == S_OK)
-        {
-            const auto attributes = GetFileAttributesW(normalized.c_str());
-
-            if (attributes != INVALID_FILE_ATTRIBUTES && WI_IsFlagClear(attributes, FILE_ATTRIBUTE_DIRECTORY))
-            {
-                std::filesystem::path path{ std::move(normalized) };
-
-                // canonical() will resolve symlinks, etc. for us.
-                {
-                    std::error_code ec;
-                    auto canonicalPath = std::filesystem::canonical(path, ec);
-                    if (!ec)
-                    {
-                        path = std::move(canonicalPath);
-                    }
-                }
-
-                // std::filesystem::path has no way to extract the internal path.
-                // So about that.... I own you, computer. Give me that path.
-                normalized = std::move(const_cast<std::wstring&>(path.native()));
-                break;
-            }
-        }
-        // All other error types aren't handled at the moment.
-        else if (status != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
-        {
-            break;
-        }
-        // If the file path couldn't be found by SearchPathW this could be the result of us being given a commandLine
-        // like "C:\foo bar\baz.exe -arg" which is resolved to the argv array {"C:\foo", "bar\baz.exe", "-arg"},
-        // or we were erroneously given a directory to execute (e.g. someone ran `wt .`).
-        // Just like CreateProcessW() we thus try to concatenate arguments until we successfully resolve a valid path.
-        // Of course we can only do that if we have at least 2 remaining arguments in argv.
-        if ((argc - startOfArguments) < 2)
-        {
-            break;
-        }
-
-        // As described in the comment right above, we concatenate arguments in an attempt to resolve a valid path.
-        // The code below turns argv from {"C:\foo", "bar\baz.exe", "-arg"} into {"C:\foo bar\baz.exe", "-arg"}.
-        // The code abuses the fact that CommandLineToArgvW allocates all arguments back-to-back on the heap separated by '\0'.
-        argv[startOfArguments][-1] = L' ';
-        ++startOfArguments;
-    }
-
-    // We've (hopefully) finished resolving the path to the executable.
-    // We're now going to append all remaining arguments to the resulting string.
-    // If argv is {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"},
-    // then we'll get "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
-    if (startOfArguments < argc)
-    {
-        // normalized contains a canonical form of argv[0] at this point.
-        // -1 allows us to include the \0 between argv[0] and argv[1] in the call to append().
-        const auto beg = argv[startOfArguments] - 1;
-        const auto lastArg = argv[argc - 1];
-        const auto end = lastArg + wcslen(lastArg);
-        normalized.append(beg, end);
-    }
-
-    return normalized;
-}
-
 // Method Description:
 // - Helper to get a profile given a name that could be a guid or an actual name.
 // Arguments:
diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
index a9c4b9bb325..9f193851508 100644
--- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
+++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h
@@ -136,7 +136,6 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         winrt::hstring GetSerializationErrorMessage() const;
 
         // defterm
-        static std::wstring NormalizeCommandLine(LPCWSTR commandLine);
         static bool IsDefaultTerminalAvailable() noexcept;
         static bool IsDefaultTerminalSet() noexcept;
         winrt::Windows::Foundation::Collections::IObservableVector<Model::DefaultTerminal> DefaultTerminals() noexcept;
diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp
index b211987b677..f5936c2f5be 100644
--- a/src/cascadia/TerminalSettingsModel/Command.cpp
+++ b/src/cascadia/TerminalSettingsModel/Command.cpp
@@ -602,7 +602,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
 
                 // - Escape the profile name for JSON appropriately
                 auto escapedProfileName = _escapeForJson(til::u16u8(p.Name()));
-                auto escapedProfileIcon = _escapeForJson(til::u16u8(p.Icon()));
+                auto escapedProfileIcon = _escapeForJson(til::u16u8(p.EvaluatedIcon()));
                 auto newJsonString = til::replace_needle_in_haystack(oldJsonString,
                                                                      ProfileNameToken,
                                                                      escapedProfileName);
diff --git a/src/cascadia/TerminalSettingsModel/MTSMSettings.h b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
index f9626e6b6c8..65b69c51958 100644
--- a/src/cascadia/TerminalSettingsModel/MTSMSettings.h
+++ b/src/cascadia/TerminalSettingsModel/MTSMSettings.h
@@ -85,7 +85,6 @@ Author(s):
     X(hstring, StartingDirectory, "startingDirectory")                                                                                                         \
     X(bool, SuppressApplicationTitle, "suppressApplicationTitle", false)                                                                                       \
     X(guid, ConnectionType, "connectionType")                                                                                                                  \
-    X(hstring, Icon, "icon", L"\uE756")                                                                                                                        \
     X(CloseOnExitMode, CloseOnExit, "closeOnExit", CloseOnExitMode::Automatic)                                                                                 \
     X(hstring, TabTitle, "tabTitle")                                                                                                                           \
     X(Model::BellStyle, BellStyle, "bellStyle", BellStyle::Audible)                                                                                            \
diff --git a/src/cascadia/TerminalSettingsModel/Profile.cpp b/src/cascadia/TerminalSettingsModel/Profile.cpp
index 3aa896f50da..f31cd4e85f7 100644
--- a/src/cascadia/TerminalSettingsModel/Profile.cpp
+++ b/src/cascadia/TerminalSettingsModel/Profile.cpp
@@ -12,6 +12,8 @@
 
 #include "Profile.g.cpp"
 
+#include <shellapi.h>
+
 using namespace Microsoft::Terminal::Settings::Model;
 using namespace winrt::Microsoft::Terminal::Settings::Model::implementation;
 using namespace winrt::Microsoft::Terminal::Control;
@@ -25,6 +27,7 @@ static constexpr std::string_view NameKey{ "name" };
 static constexpr std::string_view GuidKey{ "guid" };
 static constexpr std::string_view SourceKey{ "source" };
 static constexpr std::string_view HiddenKey{ "hidden" };
+static constexpr std::string_view IconKey{ "icon" };
 
 static constexpr std::string_view FontInfoKey{ "font" };
 static constexpr std::string_view PaddingKey{ "padding" };
@@ -104,6 +107,7 @@ winrt::com_ptr<Profile> Profile::CopySettings() const
     profile->_Hidden = _Hidden;
     profile->_TabColor = _TabColor;
     profile->_Padding = _Padding;
+    profile->_Icon = _Icon;
 
     profile->_Origin = _Origin;
     profile->_FontInfo = *fontInfo;
@@ -170,6 +174,7 @@ void Profile::LayerJson(const Json::Value& json)
     JsonUtils::GetValueForKey(json, GuidKey, _Guid);
     JsonUtils::GetValueForKey(json, HiddenKey, _Hidden);
     JsonUtils::GetValueForKey(json, SourceKey, _Source);
+    JsonUtils::GetValueForKey(json, IconKey, _Icon);
 
     // Padding was never specified as an integer, but it was a common working mistake.
     // Allow it to be permissive.
@@ -312,6 +317,7 @@ Json::Value Profile::ToJson() const
     JsonUtils::SetValueForKey(json, GuidKey, writeBasicSettings ? Guid() : _Guid);
     JsonUtils::SetValueForKey(json, HiddenKey, writeBasicSettings ? Hidden() : _Hidden);
     JsonUtils::SetValueForKey(json, SourceKey, writeBasicSettings ? Source() : _Source);
+    JsonUtils::SetValueForKey(json, IconKey, writeBasicSettings ? Icon() : _Icon);
 
     // PermissiveStringConverter is unnecessary for serialization
     JsonUtils::SetValueForKey(json, PaddingKey, _Padding);
@@ -336,3 +342,167 @@ Json::Value Profile::ToJson() const
 
     return json;
 }
+
+// This is the implementation for and INHERITABLE_SETTING, but with one addition
+// in the setter. We want to make sure to clear out our cached icon, so that we
+// can re-evaluate it as it changes in the SUI.
+void Profile::Icon(const winrt::hstring& value)
+{
+    _evaluatedIcon = std::nullopt;
+    _Icon = value;
+}
+winrt::hstring Profile::Icon() const
+{
+    const auto val{ _getIconImpl() };
+    return val ? *val : hstring{ L"\uE756" };
+}
+
+winrt::hstring Profile::EvaluatedIcon()
+{
+    // We cache the result here, so we don't search the path for the exe every time.
+    if (!_evaluatedIcon.has_value())
+    {
+        _evaluatedIcon = _evaluateIcon();
+    }
+    return *_evaluatedIcon;
+}
+
+winrt::hstring Profile::_evaluateIcon() const
+{
+    // If the profile has an icon, return it.
+    if (!Icon().empty())
+    {
+        return Icon();
+    }
+
+    // Otherwise, use NormalizeCommandLine to find the actual exe name. This
+    // will actually search for the exe, including spaces, in the same way that
+    // CreateProcess does.
+    std::wstring cmdline{ NormalizeCommandLine(Commandline().c_str()) };
+    // NormalizeCommandLine will return a string with embedded nulls after each
+    // arg. We just want the first one.
+    return winrt::hstring{ cmdline.c_str() };
+}
+
+// Given a commandLine like the following:
+// * "C:\WINDOWS\System32\cmd.exe"
+// * "pwsh -WorkingDirectory ~"
+// * "C:\Program Files\PowerShell\7\pwsh.exe"
+// * "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~"
+//
+// This function returns:
+// * "C:\Windows\System32\cmd.exe"
+// * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
+// * "C:\Program Files\PowerShell\7\pwsh.exe"
+// * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
+//
+// The resulting strings are then used for comparisons in _getProfileForCommandLine().
+// For instance a resulting string of
+//   "C:\Program Files\PowerShell\7\pwsh.exe"
+// is considered a compatible profile with
+//   "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~"
+// as it shares the same (normalized) prefix.
+std::wstring Profile::NormalizeCommandLine(LPCWSTR commandLine)
+{
+    // Turn "%SystemRoot%\System32\cmd.exe" into "C:\WINDOWS\System32\cmd.exe".
+    // We do this early, as environment variables might occur anywhere in the commandLine.
+    std::wstring normalized;
+    THROW_IF_FAILED(wil::ExpandEnvironmentStringsW(commandLine, normalized));
+
+    // One of the most important things this function does is to strip quotes.
+    // That way the commandLine "foo.exe -bar" and "\"foo.exe\" \"-bar\"" appear identical.
+    // We'll abuse CommandLineToArgvW for that as it's close to what CreateProcessW uses.
+    auto argc = 0;
+    wil::unique_hlocal_ptr<PWSTR[]> argv{ CommandLineToArgvW(normalized.c_str(), &argc) };
+    THROW_LAST_ERROR_IF(!argc);
+
+    // The index of the first argument in argv for our executable in argv[0].
+    // Given {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"} this will be 1.
+    auto startOfArguments = 1;
+
+    // The given commandLine should start with an executable name or path.
+    // For instance given the following argv arrays:
+    // * {"C:\WINDOWS\System32\cmd.exe"}
+    // * {"pwsh", "-WorkingDirectory", "~"}
+    // * {"C:\Program", "Files\PowerShell\7\pwsh.exe"}
+    //               ^^^^
+    //   Notice how there used to be a space in the path, which was split by ExpandEnvironmentStringsW().
+    //   CreateProcessW() supports such atrocities, so we got to do the same.
+    // * {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"}
+    //
+    // This loop tries to resolve relative paths, as well as executable names in %PATH%
+    // into absolute paths and normalizes them. The results for the above would be:
+    // * "C:\Windows\System32\cmd.exe"
+    // * "C:\Program Files\PowerShell\7\pwsh.exe"
+    // * "C:\Program Files\PowerShell\7\pwsh.exe"
+    // * "C:\Program Files\PowerShell\7\pwsh.exe"
+    for (;;)
+    {
+        // CreateProcessW uses RtlGetExePath to get the lpPath for SearchPathW.
+        // The difference between the behavior of SearchPathW if lpPath is nullptr and what RtlGetExePath returns
+        // seems to be mostly whether SafeProcessSearchMode is respected and the support for relative paths.
+        // Windows Terminal makes the use of relative paths rather impractical which is why we simply dropped the call to RtlGetExePath.
+        const auto status = wil::SearchPathW(nullptr, argv[0], L".exe", normalized);
+
+        if (status == S_OK)
+        {
+            const auto attributes = GetFileAttributesW(normalized.c_str());
+
+            if (attributes != INVALID_FILE_ATTRIBUTES && WI_IsFlagClear(attributes, FILE_ATTRIBUTE_DIRECTORY))
+            {
+                std::filesystem::path path{ std::move(normalized) };
+
+                // canonical() will resolve symlinks, etc. for us.
+                {
+                    std::error_code ec;
+                    auto canonicalPath = std::filesystem::canonical(path, ec);
+                    if (!ec)
+                    {
+                        path = std::move(canonicalPath);
+                    }
+                }
+
+                // std::filesystem::path has no way to extract the internal path.
+                // So about that.... I own you, computer. Give me that path.
+                normalized = std::move(const_cast<std::wstring&>(path.native()));
+                break;
+            }
+        }
+        // All other error types aren't handled at the moment.
+        else if (status != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
+        {
+            break;
+        }
+        // If the file path couldn't be found by SearchPathW this could be the result of us being given a commandLine
+        // like "C:\foo bar\baz.exe -arg" which is resolved to the argv array {"C:\foo", "bar\baz.exe", "-arg"},
+        // or we were erroneously given a directory to execute (e.g. someone ran `wt .`).
+        // Just like CreateProcessW() we thus try to concatenate arguments until we successfully resolve a valid path.
+        // Of course we can only do that if we have at least 2 remaining arguments in argv.
+        if ((argc - startOfArguments) < 2)
+        {
+            break;
+        }
+
+        // As described in the comment right above, we concatenate arguments in an attempt to resolve a valid path.
+        // The code below turns argv from {"C:\foo", "bar\baz.exe", "-arg"} into {"C:\foo bar\baz.exe", "-arg"}.
+        // The code abuses the fact that CommandLineToArgvW allocates all arguments back-to-back on the heap separated by '\0'.
+        argv[startOfArguments][-1] = L' ';
+        ++startOfArguments;
+    }
+
+    // We've (hopefully) finished resolving the path to the executable.
+    // We're now going to append all remaining arguments to the resulting string.
+    // If argv is {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"},
+    // then we'll get "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
+    if (startOfArguments < argc)
+    {
+        // normalized contains a canonical form of argv[0] at this point.
+        // -1 allows us to include the \0 between argv[0] and argv[1] in the call to append().
+        const auto beg = argv[startOfArguments] - 1;
+        const auto lastArg = argv[argc - 1];
+        const auto end = lastArg + wcslen(lastArg);
+        normalized.append(beg, end);
+    }
+
+    return normalized;
+}
diff --git a/src/cascadia/TerminalSettingsModel/Profile.h b/src/cascadia/TerminalSettingsModel/Profile.h
index 05bd77cc720..1414dc0be1d 100644
--- a/src/cascadia/TerminalSettingsModel/Profile.h
+++ b/src/cascadia/TerminalSettingsModel/Profile.h
@@ -102,9 +102,16 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         Model::IAppearanceConfig DefaultAppearance();
         Model::FontConfig FontInfo();
 
+        winrt::hstring EvaluatedIcon();
+
+        static std::wstring NormalizeCommandLine(LPCWSTR commandLine);
+
         void _FinalizeInheritance() override;
 
         // Special fields
+        hstring Icon() const;
+        void Icon(const hstring& value);
+
         WINRT_PROPERTY(bool, Deleted, false);
         WINRT_PROPERTY(OriginTag, Origin, OriginTag::None);
         WINRT_PROPERTY(guid, Updates);
@@ -119,7 +126,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
         INHERITABLE_SETTING(Model::Profile, bool, Hidden, false);
         INHERITABLE_SETTING(Model::Profile, guid, Guid, _GenerateGuidForProfile(Name(), Source()));
         INHERITABLE_SETTING(Model::Profile, hstring, Padding, DEFAULT_PADDING);
+        // Icon is _very special_ because we want to customize its setter
+        _BASE_INHERITABLE_SETTING(Model::Profile, std::optional<hstring>, Icon, L"\uE756");
 
+    public:
 #define PROFILE_SETTINGS_INITIALIZE(type, name, jsonKey, ...) \
     INHERITABLE_SETTING(Model::Profile, type, name, ##__VA_ARGS__)
         MTSM_PROFILE_SETTINGS(PROFILE_SETTINGS_INITIALIZE)
@@ -128,10 +138,15 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
     private:
         Model::IAppearanceConfig _DefaultAppearance{ winrt::make<AppearanceConfig>(weak_ref<Model::Profile>(*this)) };
         Model::FontConfig _FontInfo{ winrt::make<FontConfig>(weak_ref<Model::Profile>(*this)) };
+
+        std::optional<winrt::hstring> _evaluatedIcon{ std::nullopt };
+
         static std::wstring EvaluateStartingDirectory(const std::wstring& directory);
 
         static guid _GenerateGuidForProfile(const std::wstring_view& name, const std::wstring_view& source) noexcept;
 
+        winrt::hstring _evaluateIcon() const;
+
         friend class SettingsModelLocalTests::DeserializationTests;
         friend class SettingsModelLocalTests::ProfileTests;
         friend class SettingsModelLocalTests::ColorSchemeTests;
diff --git a/src/cascadia/TerminalSettingsModel/Profile.idl b/src/cascadia/TerminalSettingsModel/Profile.idl
index 16a17b15ee0..14e074bc6db 100644
--- a/src/cascadia/TerminalSettingsModel/Profile.idl
+++ b/src/cascadia/TerminalSettingsModel/Profile.idl
@@ -53,6 +53,10 @@ namespace Microsoft.Terminal.Settings.Model
         Boolean Deleted { get; };
         OriginTag Origin { get; };
 
+        // Helper for magically using a commandline for an icon for a profile
+        // without an explicit icon.
+        String EvaluatedIcon { get; };
+
         INHERITABLE_PROFILE_SETTING(Guid, Guid);
         INHERITABLE_PROFILE_SETTING(String, Name);
         INHERITABLE_PROFILE_SETTING(String, Source);


Test patch: