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

@@ -1692,6 +1692,7 @@ stringtable
stringval stringval
Strm Strm
strret strret
STRSAFE
stscanf stscanf
sttngs sttngs
Stubless Stubless

View File

@@ -0,0 +1,12 @@
// 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.CmdPal.Ext.WebSearch.Helpers.Browser;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
public class MockBrowserInfoService : IBrowserInfoService
{
public BrowserInfo GetDefaultBrowser() => new() { Name = "mocked browser", Path = "C:\\mockery\\mock.exe" };
}

View File

@@ -26,8 +26,9 @@ public class QueryTests : CommandPaletteUnitTestBase
{ {
// Setup // Setup
var settings = new MockSettingsInterface(); var settings = new MockSettingsInterface();
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings, browserInfoService);
// Act // Act
page.UpdateSearchText(string.Empty, query); page.UpdateSearchText(string.Empty, query);
@@ -55,8 +56,9 @@ public class QueryTests : CommandPaletteUnitTestBase
}; };
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5); var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings, browserInfoService);
// Act // Act
page.UpdateSearchText("abcdef", string.Empty); page.UpdateSearchText("abcdef", string.Empty);
@@ -90,8 +92,9 @@ public class QueryTests : CommandPaletteUnitTestBase
}; };
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5); var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings, browserInfoService);
mockHistoryItems.Add(new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture))); mockHistoryItems.Add(new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)));
@@ -123,8 +126,9 @@ public class QueryTests : CommandPaletteUnitTestBase
}; };
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0); var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings, browserInfoService);
// Act // Act
page.UpdateSearchText("abcdef", string.Empty); page.UpdateSearchText("abcdef", string.Empty);

View File

@@ -20,7 +20,9 @@ public class SettingsManagerTests : CommandPaletteUnitTestBase
{ {
// Setup // Setup
var settings = new MockSettingsInterface(historyItemCount: 5); var settings = new MockSettingsInterface(historyItemCount: 5);
var page = new WebSearchListPage(settings); var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings, browserInfoService);
var eventRaised = false; var eventRaised = false;

View File

@@ -2,32 +2,28 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands; namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class OpenURLCommand : InvokableCommand internal sealed partial class OpenURLCommand : InvokableCommand
{ {
private readonly IBrowserInfoService _browserInfoService;
public string Url { get; internal set; } = string.Empty; public string Url { get; internal set; } = string.Empty;
internal OpenURLCommand(string url) internal OpenURLCommand(string url, IBrowserInfoService browserInfoService)
{ {
_browserInfoService = browserInfoService;
Url = url; Url = url;
BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
Name = string.Empty; Name = string.Empty;
} }
public override CommandResult Invoke() public override CommandResult Invoke()
{ {
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{Url}")) // TODO GH# 138 --> actually display feedback from the extension somewhere.
{ return _browserInfoService.Open(Url) ? CommandResult.Dismiss() : CommandResult.KeepOpen();
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen();
}
return CommandResult.Dismiss();
} }
} }

View File

@@ -4,31 +4,31 @@
using System; using System;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands; namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class SearchWebCommand : InvokableCommand internal sealed partial class SearchWebCommand : InvokableCommand
{ {
private readonly ISettingsInterface _settingsManager; private readonly ISettingsInterface _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
public string Arguments { get; internal set; } = string.Empty; public string Arguments { get; internal set; }
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager) internal SearchWebCommand(string arguments, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{ {
Arguments = arguments; Arguments = arguments;
BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
Name = Properties.Resources.open_in_default_browser; Name = Resources.open_in_default_browser;
_settingsManager = settingsManager; _settingsManager = settingsManager;
_browserInfoService = browserInfoService;
} }
public override CommandResult Invoke() public override CommandResult Invoke()
{ {
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"? {Arguments}")) if (!_browserInfoService.Open($"? {Arguments}"))
{ {
// TODO GH# 138 --> actually display feedback from the extension somewhere. // TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen(); return CommandResult.KeepOpen();

View File

@@ -5,9 +5,9 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands; namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
@@ -16,25 +16,34 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
private readonly SearchWebCommand _executeItem; private readonly SearchWebCommand _executeItem;
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle); private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle);
private string _title;
public FallbackExecuteSearchItem(SettingsManager settings) private readonly IBrowserInfoService _browserInfoService;
: base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
public FallbackExecuteSearchItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
: base(new SearchWebCommand(string.Empty, settings, browserInfoService) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
{ {
_executeItem = (SearchWebCommand)this.Command!; _executeItem = (SearchWebCommand)Command!;
_browserInfoService = browserInfoService;
Title = string.Empty; Title = string.Empty;
Subtitle = string.Empty; Subtitle = string.Empty;
_executeItem.Name = string.Empty; _executeItem.Name = string.Empty;
_title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
} }
private static string UpdateBrowserName(IBrowserInfoService browserInfoService)
{
var browserName = browserInfoService.GetDefaultBrowser()?.Name;
return string.IsNullOrWhiteSpace(browserName)
? Resources.open_in_default_browser
: string.Format(CultureInfo.CurrentCulture, PluginOpen, browserName);
}
public override void UpdateQuery(string query) public override void UpdateQuery(string query)
{ {
_executeItem.Arguments = query; _executeItem.Arguments = query;
var isEmpty = string.IsNullOrEmpty(query); var isEmpty = string.IsNullOrEmpty(query);
_executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser; _executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
Title = isEmpty ? string.Empty : _title; Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query); Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
} }
} }

View File

@@ -7,21 +7,26 @@ using System.Globalization;
using System.Text; using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch; namespace Microsoft.CmdPal.Ext.WebSearch;
internal sealed partial class FallbackOpenURLItem : FallbackCommandItem internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
{ {
private readonly IBrowserInfoService _browserInfoService;
private readonly OpenURLCommand _executeItem; private readonly OpenURLCommand _executeItem;
private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url); private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url);
private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser); private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser);
public FallbackOpenURLItem(SettingsManager settings) public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
: base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title) : base(new OpenURLCommand(string.Empty, browserInfoService), Resources.open_url_fallback_title)
{ {
_executeItem = (OpenURLCommand)this.Command!; ArgumentNullException.ThrowIfNull(browserInfoService);
_browserInfoService = browserInfoService;
_executeItem = (OpenURLCommand)Command!;
Title = string.Empty; Title = string.Empty;
_executeItem.Name = string.Empty; _executeItem.Name = string.Empty;
Subtitle = string.Empty; Subtitle = string.Empty;
@@ -39,7 +44,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
return; return;
} }
var success = Uri.TryCreate(query, UriKind.Absolute, out var uri); var success = Uri.TryCreate(query, UriKind.Absolute, out _);
// if url not contain schema, add http:// by default. // if url not contain schema, add http:// by default.
if (!success) if (!success)
@@ -48,13 +53,15 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
} }
_executeItem.Url = query; _executeItem.Url = query;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.open_in_default_browser; _executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Resources.open_in_default_browser;
Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query); Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
var browserName = _browserInfoService.GetDefaultBrowser()?.Name;
Subtitle = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
} }
public static bool IsValidUrl(string url) private static bool IsValidUrl(string url)
{ {
if (string.IsNullOrWhiteSpace(url)) if (string.IsNullOrWhiteSpace(url))
{ {

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

View File

@@ -9,23 +9,24 @@ using System.Text;
using System.Threading; using System.Threading;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Pages; namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{ {
private readonly ISettingsInterface _settingsManager; private readonly ISettingsInterface _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
private readonly Lock _sync = new(); private readonly Lock _sync = new();
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name); private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private IListItem[] _allItems = []; private IListItem[] _allItems = [];
private List<ListItem> _historyItems = []; private List<ListItem> _historyItems = [];
public WebSearchListPage(ISettingsInterface settingsManager) public WebSearchListPage(ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{ {
ArgumentNullException.ThrowIfNull(settingsManager); ArgumentNullException.ThrowIfNull(settingsManager);
@@ -35,6 +36,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
Id = "com.microsoft.cmdpal.websearch"; Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager; _settingsManager = settingsManager;
_browserInfoService = browserInfoService;
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged; _settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
// It just looks viewer to have string twice on the page, and default placeholder is good enough // It just looks viewer to have string twice on the page, and default placeholder is good enough
@@ -43,8 +45,8 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
EmptyContent = new CommandItem(new NoOpCommand()) EmptyContent = new CommandItem(new NoOpCommand())
{ {
Icon = Icon, Icon = Icon,
Title = Properties.Resources.plugin_description, Title = Resources.plugin_description,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
}; };
UpdateHistory(); UpdateHistory();
@@ -67,7 +69,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
for (var index = items.Count - 1; index >= 0; index--) for (var index = items.Count - 1; index >= 0; index--)
{ {
var historyItem = items[index]; var historyItem = items[index];
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager)) history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager, _browserInfoService))
{ {
Icon = Icons.History, Icon = Icons.History,
Title = historyItem.SearchString, Title = historyItem.SearchString,
@@ -82,7 +84,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
} }
} }
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager) private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{ {
ArgumentNullException.ThrowIfNull(query); ArgumentNullException.ThrowIfNull(query);
@@ -95,10 +97,10 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
if (!string.IsNullOrEmpty(query)) if (!string.IsNullOrEmpty(query))
{ {
var searchTerm = query; var searchTerm = query;
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager)) var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager, browserInfoService))
{ {
Title = searchTerm, Title = searchTerm,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
Icon = Icons.Search, Icon = Icons.Search,
}; };
results.Add(result); results.Add(result);
@@ -117,7 +119,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
historySnapshot = _historyItems; historySnapshot = _historyItems;
} }
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager); var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _browserInfoService);
lock (_sync) lock (_sync)
{ {

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources { public class Resources {
@@ -69,6 +69,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to default browser.
/// </summary>
public static string default_browser {
get {
return ResourceManager.GetString("default_browser", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Web Search. /// Looks up a localized string similar to Web Search.
/// </summary> /// </summary>

View File

@@ -184,4 +184,7 @@
<data name="open_url_fallback_title" xml:space="preserve"> <data name="open_url_fallback_title" xml:space="preserve">
<value>Open URL</value> <value>Open URL</value>
</data> </data>
<data name="default_browser" xml:space="preserve">
<value>default browser</value>
</data>
</root> </root>

View File

@@ -5,6 +5,7 @@
using System; using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -19,6 +20,7 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem; private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
private readonly ICommandItem[] _topLevelItems; private readonly ICommandItem[] _topLevelItems;
private readonly IFallbackCommandItem[] _fallbackCommands; private readonly IFallbackCommandItem[] _fallbackCommands;
private readonly IBrowserInfoService _browserInfoService = new DefaultBrowserInfoService();
public WebSearchCommandsProvider() public WebSearchCommandsProvider()
{ {
@@ -27,10 +29,10 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
Settings = _settingsManager.Settings; Settings = _settingsManager.Settings;
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager); _fallbackItem = new FallbackExecuteSearchItem(_settingsManager, _browserInfoService);
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager); _openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager, _browserInfoService);
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager) _webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager, _browserInfoService)
{ {
MoreCommands = MoreCommands =
[ [

View File

@@ -5,6 +5,7 @@
using System; using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Pages; using Microsoft.CmdPal.Ext.WebSearch.Pages;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
@@ -15,13 +16,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch;
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
{ {
private readonly SettingsManager _settingsManager; private readonly SettingsManager _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
public WebSearchTopLevelCommandItem(SettingsManager settingsManager) public WebSearchTopLevelCommandItem(SettingsManager settingsManager, IBrowserInfoService browserInfoService)
: base(new WebSearchListPage(settingsManager)) : base(new WebSearchListPage(settingsManager, browserInfoService))
{ {
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
SetDefaultTitle(); SetDefaultTitle();
_settingsManager = settingsManager; _settingsManager = settingsManager;
_browserInfoService = browserInfoService;
} }
private void SetDefaultTitle() => Title = Resources.command_item_title; private void SetDefaultTitle() => Title = Resources.command_item_title;
@@ -37,12 +40,12 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
if (string.IsNullOrEmpty(query)) if (string.IsNullOrEmpty(query))
{ {
SetDefaultTitle(); SetDefaultTitle();
ReplaceCommand(new WebSearchListPage(_settingsManager)); ReplaceCommand(new WebSearchListPage(_settingsManager, _browserInfoService));
} }
else else
{ {
Title = query; Title = query;
ReplaceCommand(new SearchWebCommand(query, _settingsManager)); ReplaceCommand(new SearchWebCommand(query, _settingsManager, _browserInfoService));
} }
} }