diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileAsAdminCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileAsAdminCommand.cs index f215ba5e94..4a0604a995 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileAsAdminCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileAsAdminCommand.cs @@ -20,13 +20,15 @@ internal sealed partial class LaunchProfileAsAdminCommand : InvokableCommand private readonly string _profile; private readonly bool _openNewTab; private readonly bool _openQuake; + private readonly AppSettingsManager _appSettingsManager; - internal LaunchProfileAsAdminCommand(string id, string profile, bool openNewTab, bool openQuake) + internal LaunchProfileAsAdminCommand(string id, string profile, bool openNewTab, bool openQuake, AppSettingsManager appSettingsManager) { this._id = id; this._profile = profile; this._openNewTab = openNewTab; this._openQuake = openQuake; + this._appSettingsManager = appSettingsManager; this.Name = Resources.launch_profile_as_admin; this.Icon = Icons.AdminIcon; @@ -59,6 +61,17 @@ internal sealed partial class LaunchProfileAsAdminCommand : InvokableCommand //_context.API.ShowMsg(name, message, string.Empty); Logger.LogError($"Failed to open Windows Terminal: {ex.Message}"); } + + try + { + _appSettingsManager.Current.AddRecentlyUsedProfile(id, profile); + _appSettingsManager.Save(); + } + catch (Exception ex) + { + // We don't want to fail the whole operation if we can't save the recently used profile + Logger.LogError($"Failed to save recently used profile: {ex.Message}"); + } } #pragma warning restore IDE0059, CS0168, SA1005 diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileCommand.cs index 9951ad3d68..0ea01b191d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Commands/LaunchProfileCommand.cs @@ -20,13 +20,15 @@ internal sealed partial class LaunchProfileCommand : InvokableCommand private readonly string _profile; private readonly bool _openNewTab; private readonly bool _openQuake; + private readonly AppSettingsManager _appSettingsManager; - internal LaunchProfileCommand(string id, string profile, string iconPath, bool openNewTab, bool openQuake) + internal LaunchProfileCommand(string id, string profile, string iconPath, bool openNewTab, bool openQuake, AppSettingsManager appSettingsManager) { this._id = id; this._profile = profile; this._openNewTab = openNewTab; this._openQuake = openQuake; + this._appSettingsManager = appSettingsManager; this.Name = Resources.launch_profile; this.Icon = new IconInfo(iconPath); @@ -62,6 +64,17 @@ internal sealed partial class LaunchProfileCommand : InvokableCommand // _context.API.ShowMsg(name, message, string.Empty); Logger.LogError($"Failed to open Windows Terminal: {ex.Message}"); } + + try + { + _appSettingsManager.Current.AddRecentlyUsedProfile(id, profile); + _appSettingsManager.Save(); + } + catch (Exception ex) + { + // We don't want to fail the whole operation if we can't save the recently used profile + Logger.LogError($"Failed to save recently used profile: {ex.Message}"); + } } #pragma warning restore IDE0059, CS0168 diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettings.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettings.cs index fecf798215..9bc8349e6c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettings.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettings.cs @@ -4,7 +4,9 @@ #nullable enable +using System.Collections.Generic; using System.Text.Json.Serialization; +using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; @@ -15,10 +17,30 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; /// public sealed class AppSettings { + private const int MaxRecentProfilesCount = 64; + /// /// Gets or sets the last selected channel identifier for the Windows Terminal extension. /// Empty string when no channel has been selected yet. /// [JsonPropertyName("lastSelectedChannel")] public string LastSelectedChannel { get; set; } = string.Empty; + + /// + /// Gets the list of recently used profile identifiers. + /// + [JsonPropertyName("recentlyUsedProfiles")] + public List RecentlyUsedProfiles { get; init; } = []; + + public void AddRecentlyUsedProfile(string appId, string profileName) + { + var key = new TerminalProfileKey(appId, profileName); + RecentlyUsedProfiles.Remove(key); + RecentlyUsedProfiles.Insert(0, key); + + if (RecentlyUsedProfiles.Count > MaxRecentProfilesCount) + { + RecentlyUsedProfiles.RemoveRange(MaxRecentProfilesCount, RecentlyUsedProfiles.Count - MaxRecentProfilesCount); + } + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsJsonContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsJsonContext.cs index 712970c3eb..50fb9e8dc1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsJsonContext.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsJsonContext.cs @@ -10,6 +10,4 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(AppSettings))] -internal sealed partial class AppSettingsJsonContext : JsonSerializerContext -{ -} +internal sealed partial class AppSettingsJsonContext : JsonSerializerContext; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsManager.cs index 2d5cbbd30c..6310f1123a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsManager.cs @@ -12,8 +12,6 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; -#nullable enable - public sealed class AppSettingsManager { private const string FileName = "appsettings.json"; @@ -42,7 +40,7 @@ public sealed class AppSettingsManager if (File.Exists(_filePath)) { var json = File.ReadAllText(_filePath); - var loaded = JsonSerializer.Deserialize(json, AppSettingsJsonContext.Default.AppSettings); + var loaded = JsonSerializer.Deserialize(json, AppSettingsJsonContext.Default.AppSettings!); if (loaded is not null) { Current = loaded; @@ -60,7 +58,7 @@ public sealed class AppSettingsManager { try { - var json = JsonSerializer.Serialize(Current, AppSettingsJsonContext.Default.AppSettings); + var json = JsonSerializer.Serialize(Current, AppSettingsJsonContext.Default.AppSettings!); File.WriteAllText(_filePath, json); } catch (Exception ex) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ProfileSortOrder.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ProfileSortOrder.cs new file mode 100644 index 0000000000..60be4a573e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/ProfileSortOrder.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +public enum ProfileSortOrder +{ + Default = 0, + Alphabetical = 1, + MostRecentlyUsed = 2, +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs index 38ee476bad..ba4ee3f600 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/SettingsManager.cs @@ -40,6 +40,16 @@ public class SettingsManager : JsonSettingsManager Resources.save_last_selected_channel_description!, false); + private readonly ChoiceSetSetting _profileSortOrder = new( + Namespaced(nameof(ProfileSortOrder)), + Resources.profile_sort_order!, + Resources.profile_sort_order_description!, + [ + new ChoiceSetSetting.Choice(Resources.profile_sort_order_item_default!, ProfileSortOrder.Default.ToString("G")), + new ChoiceSetSetting.Choice(Resources.profile_sort_order_item_alphabetical!, ProfileSortOrder.Alphabetical.ToString("G")), + new ChoiceSetSetting.Choice(Resources.profile_sort_order_item_mru!, ProfileSortOrder.MostRecentlyUsed.ToString("G")), + ]); + public bool ShowHiddenProfiles => _showHiddenProfiles.Value; public bool OpenNewTab => _openNewTab.Value; @@ -48,6 +58,8 @@ public class SettingsManager : JsonSettingsManager public bool SaveLastSelectedChannel => _saveLastSelectedChannel.Value; + public ProfileSortOrder ProfileSortOrder => System.Enum.TryParse(_profileSortOrder.Value, out var result) ? result : ProfileSortOrder.Default; + private static string SettingsJsonPath() { var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); @@ -65,6 +77,7 @@ public class SettingsManager : JsonSettingsManager Settings.Add(_openNewTab); Settings.Add(_openQuake); Settings.Add(_saveLastSelectedChannel); + Settings.Add(_profileSortOrder); // Load settings from file upon initialization LoadSettings(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalProfileKey.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalProfileKey.cs new file mode 100644 index 0000000000..ab0d520a32 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalProfileKey.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable enable +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +public sealed record TerminalProfileKey(string AppId, string ProfileName); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalQuery.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalQuery.cs index 39b3680470..f3e35e8af3 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalQuery.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/TerminalQuery.cs @@ -58,7 +58,7 @@ public class TerminalQuery : ITerminalQuery profiles.AddRange(TerminalHelper.ParseSettings(terminal, settingsJson)); } - return profiles.OrderBy(p => p.Name); + return profiles; } public IEnumerable GetTerminals() diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs index 89bb98ca22..e968b9848b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/ProfilesListPage.cs @@ -51,9 +51,16 @@ internal sealed partial class ProfilesListPage : ListPage, INotifyItemsChanged Icon = Icons.TerminalIcon; Name = Resources.profiles_list_page_name; _terminalSettings = terminalSettings; + _terminalSettings.Settings.SettingsChanged += Settings_SettingsChanged; _appSettingsManager = appSettingsManager; } + private void Settings_SettingsChanged(object sender, Settings args) + { + EnsureInitialized(); + RaiseItemsChanged(); + } + private List Query() { EnsureInitialized(); @@ -62,7 +69,27 @@ internal sealed partial class ProfilesListPage : ListPage, INotifyItemsChanged openNewTab = _terminalSettings.OpenNewTab; openQuake = _terminalSettings.OpenQuake; - var profiles = _terminalQuery.GetProfiles(); + var profiles = _terminalQuery.GetProfiles()!; + + switch (_terminalSettings.ProfileSortOrder) + { + case ProfileSortOrder.MostRecentlyUsed: + var mru = _appSettingsManager.Current.RecentlyUsedProfiles ?? []; + profiles = profiles.OrderBy(p => + { + var key = new TerminalProfileKey(p.Terminal?.AppUserModelId ?? string.Empty, p.Name ?? string.Empty); + var index = mru.IndexOf(key); + return index == -1 ? int.MaxValue : index; + }) + .ThenBy(static p => p.Name, StringComparer.CurrentCultureIgnoreCase) + .ToList(); + break; + case ProfileSortOrder.Default: + case ProfileSortOrder.Alphabetical: + default: + profiles = profiles.OrderBy(static p => p.Name, StringComparer.CurrentCultureIgnoreCase); + break; + } if (terminalFilters?.IsAllSelected == false) { @@ -78,12 +105,12 @@ internal sealed partial class ProfilesListPage : ListPage, INotifyItemsChanged continue; } - result.Add(new ListItem(new LaunchProfileCommand(profile.Terminal.AppUserModelId, profile.Name, profile.Terminal.LogoPath, openNewTab, openQuake)) + result.Add(new ListItem(new LaunchProfileCommand(profile.Terminal.AppUserModelId, profile.Name, profile.Terminal.LogoPath, openNewTab, openQuake, _appSettingsManager)) { Title = profile.Name, Subtitle = profile.Terminal.DisplayName, MoreCommands = [ - new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake)), + new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake, _appSettingsManager)), ], }); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs index 21faa322ec..81d82638f0 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs @@ -159,6 +159,60 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties { } } + /// + /// Looks up a localized string similar to Profiles order. + /// + internal static string profile_sort_order { + get { + return ResourceManager.GetString("profile_sort_order", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Profiles order. + /// + internal static string profile_sort_order_description { + get { + return ResourceManager.GetString("profile_sort_order_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Alphabetical. + /// + internal static string profile_sort_order_item_alphabetical { + get { + return ResourceManager.GetString("profile_sort_order_item_alphabetical", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to As defined in Terminal. + /// + internal static string profile_sort_order_item_as_in_terminal { + get { + return ResourceManager.GetString("profile_sort_order_item_as_in_terminal", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Default (alphabetical). + /// + internal static string profile_sort_order_item_default { + get { + return ResourceManager.GetString("profile_sort_order_item_default", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Most recently used. + /// + internal static string profile_sort_order_item_mru { + get { + return ResourceManager.GetString("profile_sort_order_item_mru", resourceCulture); + } + } + /// /// Looks up a localized string similar to Windows Terminal Profiles. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx index 774a021c02..23a1533fab 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx @@ -173,4 +173,22 @@ Remember the last selected channel instead of resetting to All Channels. + + Profiles order + + + Profiles order + + + Default (alphabetical) + + + Most recently used + + + As defined in Terminal + + + Alphabetical + \ No newline at end of file