diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 1ce6f88899..9f18bbb300 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -1692,6 +1692,7 @@ stringtable
stringval
Strm
strret
+STRSAFE
stscanf
sttngs
Stubless
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockBrowserInfoService.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockBrowserInfoService.cs
new file mode 100644
index 0000000000..ee27aa737e
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockBrowserInfoService.cs
@@ -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" };
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs
index 00f1235c0e..63e35314cd 100644
--- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs
@@ -26,8 +26,9 @@ public class QueryTests : CommandPaletteUnitTestBase
{
// Setup
var settings = new MockSettingsInterface();
+ var browserInfoService = new MockBrowserInfoService();
- var page = new WebSearchListPage(settings);
+ var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText(string.Empty, query);
@@ -55,8 +56,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
+ var browserInfoService = new MockBrowserInfoService();
- var page = new WebSearchListPage(settings);
+ var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText("abcdef", string.Empty);
@@ -90,8 +92,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
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)));
@@ -123,8 +126,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
+ var browserInfoService = new MockBrowserInfoService();
- var page = new WebSearchListPage(settings);
+ var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText("abcdef", string.Empty);
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/SettingsManagerTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/SettingsManagerTests.cs
index fd19427ca1..2ec5546daa 100644
--- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/SettingsManagerTests.cs
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/SettingsManagerTests.cs
@@ -20,7 +20,9 @@ public class SettingsManagerTests : CommandPaletteUnitTestBase
{
// Setup
var settings = new MockSettingsInterface(historyItemCount: 5);
- var page = new WebSearchListPage(settings);
+ var browserInfoService = new MockBrowserInfoService();
+
+ var page = new WebSearchListPage(settings, browserInfoService);
var eventRaised = false;
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/OpenURLCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/OpenURLCommand.cs
index 08d0a114f5..937be16ac2 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/OpenURLCommand.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/OpenURLCommand.cs
@@ -2,32 +2,28 @@
// 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;
using Microsoft.CommandPalette.Extensions.Toolkit;
-using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
-
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class OpenURLCommand : InvokableCommand
{
+ private readonly IBrowserInfoService _browserInfoService;
+
public string Url { get; internal set; } = string.Empty;
- internal OpenURLCommand(string url)
+ internal OpenURLCommand(string url, IBrowserInfoService browserInfoService)
{
+ _browserInfoService = browserInfoService;
Url = url;
- BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch;
Name = string.Empty;
}
public override CommandResult Invoke()
{
- if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{Url}"))
- {
- // TODO GH# 138 --> actually display feedback from the extension somewhere.
- return CommandResult.KeepOpen();
- }
-
- return CommandResult.Dismiss();
+ // TODO GH# 138 --> actually display feedback from the extension somewhere.
+ return _browserInfoService.Open(Url) ? CommandResult.Dismiss() : CommandResult.KeepOpen();
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs
index 2cc8953048..98921aa8ae 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs
@@ -4,31 +4,31 @@
using System;
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 BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
-
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class SearchWebCommand : InvokableCommand
{
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;
- BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch;
- Name = Properties.Resources.open_in_default_browser;
+ Name = Resources.open_in_default_browser;
_settingsManager = settingsManager;
+ _browserInfoService = browserInfoService;
}
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.
return CommandResult.KeepOpen();
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs
index c942e668d3..61557d996a 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs
@@ -5,9 +5,9 @@
using System.Globalization;
using System.Text;
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 BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
@@ -16,25 +16,34 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
private readonly SearchWebCommand _executeItem;
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 string _title;
- public FallbackExecuteSearchItem(SettingsManager settings)
- : base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
+ private readonly IBrowserInfoService _browserInfoService;
+
+ 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;
Subtitle = string.Empty;
_executeItem.Name = string.Empty;
- _title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
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)
{
_executeItem.Arguments = query;
var isEmpty = string.IsNullOrEmpty(query);
- _executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser;
- Title = isEmpty ? string.Empty : _title;
+ _executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
+ Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs
index 9f5d9d86ca..7feb53b1de 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs
@@ -7,21 +7,26 @@ using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
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 BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch;
internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
{
+ private readonly IBrowserInfoService _browserInfoService;
private readonly OpenURLCommand _executeItem;
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);
- public FallbackOpenURLItem(SettingsManager settings)
- : base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title)
+ public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
+ : 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;
_executeItem.Name = string.Empty;
Subtitle = string.Empty;
@@ -39,7 +44,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
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 (!success)
@@ -48,13 +53,15 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
}
_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);
- 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))
{
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfo.cs
new file mode 100644
index 0000000000..9da978f481
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfo.cs
@@ -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; }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfoServiceExtensions.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfoServiceExtensions.cs
new file mode 100644
index 0000000000..1614273d83
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfoServiceExtensions.cs
@@ -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;
+
+///
+/// Extension methods for .
+///
+///
+internal static class BrowserInfoServiceExtensions
+{
+ ///
+ /// Opens the specified URL in the system's default web browser.
+ ///
+ /// The browser information service used to resolve the system's default browser.
+ /// The URL to open.
+ ///
+ /// if a default browser is found and the URL launch command is issued successfully;
+ /// otherwise, .
+ ///
+ ///
+ /// Returns if the default browser cannot be determined.
+ ///
+ public static bool Open(this IBrowserInfoService browserInfoService, string url)
+ {
+ var defaultBrowser = browserInfoService.GetDefaultBrowser();
+ return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url);
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/DefaultBrowserInfoService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/DefaultBrowserInfoService.cs
new file mode 100644
index 0000000000..51312fe4c0
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/DefaultBrowserInfoService.cs
@@ -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;
+
+///
+/// Service to get information about the default browser.
+///
+internal class DefaultBrowserInfoService : IBrowserInfoService
+{
+ private static readonly IDefaultBrowserProvider[] Providers =
+ [
+ new ShellAssociationProvider(),
+ new LegacyRegistryAssociationProvider(),
+ new FallbackMsEdgeBrowserProvider(),
+ ];
+
+ private readonly Lock _updateLock = new();
+
+ private readonly Dictionary _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;
+ }
+
+ ///
+ /// Updates only if at least more than 3000ms has passed since the last update, to avoid multiple calls to .
+ /// (because of multiple plugins calling update at the same time.)
+ ///
+ private void UpdateIfTimePassed()
+ {
+ lock (_updateLock)
+ {
+ var curTickCount = Environment.TickCount64;
+ if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
+ {
+ return;
+ }
+
+ var newDefaultBrowser = UpdateCore();
+ _defaultBrowser = newDefaultBrowser;
+ _lastUpdateTickCount = curTickCount;
+ }
+ }
+
+ ///
+ /// Consider using to avoid updating multiple times.
+ /// (because of multiple plugins calling update at the same time.)
+ ///
+ 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");
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/IBrowserInfoService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/IBrowserInfoService.cs
new file mode 100644
index 0000000000..5d82193e5d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/IBrowserInfoService.cs
@@ -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;
+
+///
+/// Provides functionality to retrieve information about the system's default web browser.
+///
+public interface IBrowserInfoService
+{
+ ///
+ /// Gets information about the system's default web browser.
+ ///
+ ///
+ BrowserInfo? GetDefaultBrowser();
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociatedApp.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociatedApp.cs
new file mode 100644
index 0000000000..3c6ba74d67
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociatedApp.cs
@@ -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);
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociationProviderBase.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociationProviderBase.cs
new file mode 100644
index 0000000000..43ed130401
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociationProviderBase.cs
@@ -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;
+
+///
+/// Base class for providers that determine the default browser via application associations.
+///
+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}");
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/FallbackMsEdgeBrowserProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/FallbackMsEdgeBrowserProvider.cs
new file mode 100644
index 0000000000..8489362004
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/FallbackMsEdgeBrowserProvider.cs
@@ -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;
+
+///
+/// Provides a fallback implementation of the default browser provider that returns information for Microsoft Edge.
+///
+/// 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.
+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,
+ };
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/IDefaultBrowserProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/IDefaultBrowserProvider.cs
new file mode 100644
index 0000000000..82a0b679fb
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/IDefaultBrowserProvider.cs
@@ -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;
+
+///
+/// Retrieves information about the default browser.
+///
+internal interface IDefaultBrowserProvider
+{
+ BrowserInfo GetDefaultBrowserInfo();
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/LegacyRegistryAssociationProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/LegacyRegistryAssociationProvider.cs
new file mode 100644
index 0000000000..28fe40f995
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/LegacyRegistryAssociationProvider.cs
@@ -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;
+
+///
+/// Provides the default web browser by reading registry keys. This is a legacy method and may not work on all systems.
+///
+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;
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/ShellAssociationProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/ShellAssociationProvider.cs
new file mode 100644
index 0000000000..a70c3476d4
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/ShellAssociationProvider.cs
@@ -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;
+
+///
+/// Retrieves the default web browser using the system shell functions.
+///
+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]);
+ }
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs
deleted file mode 100644
index f6b82ecfbb..0000000000
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs
+++ /dev/null
@@ -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;
-
-///
-/// Contains information (e.g. path to executable, name...) about the default browser.
-///
-public static class DefaultBrowserInfo
-{
- private static readonly Lock _updateLock = new();
-
- /// Gets the path to the MS Edge browser executable.
- public static string MSEdgePath => System.IO.Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
- @"Microsoft\Edge\Application\msedge.exe");
-
- /// Gets the command line pattern of the MS Edge.
- public const string MSEdgeArgumentsPattern = "--single-argument %1";
-
- public const string MSEdgeName = "Microsoft Edge";
-
- /// Gets the path to default browser's executable.
- public static string? Path { get; private set; }
-
- /// Gets since the icon is embedded in the executable.
- public static string? IconPath => Path;
-
- /// Gets the user-friendly name of the default browser.
- public static string? Name { get; private set; }
-
- /// Gets the command line pattern of the default browser.
- 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;
-
- ///
- /// Updates only if at least more than 300ms has passed since the last update, to avoid multiple calls to .
- /// (because of multiple plugins calling update at the same time.)
- ///
- public static void UpdateIfTimePassed()
- {
- var curTickCount = Environment.TickCount64;
- if (curTickCount - _lastUpdateTickCount >= UpdateTimeout)
- {
- _lastUpdateTickCount = curTickCount;
- Update();
- }
- }
-
- ///
- /// Consider using to avoid updating multiple times.
- /// (because of multiple plugins calling update at the same time.)
- ///
- 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.");
- }
- }
- }
-}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/NativeMethods.cs
new file mode 100644
index 0000000000..dee5b33fc5
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/NativeMethods.cs
@@ -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
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
index 641d5f6135..bf21f7c912 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
@@ -9,23 +9,24 @@ using System.Text;
using System.Threading;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
+using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
-using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{
private readonly ISettingsInterface _settingsManager;
+ private readonly IBrowserInfoService _browserInfoService;
private readonly Lock _sync = new();
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 IListItem[] _allItems = [];
private List _historyItems = [];
- public WebSearchListPage(ISettingsInterface settingsManager)
+ public WebSearchListPage(ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{
ArgumentNullException.ThrowIfNull(settingsManager);
@@ -35,6 +36,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager;
+ _browserInfoService = browserInfoService;
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
// 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())
{
Icon = Icon,
- Title = Properties.Resources.plugin_description,
- Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
+ Title = Resources.plugin_description,
+ Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
};
UpdateHistory();
@@ -67,7 +69,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
for (var index = items.Count - 1; index >= 0; 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,
Title = historyItem.SearchString,
@@ -82,7 +84,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
}
}
- private static IListItem[] Query(string query, List historySnapshot, ISettingsInterface settingsManager)
+ private static IListItem[] Query(string query, List historySnapshot, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{
ArgumentNullException.ThrowIfNull(query);
@@ -95,10 +97,10 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
if (!string.IsNullOrEmpty(query))
{
var searchTerm = query;
- var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
+ var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager, browserInfoService))
{
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,
};
results.Add(result);
@@ -117,7 +119,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
historySnapshot = _historyItems;
}
- var items = Query(search ?? string.Empty, historySnapshot, _settingsManager);
+ var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _browserInfoService);
lock (_sync)
{
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs
index 39ebd6bf2b..090b54375d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// 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.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -69,6 +69,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to default browser.
+ ///
+ public static string default_browser {
+ get {
+ return ResourceManager.GetString("default_browser", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Web Search.
///
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx
index 5a406eca60..032f89e7a5 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx
@@ -184,4 +184,7 @@
Open URL
+
+ default browser
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs
index 1a15991120..89cfe5a183 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs
@@ -5,6 +5,7 @@
using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
+using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -19,6 +20,7 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
private readonly ICommandItem[] _topLevelItems;
private readonly IFallbackCommandItem[] _fallbackCommands;
+ private readonly IBrowserInfoService _browserInfoService = new DefaultBrowserInfoService();
public WebSearchCommandsProvider()
{
@@ -27,10 +29,10 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
Icon = Icons.WebSearch;
Settings = _settingsManager.Settings;
- _fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
- _openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
+ _fallbackItem = new FallbackExecuteSearchItem(_settingsManager, _browserInfoService);
+ _openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager, _browserInfoService);
- _webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
+ _webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager, _browserInfoService)
{
MoreCommands =
[
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs
index bc161991ca..b2aaa95f5e 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs
@@ -5,6 +5,7 @@
using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
+using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Pages;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions;
@@ -15,13 +16,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch;
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
{
private readonly SettingsManager _settingsManager;
+ private readonly IBrowserInfoService _browserInfoService;
- public WebSearchTopLevelCommandItem(SettingsManager settingsManager)
- : base(new WebSearchListPage(settingsManager))
+ public WebSearchTopLevelCommandItem(SettingsManager settingsManager, IBrowserInfoService browserInfoService)
+ : base(new WebSearchListPage(settingsManager, browserInfoService))
{
Icon = Icons.WebSearch;
SetDefaultTitle();
_settingsManager = settingsManager;
+ _browserInfoService = browserInfoService;
}
private void SetDefaultTitle() => Title = Resources.command_item_title;
@@ -37,12 +40,12 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
if (string.IsNullOrEmpty(query))
{
SetDefaultTitle();
- ReplaceCommand(new WebSearchListPage(_settingsManager));
+ ReplaceCommand(new WebSearchListPage(_settingsManager, _browserInfoService));
}
else
{
Title = query;
- ReplaceCommand(new SearchWebCommand(query, _settingsManager));
+ ReplaceCommand(new SearchWebCommand(query, _settingsManager, _browserInfoService));
}
}