diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 81b9aba97e7..fe2b8a73553 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -108,3 +108,4 @@ Msix dummyprofile browserbookmark copyurl +PInvoke diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x64/Everything3.dll b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x64/Everything3.dll new file mode 100644 index 00000000000..be6a6f1409d Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x64/Everything3.dll differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x86/Everything3.dll b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x86/Everything3.dll new file mode 100644 index 00000000000..3d2659ada52 Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x86/Everything3.dll differ diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj index a837a49b49b..93fc6f954e8 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj @@ -32,6 +32,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index 1eae757083c..af9d61177ea 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml @@ -1,4 +1,4 @@ - @@ -44,6 +44,7 @@ Date and time format Sort Option: Everything Path: + Everything 1.5 instance name (requires app restart): Launch Hidden Editor Path Shell Path @@ -151,7 +152,11 @@ Failed to load Everything SDK + Please check whether your system is x86 or x64 Warning: Everything service is not running + Warning: Everything 1.5 is unavailable + Ensure Everything is running and verify the configured instance name + Warning: Everything 1.5 is unavailable. The service may not be running, or the instance name may be invalid Error while querying Everything Sort By Name ↑ @@ -186,6 +191,7 @@ Search Full Path Enable File/Folder Run Count + Use Everything 1.5 (requires app restart) Click to launch or install Everything Everything Installation @@ -214,3 +220,4 @@ 1 year ago {0} years ago + diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs index f5b8b932581..f23f897ee2f 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -46,8 +46,9 @@ public Task InitAsync(PluginInitContext context) searchManager = new SearchManager(Settings, Context); ResultManager.Init(Context, Settings); - EverythingApiDllImport.Load(Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "EverythingSDK", - Environment.Is64BitProcess ? "x64" : "x86")); + var sdkDirectory = Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "EverythingSDK", + Environment.Is64BitProcess ? "x64" : "x86"); + Settings.EverythingManagerInstance.InitializeApi(sdkDirectory); return Task.CompletedTask; } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Everything3ApiDllImport.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Everything3ApiDllImport.cs new file mode 100644 index 00000000000..5e24d7298a4 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Everything3ApiDllImport.cs @@ -0,0 +1,137 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + internal static class Everything3ApiDllImport + { + private static IntPtr _dllHandle = IntPtr.Zero; + internal static bool IsLoaded => _dllHandle != IntPtr.Zero; + + public static void Load(string directory) + { + if (_dllHandle != IntPtr.Zero) + { + return; + } + + var path = Path.Combine(directory, Dll); + _dllHandle = LoadLibrary(path); + if (_dllHandle == IntPtr.Zero) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr LoadLibrary(string name); + + private const string Dll = "Everything3.dll"; + + [DllImport(Dll, CharSet = CharSet.Unicode)] + internal static extern IntPtr Everything3_ConnectW(string instanceName); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_DestroyClient(IntPtr client); + + [DllImport(Dll)] + internal static extern IntPtr Everything3_CreateSearchState(); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_DestroySearchState(IntPtr searchState); + + [DllImport(Dll, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_SetSearchTextW(IntPtr searchState, string search); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_SetSearchRegex(IntPtr searchState, bool matchRegex); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_SetSearchMatchPath(IntPtr searchState, bool matchPath); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_SetSearchHideResultOmissions(IntPtr searchState, bool hideResultOmissions); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_SetSearchViewportOffset(IntPtr searchState, nuint offset); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_SetSearchViewportCount(IntPtr searchState, nuint count); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_ClearSearchSorts(IntPtr searchState); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_AddSearchSort(IntPtr searchState, uint propertyId, bool ascending); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_ClearSearchPropertyRequests(IntPtr searchState); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_AddSearchPropertyRequest(IntPtr searchState, uint propertyId); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_AddSearchPropertyRequestHighlighted(IntPtr searchState, uint propertyId); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_GetSearchPropertyRequestHighlight(IntPtr searchState, nuint index); + + [DllImport(Dll)] + internal static extern IntPtr Everything3_Search(IntPtr client, IntPtr searchState); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_DestroyResultList(IntPtr resultList); + + [DllImport(Dll)] + internal static extern nuint Everything3_GetResultListViewportCount(IntPtr resultList); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_IsFolderResult(IntPtr resultList, nuint resultIndex); + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_IsRootResult(IntPtr resultList, nuint resultIndex); + + [DllImport(Dll, CharSet = CharSet.Unicode)] + internal static extern nuint Everything3_GetResultFullPathNameW(IntPtr resultList, nuint resultIndex, StringBuilder buffer, nuint bufferSizeInWChars); + + [DllImport(Dll, CharSet = CharSet.Unicode)] + internal static extern nuint Everything3_GetResultPropertyTextHighlightedW(IntPtr resultList, nuint resultIndex, uint propertyId, StringBuilder buffer, nuint bufferSizeInWChars); + + [DllImport(Dll, CharSet = CharSet.Unicode)] + internal static extern nuint Everything3_GetResultPropertyTextW(IntPtr resultList, nuint resultIndex, uint propertyId, StringBuilder buffer, nuint bufferSizeInWChars); + + [DllImport(Dll)] + internal static extern uint Everything3_GetResultRunCount(IntPtr resultList, nuint resultIndex); + + [DllImport(Dll, CharSet = CharSet.Unicode)] + internal static extern uint Everything3_IncRunCountFromFilenameW(IntPtr client, string fileName); + + [DllImport(Dll)] + internal static extern uint Everything3_GetLastError(); + + + [DllImport(Dll)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Everything3_IsPropertyFastSort(IntPtr client, uint propertyId); + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index a786284699b..7d3030c058b 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -1,52 +1,36 @@ -using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions; -using System; +using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions; namespace Flow.Launcher.Plugin.Explorer.Search.Everything { - public static class EverythingApi + public class LegacyEverythingApi : IEverythingApi { private const int BufferSize = 4096; - private static readonly SemaphoreSlim _semaphore = new(1, 1); - // cached buffer to remove redundant allocations. private static readonly StringBuilder buffer = new(BufferSize); - public enum StateCode - { - OK, - MemoryError, - IPCError, - RegisterClassExError, - CreateWindowError, - CreateThreadError, - InvalidIndexError, - InvalidCallError - } - const uint EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME = 0x00000004u; const uint EVERYTHING_REQUEST_RUN_COUNT = 0x00000400u; /// /// Checks whether the sort option is Fast Sort. /// - public static bool IsFastSortOption(EverythingSortOption sortOption) + public bool IsFastSortOption(EverythingSortOption sortOption) { var fastSortOptionEnabled = EverythingApiDllImport.Everything_IsFastSort(sortOption); - - // If the Everything service is not running, then this call will incorrectly report + // If the Everything service is not running, then this call will incorrectly report // the state as false. This checks for errors thrown by the api and up to the caller to handle. CheckAndThrowExceptionOnError(); - return fastSortOptionEnabled; } - public static async ValueTask IsEverythingRunningAsync(CancellationToken token = default) + public async ValueTask IsEverythingRunningAsync(CancellationToken token = default) { try { @@ -60,8 +44,7 @@ public static async ValueTask IsEverythingRunningAsync(CancellationToken t try { _ = EverythingApiDllImport.Everything_GetMajorVersion(); - var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError; - return result; + return EverythingApiDllImport.Everything_GetLastError() != EverythingStateCode.IPCError; } finally { @@ -70,19 +53,15 @@ public static async ValueTask IsEverythingRunningAsync(CancellationToken t } /// - /// Searches the specified key word and reset the everything API afterwards + /// Searches using the specified criteria and resets the Everything API afterwards. /// - /// Search Criteria - /// when cancelled the current search will stop and exit (and would not reset) - /// An IAsyncEnumerable that will enumerate all results searched by the specific query and option - public static async IAsyncEnumerable SearchAsync(EverythingSearchOption option, - [EnumeratorCancellation] CancellationToken token) + /// The search criteria. + /// A cancellation token that stops the current search when cancellation is requested. + /// An asynchronous sequence of search results that match the specified criteria. + public async IAsyncEnumerable SearchAsync(EverythingSearchOption option, [EnumeratorCancellation] CancellationToken token = default) { - if (option.Offset < 0) - throw new ArgumentOutOfRangeException(nameof(option.Offset), option.Offset, "Offset must be greater than or equal to 0"); - - if (option.MaxCount < 0) - throw new ArgumentOutOfRangeException(nameof(option.MaxCount), option.MaxCount, "MaxCount must be greater than or equal to 0"); + var query = EverythingHelper.PrepareQuery(option); + var preparedOption = query.Option; try { @@ -98,33 +77,15 @@ public static async IAsyncEnumerable SearchAsync(EverythingSearchO if (token.IsCancellationRequested) yield break; - if (option.Keyword.StartsWith("@")) - { - EverythingApiDllImport.Everything_SetRegex(true); - option.Keyword = option.Keyword[1..]; - } + EverythingApiDllImport.Everything_SetRegex(preparedOption.UseRegex); + EverythingApiDllImport.Everything_SetSearchW(query.SearchText); + EverythingApiDllImport.Everything_SetOffset(preparedOption.Offset); + EverythingApiDllImport.Everything_SetMax(preparedOption.MaxCount); - var builder = new StringBuilder(); - builder.Append(option.Keyword); + EverythingApiDllImport.Everything_SetSort(preparedOption.SortOption); + EverythingApiDllImport.Everything_SetMatchPath(preparedOption.IsFullPathSearch); - if (!string.IsNullOrWhiteSpace(option.ParentPath)) - { - builder.Append($" {(option.IsRecursive ? "" : "parent:")}\"{option.ParentPath}\""); - } - - if (option.IsContentSearch) - { - builder.Append($" content:\"{option.ContentSearchKeyword}\""); - } - - EverythingApiDllImport.Everything_SetSearchW(builder.ToString()); - EverythingApiDllImport.Everything_SetOffset(option.Offset); - EverythingApiDllImport.Everything_SetMax(option.MaxCount); - - EverythingApiDllImport.Everything_SetSort(option.SortOption); - EverythingApiDllImport.Everything_SetMatchPath(option.IsFullPathSearch); - - if (option.SortOption == EverythingSortOption.RUN_COUNT_DESCENDING) + if (preparedOption.SortOption == EverythingSortOption.RUN_COUNT_DESCENDING) { EverythingApiDllImport.Everything_SetRequestFlags(EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME | EVERYTHING_REQUEST_RUN_COUNT); } @@ -152,13 +113,12 @@ public static async IAsyncEnumerable SearchAsync(EverythingSearchO var result = new SearchResult { - // todo the types are wrong. Everything expects uint everywhere, but we send int just above/below. how to fix? Is EverythingApiDllImport autogenerated or handmade? FullPath = buffer.ToString(), Type = EverythingApiDllImport.Everything_IsFolderResult(idx) ? ResultType.Folder : EverythingApiDllImport.Everything_IsFileResult(idx) ? ResultType.File : ResultType.Volume, Score = Convert.ToInt32(EverythingApiDllImport.Everything_GetResultRunCount((uint)idx)), - HighlightData = EverythingHighlightStringToHighlightList(EverythingApiDllImport.Everything_GetResultHighlightedFileName((uint)idx)) + HighlightData = EverythingHelper.EverythingHighlightStringToHighlightList(EverythingApiDllImport.Everything_GetResultHighlightedFileName((uint)idx)) }; yield return result; @@ -175,30 +135,35 @@ private static void CheckAndThrowExceptionOnError() { switch (EverythingApiDllImport.Everything_GetLastError()) { - case StateCode.CreateThreadError: + case EverythingStateCode.CreateThreadError: throw new CreateThreadException(); - case StateCode.CreateWindowError: + case EverythingStateCode.CreateWindowError: throw new CreateWindowException(); - case StateCode.InvalidCallError: + case EverythingStateCode.InvalidCallError: throw new InvalidCallException(); - case StateCode.InvalidIndexError: + case EverythingStateCode.InvalidIndexError: throw new InvalidIndexException(); - case StateCode.IPCError: + case EverythingStateCode.IPCError: throw new IPCErrorException(); - case StateCode.MemoryError: + case EverythingStateCode.MemoryError: throw new MemoryErrorException(); - case StateCode.RegisterClassExError: + case EverythingStateCode.RegisterClassExError: throw new RegisterClassExException(); - case StateCode.OK: + case EverythingStateCode.OK: break; default: throw new ArgumentOutOfRangeException(); } } - public static async Task IncrementRunCounterAsync(string fileOrFolder) + public async Task IncrementRunCounterAsync(string fileOrFolder) { - await _semaphore.WaitAsync(TimeSpan.FromSeconds(1)); + var _entered = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1)); + if (!_entered) + { + // If we can't acquire the semaphore within the timeout, we skip incrementing the run count to avoid blocking. + return; + } try { _ = EverythingApiDllImport.Everything_IncRunCountFromFileName(fileOrFolder); @@ -207,57 +172,10 @@ public static async Task IncrementRunCounterAsync(string fileOrFolder) { /*ignored*/ } - finally { _semaphore.Release(); } - } - - /// - /// Convert the highlighted string from Everything API to a list of highlight indexes for our Result. - /// - /// Text inside a * quote is highlighted, two consecutive *'s is a single literal *. For example, in the highlighted text: abc*123* the 123 part is highlighted. - /// A list of zero-based character indices that should be highlighted. - public static List EverythingHighlightStringToHighlightList(string highlightString) - { - var highlightData = new List(); - - if (string.IsNullOrEmpty(highlightString)) - return highlightData; - - var isHighlighted = false; - var actualIndex = 0; // Index in the actual string (without * markers) - var length = highlightString.Length; - - for (var i = 0; i < length; i++) - { - if (highlightString[i] == '*') - { - // Check if it's a literal * (two consecutive *) - if (i + 1 < length && highlightString[i + 1] == '*') - { - // Two consecutive *'s represent a single literal * - if (isHighlighted) - { - highlightData.Add(actualIndex); - } - actualIndex++; - i++; // Skip the next * - } - else - { - isHighlighted = !isHighlighted; - } - } - else - { - // Regular character - if (isHighlighted) - { - highlightData.Add(actualIndex); - } - actualIndex++; - } + finally + { + _semaphore.Release(); } - - return highlightData; } } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiDllImport.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiDllImport.cs index 344c1a42c16..9f2a5b739bc 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiDllImport.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiDllImport.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; using System.Text; @@ -7,19 +8,26 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything { public static class EverythingApiDllImport { + private static IntPtr _dllHandle = IntPtr.Zero; + public static bool IsLoaded => _dllHandle != IntPtr.Zero; + public static void Load(string directory) { + if (_dllHandle != IntPtr.Zero) + { + return; + } + var path = Path.Combine(directory, DLL); - int code = LoadLibrary(path); - if (code == 0) + _dllHandle = LoadLibrary(path); + if (_dllHandle == IntPtr.Zero) { - int err = Marshal.GetLastPInvokeError(); - Marshal.ThrowExceptionForHR(err); + throw new Win32Exception(Marshal.GetLastPInvokeError()); } } [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern int LoadLibrary(string name); + private static extern IntPtr LoadLibrary(string name); private const string DLL = "Everything.dll"; @@ -66,7 +74,7 @@ public static void Load(string directory) internal static extern string Everything_GetSearchW(); [DllImport(DLL)] - internal static extern EverythingApi.StateCode Everything_GetLastError(); + internal static extern EverythingStateCode Everything_GetLastError(); [DllImport(DLL, CharSet = CharSet.Unicode)] internal static extern bool Everything_QueryW(bool bWait); diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiFactory.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiFactory.cs new file mode 100644 index 00000000000..609222dd097 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiFactory.cs @@ -0,0 +1,22 @@ +using System; + +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + internal static class EverythingApiFactory + { + internal const string DefaultEverything15InstanceName = "1.5a"; + + public static IEverythingApi Create(Settings settings) + { + ArgumentNullException.ThrowIfNull(settings); + + return settings.EnableEverything15Support + ? new EverythingApiV3(GetNormalizedInstanceName(settings.Everything15InstanceName)) + : new LegacyEverythingApi(); + } + + internal static string GetNormalizedInstanceName(string instanceName) => string.IsNullOrWhiteSpace(instanceName) + ? DefaultEverything15InstanceName + : instanceName.Trim(); + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiV3.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiV3.cs new file mode 100644 index 00000000000..b7c1e932e71 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiV3.cs @@ -0,0 +1,457 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions; + +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + public class EverythingApiV3 : IEverythingApi + { + private const int BufferSize = 4096; + private static readonly SemaphoreSlim _semaphore = new(1, 1); + private static readonly StringBuilder _buffer = new(BufferSize); + + private readonly string _instanceName; + + public EverythingApiV3(string instanceName) + { + _instanceName = instanceName; + } + + const uint EVERYTHING3_PROPERTY_ID_NAME = 0; + const uint EVERYTHING3_PROPERTY_ID_PATH = 1; + const uint EVERYTHING3_PROPERTY_ID_SIZE = 2; + const uint EVERYTHING3_PROPERTY_ID_EXTENSION = 3; + const uint EVERYTHING3_PROPERTY_ID_TYPE = 4; + const uint EVERYTHING3_PROPERTY_ID_DATE_MODIFIED = 5; + const uint EVERYTHING3_PROPERTY_ID_DATE_CREATED = 6; + const uint EVERYTHING3_PROPERTY_ID_DATE_ACCESSED = 7; + const uint EVERYTHING3_PROPERTY_ID_ATTRIBUTES = 8; + const uint EVERYTHING3_PROPERTY_ID_DATE_RECENTLY_CHANGED = 9; + const uint EVERYTHING3_PROPERTY_ID_RUN_COUNT = 10; + const uint EVERYTHING3_PROPERTY_ID_DATE_RUN = 11; + const uint EVERYTHING3_PROPERTY_ID_FILE_LIST_NAME = 12; + const uint EVERYTHING3_PROPERTY_ID_PATH_AND_NAME = 240; + + const uint EVERYTHING3_ERROR_OUT_OF_MEMORY = 0xE0000001; + const uint EVERYTHING3_ERROR_IPC_PIPE_NOT_FOUND = 0xE0000002; + const uint EVERYTHING3_ERROR_DISCONNECTED = 0xE0000003; + const uint EVERYTHING3_ERROR_INVALID_PARAMETER = 0xE0000004; + const uint EVERYTHING3_ERROR_PROPERTY_NOT_FOUND = 0xE0000007; + + public async ValueTask IsEverythingRunningAsync(CancellationToken token = default) + { + try + { + await _semaphore.WaitAsync(token); + } + catch (OperationCanceledException) + { + return false; + } + + try + { + if (!TryConnectEverything3(out var client)) + return false; + + _ = Everything3ApiDllImport.Everything3_DestroyClient(client); + return true; + } + finally + { + _semaphore.Release(); + } + } + + public async IAsyncEnumerable SearchAsync(EverythingSearchOption option, [EnumeratorCancellation] CancellationToken token = default) + { + var query = EverythingHelper.PrepareQuery(option); + var preparedOption = query.Option; + + try + { + await _semaphore.WaitAsync(token); + } + catch (OperationCanceledException) + { + yield break; + } + + try + { + if (token.IsCancellationRequested) + yield break; + + if (!TryConnectEverything3(out var client)) + throw new IPCErrorException(); + + await foreach (var result in SearchWithEverything3Async(client, preparedOption, query, token)) + yield return result; + } + finally + { + _semaphore.Release(); + } + } + + public async Task IncrementRunCounterAsync(string fileOrFolder) + { + var _entered = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1)); + if (!_entered) + { + // If we can't acquire the semaphore within the timeout, we skip incrementing the run count to avoid blocking. + return; + } + try + { + if (TryConnectEverything3(out var client)) + { + try + { + Everything3ApiDllImport.Everything3_IncRunCountFromFilenameW(client, fileOrFolder); + } + finally + { + _ = Everything3ApiDllImport.Everything3_DestroyClient(client); + } + } + } + catch (Exception) + { + /*ignored*/ + } + finally + { + _semaphore.Release(); + } + } + + public bool IsFastSortOption(EverythingSortOption sortOption) + { + if (!TryConnectEverything3(out var client)) + throw new IPCErrorException(); + + try + { + if (TryConvertSortOption(sortOption, out var propertyId, out _)) + { + var isFastSort = Everything3ApiDllImport.Everything3_IsPropertyFastSort(client, propertyId); + CheckAndThrowExceptionOnErrorFromEverything3(); + return isFastSort; + } + } + finally + { + _ = Everything3ApiDllImport.Everything3_DestroyClient(client); + CheckAndThrowExceptionOnErrorFromEverything3(); + } + + return true; + } + + private static async IAsyncEnumerable SearchWithEverything3Async(IntPtr client, + EverythingSearchOption option, + EverythingHelper.PreparedQuery query, + [EnumeratorCancellation] CancellationToken token) + { + IntPtr searchState = IntPtr.Zero; + IntPtr resultList = IntPtr.Zero; + try + { + searchState = Everything3ApiDllImport.Everything3_CreateSearchState(); + if (searchState == IntPtr.Zero) + { + CheckAndThrowExceptionOnErrorFromEverything3(); + yield break; + } + + _ = Everything3ApiDllImport.Everything3_SetSearchRegex(searchState, option.UseRegex); + _ = Everything3ApiDllImport.Everything3_SetSearchMatchPath(searchState, option.IsFullPathSearch); + _ = Everything3ApiDllImport.Everything3_SetSearchTextW(searchState, query.SearchText); + _ = Everything3ApiDllImport.Everything3_SetSearchHideResultOmissions(searchState, true); + _ = Everything3ApiDllImport.Everything3_SetSearchViewportOffset(searchState, (nuint)option.Offset); + _ = Everything3ApiDllImport.Everything3_SetSearchViewportCount(searchState, (nuint)option.MaxCount); + + if (TryConvertSortOption(option.SortOption, out var sortPropertyId, out var ascending)) + { + if (!Everything3ApiDllImport.Everything3_AddSearchSort(searchState, sortPropertyId, ascending)) + { + CheckAndThrowExceptionOnErrorFromEverything3(); + yield break; + } + } + + _ = Everything3ApiDllImport.Everything3_ClearSearchPropertyRequests(searchState); + _ = Everything3ApiDllImport.Everything3_AddSearchPropertyRequestHighlighted(searchState, EVERYTHING3_PROPERTY_ID_NAME); + _ = Everything3ApiDllImport.Everything3_AddSearchPropertyRequestHighlighted(searchState, EVERYTHING3_PROPERTY_ID_PATH); + _ = Everything3ApiDllImport.Everything3_AddSearchPropertyRequest(searchState, EVERYTHING3_PROPERTY_ID_PATH_AND_NAME); + _ = Everything3ApiDllImport.Everything3_AddSearchPropertyRequest(searchState, EVERYTHING3_PROPERTY_ID_RUN_COUNT); + + if (token.IsCancellationRequested) + yield break; + + resultList = Everything3ApiDllImport.Everything3_Search(client, searchState); + if (resultList == IntPtr.Zero) + { + CheckAndThrowExceptionOnErrorFromEverything3(); + yield break; + } + + var resultCount = Everything3ApiDllImport.Everything3_GetResultListViewportCount(resultList); + for (nuint idx = 0; idx < resultCount; ++idx) + { + if (token.IsCancellationRequested) + { + yield break; + } + + if (!TryCreateSearchResult(resultList, idx, out var result)) + continue; + + yield return result; + } + } + finally + { + if (resultList != IntPtr.Zero) + _ = Everything3ApiDllImport.Everything3_DestroyResultList(resultList); + + if (searchState != IntPtr.Zero) + _ = Everything3ApiDllImport.Everything3_DestroySearchState(searchState); + + _ = Everything3ApiDllImport.Everything3_DestroyClient(client); + } + + await Task.CompletedTask; + } + + private static bool TryCreateSearchResult(IntPtr resultList, nuint resultIndex, out SearchResult result) + { + result = default; + + if (!TryGetResultFullPath(resultList, resultIndex, out var fullPath)) + return false; + + result = new SearchResult + { + FullPath = fullPath, + Type = GetResultType(resultList, resultIndex), + Score = Convert.ToInt32(Everything3ApiDllImport.Everything3_GetResultRunCount(resultList, resultIndex)), + HighlightData = GetHighlightData(resultList, resultIndex) + }; + + return true; + } + + private static bool TryGetResultFullPath(IntPtr resultList, nuint resultIndex, out string fullPath) + { + _buffer.Clear(); + var fullPathLength = Everything3ApiDllImport.Everything3_GetResultFullPathNameW(resultList, resultIndex, _buffer, BufferSize); + if (fullPathLength == 0) + { + CheckAndThrowExceptionOnErrorFromEverything3(); + fullPath = string.Empty; + return false; + } + + fullPath = _buffer.ToString(); + return !string.IsNullOrEmpty(fullPath); + } + + private static ResultType GetResultType(IntPtr resultList, nuint resultIndex) + { + return Everything3ApiDllImport.Everything3_IsFolderResult(resultList, resultIndex) + ? ResultType.Folder + : Everything3ApiDllImport.Everything3_IsRootResult(resultList, resultIndex) + ? ResultType.Volume + : ResultType.File; + } + + private static List GetHighlightData(IntPtr resultList, nuint resultIndex) + { + _buffer.Clear(); + var highlightedFileNameLength = Everything3ApiDllImport.Everything3_GetResultPropertyTextHighlightedW( + resultList, + resultIndex, + EVERYTHING3_PROPERTY_ID_NAME, + _buffer, + BufferSize); + + return highlightedFileNameLength > 0 + ? EverythingHelper.EverythingHighlightStringToHighlightList(_buffer.ToString()) + : []; + } + + private bool TryUseEverything3Client(Action action) + { + if (!TryConnectEverything3(out var client)) + return false; + + try + { + action(client); + return true; + } + finally + { + _ = Everything3ApiDllImport.Everything3_DestroyClient(client); + } + } + + private bool TryConnectEverything3(out IntPtr client) + { + client = IntPtr.Zero; + var normalizedInstanceName = string.IsNullOrWhiteSpace(_instanceName) + ? EverythingApiFactory.DefaultEverything15InstanceName + : _instanceName.Trim(); + + try + { + client = Everything3ApiDllImport.Everything3_ConnectW(normalizedInstanceName); + return client != IntPtr.Zero; + } + catch (DllNotFoundException) + { + return false; + } + catch (EntryPointNotFoundException) + { + return false; + } + } + + /// + /// Covert the old Everything 1.4 sort options in our UI to Everything 3 property ID and sort direction for compatibility. + /// + private static bool TryConvertSortOption(EverythingSortOption sortOption, out uint propertyId, out bool ascending) + { + switch (sortOption) + { + case EverythingSortOption.NAME_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_NAME; + ascending = true; + return true; + case EverythingSortOption.NAME_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_NAME; + ascending = false; + return true; + case EverythingSortOption.PATH_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_PATH; + ascending = true; + return true; + case EverythingSortOption.PATH_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_PATH; + ascending = false; + return true; + case EverythingSortOption.SIZE_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_SIZE; + ascending = true; + return true; + case EverythingSortOption.SIZE_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_SIZE; + ascending = false; + return true; + case EverythingSortOption.EXTENSION_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_EXTENSION; + ascending = true; + return true; + case EverythingSortOption.EXTENSION_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_EXTENSION; + ascending = false; + return true; + case EverythingSortOption.TYPE_NAME_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_TYPE; + ascending = true; + return true; + case EverythingSortOption.TYPE_NAME_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_TYPE; + ascending = false; + return true; + case EverythingSortOption.DATE_CREATED_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_CREATED; + ascending = true; + return true; + case EverythingSortOption.DATE_CREATED_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_CREATED; + ascending = false; + return true; + case EverythingSortOption.DATE_MODIFIED_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_MODIFIED; + ascending = true; + return true; + case EverythingSortOption.DATE_MODIFIED_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_MODIFIED; + ascending = false; + return true; + case EverythingSortOption.ATTRIBUTES_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_ATTRIBUTES; + ascending = true; + return true; + case EverythingSortOption.ATTRIBUTES_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_ATTRIBUTES; + ascending = false; + return true; + case EverythingSortOption.FILE_LIST_FILENAME_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_FILE_LIST_NAME; + ascending = true; + return true; + case EverythingSortOption.FILE_LIST_FILENAME_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_FILE_LIST_NAME; + ascending = false; + return true; + case EverythingSortOption.RUN_COUNT_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_RUN_COUNT; + ascending = false; + return true; + case EverythingSortOption.DATE_RECENTLY_CHANGED_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_RECENTLY_CHANGED; + ascending = true; + return true; + case EverythingSortOption.DATE_RECENTLY_CHANGED_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_RECENTLY_CHANGED; + ascending = false; + return true; + case EverythingSortOption.DATE_ACCESSED_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_ACCESSED; + ascending = true; + return true; + case EverythingSortOption.DATE_ACCESSED_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_ACCESSED; + ascending = false; + return true; + case EverythingSortOption.DATE_RUN_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_RUN; + ascending = true; + return true; + case EverythingSortOption.DATE_RUN_DESCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_DATE_RUN; + ascending = false; + return true; + default: + propertyId = EVERYTHING3_PROPERTY_ID_NAME; + ascending = true; + return false; + } + } + + private static void CheckAndThrowExceptionOnErrorFromEverything3() + { + switch (Everything3ApiDllImport.Everything3_GetLastError()) + { + case EVERYTHING3_ERROR_OUT_OF_MEMORY: + throw new MemoryErrorException(); + case EVERYTHING3_ERROR_IPC_PIPE_NOT_FOUND: + case EVERYTHING3_ERROR_DISCONNECTED: + throw new IPCErrorException(); + case EVERYTHING3_ERROR_INVALID_PARAMETER: + throw new InvalidCallException(); + case EVERYTHING3_ERROR_PROPERTY_NOT_FOUND: + throw new ArgumentException("EVERYTHING3_ERROR_PROPERTY_NOT_FOUND"); + } + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAvailabilityService.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAvailabilityService.cs new file mode 100644 index 00000000000..dbde7ff3093 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAvailabilityService.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Plugin.Explorer.Exceptions; + +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + internal class EverythingAvailabilityService + { + private readonly Settings _settings; + + public EverythingAvailabilityService(Settings settings) + { + _settings = settings; + } + + public async ValueTask EnsureAvailableAsync(IEverythingApi api, CancellationToken token = default) + { + try + { + if (!await api.IsEverythingRunningAsync(token)) + { + if (_settings.EnableEverything15Support) + { + throw new EngineNotAvailableException( + Enum.GetName(Settings.IndexSearchEngineOption.Everything)!, + Localize.flowlauncher_plugin_everything_15_resolution(), + Localize.flowlauncher_plugin_everything_15_unavailable(), + Constants.EverythingErrorImagePath, + ClickToInstallEverythingAsync); + } + else + { + throw new EngineNotAvailableException( + Enum.GetName(Settings.IndexSearchEngineOption.Everything)!, + Localize.flowlauncher_plugin_everything_click_to_launch_or_install(), + Localize.flowlauncher_plugin_everything_15_unavailable(), + Constants.EverythingErrorImagePath, + ClickToInstallEverythingAsync); + } + } + } + catch (Exception ex) when (ex is DllNotFoundException || ex is EntryPointNotFoundException) + { + throw new EngineNotAvailableException( + Enum.GetName(Settings.IndexSearchEngineOption.Everything)!, + Localize.flowlauncher_plugin_everything_architecture_check(), + Constants.GeneralSearchErrorImagePath, + Localize.flowlauncher_plugin_everything_sdk_issue()); + } + } + + private async ValueTask ClickToInstallEverythingAsync(ActionContext _) + { + try + { + var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(_settings.EverythingInstalledPath, Main.Context.API); + + if (installedPath == null) + { + Main.Context.API.ShowMsgError(Localize.flowlauncher_plugin_everything_not_found()); + Main.Context.API.LogError(nameof(EverythingAvailabilityService), "Unable to find Everything.exe"); + + return false; + } + + _settings.EverythingInstalledPath = installedPath; + Process.Start(installedPath, "-startup"); + + return true; + } + catch (Exception e) + { + Main.Context.API.ShowMsgError(Localize.flowlauncher_plugin_everything_install_issue()); + Main.Context.API.LogException(nameof(EverythingAvailabilityService), "Failed to install Everything", e); + + return false; + } + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingHelper.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingHelper.cs new file mode 100644 index 00000000000..0be29b4ab57 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingHelper.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + public enum EverythingStateCode + { + OK, + MemoryError, + IPCError, + RegisterClassExError, + CreateWindowError, + CreateThreadError, + InvalidIndexError, + InvalidCallError + } + + public static class EverythingHelper + { + #region Query + + public readonly record struct PreparedQuery( + EverythingSearchOption Option, + string SearchText); + + public static PreparedQuery PrepareQuery(EverythingSearchOption option) + { + if (option.Offset < 0) + throw new ArgumentOutOfRangeException(nameof(option.Offset), option.Offset, "Offset must be greater than or equal to 0"); + if (option.MaxCount < 0) + throw new ArgumentOutOfRangeException(nameof(option.MaxCount), option.MaxCount, "MaxCount must be greater than or equal to 0"); + + var keyword = option.Keyword; + if (!string.IsNullOrEmpty(keyword) && keyword.StartsWith("@", StringComparison.Ordinal)) + { + option.UseRegex = true; + keyword = keyword[1..]; + } + + var builder = new StringBuilder(); + builder.Append(keyword); + + if (!string.IsNullOrWhiteSpace(option.ParentPath)) + { + builder.Append($" {(option.IsRecursive ? "" : "parent:")}\"{option.ParentPath}\""); + } + + if (option.IsContentSearch) + { + builder.Append($" content:\"{option.ContentSearchKeyword}\""); + } + + return new PreparedQuery(option with { Keyword = keyword }, builder.ToString()); + } + + #endregion + + #region Result + + /// + /// Convert the highlighted string from Everything API to a list of highlight indexes for our Result. + /// + /// Text inside a * quote is highlighted, two consecutive *'s is a single literal *. For example, in the highlighted text: abc*123* the 123 part is highlighted. + /// A list of zero-based character indices that should be highlighted. + public static List EverythingHighlightStringToHighlightList(string highlightString) + { + var highlightData = new List(); + + if (string.IsNullOrEmpty(highlightString)) + return highlightData; + + var isHighlighted = false; + var actualIndex = 0; // Index in the actual string (without * markers) + var length = highlightString.Length; + + for (var i = 0; i < length; i++) + { + if (highlightString[i] == '*') + { + // Check if it's a literal * (two consecutive *) + if (i + 1 < length && highlightString[i + 1] == '*') + { + // Two consecutive *'s represent a single literal * + if (isHighlighted) + { + highlightData.Add(actualIndex); + } + actualIndex++; + i++; // Skip the next * + } + else + { + isHighlighted = !isHighlighted; + } + } + else + { + // Regular character + if (isHighlighted) + { + highlightData.Add(actualIndex); + } + actualIndex++; + } + } + + return highlightData; + } + + #endregion + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSdkLoader.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSdkLoader.cs new file mode 100644 index 00000000000..e36b808c07e --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSdkLoader.cs @@ -0,0 +1,31 @@ +using System.Threading; + +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + internal static class EverythingSdkLoader + { + private static readonly SemaphoreSlim _semaphore = new(1, 1); + + public static void EnsureLoaded(string sdkDirectory, bool enableEverything15Support) + { + _semaphore.Wait(); + try + { + if (enableEverything15Support) + { + if (!Everything3ApiDllImport.IsLoaded) + Everything3ApiDllImport.Load(sdkDirectory); + } + else + { + if (!EverythingApiDllImport.IsLoaded) + EverythingApiDllImport.Load(sdkDirectory); + } + } + finally + { + _semaphore.Release(); + } + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs index eb994a6f98f..787b5e3bb40 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -11,70 +10,22 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything { public class EverythingSearchManager : IIndexProvider, IContentIndexProvider, IPathIndexProvider { - private static readonly string ClassName = nameof(EverythingSearchManager); - private Settings Settings { get; } + private readonly EverythingAvailabilityService _availabilityService; + private readonly Lock _syncRoot = new(); + private bool isApiInitialized; + private IEverythingApi api; public EverythingSearchManager(Settings settings) { Settings = settings; - } - - private async ValueTask ThrowIfEverythingNotAvailableAsync(CancellationToken token = default) - { - try - { - if (!await EverythingApi.IsEverythingRunningAsync(token)) - throw new EngineNotAvailableException( - Enum.GetName(Settings.IndexSearchEngineOption.Everything)!, - Localize.flowlauncher_plugin_everything_click_to_launch_or_install(), - Localize.flowlauncher_plugin_everything_is_not_running(), - Constants.EverythingErrorImagePath, - ClickToInstallEverythingAsync); - } - catch (DllNotFoundException) - { - throw new EngineNotAvailableException( - Enum.GetName(Settings.IndexSearchEngineOption.Everything)!, - "Please check whether your system is x86 or x64", - Constants.GeneralSearchErrorImagePath, - Localize.flowlauncher_plugin_everything_sdk_issue()); - } - } - - private async ValueTask ClickToInstallEverythingAsync(ActionContext _) - { - try - { - var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(Settings.EverythingInstalledPath, Main.Context.API); - - if (installedPath == null) - { - Main.Context.API.ShowMsgError(Localize.flowlauncher_plugin_everything_not_found()); - Main.Context.API.LogError(ClassName, "Unable to find Everything.exe"); - - return false; - } - - Settings.EverythingInstalledPath = installedPath; - Process.Start(installedPath, "-startup"); - - return true; - } - // Sometimes Everything installation will fail because of permission issues or file not found issues - // Just let the user know that Everything is not installed properly and ask them to install it manually - catch (Exception e) - { - Main.Context.API.ShowMsgError(Localize.flowlauncher_plugin_everything_install_issue()); - Main.Context.API.LogException(ClassName, "Failed to install Everything", e); - - return false; - } + _availabilityService = new EverythingAvailabilityService(settings); + api = EverythingApiFactory.Create(settings); } public async IAsyncEnumerable SearchAsync(string search, [EnumeratorCancellation] CancellationToken token) { - await ThrowIfEverythingNotAvailableAsync(token); + await _availabilityService.EnsureAvailableAsync(api, token); if (token.IsCancellationRequested) yield break; @@ -85,14 +36,14 @@ public async IAsyncEnumerable SearchAsync(string search, [Enumerat IsFullPathSearch: Settings.EverythingSearchFullPath, IsRunCounterEnabled: Settings.EverythingEnableRunCount); - await foreach (var result in EverythingApi.SearchAsync(option, token)) + await foreach (var result in api.SearchAsync(option, token)) yield return result; } public async IAsyncEnumerable ContentSearchAsync(string plainSearch, string contentSearch, [EnumeratorCancellation] CancellationToken token) { - await ThrowIfEverythingNotAvailableAsync(token); + await _availabilityService.EnsureAvailableAsync(api, token); if (!Settings.EnableEverythingContentSearch) { @@ -119,7 +70,7 @@ public async IAsyncEnumerable ContentSearchAsync(string plainSearc IsFullPathSearch: Settings.EverythingSearchFullPath, IsRunCounterEnabled: Settings.EverythingEnableRunCount); - await foreach (var result in EverythingApi.SearchAsync(option, token)) + await foreach (var result in api.SearchAsync(option, token)) { yield return result; } @@ -127,7 +78,7 @@ public async IAsyncEnumerable ContentSearchAsync(string plainSearc public async IAsyncEnumerable EnumerateAsync(string path, string search, bool recursive, [EnumeratorCancellation] CancellationToken token) { - await ThrowIfEverythingNotAvailableAsync(token); + await _availabilityService.EnsureAvailableAsync(api, token); if (token.IsCancellationRequested) yield break; @@ -140,8 +91,25 @@ public async IAsyncEnumerable EnumerateAsync(string path, string s IsFullPathSearch: Settings.EverythingSearchFullPath, IsRunCounterEnabled: Settings.EverythingEnableRunCount); - await foreach (var result in EverythingApi.SearchAsync(option, token)) + await foreach (var result in api.SearchAsync(option, token)) yield return result; } + + public void InitializeApi(string sdkDirectory) + { + lock (_syncRoot) + { + if (isApiInitialized) + return; + + EverythingSdkLoader.EnsureLoaded(sdkDirectory, Settings.EnableEverything15Support); + api = EverythingApiFactory.Create(Settings); + isApiInitialized = true; + } + } + + public bool IsFastSortOption(EverythingSortOption sortOption) => api.IsFastSortOption(sortOption); + + public Task IncrementRunCounterAsync(string fileOrFolder) => api.IncrementRunCounterAsync(fileOrFolder); } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs index d8b670a0895..1d6446d396a 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs @@ -10,6 +10,7 @@ public record struct EverythingSearchOption( int Offset = 0, int MaxCount = 100, bool IsFullPathSearch = true, - bool IsRunCounterEnabled = true + bool IsRunCounterEnabled = true, + bool UseRegex = false ); } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/IEverythingApi.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/IEverythingApi.cs new file mode 100644 index 00000000000..3c7ed7e896a --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/IEverythingApi.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + public interface IEverythingApi + { + ValueTask IsEverythingRunningAsync(CancellationToken token = default); + IAsyncEnumerable SearchAsync(EverythingSearchOption option, CancellationToken token = default); + Task IncrementRunCounterAsync(string fileOrFolder); + bool IsFastSortOption(EverythingSortOption sortOption); + } +} diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index b73a59bcdc1..7845498a963 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -413,7 +413,7 @@ private static void OpenFolder(string folderPath, string fileNameOrFilePath = nu private static void IncrementEverythingRunCounterIfNeeded(string fileOrFolder) { if (Settings.EverythingEnabled && Settings.EverythingEnableRunCount) - _ = Task.Run(() => EverythingApi.IncrementRunCounterAsync(fileOrFolder)); + _ = Task.Run(() => Settings.EverythingManagerInstance.IncrementRunCounterAsync(fileOrFolder)); } private static string GetFileMoreInfoTooltip(string filePath) diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 28ab1d2e24b..46216f931c6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -291,6 +291,9 @@ private bool UseWindowsIndexForDirectorySearch(string locationPath) private bool IsExcludedFile(SearchResult result) { + if (string.IsNullOrEmpty(result.FullPath)) + return false; + string[] excludedFileTypes = Settings.ExcludedFileTypes.Split([','], StringSplitOptions.RemoveEmptyEntries); string fileExtension = Path.GetExtension(result.FullPath).TrimStart('.'); diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs index b0fbad84fbe..e746bc453e6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs @@ -89,7 +89,7 @@ public class Settings #region SearchEngine - private EverythingSearchManager EverythingManagerInstance => _everythingManagerInstance ??= new EverythingSearchManager(this); + internal EverythingSearchManager EverythingManagerInstance => _everythingManagerInstance ??= new EverythingSearchManager(this); private WindowsIndexSearchManager WindowsIndexSearchManager => _windowsIndexSearchManager ??= new WindowsIndexSearchManager(this); public IndexSearchEngineOption IndexSearchEngine { get; set; } = IndexSearchEngineOption.WindowsIndex; @@ -163,6 +163,8 @@ public enum ContentIndexSearchEngineOption public bool EverythingSearchFullPath { get; set; } = false; public bool EverythingEnableRunCount { get; set; } = true; + public bool EnableEverything15Support { get; set; } = false; + public string Everything15InstanceName { get; set; } = EverythingApiFactory.DefaultEverything15InstanceName; #endregion diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index a9b2e6a89e3..22093a9abf1 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -619,7 +619,7 @@ public Visibility FastSortWarningVisibility { try { - return EverythingApi.IsFastSortOption(Settings.SortOption) ? Visibility.Collapsed : Visibility.Visible; + return Settings.EverythingManagerInstance.IsFastSortOption(Settings.SortOption) ? Visibility.Collapsed : Visibility.Visible; } catch (IPCErrorException) { @@ -627,7 +627,7 @@ public Visibility FastSortWarningVisibility // update the message to let user know in the settings panel. return Visibility.Visible; } - catch (DllNotFoundException) + catch (Exception ex) when (ex is DllNotFoundException || ex is EntryPointNotFoundException) { return Visibility.Collapsed; } @@ -642,14 +642,16 @@ public string SortOptionWarningMessage { // this method is used to determine if Everything service is running because as at Everything v1.4.1 // the sdk does not provide a dedicated interface to determine if it is running. - return EverythingApi.IsFastSortOption(Settings.SortOption) ? string.Empty + return Settings.EverythingManagerInstance.IsFastSortOption(Settings.SortOption) ? string.Empty : Localize.flowlauncher_plugin_everything_nonfastsort_warning(); } catch (IPCErrorException) { - return Localize.flowlauncher_plugin_everything_is_not_running(); + return Settings.EnableEverything15Support + ? Localize.flowlauncher_plugin_everything_15_sort_warning() + : Localize.flowlauncher_plugin_everything_is_not_running(); } - catch (DllNotFoundException) + catch (Exception ex) when (ex is DllNotFoundException || ex is EntryPointNotFoundException) { return Localize.flowlauncher_plugin_everything_sdk_issue(); } @@ -666,6 +668,46 @@ public string EverythingInstalledPath } } + public bool EnableEverything15Support + { + get => Settings.EnableEverything15Support; + set + { + if (Settings.EnableEverything15Support == value) + return; + + Settings.EnableEverything15Support = value; + + OnPropertyChanged(); + OnPropertyChanged(nameof(FastSortWarningVisibility)); + OnPropertyChanged(nameof(SortOptionWarningMessage)); + } + } + + public string Everything15InstanceName + { + get => Settings.Everything15InstanceName; + set + { + var instanceName = string.IsNullOrWhiteSpace(value) + ? EverythingApiFactory.DefaultEverything15InstanceName + : value.Trim(); + + if (Settings.Everything15InstanceName == instanceName) + return; + + Settings.Everything15InstanceName = instanceName; + + if (EnableEverything15Support) + { + OnPropertyChanged(nameof(FastSortWarningVisibility)); + OnPropertyChanged(nameof(SortOptionWarningMessage)); + } + + OnPropertyChanged(); + } + } + #endregion } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml index d6066df953f..90a0213c301 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml @@ -13,6 +13,8 @@ mc:Ignorable="d"> + + @@ -435,6 +437,8 @@ + + @@ -602,6 +606,8 @@ + + @@ -614,11 +620,37 @@ Grid.ColumnSpan="2" Margin="{StaticResource SettingPanelItemTopBottomMargin}" HorizontalAlignment="Left" + Content="{DynamicResource flowlauncher_plugin_everything_enable_15_support}" + IsChecked="{Binding EnableEverything15Support}" /> + + + + +