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