From 1b2aad6504cbba22de6ea86a56612bf38cd053bb Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Wed, 13 Aug 2025 15:43:27 -0700 Subject: [PATCH] Add SSH folder to NTM for dynamic SSH profiles (#19239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatically generates an "SSH" folder in the new tab menu that contains all profiles generated by the SSH profile generator. This folder is created if the SSH generator created some profiles and the folder hasn't been created before. Detecting if the folder was generated is done via the new `bool ApplicationState::SSHFolderGenerated`. The logic is similar to `SettingsLoader::DisableDeletedProfiles()`. Found a bug on new tab menu's folder inlining feature where we were counting the number of raw entries to determine whether to inline or not. Since the folder only contained the match profiles entry, this bug made it so that the profile entries would always be inlined. The fix was very simple: count the number of _resolved_ entries instead of the raw entries. This can be pulled into its own PR and serviced, if desired. ## References and Relevant Issues #18814 #14042 ## Validation Steps Performed ✅ Existing users get an SSH folder if profiles were generated ## PR Checklist Closes #19043 --- src/cascadia/TerminalApp/TerminalPage.cpp | 2 +- .../TerminalSettingsModel/ApplicationState.h | 3 +- .../TerminalSettingsModel/CascadiaSettings.h | 2 ++ .../CascadiaSettingsSerialization.cpp | 33 ++++++++++++++++++- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index fbbc8a4076..47dee8aae7 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1069,7 +1069,7 @@ namespace winrt::TerminalApp::implementation auto folderEntryItems = _CreateNewTabFlyoutItems(folderEntries); // If the folder should auto-inline and there is only one item, do so. - if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntries.Size() == 1) + if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntryItems.size() == 1) { for (auto const& folderEntryItem : folderEntryItems) { diff --git a/src/cascadia/TerminalSettingsModel/ApplicationState.h b/src/cascadia/TerminalSettingsModel/ApplicationState.h index 999ab0c0fc..f998fc5ead 100644 --- a/src/cascadia/TerminalSettingsModel/ApplicationState.h +++ b/src/cascadia/TerminalSettingsModel/ApplicationState.h @@ -41,7 +41,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation X(FileSource::Shared, Windows::Foundation::Collections::IVector, RecentCommands, "recentCommands") \ X(FileSource::Shared, Windows::Foundation::Collections::IVector, DismissedMessages, "dismissedMessages") \ X(FileSource::Local, Windows::Foundation::Collections::IVector, AllowedCommandlines, "allowedCommandlines") \ - X(FileSource::Local, std::unordered_set, DismissedBadges, "dismissedBadges") + X(FileSource::Local, std::unordered_set, DismissedBadges, "dismissedBadges") \ + X(FileSource::Shared, bool, SSHFolderGenerated, "sshFolderGenerated", false) struct WindowLayout : WindowLayoutT { diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h index 5e235d62af..8336db13f0 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettings.h +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettings.h @@ -93,6 +93,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation void MergeFragmentIntoUserSettings(const winrt::hstring& source, const winrt::hstring& basePath, const std::string_view& content); void FinalizeLayering(); bool DisableDeletedProfiles(); + bool AddDynamicProfileFolders(); bool RemapColorSchemeForProfile(const winrt::com_ptr& profile); bool FixupUserSettings(); @@ -100,6 +101,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation ParsedSettings userSettings; std::unordered_map> extensionPackageMap; bool duplicateProfile = false; + bool sshProfilesGenerated = false; private: struct JsonSettings diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index bb00ac550e..f251d3e027 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -213,8 +213,11 @@ void SettingsLoader::GenerateProfiles() auto generateProfiles = [&](const IDynamicProfileGenerator& generator) { if (!_ignoredNamespaces.contains(generator.GetNamespace())) { + const auto oldProfileCount = inboxSettings.profiles.size(); _executeGenerator(generator, inboxSettings.profiles); + return oldProfileCount != inboxSettings.profiles.size(); } + return false; }; // Generate profiles for each generator and add them to the inbox settings. @@ -224,7 +227,7 @@ void SettingsLoader::GenerateProfiles() generateProfiles(AzureCloudShellGenerator{}); generateProfiles(VisualStudioGenerator{}); #if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED - generateProfiles(SshHostGenerator{}); + sshProfilesGenerated = generateProfiles(SshHostGenerator{}); #endif } @@ -536,6 +539,33 @@ bool SettingsLoader::DisableDeletedProfiles() return newGeneratedProfiles; } +// Returns true if something got changed and +// the settings need to be saved to disk. +bool SettingsLoader::AddDynamicProfileFolders() +{ + // Keep track of generated folders to avoid regenerating them + const auto state = get_self(ApplicationState::SharedInstance()); + + // If the SSH generator is enabled, try to create an "SSH" folder with all the generated profiles + if (sshProfilesGenerated && !state->SSHFolderGenerated()) + { + SshHostGenerator sshGenerator; + auto matchProfilesEntry = make_self(); + matchProfilesEntry->Source(hstring{ sshGenerator.GetNamespace() }); + + auto folderEntry = make_self(); + folderEntry->Name(L"SSH"); + folderEntry->Icon(MediaResource::FromString(hstring{ sshGenerator.GetIcon() })); + folderEntry->Inlining(FolderEntryInlining::Auto); + folderEntry->RawEntries(winrt::single_threaded_vector({ *matchProfilesEntry })); + + userSettings.globals->NewTabMenu().Append(folderEntry.as()); + state->SSHFolderGenerated(true); + return true; + } + return false; +} + bool winrt::Microsoft::Terminal::Settings::Model::implementation::SettingsLoader::RemapColorSchemeForProfile(const winrt::com_ptr& profile) { bool modified{ false }; @@ -1229,6 +1259,7 @@ try // DisableDeletedProfiles returns true whenever we encountered any new generated/dynamic profiles. // Similarly FixupUserSettings returns true, when it encountered settings that were patched up. mustWriteToDisk |= loader.DisableDeletedProfiles(); + mustWriteToDisk |= loader.AddDynamicProfileFolders(); mustWriteToDisk |= loader.FixupUserSettings(); // If this throws, the app will catch it and use the default settings.