When the profile icon is set to null, fall back to the icon of the commandline (#15843)

Basically, title. If you null out the icon, we'll automatically try to
use the `commandline` as an icon (because we can now). We'll even be
smart about it - `cmd.exe /k echo wassup` will still just use the ico of
`cmd.exe`.

This doesn't work for `ubuntu.exe` (et. al), because that commandline is
technically a reparse point, that doesn't actually have an icon
associated with it.
Closes #705

`"none"` becomes our sentinel value for "no icon". 

This will also use the same `NormalizeCommandLine` we use for
commandline matching for finding the full path to the exe.
This commit is contained in:
Mike Griese 2024-02-26 13:32:19 -08:00 committed by GitHub
parent fefee50757
commit 4ff38c260f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 265 additions and 140 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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) };

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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>

View File

@ -1042,6 +1042,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>

View File

@ -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:

View File

@ -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;

View File

@ -606,7 +606,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);

View File

@ -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) \

View File

@ -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;
}

View File

@ -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;

View File

@ -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);