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 new file mode 100644 index 0000000000..fecf798215 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettings.cs @@ -0,0 +1,24 @@ +// 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 + +using System.Text.Json.Serialization; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +/// +/// Strongly typed application-level settings for the Windows Terminal extension. +/// These are distinct from the dynamic command palette based settings +/// and are meant for simple persisted state (e.g. last selections). +/// +public sealed class AppSettings +{ + /// + /// 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; +} 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 new file mode 100644 index 0000000000..712970c3eb --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsJsonContext.cs @@ -0,0 +1,15 @@ +// 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 + +using System.Text.Json.Serialization; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(AppSettings))] +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 new file mode 100644 index 0000000000..2d5cbbd30c --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Helpers/AppSettingsManager.cs @@ -0,0 +1,72 @@ +// 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 + +using System; +using System.IO; +using System.Text.Json; +using ManagedCommon; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; + +#nullable enable + +public sealed class AppSettingsManager +{ + private const string FileName = "appsettings.json"; + + private static string SettingsPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + return Path.Combine(directory, FileName); + } + + private readonly string _filePath; + + public AppSettings Current { get; private set; } = new(); + + public AppSettingsManager() + { + _filePath = SettingsPath(); + Load(); + } + + public void Load() + { + try + { + if (File.Exists(_filePath)) + { + var json = File.ReadAllText(_filePath); + var loaded = JsonSerializer.Deserialize(json, AppSettingsJsonContext.Default.AppSettings); + if (loaded is not null) + { + Current = loaded; + } + } + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage { Message = ex.ToString() }); + Logger.LogError("Failed to load app settings", ex); + } + } + + public void Save() + { + try + { + var json = JsonSerializer.Serialize(Current, AppSettingsJsonContext.Default.AppSettings); + File.WriteAllText(_filePath, json); + } + catch (Exception ex) + { + ExtensionHost.LogMessage(new LogMessage { Message = ex.ToString() }); + Logger.LogError("Failed to save app settings", ex); + } + } +} 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 d1c8be21f4..38ee476bad 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 @@ -34,13 +34,21 @@ public class SettingsManager : JsonSettingsManager Resources.open_quake_description, false); + private readonly ToggleSetting _saveLastSelectedChannel = new( + Namespaced(nameof(SaveLastSelectedChannel)), + Resources.save_last_selected_channel!, + Resources.save_last_selected_channel_description!, + false); + public bool ShowHiddenProfiles => _showHiddenProfiles.Value; public bool OpenNewTab => _openNewTab.Value; public bool OpenQuake => _openQuake.Value; - internal static string SettingsJsonPath() + public bool SaveLastSelectedChannel => _saveLastSelectedChannel.Value; + + private static string SettingsJsonPath() { var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); Directory.CreateDirectory(directory); @@ -56,6 +64,7 @@ public class SettingsManager : JsonSettingsManager Settings.Add(_showHiddenProfiles); Settings.Add(_openNewTab); Settings.Add(_openQuake); + Settings.Add(_saveLastSelectedChannel); // Load settings from file upon initialization LoadSettings(); 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 0665004018..39b3680470 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 @@ -61,7 +61,7 @@ public class TerminalQuery : ITerminalQuery return profiles.OrderBy(p => p.Name); } - private IEnumerable GetTerminals() + public IEnumerable GetTerminals() { var user = WindowsIdentity.GetCurrent().User; var localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA"); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Icons.cs index 57abd2c01d..9d3072f25e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Icons.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Icons.cs @@ -6,9 +6,11 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.WindowsTerminal; -internal sealed class Icons +internal static class Icons { internal static IconInfo TerminalIcon { get; } = IconHelpers.FromRelativePath("Assets\\WindowsTerminal.svg"); internal static IconInfo AdminIcon { get; } = new IconInfo("\xE7EF"); // Admin icon + + internal static IconInfo FilterIcon { get; } = new IconInfo("\uE71C"); // Funnel icon } 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 7e9d2cec31..89bb98ca22 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 @@ -2,40 +2,73 @@ // 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 + +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using ManagedCommon; using Microsoft.CmdPal.Ext.WindowsTerminal.Commands; using Microsoft.CmdPal.Ext.WindowsTerminal.Helpers; using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Foundation; namespace Microsoft.CmdPal.Ext.WindowsTerminal.Pages; -internal sealed partial class ProfilesListPage : ListPage +internal sealed partial class ProfilesListPage : ListPage, INotifyItemsChanged { + event TypedEventHandler INotifyItemsChanged.ItemsChanged + { + add + { + ItemsChanged += value; + EnsureInitialized(); + SelectTerminalFilter(); + } + + remove + { + ItemsChanged -= value; + } + } + private readonly TerminalQuery _terminalQuery = new(); private readonly SettingsManager _terminalSettings; + private readonly AppSettingsManager _appSettingsManager; private bool showHiddenProfiles; private bool openNewTab; private bool openQuake; - public ProfilesListPage(SettingsManager terminalSettings) + private bool initialized; + private TerminalChannelFilters? terminalFilters; + + public ProfilesListPage(SettingsManager terminalSettings, AppSettingsManager appSettingsManager) { Icon = Icons.TerminalIcon; Name = Resources.profiles_list_page_name; _terminalSettings = terminalSettings; + _appSettingsManager = appSettingsManager; } -#pragma warning disable SA1108 - public List Query() + private List Query() { + EnsureInitialized(); + showHiddenProfiles = _terminalSettings.ShowHiddenProfiles; openNewTab = _terminalSettings.OpenNewTab; openQuake = _terminalSettings.OpenQuake; var profiles = _terminalQuery.GetProfiles(); + if (terminalFilters?.IsAllSelected == false) + { + profiles = profiles.Where(profile => profile.Terminal.AppUserModelId == terminalFilters.CurrentFilterId); + } + var result = new List(); foreach (var profile in profiles) @@ -52,12 +85,66 @@ internal sealed partial class ProfilesListPage : ListPage MoreCommands = [ new CommandContextItem(new LaunchProfileAsAdminCommand(profile.Terminal.AppUserModelId, profile.Name, openNewTab, openQuake)), ], -#pragma warning restore SA1108 }); } return result; } - public override IListItem[] GetItems() => Query().ToArray(); + private void EnsureInitialized() + { + if (initialized) + { + return; + } + + var terminals = _terminalQuery.GetTerminals(); + terminalFilters = new TerminalChannelFilters(terminals); + terminalFilters.PropChanged += TerminalFiltersOnPropChanged; + SelectTerminalFilter(); + Filters = terminalFilters; + initialized = true; + } + + private void SelectTerminalFilter() + { + Trace.Assert(terminalFilters != null); + + // Select the preferred channel if it exists; we always select the preferred channel, + // but user have an option to save the preferred channel when he changes the filter + if (_terminalSettings.SaveLastSelectedChannel) + { + if (!string.IsNullOrWhiteSpace(_appSettingsManager.Current.LastSelectedChannel) && + terminalFilters.ContainsFilter(_appSettingsManager.Current.LastSelectedChannel)) + { + terminalFilters.CurrentFilterId = _appSettingsManager.Current.LastSelectedChannel; + } + } + else + { + terminalFilters.CurrentFilterId = TerminalChannelFilters.AllTerminalsFilterId; + } + } + + private void TerminalFiltersOnPropChanged(object sender, IPropChangedEventArgs args) + { + Trace.Assert(terminalFilters != null); + + RaiseItemsChanged(); + _appSettingsManager.Current.LastSelectedChannel = terminalFilters.CurrentFilterId; + _appSettingsManager.Save(); + } + + public override IListItem[] GetItems() + { + try + { + return [.. Query()]; + } + catch (Exception ex) + { + Logger.LogError("Failed to list Windows Terminal profiles", ex); + throw; + } + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/TerminalChannelFilters.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/TerminalChannelFilters.cs new file mode 100644 index 0000000000..525edb3d41 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Pages/TerminalChannelFilters.cs @@ -0,0 +1,56 @@ +// 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. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.WindowsTerminal.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WindowsTerminal.Pages; + +internal sealed partial class TerminalChannelFilters : Filters +{ + internal const string AllTerminalsFilterId = "all"; + + private readonly List _terminals; + + public bool IsAllSelected => CurrentFilterId == AllTerminalsFilterId; + + public TerminalChannelFilters(IEnumerable terminals, string preselectedFilterId = AllTerminalsFilterId) + { + CurrentFilterId = preselectedFilterId; + _terminals = [.. terminals]; + } + + public override IFilterItem[] GetFilters() + { + var items = new List + { + new Filter() + { + Id = AllTerminalsFilterId, + Name = Resources.all_channels, + Icon = Icons.FilterIcon, + }, + new Separator(), + }; + + foreach (var terminalPackage in _terminals) + { + items.Add(new Filter() + { + Id = terminalPackage.AppUserModelId, + Name = terminalPackage.DisplayName, + Icon = new IconInfo(terminalPackage.LogoPath), + }); + } + + return [.. items]; + } + + public bool ContainsFilter(string id) + { + return _terminals.FindIndex(terminal => terminal.AppUserModelId == id) > -1; + } +} 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 d6fa47029c..21faa322ec 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 @@ -60,6 +60,15 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties { } } + /// + /// Looks up a localized string similar to All channels. + /// + internal static string all_channels { + get { + return ResourceManager.GetString("all_channels", resourceCulture); + } + } + /// /// Looks up a localized string similar to Windows Terminal Profiles. /// @@ -132,6 +141,24 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties { } } + /// + /// Looks up a localized string similar to Preferred channel. + /// + internal static string preferred_channel { + get { + return ResourceManager.GetString("preferred_channel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Preferred channel. + /// + internal static string preferred_channel_description { + get { + return ResourceManager.GetString("preferred_channel_description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Windows Terminal Profiles. /// @@ -159,6 +186,24 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties { } } + /// + /// Looks up a localized string similar to Keep last channel filter. + /// + internal static string save_last_selected_channel { + get { + return ResourceManager.GetString("save_last_selected_channel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remember the last selected channel instead of resetting to All Channels.. + /// + internal static string save_last_selected_channel_description { + get { + return ResourceManager.GetString("save_last_selected_channel_description", resourceCulture); + } + } + /// /// Looks up a localized string similar to Settings. /// 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 3b2ab8da6a..774a021c02 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 @@ -158,4 +158,19 @@ Open Windows Terminal Profiles + + Preferred channel + + + Preferred channel + + + All channels + + + Keep last channel filter + + + Remember the last selected channel instead of resetting to All Channels. + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalTopLevelCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalTopLevelCommandItem.cs index 0eebd98adf..ce5a56e7a7 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalTopLevelCommandItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/TerminalTopLevelCommandItem.cs @@ -11,8 +11,8 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal; public partial class TerminalTopLevelCommandItem : CommandItem { - public TerminalTopLevelCommandItem(SettingsManager settingsManager) - : base(new ProfilesListPage(settingsManager)) + public TerminalTopLevelCommandItem(SettingsManager settingsManager, AppSettingsManager appSettingsManager) + : base(new ProfilesListPage(settingsManager, appSettingsManager)) { Icon = Icons.TerminalIcon; Title = Resources.list_item_title; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/WindowsTerminalCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/WindowsTerminalCommandsProvider.cs index bd825cbddb..31dc9077bb 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/WindowsTerminalCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/WindowsTerminalCommandsProvider.cs @@ -13,6 +13,7 @@ public partial class WindowsTerminalCommandsProvider : CommandProvider { private readonly TerminalTopLevelCommandItem _terminalCommand; private readonly SettingsManager _settingsManager = new(); + private readonly AppSettingsManager _appSettingsManager = new(); public WindowsTerminalCommandsProvider() { @@ -21,7 +22,7 @@ public partial class WindowsTerminalCommandsProvider : CommandProvider Icon = Icons.TerminalIcon; Settings = _settingsManager.Settings; - _terminalCommand = new TerminalTopLevelCommandItem(_settingsManager) + _terminalCommand = new TerminalTopLevelCommandItem(_settingsManager, _appSettingsManager) { MoreCommands = [ new CommandContextItem(Settings.SettingsPage),