CmdPal: Add filter by Terminal channel (#41582)

## Summary of the Pull Request

- Introduces a new filter on the Profiles page to filter by Terminal
channel.
- Adds a new option to remember the last select Terminal channel (or
automatically reset to All Channels).
- Adds new classes `AppSettings` and `AppSettingsManager` to hold
non-user settings.

Pictures? Pictures!

<img width="1485" height="931" alt="image"
src="https://github.com/user-attachments/assets/2cec7a8d-efe6-4692-a7ba-9608fb181624"
/>

<img width="1730" height="1014" alt="image"
src="https://github.com/user-attachments/assets/87984b82-e085-42a5-b71c-5ddc71ff52ec"
/>

<img width="1722" height="1063" alt="image"
src="https://github.com/user-attachments/assets/97baff23-3db0-404b-8a8d-622f841b344b"
/>

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #41432 
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2025-09-22 17:24:14 +02:00
committed by GitHub
parent bb706fb5f1
commit 879e03b436
12 changed files with 338 additions and 12 deletions

View File

@@ -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;
/// <summary>
/// Strongly typed application-level settings for the Windows Terminal extension.
/// These are distinct from the dynamic command palette <see cref="JsonSettingsManager"/> based settings
/// and are meant for simple persisted state (e.g. last selections).
/// </summary>
public sealed class AppSettings
{
/// <summary>
/// Gets or sets the last selected channel identifier for the Windows Terminal extension.
/// Empty string when no channel has been selected yet.
/// </summary>
[JsonPropertyName("lastSelectedChannel")]
public string LastSelectedChannel { get; set; } = string.Empty;
}

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ public class TerminalQuery : ITerminalQuery
return profiles.OrderBy(p => p.Name);
}
private IEnumerable<TerminalPackage> GetTerminals()
public IEnumerable<TerminalPackage> GetTerminals()
{
var user = WindowsIdentity.GetCurrent().User;
var localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");

View File

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

View File

@@ -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<object, IItemsChangedEventArgs> 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<ListItem> Query()
private List<ListItem> 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<ListItem>();
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;
}
}
}

View File

@@ -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<TerminalPackage> _terminals;
public bool IsAllSelected => CurrentFilterId == AllTerminalsFilterId;
public TerminalChannelFilters(IEnumerable<TerminalPackage> terminals, string preselectedFilterId = AllTerminalsFilterId)
{
CurrentFilterId = preselectedFilterId;
_terminals = [.. terminals];
}
public override IFilterItem[] GetFilters()
{
var items = new List<IFilterItem>
{
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;
}
}

View File

@@ -60,6 +60,15 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to All channels.
/// </summary>
internal static string all_channels {
get {
return ResourceManager.GetString("all_channels", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Windows Terminal Profiles.
/// </summary>
@@ -132,6 +141,24 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Preferred channel.
/// </summary>
internal static string preferred_channel {
get {
return ResourceManager.GetString("preferred_channel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Preferred channel.
/// </summary>
internal static string preferred_channel_description {
get {
return ResourceManager.GetString("preferred_channel_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Windows Terminal Profiles.
/// </summary>
@@ -159,6 +186,24 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Keep last channel filter.
/// </summary>
internal static string save_last_selected_channel {
get {
return ResourceManager.GetString("save_last_selected_channel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remember the last selected channel instead of resetting to All Channels..
/// </summary>
internal static string save_last_selected_channel_description {
get {
return ResourceManager.GetString("save_last_selected_channel_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings.
/// </summary>

View File

@@ -158,4 +158,19 @@
<data name="list_item_title" xml:space="preserve">
<value>Open Windows Terminal Profiles</value>
</data>
<data name="preferred_channel" xml:space="preserve">
<value>Preferred channel</value>
</data>
<data name="preferred_channel_description" xml:space="preserve">
<value>Preferred channel</value>
</data>
<data name="all_channels" xml:space="preserve">
<value>All channels</value>
</data>
<data name="save_last_selected_channel" xml:space="preserve">
<value>Keep last channel filter</value>
</data>
<data name="save_last_selected_channel_description" xml:space="preserve">
<value>Remember the last selected channel instead of resetting to All Channels.</value>
</data>
</root>

View File

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

View File

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