CmdPal: Use Shell API to determine the default browser in WebSearch (#43339)

## Summary of the Pull Request

This PR introduces a new method for determining the default browser
using the Windows Shell API. The new provider selects the browser
associated with the HTTPS protocol (falling back to HTTP if necessary).
The original implementation is retained as a fallback for now, and the
codebase is prepared for future extensions (e.g., manual default-browser
selection).

As a flyby, it also fixes an issue where commands continued showing the
previous browser name if the user changed their default browser while
the Command Palette was running.

## One-liner for change log

Fixed default browser selection in the Web Search built-in extension.

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

- [x] Closes: #42343
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **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-11-27 16:31:10 +01:00
committed by GitHub
parent 47d4a65223
commit 0de60445ea
25 changed files with 637 additions and 271 deletions

View File

@@ -0,0 +1,14 @@
// 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.CmdPal.Ext.WebSearch.Helpers.Browser;
public record BrowserInfo
{
public required string Path { get; init; }
public required string Name { get; init; }
public string? ArgumentsPattern { get; init; }
}

View File

@@ -0,0 +1,32 @@
// 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.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Extension methods for <see cref="IBrowserInfoService"/>.
/// </summary>
/// <seealso cref="IBrowserInfoService"/>
internal static class BrowserInfoServiceExtensions
{
/// <summary>
/// Opens the specified URL in the system's default web browser.
/// </summary>
/// <param name="browserInfoService">The browser information service used to resolve the system's default browser.</param>
/// <param name="url">The URL to open.</param>
/// <returns>
/// <see langword="true"/> if a default browser is found and the URL launch command is issued successfully;
/// otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// Returns <see langword="false"/> if the default browser cannot be determined.
/// </remarks>
public static bool Open(this IBrowserInfoService browserInfoService, string url)
{
var defaultBrowser = browserInfoService.GetDefaultBrowser();
return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url);
}
}

View File

@@ -0,0 +1,99 @@
// 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;
using System.Collections.Generic;
using System.Threading;
using ManagedCommon;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Service to get information about the default browser.
/// </summary>
internal class DefaultBrowserInfoService : IBrowserInfoService
{
private static readonly IDefaultBrowserProvider[] Providers =
[
new ShellAssociationProvider(),
new LegacyRegistryAssociationProvider(),
new FallbackMsEdgeBrowserProvider(),
];
private readonly Lock _updateLock = new();
private readonly Dictionary<Type, string> _lastLoggedErrors = [];
private const long UpdateTimeout = 3000;
private long _lastUpdateTickCount = -UpdateTimeout;
private BrowserInfo? _defaultBrowser;
public BrowserInfo? GetDefaultBrowser()
{
try
{
UpdateIfTimePassed();
}
catch (Exception)
{
// exception is already logged at this point
}
return _defaultBrowser;
}
/// <summary>
/// Updates only if at least more than 3000ms has passed since the last update, to avoid multiple calls to <see cref="UpdateCore"/>.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
private void UpdateIfTimePassed()
{
lock (_updateLock)
{
var curTickCount = Environment.TickCount64;
if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
{
return;
}
var newDefaultBrowser = UpdateCore();
_defaultBrowser = newDefaultBrowser;
_lastUpdateTickCount = curTickCount;
}
}
/// <summary>
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
private BrowserInfo UpdateCore()
{
foreach (var provider in Providers)
{
try
{
var result = provider.GetDefaultBrowserInfo();
#if DEBUG
result = result with { Name = result.Name + " (" + provider.GetType().Name + ")" };
#endif
return result;
}
catch (Exception ex)
{
// since we run this fairly often, avoid logging the same error multiple times
var lastLoggedError = _lastLoggedErrors.GetValueOrDefault(provider.GetType());
var error = ex.ToString();
if (error != lastLoggedError)
{
_lastLoggedErrors[provider.GetType()] = error;
Logger.LogError($"Exception when retrieving browser using provider {provider.GetType()}", ex);
}
}
}
throw new InvalidOperationException("Unable to determine default browser");
}
}

View File

@@ -0,0 +1,17 @@
// 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.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Provides functionality to retrieve information about the system's default web browser.
/// </summary>
public interface IBrowserInfoService
{
/// <summary>
/// Gets information about the system's default web browser.
/// </summary>
/// <returns></returns>
BrowserInfo? GetDefaultBrowser();
}

View File

@@ -0,0 +1,7 @@
// 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.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
internal record AssociatedApp(string? Command, string? FriendlyName);

View File

@@ -0,0 +1,154 @@
// 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;
using System.IO;
using Windows.Win32;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Base class for providers that determine the default browser via application associations.
/// </summary>
internal abstract class AssociationProviderBase : IDefaultBrowserProvider
{
protected abstract AssociatedApp? FindAssociation();
public BrowserInfo GetDefaultBrowserInfo()
{
var appAssociation = FindAssociation();
if (appAssociation is null)
{
throw new ArgumentNullException(nameof(appAssociation), "Could not determine default browser application.");
}
var commandPattern = appAssociation.Command;
var appAndArgs = SplitAppAndArgs(commandPattern);
if (string.IsNullOrEmpty(appAndArgs.Path))
{
throw new ArgumentOutOfRangeException(nameof(appAndArgs.Path), "Default browser program path could not be determined.");
}
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
if (!Path.Exists(appAndArgs.Path) && !Uri.TryCreate(appAndArgs.Path, UriKind.Absolute, out _))
{
throw new ArgumentException($"Command validation failed: {commandPattern}", nameof(commandPattern));
}
return new BrowserInfo
{
Path = appAndArgs.Path,
Name = appAssociation.FriendlyName ?? Path.GetFileNameWithoutExtension(appAndArgs.Path),
ArgumentsPattern = appAndArgs.Arguments,
};
}
private static (string? Path, string? Arguments) SplitAppAndArgs(string? commandPattern)
{
if (string.IsNullOrEmpty(commandPattern))
{
throw new ArgumentOutOfRangeException(nameof(commandPattern), "Default browser program command is not specified.");
}
commandPattern = GetIndirectString(commandPattern);
// HACK: for firefox installed through Microsoft store
// When installed through Microsoft Firefox the commandPattern does not have
// quotes for the path. As the Program Files does have a space
// the extracted path would be invalid, here we add the quotes to fix it
const string FirefoxExecutableName = "firefox.exe";
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") &&
!commandPattern.StartsWith('\"'))
{
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) +
FirefoxExecutableName.Length;
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
commandPattern = commandPattern.Insert(0, "\"");
}
if (commandPattern.StartsWith('\"'))
{
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
if (endQuoteIndex != -1)
{
return (commandPattern[1..endQuoteIndex], commandPattern[(endQuoteIndex + 1)..].Trim());
}
}
else
{
var spaceIndex = commandPattern.IndexOf(' ');
if (spaceIndex != -1)
{
return (commandPattern[..spaceIndex], commandPattern[(spaceIndex + 1)..].Trim());
}
}
return (null, null);
}
protected static string GetIndirectString(string str)
{
if (string.IsNullOrEmpty(str) || str[0] != '@')
{
return str;
}
const int initialCapacity = 128;
const int maxCapacity = 8192; // Reasonable upper limit
int hresult;
unsafe
{
// Try with stack allocation first for common cases
var stackBuffer = stackalloc char[initialCapacity];
fixed (char* pszSource = str)
{
hresult = PInvoke.SHLoadIndirectString(
pszSource,
stackBuffer,
initialCapacity,
null);
// S_OK (0) means success
if (hresult == 0)
{
return new string(stackBuffer);
}
// STRSAFE_E_INSUFFICIENT_BUFFER (0x8007007A) means buffer too small
// Try with progressively larger heap buffers
if (unchecked((uint)hresult) == 0x8007007A)
{
for (var capacity = initialCapacity * 2; capacity <= maxCapacity; capacity *= 2)
{
var heapBuffer = new char[capacity];
fixed (char* pBuffer = heapBuffer)
{
hresult = PInvoke.SHLoadIndirectString(
pszSource,
pBuffer,
(uint)capacity,
null);
if (hresult == 0)
{
return new string(pBuffer);
}
if (unchecked((uint)hresult) != 0x8007007A)
{
break; // Different error, stop retrying
}
}
}
}
}
}
throw new InvalidOperationException(
$"Could not load indirect string. HRESULT: 0x{unchecked((uint)hresult):X8}");
}
}

View File

@@ -0,0 +1,31 @@
// 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;
using System.IO;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Provides a fallback implementation of the default browser provider that returns information for Microsoft Edge.
/// </summary>
/// <remarks>This class is used when no other default browser provider is available. It supplies the path,
/// arguments pattern, and name for Microsoft Edge as the default browser information.</remarks>
internal sealed class FallbackMsEdgeBrowserProvider : IDefaultBrowserProvider
{
private const string MsEdgeArgumentsPattern = "--single-argument %1";
private const string MsEdgeName = "Microsoft Edge";
private static string MsEdgePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
@"Microsoft\Edge\Application\msedge.exe");
public BrowserInfo GetDefaultBrowserInfo() => new()
{
Path = MsEdgePath,
ArgumentsPattern = MsEdgeArgumentsPattern,
Name = MsEdgeName,
};
}

View File

@@ -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.
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Retrieves information about the default browser.
/// </summary>
internal interface IDefaultBrowserProvider
{
BrowserInfo GetDefaultBrowserInfo();
}

View File

@@ -0,0 +1,46 @@
// 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;
using Microsoft.Win32;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Provides the default web browser by reading registry keys. This is a legacy method and may not work on all systems.
/// </summary>
internal sealed class LegacyRegistryAssociationProvider : AssociationProviderBase
{
protected override AssociatedApp? FindAssociation()
{
var progId = GetRegistryValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId",
"ProgId")
?? GetRegistryValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
"ProgId");
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");
if (appName is not null)
{
appName = GetIndirectString(appName);
appName = appName
.Replace("URL", null, StringComparison.OrdinalIgnoreCase)
.Replace("HTML", null, StringComparison.OrdinalIgnoreCase)
.Replace("Document", null, StringComparison.OrdinalIgnoreCase)
.Replace("Web", null, StringComparison.OrdinalIgnoreCase)
.TrimEnd();
}
var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null);
return commandPattern is null ? null : new AssociatedApp(commandPattern, appName);
static string? GetRegistryValue(string registryLocation, string? valueName)
{
return Registry.GetValue(registryLocation, valueName, null) as string;
}
}
}

View File

@@ -0,0 +1,64 @@
// 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.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Retrieves the default web browser using the system shell functions.
/// </summary>
internal sealed class ShellAssociationProvider : AssociationProviderBase
{
private static readonly string[] Protocols = ["https", "http"];
protected override AssociatedApp FindAssociation()
{
foreach (var protocol in Protocols)
{
var command = AssocQueryStringSafe(NativeMethods.AssocStr.Command, protocol);
if (string.IsNullOrWhiteSpace(command))
{
continue;
}
var appName = AssocQueryStringSafe(NativeMethods.AssocStr.FriendlyAppName, protocol);
return new AssociatedApp(command, appName);
}
return new AssociatedApp(null, null);
}
private static unsafe string? AssocQueryStringSafe(NativeMethods.AssocStr what, string protocol)
{
uint cch = 0;
// First call: get required length (incl. null)
_ = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, null, ref cch);
if (cch == 0)
{
return null;
}
// Small buffers on stack; large on heap
var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch];
fixed (char* p = span)
{
var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch);
if (hr != 0 || cch == 0)
{
return null;
}
// cch includes the null terminator; slice it off
var len = (int)cch - 1;
if (len < 0)
{
len = 0;
}
return new string(span[..len]);
}
}
}

View File

@@ -1,215 +0,0 @@
// 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;
using System.Text;
using System.Threading;
using ManagedCommon;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
/// <summary>
/// Contains information (e.g. path to executable, name...) about the default browser.
/// </summary>
public static class DefaultBrowserInfo
{
private static readonly Lock _updateLock = new();
/// <summary>Gets the path to the MS Edge browser executable.</summary>
public static string MSEdgePath => System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
@"Microsoft\Edge\Application\msedge.exe");
/// <summary>Gets the command line pattern of the MS Edge.</summary>
public const string MSEdgeArgumentsPattern = "--single-argument %1";
public const string MSEdgeName = "Microsoft Edge";
/// <summary>Gets the path to default browser's executable.</summary>
public static string? Path { get; private set; }
/// <summary>Gets <see cref="Path"/> since the icon is embedded in the executable.</summary>
public static string? IconPath => Path;
/// <summary>Gets the user-friendly name of the default browser.</summary>
public static string? Name { get; private set; }
/// <summary>Gets the command line pattern of the default browser.</summary>
public static string? ArgumentsPattern { get; private set; }
public static bool IsDefaultBrowserSet => !string.IsNullOrEmpty(Path);
public const long UpdateTimeout = 300;
private static long _lastUpdateTickCount = -UpdateTimeout;
private static bool _updatedOnce;
private static bool _errorLogged;
/// <summary>
/// Updates only if at least more than 300ms has passed since the last update, to avoid multiple calls to <see cref="Update"/>.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
public static void UpdateIfTimePassed()
{
var curTickCount = Environment.TickCount64;
if (curTickCount - _lastUpdateTickCount >= UpdateTimeout)
{
_lastUpdateTickCount = curTickCount;
Update();
}
}
/// <summary>
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
public static void Update()
{
lock (_updateLock)
{
if (!_updatedOnce)
{
// Log.Info("I've tried updating the chosen Web Browser info at least once.", typeof(DefaultBrowserInfo));
_updatedOnce = true;
}
try
{
var progId = GetRegistryValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId",
"ProgId")
?? GetRegistryValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
"ProgId");
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");
if (appName is not null)
{
// Handle indirect strings:
if (appName.StartsWith('@'))
{
appName = GetIndirectString(appName);
}
appName = appName
.Replace("URL", null, StringComparison.OrdinalIgnoreCase)
.Replace("HTML", null, StringComparison.OrdinalIgnoreCase)
.Replace("Document", null, StringComparison.OrdinalIgnoreCase)
.Replace("Web", null, StringComparison.OrdinalIgnoreCase)
.TrimEnd();
}
Name = appName;
var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null);
if (string.IsNullOrEmpty(commandPattern))
{
throw new ArgumentOutOfRangeException(
nameof(commandPattern),
"Default browser program command is not specified.");
}
if (commandPattern.StartsWith('@'))
{
commandPattern = GetIndirectString(commandPattern);
}
// HACK: for firefox installed through Microsoft store
// When installed through Microsoft Firefox the commandPattern does not have
// quotes for the path. As the Program Files does have a space
// the extracted path would be invalid, here we add the quotes to fix it
const string FirefoxExecutableName = "firefox.exe";
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") && (!commandPattern.StartsWith('\"')))
{
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) + FirefoxExecutableName.Length;
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
commandPattern = commandPattern.Insert(0, "\"");
}
if (commandPattern.StartsWith('\"'))
{
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
if (endQuoteIndex != -1)
{
Path = commandPattern.Substring(1, endQuoteIndex - 1);
ArgumentsPattern = commandPattern.Substring(endQuoteIndex + 1).Trim();
}
}
else
{
var spaceIndex = commandPattern.IndexOf(' ');
if (spaceIndex != -1)
{
Path = commandPattern.Substring(0, spaceIndex);
ArgumentsPattern = commandPattern.Substring(spaceIndex + 1).Trim();
}
}
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
if (!System.IO.Path.Exists(Path) && !Uri.TryCreate(Path, UriKind.Absolute, out _))
{
throw new ArgumentException(
$"Command validation failed: {commandPattern}",
nameof(commandPattern));
}
if (string.IsNullOrEmpty(Path))
{
throw new ArgumentOutOfRangeException(
nameof(Path),
"Default browser program path could not be determined.");
}
}
catch (Exception)
{
// Fallback to MS Edge
Path = MSEdgePath;
Name = MSEdgeName;
ArgumentsPattern = MSEdgeArgumentsPattern;
if (!_errorLogged)
{
// Log.Exception("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.", e, typeof(DefaultBrowserInfo));
Logger.LogError("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.");
_errorLogged = true;
}
}
string? GetRegistryValue(string registryLocation, string? valueName)
{
return Microsoft.Win32.Registry.GetValue(registryLocation, valueName, null) as string;
}
string GetIndirectString(string str)
{
var stringBuilder = new StringBuilder(128);
unsafe
{
var buffer = stackalloc char[128];
var capacity = 128;
var firstChar = str[0];
var strPtr = &firstChar;
// S_OK == 0
fixed (char* pszSourceLocal = str)
{
if (global::Windows.Win32.PInvoke.SHLoadIndirectString(
pszSourceLocal,
buffer,
(uint)capacity,
default) == 0)
{
return new string(buffer);
}
}
}
throw new ArgumentNullException(nameof(str), "Could not load indirect string.");
}
}
}
}

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.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
internal static partial class NativeMethods
{
[LibraryImport("shlwapi.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = false)]
internal static unsafe partial int AssocQueryStringW(
AssocF flags,
AssocStr str,
string pszAssoc,
string? pszExtra,
char* pszOut,
ref uint pcchOut);
[Flags]
public enum AssocF : uint
{
None = 0,
IsProtocol = 0x00001000,
}
public enum AssocStr
{
Command = 1,
Executable,
FriendlyDocName,
FriendlyAppName,
NoOpen,
ShellNewValue,
DDECommand,
DDEIfExec,
DDEApplication,
DDETopic,
InfoTip,
QuickTip,
TileInfo,
ContentType,
DefaultIcon,
ShellExtension,
DropTarget,
DelegateExecute,
SupportedUriProtocols,
ProgId,
AppId,
AppPublisher,
AppIconReference, // sometimes present, but DefaultIcon is most common
}
}