mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 00:48:23 -06:00
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:
parent
fefee50757
commit
4ff38c260f
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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) };
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) \
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user