Add Host Settings API for extension configuration

Introduced a Host Settings API to enable extensions to access
and respond to the host application's settings. Key changes
include:

- Added `NotifyHostSettingsChanged` to `IExtensionService`
  and `IExtensionWrapper` for notifying extensions of settings
  updates.
- Implemented `HostSettingsConverter` for converting
  `SettingsModel` to `IHostSettings`.
- Introduced `HostSettingsManager` to cache settings and
  notify extensions via the `SettingsChanged` event.
- Enhanced `CommandProvider` and `CommandProviderWrapper`
  to handle `IHostSettingsChanged` for initial and dynamic
  settings updates.
- Added `HostSettingsPage` sample to demonstrate API usage.
- Updated documentation and SDK specifications to include
  the new API.

These changes ensure cross-process safety, real-time updates,
and backward compatibility, allowing extensions to adapt to
host settings dynamically.
This commit is contained in:
Yu Leng
2025-11-24 22:49:48 +08:00
parent 09c8c1d79a
commit 3e6fc61fd7
16 changed files with 1056 additions and 9 deletions

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Windows.Foundation;
namespace Microsoft.CmdPal.Core.Common.Services;
@@ -27,6 +28,12 @@ public interface IExtensionService
void DisableExtension(string extensionUniqueId);
/// <summary>
/// Notifies all extensions that support the HostSettings capability about a settings change.
/// </summary>
/// <param name="settings">The updated host settings.</param>
void NotifyHostSettingsChanged(IHostSettings settings);
///// <summary>
///// Gets a boolean indicating whether the extension was disabled due to the corresponding Windows optional feature
///// being absent from the machine or in an unknown state.

View File

@@ -110,4 +110,10 @@ public interface IExtensionWrapper
/// <returns>Nullable instance of the provider</returns>
Task<IEnumerable<T>> GetListOfProvidersAsync<T>()
where T : class;
/// <summary>
/// Notifies the extension that host settings have changed.
/// </summary>
/// <param name="settings">The updated host settings.</param>
void NotifyHostSettingsChanged(IHostSettings settings);
}

View File

@@ -5,6 +5,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -13,6 +14,12 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public abstract partial class AppExtensionHost : IExtensionHost
{
/// <summary>
/// Gets or sets a function that returns the current IHostSettings.
/// This is set by the application to provide settings to extensions.
/// </summary>
public static Func<IHostSettings?>? GetHostSettingsFunc { get; set; }
private static readonly GlobalLogPageContext _globalLogPageContext = new();
private static ulong _hostingHwnd;

View File

@@ -214,10 +214,29 @@ public sealed class CommandProviderWrapper
Logger.LogDebug($"Provider supports {apiExtensions.Length} extensions");
foreach (var a in apiExtensions)
{
if (a is IExtendedAttributesProvider command2)
if (a is IExtendedAttributesProvider)
{
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
}
if (a is IHostSettingsChanged handler)
{
Logger.LogDebug($"{ProviderId}: Found an IHostSettingsChanged, sending initial settings");
// Send initial settings to the extension
try
{
var settings = AppExtensionHost.GetHostSettingsFunc?.Invoke();
if (settings != null)
{
handler.OnHostSettingsChanged(settings);
}
}
catch (Exception e)
{
Logger.LogDebug($"Failed to send initial settings to {ProviderId}: {e.Message}");
}
}
}
}

View File

@@ -0,0 +1,49 @@
// 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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Provides conversion between SettingsModel and IHostSettings for extension communication.
/// </summary>
public static class HostSettingsConverter
{
/// <summary>
/// Converts a SettingsModel to an IHostSettings object that can be passed to extensions.
/// </summary>
/// <param name="settings">The settings model to convert.</param>
/// <returns>An IHostSettings object with the current settings values.</returns>
public static IHostSettings ToHostSettings(this SettingsModel settings)
{
return new HostSettings
{
Hotkey = settings.Hotkey?.ToString() ?? string.Empty,
ShowAppDetails = settings.ShowAppDetails,
HotkeyGoesHome = settings.HotkeyGoesHome,
BackspaceGoesBack = settings.BackspaceGoesBack,
SingleClickActivates = settings.SingleClickActivates,
HighlightSearchOnActivate = settings.HighlightSearchOnActivate,
ShowSystemTrayIcon = settings.ShowSystemTrayIcon,
IgnoreShortcutWhenFullscreen = settings.IgnoreShortcutWhenFullscreen,
DisableAnimations = settings.DisableAnimations,
SummonOn = ConvertSummonTarget(settings.SummonOn),
};
}
private static SummonTarget ConvertSummonTarget(MonitorBehavior behavior)
{
return behavior switch
{
MonitorBehavior.ToMouse => SummonTarget.ToMouse,
MonitorBehavior.ToPrimary => SummonTarget.ToPrimary,
MonitorBehavior.ToFocusedWindow => SummonTarget.ToFocusedWindow,
MonitorBehavior.InPlace => SummonTarget.InPlace,
MonitorBehavior.ToLast => SummonTarget.ToLast,
_ => SummonTarget.ToMouse,
};
}
}

View File

@@ -400,6 +400,29 @@ public partial class ExtensionService : IExtensionService, IDisposable
_enabledExtensions.Remove(extension.First());
}
/// <summary>
/// Notifies all extensions about a settings change.
/// </summary>
/// <param name="settings">The updated host settings.</param>
public void NotifyHostSettingsChanged(IHostSettings settings)
{
Task.Run(async () =>
{
var extensions = await GetInstalledExtensionsAsync();
foreach (var extension in extensions)
{
try
{
extension.NotifyHostSettingsChanged(settings);
}
catch (Exception ex)
{
Logger.LogError($"Failed to notify extension {extension.ExtensionUniqueId} of settings change", ex);
}
}
});
}
/*
///// <inheritdoc cref="IExtensionService.DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper)"/>
//public async Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension)

View File

@@ -12,6 +12,9 @@ using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
#pragma warning disable IDE0005 // Using directive is unnecessary - needed for HostSettings
#pragma warning restore IDE0005
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionWrapper : IExtensionWrapper
@@ -201,4 +204,37 @@ public class ExtensionWrapper : IExtensionWrapper
public void AddProviderType(ProviderType providerType) => _providerTypes.Add(providerType);
public bool HasProviderType(ProviderType providerType) => _providerTypes.Contains(providerType);
/// <summary>
/// Notifies the extension that host settings have changed.
/// </summary>
/// <param name="settings">The updated host settings.</param>
public void NotifyHostSettingsChanged(IHostSettings settings)
{
try
{
var provider = GetExtensionObject()?.GetProvider(ProviderType.Commands);
if (provider is not ICommandProvider2 provider2)
{
return;
}
// Get the IHostSettingsChanged handler from GetApiExtensionStubs().
// This pattern works reliably in OOP scenarios because the stub objects
// are properly marshalled across the process boundary.
var apiExtensions = provider2.GetApiExtensionStubs();
foreach (var stub in apiExtensions)
{
if (stub is IHostSettingsChanged handler)
{
handler.OnHostSettingsChanged(settings);
return;
}
}
}
catch (Exception e)
{
Logger.LogDebug($"Failed to notify {ExtensionDisplayName} of settings change: {e.Message}");
}
}
}

View File

@@ -160,10 +160,23 @@ public partial class App : Application
services.AddSingleton(sm);
var state = AppStateModel.LoadState();
services.AddSingleton(state);
services.AddSingleton<IExtensionService, ExtensionService>();
var extensionService = new ExtensionService();
services.AddSingleton<IExtensionService>(extensionService);
services.AddSingleton<TrayIconService>();
services.AddSingleton<IRunHistoryService, RunHistoryService>();
// Set up host settings for extensions
AppExtensionHost.GetHostSettingsFunc = () => sm.ToHostSettings();
// Subscribe to settings changes and notify extensions
sm.SettingsChanged += (sender, _) =>
{
if (sender is SettingsModel settings)
{
extensionService.NotifyHostSettingsChanged(settings.ToHostSettings());
}
};
services.AddSingleton<IRootPageService, PowerToysRootPageService>();
services.AddSingleton<IAppHostService, PowerToysAppHostService>();
services.AddSingleton<ITelemetryService, TelemetryForwarder>();

View File

@@ -0,0 +1,442 @@
---
author: Yu Leng
created on: 2025-01-24
last updated: 2025-01-24
---
# Extension Host Settings API
## Abstract
This document describes the Host Settings API, which allows Command Palette extensions to access and respond to host application settings. Extensions can read settings like hotkey configuration, UI preferences, and behavior options, enabling them to adapt their behavior and display relevant information to users.
## Table of Contents
- [Background](#background)
- [Problem Statement](#problem-statement)
- [User Scenarios](#user-scenarios)
- [Design Goals](#design-goals)
- [High-Level Design](#high-level-design)
- [Architecture Overview](#architecture-overview)
- [Key Design Decisions](#key-design-decisions)
- [Detailed Design](#detailed-design)
- [Data Flow](#data-flow)
- [Cross-Process Communication](#cross-process-communication)
- [Implementation Files](#implementation-files)
- [API Reference](#api-reference)
- [WinRT Interfaces](#winrt-interfaces)
- [Toolkit Classes](#toolkit-classes)
- [Usage Examples](#usage-examples)
- [Compatibility](#compatibility)
- [Future Considerations](#future-considerations)
## Background
### Problem Statement
Command Palette extensions run in separate processes (out-of-process/OOP) from the host application for security and stability. However, some extensions need to be aware of the host's configuration to provide a better user experience. For example:
1. An extension displaying keyboard shortcuts needs to know the current hotkey setting
2. An extension with animations should respect the "Disable Animations" preference
3. A diagnostic extension might want to display all current settings for troubleshooting
Without a formal API, extensions have no way to access this information, limiting their ability to integrate seamlessly with the host application.
### User Scenarios
**Scenario 1: Settings Display Extension**
A developer creates a diagnostic page that displays all current Command Palette settings. This helps users understand their configuration and troubleshoot issues.
**Scenario 2: Adaptive UI Extension**
An extension with rich animations checks the `DisableAnimations` setting and disables its own animations when the user prefers reduced motion.
**Scenario 3: Keyboard Shortcut Helper**
An extension that teaches keyboard shortcuts displays the user's configured hotkey so instructions match their actual configuration.
### Design Goals
1. **Cross-Process Safety**: Work reliably across the OOP boundary between host and extension
2. **Real-Time Updates**: Extensions receive notifications when settings change
3. **AOT Compatibility**: All code must be compatible with Ahead-of-Time compilation
4. **Minimal API Surface**: Simple, focused interfaces that are easy to use
5. **Backward Compatibility**: Old extensions gracefully ignore new capabilities; old hosts don't break new extensions
## High-Level Design
### Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ Host (CmdPal.UI) │
│ │
│ SettingsModel ──(SettingsChanged)──► ExtensionService │
│ │ │ │
│ │ ▼ │
│ │ ExtensionWrapper │
│ │ │ │
│ │ GetApiExtensionStubs() │
│ │ │ │
│ │ ┌──────────┴──────────┐ │
│ │ ▼ ▼ │
│ │ IExtendedAttributesProvider IHostSettingsChanged
│ │ │ │
│ └──(initial settings)───────────────────────────┤ │
│ via CommandProviderWrapper │ │
└───────────────────────────────────────────────────────│─────────┘
OOP Boundary │
┌───────────────────────────────────────────────────────│─────────┐
│ Extension Process │ │
│ ▼ │
│ CommandProvider │
│ │ │
│ ├── GetApiExtensionStubs() returns: │
│ │ • SupportCommandsWithProperties │
│ │ • HostSettingsChangedHandler ◄─────────────────┐ │
│ │ │ │
│ └── OnHostSettingsChanged(settings) ◄─────────────────┘ │
│ │ │
│ ▼ │
│ HostSettingsManager.Update(settings) │
│ │ │
│ ▼ │
│ SettingsChanged event │
│ │ │
│ ▼ │
│ Extension Pages (e.g., HostSettingsPage) │
└─────────────────────────────────────────────────────────────────┘
```
### Key Design Decisions
#### 1. Cross-Process Interface Detection via GetApiExtensionStubs
**Problem:** The `is` operator for interface detection doesn't work reliably across process boundaries in WinRT/COM scenarios. When the host tries to check `if (provider is IHostSettingsChanged)`, it fails because the interface isn't in the direct inheritance chain of the proxy object.
**Solution:** Use the `GetApiExtensionStubs()` pattern. The extension returns stub objects that are properly marshalled across the process boundary, allowing reliable interface detection on the host side.
```csharp
// In CommandProvider.cs (Toolkit)
public object[] GetApiExtensionStubs()
{
return [
new SupportCommandsWithProperties(), // For IExtendedAttributesProvider
new HostSettingsChangedHandler(this) // For IHostSettingsChanged
];
}
// In ExtensionWrapper.cs (Host)
foreach (var stub in provider2.GetApiExtensionStubs())
{
if (stub is IHostSettingsChanged handler) // This works across OOP!
{
handler.OnHostSettingsChanged(settings);
return;
}
}
```
#### 2. Unified Settings Delivery
Both initial settings and change notifications use the same `OnHostSettingsChanged` path:
- **Initial Settings:** Sent by `CommandProviderWrapper.UnsafePreCacheApiAdditions()` when discovering the handler during extension startup
- **Change Notifications:** Sent by `ExtensionWrapper.NotifyHostSettingsChanged()` when the user modifies settings
This simplifies the design by having a single code path for settings delivery.
#### 3. Capability Detection via ICommandProvider2
Use `is ICommandProvider2` as the capability gate. If an extension implements `ICommandProvider2`, it's using the modern toolkit and may support host settings (if it provides an `IHostSettingsChanged` stub).
```csharp
if (provider is ICommandProvider2 provider2)
{
// Extension uses modern toolkit, check for settings support
var stubs = provider2.GetApiExtensionStubs();
// ...
}
```
## Detailed Design
### Data Flow
#### Extension Startup (Initial Settings)
```
Extension starts
└─► CommandProviderWrapper created
└─► LoadTopLevelCommands()
└─► UnsafePreCacheApiAdditions(provider2)
└─► GetApiExtensionStubs()
└─► Find IHostSettingsChanged handler
└─► handler.OnHostSettingsChanged(currentSettings)
└─► HostSettingsManager.Update(settings)
```
#### Settings Change Notification
```
User changes settings in CmdPal
└─► SettingsModel.SettingsChanged event
└─► ExtensionService.NotifyHostSettingsChanged(settings)
└─► foreach extension:
└─► ExtensionWrapper.NotifyHostSettingsChanged(settings)
└─► GetApiExtensionStubs()
└─► Find IHostSettingsChanged handler
└─► handler.OnHostSettingsChanged(settings)
└─► HostSettingsManager.Update(settings)
└─► SettingsChanged?.Invoke()
```
### Cross-Process Communication
The settings are passed as an `IHostSettings` WinRT interface, which is automatically marshalled by the WinRT runtime across the process boundary. The host creates a `HostSettings` object (via `HostSettingsConverter.ToHostSettings()`), and the extension receives a proxy that implements the same interface.
Key considerations:
1. **No Reflection:** All type checking uses WinRT's native QueryInterface mechanism
2. **AOT Safe:** All types are known at compile time
3. **Error Handling:** All cross-process calls are wrapped in try-catch to handle extension crashes gracefully
### Implementation Files
#### Host Side
| File | Purpose |
|------|---------|
| `App.xaml.cs` | Subscribes to `SettingsModel.SettingsChanged`, sets up `GetHostSettingsFunc` |
| `AppExtensionHost.cs` | Provides `GetHostSettingsFunc` static property for settings access |
| `HostSettingsConverter.cs` | Extension method to convert `SettingsModel` to `IHostSettings` |
| `ExtensionService.cs` | Broadcasts settings changes to all extensions via `NotifyHostSettingsChanged()` |
| `ExtensionWrapper.cs` | Sends settings to individual extension via `NotifyHostSettingsChanged()` |
| `CommandProviderWrapper.cs` | Sends initial settings when discovering `IHostSettingsChanged` handler |
#### Extension/Toolkit Side
| File | Purpose |
|------|---------|
| `CommandProvider.cs` | Base class providing `IHostSettingsChanged` stub via `GetApiExtensionStubs()` |
| `HostSettingsManager.cs` | Static manager with `Current` property and `SettingsChanged` event |
| `HostSettings.cs` | Toolkit implementation of `IHostSettings` interface |
## API Reference
### WinRT Interfaces
#### IHostSettings
Represents the host application settings that can be passed to extensions.
```idl
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IHostSettings
{
String Hotkey { get; };
Boolean ShowAppDetails { get; };
Boolean HotkeyGoesHome { get; };
Boolean BackspaceGoesBack { get; };
Boolean SingleClickActivates { get; };
Boolean HighlightSearchOnActivate { get; };
Boolean ShowSystemTrayIcon { get; };
Boolean IgnoreShortcutWhenFullscreen { get; };
Boolean DisableAnimations { get; };
SummonTarget SummonOn { get; };
}
```
| Property | Type | Description |
|----------|------|-------------|
| `Hotkey` | String | The keyboard shortcut to activate Command Palette (e.g., "Alt+Space") |
| `ShowAppDetails` | Boolean | Whether to show application details in the UI |
| `HotkeyGoesHome` | Boolean | Whether pressing the hotkey returns to the home page |
| `BackspaceGoesBack` | Boolean | Whether backspace navigates back when search is empty |
| `SingleClickActivates` | Boolean | Whether single-click activates items (vs double-click) |
| `HighlightSearchOnActivate` | Boolean | Whether to highlight search text on activation |
| `ShowSystemTrayIcon` | Boolean | Whether to show the system tray icon |
| `IgnoreShortcutWhenFullscreen` | Boolean | Whether to ignore the hotkey when a fullscreen app is active |
| `DisableAnimations` | Boolean | Whether animations are disabled |
| `SummonOn` | SummonTarget | Where to position the window when summoned |
#### SummonTarget
Enum representing the position behavior when summoning Command Palette.
```idl
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
enum SummonTarget
{
ToMouse = 0,
ToPrimary = 1,
ToFocusedWindow = 2,
InPlace = 3,
ToLast = 4,
};
```
#### IHostSettingsChanged
Interface for extensions to receive settings change notifications.
```idl
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IHostSettingsChanged
{
void OnHostSettingsChanged(IHostSettings settings);
}
```
### Toolkit Classes
#### HostSettingsManager
Static class providing access to current host settings and change notifications.
```csharp
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public static class HostSettingsManager
{
/// <summary>
/// Occurs when the host settings have changed.
/// </summary>
public static event Action? SettingsChanged;
/// <summary>
/// Gets the current host settings, or null if not yet initialized.
/// </summary>
public static IHostSettings? Current { get; }
/// <summary>
/// Gets whether host settings are available.
/// </summary>
public static bool IsAvailable { get; }
}
```
#### CommandProvider Virtual Method
Extensions can override this method for custom settings handling:
```csharp
public abstract partial class CommandProvider
{
/// <summary>
/// Called when host settings change. Override to handle settings changes.
/// The default implementation updates HostSettingsManager.
/// </summary>
public virtual void OnHostSettingsChanged(IHostSettings settings)
{
HostSettingsManager.Update(settings);
}
}
```
## Usage Examples
### Reading Settings in a Page
```csharp
public class MyPage : ListPage
{
public MyPage()
{
// Subscribe to changes
HostSettingsManager.SettingsChanged += () => RaiseItemsChanged();
}
public override IListItem[] GetItems()
{
var settings = HostSettingsManager.Current;
if (settings == null)
{
return [new ListItem(new NoOpCommand())
{
Title = "Settings not available"
}];
}
return [
new ListItem(new NoOpCommand())
{
Title = $"Hotkey: {settings.Hotkey}"
},
new ListItem(new NoOpCommand())
{
Title = $"Animations: {(settings.DisableAnimations ? "Off" : "On")}"
},
];
}
}
```
### Custom Settings Handler in CommandProvider
```csharp
public class MyCommandProvider : CommandProvider
{
public override void OnHostSettingsChanged(IHostSettings settings)
{
base.OnHostSettingsChanged(settings); // Update HostSettingsManager
// Custom logic
if (settings.DisableAnimations)
{
DisableMyAnimations();
}
}
}
```
### Checking Settings Availability
```csharp
public void DoSomething()
{
if (!HostSettingsManager.IsAvailable)
{
// Running with old host, use defaults
return;
}
var settings = HostSettingsManager.Current!;
// Use settings...
}
```
## Compatibility
### Extension Compatibility
| Extension Type | Behavior |
|----------------|----------|
| Old extensions (no ICommandProvider2) | Won't receive settings, gracefully skipped |
| Extensions without IHostSettingsChanged stub | Won't receive settings, gracefully skipped |
| New extensions with IHostSettingsChanged | Receive initial settings and change notifications |
### Host Compatibility
| Host Version | Behavior |
|--------------|----------|
| Old hosts (no settings support) | `HostSettingsManager.Current` will be null |
| New hosts | Full settings support |
Extensions should always check `HostSettingsManager.IsAvailable` or handle null `Current` values.
## Future Considerations
### Adding New Settings
To add a new setting property:
1. Add property to `IHostSettings` interface in IDL
2. Add property to `HostSettings.cs` class in Toolkit
3. Update `HostSettingsConverter.ToHostSettings()` to include the new property
### Extension-Specific Settings
Future versions could allow extensions to register their own settings that the host would store and provide back, enabling a unified settings experience.
### Bi-Directional Settings
Currently, settings flow one-way from host to extension. A future enhancement could allow extensions to request settings changes (with user consent).

View File

@@ -74,6 +74,7 @@ functionality.
- [Settings helpers](#settings-helpers)
- [Advanced scenarios](#advanced-scenarios)
- [Status messages](#status-messages)
- [Host Settings](#host-settings)
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Class diagram](#class-diagram)
- [Future considerations](#future-considerations)
@@ -1904,6 +1905,160 @@ instance from the host app.
to remember that these are x-proc calls, and should be treated asynchronously.
Should the other properties be async too?
### Host Settings
Extensions may need to be aware of the host application's settings to provide a
better user experience. For example, an extension displaying keyboard shortcuts
needs to know the current hotkey setting, or an extension with animations should
respect the "Disable Animations" preference.
The Host Settings API allows extensions to read host settings and receive
notifications when those settings change. This works across the out-of-process
(OOP) boundary using the `GetApiExtensionStubs()` pattern described in
[Addenda I](#addenda-i-api-additions-icommandprovider2).
#### IHostSettings Interface
The `IHostSettings` interface provides read-only access to the host's current
settings:
```c#
enum SummonTarget
{
ToMouse = 0,
ToPrimary = 1,
ToFocusedWindow = 2,
InPlace = 3,
ToLast = 4,
};
interface IHostSettings
{
String Hotkey { get; };
Boolean ShowAppDetails { get; };
Boolean HotkeyGoesHome { get; };
Boolean BackspaceGoesBack { get; };
Boolean SingleClickActivates { get; };
Boolean HighlightSearchOnActivate { get; };
Boolean ShowSystemTrayIcon { get; };
Boolean IgnoreShortcutWhenFullscreen { get; };
Boolean DisableAnimations { get; };
SummonTarget SummonOn { get; };
}
```
| Property | Description |
|----------|-------------|
| `Hotkey` | The keyboard shortcut to activate Command Palette (e.g., "Alt+Space") |
| `ShowAppDetails` | Whether to show application details in the UI |
| `HotkeyGoesHome` | Whether pressing the hotkey returns to the home page |
| `BackspaceGoesBack` | Whether backspace navigates back when search is empty |
| `SingleClickActivates` | Whether single-click activates items (vs double-click) |
| `HighlightSearchOnActivate` | Whether to highlight search text on activation |
| `ShowSystemTrayIcon` | Whether to show the system tray icon |
| `IgnoreShortcutWhenFullscreen` | Whether to ignore the hotkey when a fullscreen app is active |
| `DisableAnimations` | Whether animations are disabled |
| `SummonOn` | Where to position the window when summoned |
#### IHostSettingsChanged Interface
Extensions that want to receive settings must provide an `IHostSettingsChanged`
stub via the `GetApiExtensionStubs()` pattern:
```c#
interface IHostSettingsChanged
{
void OnHostSettingsChanged(IHostSettings settings);
}
```
The host will call `OnHostSettingsChanged` in two scenarios:
1. **Initial settings**: When the extension is first loaded
2. **Settings changes**: When the user modifies Command Palette settings
#### Using Host Settings (Toolkit)
For extensions using the Toolkit, host settings are automatically managed. The
`CommandProvider` base class provides the `IHostSettingsChanged` stub, and
settings are cached in `HostSettingsManager`:
```cs
// Access current settings anywhere in your extension
var settings = HostSettingsManager.Current;
if (settings != null)
{
var hotkey = settings.Hotkey;
var animationsDisabled = settings.DisableAnimations;
}
// Check if settings are available
if (HostSettingsManager.IsAvailable)
{
// Host supports settings
}
```
To respond to settings changes, subscribe to the `SettingsChanged` event:
```cs
public class MyPage : ListPage
{
public MyPage()
{
// Refresh the page when settings change
HostSettingsManager.SettingsChanged += () => RaiseItemsChanged();
}
public override IListItem[] GetItems()
{
var settings = HostSettingsManager.Current;
if (settings == null)
{
return [new ListItem(new NoOpCommand())
{
Title = "Settings not available"
}];
}
return [
new ListItem(new NoOpCommand())
{
Title = $"Current Hotkey: {settings.Hotkey}"
},
];
}
}
```
#### Custom Settings Handler
Extensions can override `OnHostSettingsChanged` in their `CommandProvider` to
add custom handling logic:
```cs
public class MyCommandProvider : CommandProvider
{
public override void OnHostSettingsChanged(IHostSettings settings)
{
base.OnHostSettingsChanged(settings); // Update HostSettingsManager
// Custom logic
if (settings.DisableAnimations)
{
DisableMyAnimations();
}
}
}
```
#### Compatibility
- **Old extensions** (no `ICommandProvider2`): Won't receive settings, gracefully skipped
- **Old hosts** (no settings support): `HostSettingsManager.Current` will be null
Extensions should always check `HostSettingsManager.IsAvailable` or handle null
`Current` values to ensure compatibility with older hosts.
### Rendering of ICommandItems in Lists and Menus
When displaying a list item:
@@ -2032,19 +2187,31 @@ public partial class SamplePagesCommandsProvider : CommandProvider, ICommandProv
// Here is where we enable support for future additions to the API
public object[] GetApiExtensionStubs() {
return [new SupportCommandsWithProperties()];
return [
new SupportCommandsWithProperties(),
new HostSettingsChangedHandler(this)
];
}
private sealed partial class SupportCommandsWithProperties : IExtendedAttributesProvider {
public IDictionary<string, object>? GetProperties() => null;
}
private sealed partial class HostSettingsChangedHandler : IHostSettingsChanged {
private readonly SamplePagesCommandsProvider _provider;
public HostSettingsChangedHandler(SamplePagesCommandsProvider provider) => _provider = provider;
public void OnHostSettingsChanged(IHostSettings settings) => _provider.OnHostSettingsChanged(settings);
}
}
```
This pattern is used for multiple API extensions:
- `IExtendedAttributesProvider`: Allows commands to expose additional properties
- `IHostSettingsChanged`: Allows extensions to receive host settings updates (see [Host Settings](#host-settings))
Fortunately, we can put all of that (`GetApiExtensionStubs`,
`SupportCommandsWithProperties`) directly in `Toolkit.CommandProvider`, so
developers won't have to do anything. The toolkit will just do the right thing
for them.
`SupportCommandsWithProperties`, `HostSettingsChangedHandler`) directly in
`Toolkit.CommandProvider`, so developers won't have to do anything. The toolkit
will just do the right thing for them.
## Class diagram

View File

@@ -0,0 +1,83 @@
// 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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
/// <summary>
/// A sample page that displays the current Host Settings values.
/// This demonstrates how extensions can access and display the Command Palette's global settings.
/// </summary>
internal sealed partial class HostSettingsPage : ListPage
{
public HostSettingsPage()
{
Icon = new IconInfo("\uE713"); // Settings icon
Name = "Host Settings";
Title = "Current Host Settings";
// Subscribe to settings changes to refresh the page when settings are updated
HostSettingsManager.SettingsChanged += OnSettingsChanged;
ExtensionHost.LogMessage($"[HostSettingsPage] Constructor called, subscribed to SettingsChanged");
}
private void OnSettingsChanged()
{
ExtensionHost.LogMessage($"[HostSettingsPage] OnSettingsChanged called, invoking RaiseItemsChanged");
// Notify the UI to refresh the items list
RaiseItemsChanged();
ExtensionHost.LogMessage($"[HostSettingsPage] RaiseItemsChanged completed");
}
public override IListItem[] GetItems()
{
ExtensionHost.LogMessage($"[HostSettingsPage] GetItems called");
var settings = HostSettingsManager.Current;
if (settings == null)
{
return [
new ListItem(new NoOpCommand())
{
Title = "Host Settings not available",
Subtitle = "Settings have not been received from the host yet",
Icon = new IconInfo("\uE7BA"), // Warning icon
},
];
}
return [
CreateSettingItem("Hotkey", settings.Hotkey, "\uE765"), // Keyboard icon
CreateSettingItem("Show App Details", settings.ShowAppDetails, "\uE946"), // View icon
CreateSettingItem("Hotkey Goes Home", settings.HotkeyGoesHome, "\uE80F"), // Home icon
CreateSettingItem("Backspace Goes Back", settings.BackspaceGoesBack, "\uE72B"), // Back icon
CreateSettingItem("Single Click Activates", settings.SingleClickActivates, "\uE8B0"), // Mouse icon
CreateSettingItem("Highlight Search On Activate", settings.HighlightSearchOnActivate, "\uE8D6"), // Highlight icon
CreateSettingItem("Show System Tray Icon", settings.ShowSystemTrayIcon, "\uE8A5"), // System icon
CreateSettingItem("Ignore Shortcut When Fullscreen", settings.IgnoreShortcutWhenFullscreen, "\uE740"), // Fullscreen icon
CreateSettingItem("Disable Animations", settings.DisableAnimations, "\uE916"), // Play icon
CreateSettingItem("Summon On", settings.SummonOn.ToString(), "\uE7C4"), // Position icon
];
}
private static ListItem CreateSettingItem(string name, object value, string iconGlyph)
{
var displayValue = value switch
{
bool b => b ? "Enabled" : "Disabled",
string s when string.IsNullOrEmpty(s) => "(not set)",
_ => value?.ToString() ?? "null",
};
return new ListItem(new NoOpCommand())
{
Title = name,
Subtitle = displayValue,
Icon = new IconInfo(iconGlyph),
};
}
}

View File

@@ -101,6 +101,14 @@ public partial class SamplesListPage : ListPage
Subtitle = "A demo of the settings helpers",
},
// Host Settings
new ListItem(new HostSettingsPage())
{
Title = "Host Settings",
Subtitle = "View current Command Palette host settings",
Icon = new IconInfo("\uE713"), // Settings icon
},
// Evil edge cases
// Anything weird that might break the palette - put that in here.
new ListItem(new EvilSamplesPage())

View File

@@ -6,7 +6,7 @@ using Windows.Foundation;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2, IHostSettingsChanged
{
public virtual string Id { get; protected set; } = string.Empty;
@@ -30,6 +30,16 @@ public abstract partial class CommandProvider : ICommandProvider, ICommandProvid
public virtual void InitializeWithHost(IExtensionHost host) => ExtensionHost.Initialize(host);
/// <summary>
/// Called when the host settings have changed. Override this method to respond to settings changes.
/// The base implementation updates the HostSettingsManager cache.
/// </summary>
/// <param name="settings">The updated host settings.</param>
public virtual void OnHostSettingsChanged(IHostSettings settings)
{
HostSettingsManager.Update(settings);
}
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
public virtual void Dispose()
{
@@ -57,7 +67,7 @@ public abstract partial class CommandProvider : ICommandProvider, ICommandProvid
/// <returns>an array of objects that implement all the leaf interfaces we support</returns>
public object[] GetApiExtensionStubs()
{
return [new SupportCommandsWithProperties()];
return [new SupportCommandsWithProperties(), new HostSettingsChangedHandler(this)];
}
/// <summary>
@@ -69,4 +79,18 @@ public abstract partial class CommandProvider : ICommandProvider, ICommandProvid
{
public IDictionary<string, object>? GetProperties() => null;
}
/// <summary>
/// A stub class which implements IHostSettingsChanged. This is marshalled across
/// the ABI so that the host can call OnHostSettingsChanged on this object,
/// which then delegates to the CommandProvider's OnHostSettingsChanged method.
/// </summary>
private sealed partial class HostSettingsChangedHandler : IHostSettingsChanged
{
private readonly CommandProvider _provider;
public HostSettingsChangedHandler(CommandProvider provider) => _provider = provider;
public void OnHostSettingsChanged(IHostSettings settings) => _provider.OnHostSettingsChanged(settings);
}
}

View File

@@ -0,0 +1,62 @@
// 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.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
/// <summary>
/// Concrete implementation of IHostSettings for the Command Palette host settings.
/// This class is used by the host to create settings objects that can be passed to extensions.
/// </summary>
public partial class HostSettings : IHostSettings
{
/// <summary>
/// Gets or sets the global hotkey for summoning the Command Palette.
/// </summary>
public string Hotkey { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether to show app details in the UI.
/// </summary>
public bool ShowAppDetails { get; set; }
/// <summary>
/// Gets or sets a value indicating whether pressing the hotkey goes to the home page.
/// </summary>
public bool HotkeyGoesHome { get; set; }
/// <summary>
/// Gets or sets a value indicating whether backspace navigates back.
/// </summary>
public bool BackspaceGoesBack { get; set; }
/// <summary>
/// Gets or sets a value indicating whether single click activates items.
/// </summary>
public bool SingleClickActivates { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to highlight the search box on activation.
/// </summary>
public bool HighlightSearchOnActivate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show the system tray icon.
/// </summary>
public bool ShowSystemTrayIcon { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to ignore the shortcut when in fullscreen mode.
/// </summary>
public bool IgnoreShortcutWhenFullscreen { get; set; }
/// <summary>
/// Gets or sets a value indicating whether animations are disabled.
/// </summary>
public bool DisableAnimations { get; set; }
/// <summary>
/// Gets or sets the target monitor behavior when summoning the Command Palette.
/// </summary>
public SummonTarget SummonOn { get; set; }
}

View File

@@ -0,0 +1,54 @@
// 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.
namespace Microsoft.CommandPalette.Extensions.Toolkit;
/// <summary>
/// Provides static access to the current Command Palette host settings.
/// Extensions can use this class to read the current settings values and respond to changes.
/// </summary>
public static class HostSettingsManager
{
private static IHostSettings? _current;
/// <summary>
/// Occurs when the host settings have changed.
/// Extensions can subscribe to this event to refresh their UI when settings are updated.
/// </summary>
public static event Action? SettingsChanged;
/// <summary>
/// Gets the current host settings, or null if not yet initialized.
/// </summary>
public static IHostSettings? Current => _current;
/// <summary>
/// Gets a value indicating whether host settings are available.
/// Returns false if the extension is running with an older host that doesn't support settings.
/// </summary>
public static bool IsAvailable => _current != null;
/// <summary>
/// Initializes the host settings. Called internally by ExtensionHost during initialization.
/// </summary>
/// <param name="settings">The initial host settings.</param>
internal static void Initialize(IHostSettings settings) => _current = settings;
/// <summary>
/// Updates the cached host settings. Called internally when settings change.
/// </summary>
/// <param name="settings">The updated host settings.</param>
internal static void Update(IHostSettings settings)
{
_current = settings;
#if DEBUG
var subscriberCount = SettingsChanged?.GetInvocationList().Length ?? 0;
ExtensionHost.LogMessage($"[HostSettingsManager] Update called, subscriber count: {subscriberCount}");
#endif
SettingsChanged?.Invoke();
#if DEBUG
ExtensionHost.LogMessage($"[HostSettingsManager] SettingsChanged event invoked");
#endif
}
}

View File

@@ -391,6 +391,53 @@ namespace Microsoft.CommandPalette.Extensions
{
Object[] GetApiExtensionStubs();
};
// =========================================================================
// Host Settings and Capability System
// =========================================================================
/// <summary>
/// Represents the position behavior when summoning the Command Palette.
/// </summary>
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
enum SummonTarget
{
ToMouse = 0,
ToPrimary = 1,
ToFocusedWindow = 2,
InPlace = 3,
ToLast = 4,
};
/// <summary>
/// Interface for host settings that can be passed to extensions across process boundaries.
/// Extensions can access these settings to adapt their behavior accordingly.
/// The Toolkit provides a concrete HostSettings class that implements this interface.
/// </summary>
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IHostSettings
{
String Hotkey { get; };
Boolean ShowAppDetails { get; };
Boolean HotkeyGoesHome { get; };
Boolean BackspaceGoesBack { get; };
Boolean SingleClickActivates { get; };
Boolean HighlightSearchOnActivate { get; };
Boolean ShowSystemTrayIcon { get; };
Boolean IgnoreShortcutWhenFullscreen { get; };
Boolean DisableAnimations { get; };
SummonTarget SummonOn { get; };
}
/// <summary>
/// Interface for extensions to receive notifications when host settings change.
/// Extensions implementing this interface will be notified whenever the user
/// modifies Command Palette settings, including initial settings on startup.
/// </summary>
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IHostSettingsChanged
{
void OnHostSettingsChanged(IHostSettings settings);
}
}