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/Everything.dll b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x86/Everything.dll deleted file mode 100644 index de73b87d109..00000000000 Binary files a/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x86/Everything.dll and /dev/null 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..d2361388f40 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj @@ -1,4 +1,4 @@ - + Library @@ -29,7 +29,7 @@ PreserveNewest - + PreserveNewest diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml index 1eae757083c..2b85e2a0297 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: 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,8 @@ Search Full Path Enable File/Folder Run Count + Use Everything 1.5 + Restart Flow Launcher for this change to take effect Click to launch or install Everything Everything Installation @@ -214,3 +221,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..958d3a54481 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs @@ -46,8 +46,8 @@ 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", "x64"); + 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..3c386f710f3 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -1,52 +1,50 @@ -using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions; -using System; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using Flow.Launcher.Plugin.Explorer.Exceptions; +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) + /// + /// Searches using the specified criteria and resets the Everything API afterwards. + /// + /// 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) + { + await foreach (var result in SearchCoreAsync(option, token)) + yield return result; + } + + public async Task CheckAvailableAsync(CancellationToken token = default) { try { @@ -54,14 +52,14 @@ public static async ValueTask IsEverythingRunningAsync(CancellationToken t } catch (OperationCanceledException) { - return false; + return; } try { _ = EverythingApiDllImport.Everything_GetMajorVersion(); - var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError; - return result; + if (EverythingApiDllImport.Everything_GetLastError() == EverythingStateCode.IPCError) + throw new IPCErrorException(); } finally { @@ -69,20 +67,10 @@ public static async ValueTask IsEverythingRunningAsync(CancellationToken t } } - /// - /// Searches the specified key word and reset 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) + private async IAsyncEnumerable SearchCoreAsync(EverythingSearchOption option, [EnumeratorCancellation] CancellationToken token) { - 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 +86,16 @@ public static async IAsyncEnumerable SearchAsync(EverythingSearchO if (token.IsCancellationRequested) yield break; - if (option.Keyword.StartsWith("@")) - { - EverythingApiDllImport.Everything_SetRegex(true); - option.Keyword = option.Keyword[1..]; - } - - var builder = new StringBuilder(); - builder.Append(option.Keyword); - - if (!string.IsNullOrWhiteSpace(option.ParentPath)) - { - builder.Append($" {(option.IsRecursive ? "" : "parent:")}\"{option.ParentPath}\""); - } - - if (option.IsContentSearch) - { - builder.Append($" content:\"{option.ContentSearchKeyword}\""); - } + EverythingApiDllImport.Everything_SetRegex(preparedOption.UseRegex); + EverythingApiDllImport.Everything_SetSearchW(query.SearchText); + EverythingApiDllImport.Everything_SetOffset(preparedOption.Offset); + EverythingApiDllImport.Everything_SetMax(preparedOption.MaxCount); - EverythingApiDllImport.Everything_SetSearchW(builder.ToString()); - EverythingApiDllImport.Everything_SetOffset(option.Offset); - EverythingApiDllImport.Everything_SetMax(option.MaxCount); + EverythingApiDllImport.Everything_SetSort(preparedOption.SortOption); + EverythingApiDllImport.Everything_SetMatchPath(preparedOption.IsFullPathSearch); - EverythingApiDllImport.Everything_SetSort(option.SortOption); - EverythingApiDllImport.Everything_SetMatchPath(option.IsFullPathSearch); - - if (option.SortOption == EverythingSortOption.RUN_COUNT_DESCENDING) + if (preparedOption.SortOption == EverythingSortOption.RUN_COUNT_DESCENDING || + preparedOption.SortOption == EverythingSortOption.RUN_COUNT_ASCENDING) { EverythingApiDllImport.Everything_SetRequestFlags(EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME | EVERYTHING_REQUEST_RUN_COUNT); } @@ -144,24 +115,19 @@ public static async IAsyncEnumerable SearchAsync(EverythingSearchO for (var idx = 0; idx < EverythingApiDllImport.Everything_GetNumResults(); ++idx) { if (token.IsCancellationRequested) - { yield break; - } EverythingApiDllImport.Everything_GetResultFullPathNameW(idx, buffer, BufferSize); - var result = new SearchResult + yield return 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; } } finally @@ -175,30 +141,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 +178,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/EverythingApiV3.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiV3.cs new file mode 100644 index 00000000000..dedfbe91e6f --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiV3.cs @@ -0,0 +1,436 @@ +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.Exceptions; +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 IAsyncEnumerable SearchAsync(EverythingSearchOption option, [EnumeratorCancellation] CancellationToken token = default) + { + await foreach (var result in SearchCoreAsync(option, token)) + yield return result; + } + + public async Task CheckAvailableAsync(CancellationToken token = default) + { + try + { + await _semaphore.WaitAsync(token); + } + catch (OperationCanceledException) + { + return; + } + + try + { + if (!TryConnectEverything3(out var client)) + throw new IPCErrorException(); + _ = Everything3ApiDllImport.Everything3_DestroyClient(client); + } + finally + { + _semaphore.Release(); + } + } + + private async IAsyncEnumerable SearchCoreAsync(EverythingSearchOption option, [EnumeratorCancellation] CancellationToken token) + { + 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); + } + + return false; + } + + private static async IAsyncEnumerable SearchWithEverything3Async(IntPtr client, + EverythingSearchOption option, + EverythingHelper.PreparedQuery query, + [EnumeratorCancellation] CancellationToken token) + { + IntPtr searchState = IntPtr.Zero; + IntPtr resultList = IntPtr.Zero; + var includeRunCount = option.IsRunCounterEnabled || option.SortOption == EverythingSortOption.RUN_COUNT_DESCENDING || option.SortOption == EverythingSortOption.RUN_COUNT_ASCENDING; + + 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); + if (includeRunCount) + _ = 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 TryConnectEverything3(out IntPtr client) + { + client = Everything3ApiDllImport.Everything3_ConnectW(_instanceName); + return client != IntPtr.Zero; + } + + /// + /// 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_ASCENDING: + propertyId = EVERYTHING3_PROPERTY_ID_RUN_COUNT; + ascending = true; + 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/EverythingHelper.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingHelper.cs new file mode 100644 index 00000000000..b3f92773867 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingHelper.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + 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/EverythingSearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs index eb994a6f98f..98fbe240067 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs @@ -11,88 +11,37 @@ 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 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; - } - } - public async IAsyncEnumerable SearchAsync(string search, [EnumeratorCancellation] CancellationToken token) { - await ThrowIfEverythingNotAvailableAsync(token); + await EnsureAvailableAsync(token); if (token.IsCancellationRequested) yield break; - var option = new EverythingSearchOption(search, - Settings.SortOption, - MaxCount: Settings.MaxResult, - IsFullPathSearch: Settings.EverythingSearchFullPath, + var option = new EverythingSearchOption(search, + Settings.SortOption, + MaxCount: Settings.MaxResult, + 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 EnsureAvailableAsync(token); if (!Settings.EnableEverythingContentSearch) { @@ -103,7 +52,6 @@ public async IAsyncEnumerable ContentSearchAsync(string plainSearc _ => { Settings.EnableEverythingContentSearch = true; - return ValueTask.FromResult(true); }); } @@ -119,7 +67,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 +75,7 @@ public async IAsyncEnumerable ContentSearchAsync(string plainSearc public async IAsyncEnumerable EnumerateAsync(string path, string search, bool recursive, [EnumeratorCancellation] CancellationToken token) { - await ThrowIfEverythingNotAvailableAsync(token); + await EnsureAvailableAsync(token); if (token.IsCancellationRequested) yield break; @@ -140,8 +88,97 @@ 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; + + if (Settings.EnableEverything15Support) + { + if (!Everything3ApiDllImport.IsLoaded) + Everything3ApiDllImport.Load(sdkDirectory); + } + else + { + if (!EverythingApiDllImport.IsLoaded) + EverythingApiDllImport.Load(sdkDirectory); + } + + api = Settings.EnableEverything15Support + ? new EverythingApiV3(Settings.Everything15InstanceName) + : new LegacyEverythingApi(); + isApiInitialized = true; + } + } + + private async Task EnsureAvailableAsync(CancellationToken token) + { + var engineName = Enum.GetName(Settings.IndexSearchEngineOption.Everything)!; + try + { + await api.CheckAvailableAsync(token); + } + catch (OperationCanceledException) + { + // ignore, the search was cancelled + } + catch (Exceptions.IPCErrorException) when (api is LegacyEverythingApi) + { + throw new EngineNotAvailableException(engineName, + Localize.flowlauncher_plugin_everything_click_to_launch_or_install(), + Localize.flowlauncher_plugin_everything_is_not_running(), + Constants.EverythingErrorImagePath, + ClickToInstallEverythingAsync); + } + catch (Exceptions.IPCErrorException) + { + throw new EngineNotAvailableException(engineName, + Localize.flowlauncher_plugin_everything_15_resolution(), + Localize.flowlauncher_plugin_everything_15_unavailable(), + Constants.EverythingErrorImagePath); + } + catch (Exception ex) when (ex is DllNotFoundException || ex is EntryPointNotFoundException) + { + throw new EngineNotAvailableException(engineName, + 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(EverythingSearchManager), "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(EverythingSearchManager), "Failed to install Everything", e); + return false; + } + } + + 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/EverythingSortOption.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSortOption.cs index 6a3d7cb67bf..3490b8b8c4b 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSortOption.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSortOption.cs @@ -41,6 +41,8 @@ public enum EverythingSortOption : uint FILE_LIST_FILENAME_ASCENDING = 17u, [EnumLocalizeKey(nameof(Localize.flowlauncher_plugin_everything_sort_by_file_list_filename_descending))] FILE_LIST_FILENAME_DESCENDING = 18u, + [EnumLocalizeKey(nameof(Localize.flowlauncher_plugin_everything_sort_by_run_count_ascending))] + RUN_COUNT_ASCENDING = 19u, [EnumLocalizeKey(nameof(Localize.flowlauncher_plugin_everything_sort_by_run_count_descending))] RUN_COUNT_DESCENDING = 20u, [EnumLocalizeKey(nameof(Localize.flowlauncher_plugin_everything_sort_by_date_recently_changed_ascending))] diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingStateCode.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingStateCode.cs new file mode 100644 index 00000000000..1f7d226504a --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingStateCode.cs @@ -0,0 +1,14 @@ +namespace Flow.Launcher.Plugin.Explorer.Search.Everything +{ + public enum EverythingStateCode + { + OK, + MemoryError, + IPCError, + RegisterClassExError, + CreateWindowError, + CreateThreadError, + InvalidIndexError, + InvalidCallError + } +} 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..74a453b8e7e --- /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 + { + Task CheckAvailableAsync(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..fe78c810c80 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; @@ -164,6 +164,10 @@ public enum ContentIndexSearchEngineOption public bool EverythingSearchFullPath { get; set; } = false; public bool EverythingEnableRunCount { get; set; } = true; + // This is only used as a flag to determine whether to show the Everything 1.5 support option in the settings UI, and to determine which instance of Everything API to initialize in the EverythingSearchManager. + public bool EnableEverything15Support { get; set; } = false; + public string Everything15InstanceName { get; set; } = string.Empty; + #endregion internal enum ActionKeyword diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs index a9b2e6a89e3..16023ea7441 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs @@ -28,11 +28,16 @@ public partial class SettingsViewModel : BaseModel public IReadOnlyList> ContentIndexSearchEngines { get; set; } public IReadOnlyList> PathEnumerationEngines { get; set; } + // Cache when initializing + private readonly bool InitialUsingEverything15; + private readonly string InitialEverything15InstanceName; + public SettingsViewModel(PluginInitContext context, Settings settings) { Context = context; Settings = settings; - + InitialUsingEverything15 = settings.EnableEverything15Support; + InitialEverything15InstanceName = settings.Everything15InstanceName; InitializeEngineSelection(); InitializeActionKeywordModels(); } @@ -619,7 +624,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 +632,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 +647,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 InitialUsingEverything15 + ? 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 +673,50 @@ 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)); + OnPropertyChanged(nameof(Everything15RestartRequired)); + } + } + + public bool Everything15RestartRequired => + Settings.EnableEverything15Support != InitialUsingEverything15 || + (Settings.EnableEverything15Support && Settings.Everything15InstanceName != InitialEverything15InstanceName); + + public string Everything15InstanceName + { + get => Settings.Everything15InstanceName; + set + { + var instanceName = value?.Trim() ?? string.Empty; + + if (Settings.Everything15InstanceName == instanceName) + return; + + Settings.Everything15InstanceName = instanceName; + + if (EnableEverything15Support) + { + OnPropertyChanged(nameof(FastSortWarningVisibility)); + OnPropertyChanged(nameof(SortOptionWarningMessage)); + OnPropertyChanged(nameof(Everything15RestartRequired)); + } + + OnPropertyChanged(); + } + } + #endregion } } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml index d6066df953f..37607d8dbe3 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml @@ -1,4 +1,4 @@ - + + @@ -435,6 +437,8 @@ + + @@ -602,6 +606,9 @@ + + + @@ -614,11 +621,46 @@ Grid.ColumnSpan="2" Margin="{StaticResource SettingPanelItemTopBottomMargin}" HorizontalAlignment="Left" + Content="{DynamicResource flowlauncher_plugin_everything_enable_15_support}" + IsChecked="{Binding EnableEverything15Support}" /> + + + + + + +