diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 494d4de93..00cc67ea0 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -2,3 +2,4 @@ github https ssh ubuntu +runcount diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs index 79d106ef2..d9cf68469 100644 --- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs +++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs @@ -233,8 +233,8 @@ namespace Flow.Launcher.Plugin /// Open directory in an explorer configured by user via Flow's Settings. The default is Windows Explorer /// /// Directory Path to open - /// Extra FileName Info - public void OpenDirectory(string DirectoryPath, string FileName = null); + /// Extra FileName Info + public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null); /// /// Opens the URL with the given Uri object. diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index dc24872f5..1c4467762 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -178,7 +178,7 @@ namespace Flow.Launcher.Plugin /// public override string ToString() { - return Title + SubTitle; + return Title + SubTitle + Score; } /// diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index bd8d32ff5..6b7f0c2d3 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -14,8 +14,6 @@ namespace Flow.Launcher.Plugin.SharedCommands { private const string FileExplorerProgramName = "explorer"; - private const string FileExplorerProgramEXE = "explorer.exe"; - /// /// Copies the folder and all of its files and folders /// including subfolders to the target location @@ -151,7 +149,12 @@ namespace Flow.Launcher.Plugin.SharedCommands /// public static void OpenPath(string fileOrFolderPath) { - var psi = new ProcessStartInfo { FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' }; + var psi = new ProcessStartInfo + { + FileName = FileExplorerProgramName, + UseShellExecute = true, + Arguments = '"' + fileOrFolderPath + '"' + }; try { if (LocationExists(fileOrFolderPath) || FileExists(fileOrFolderPath)) @@ -167,15 +170,6 @@ namespace Flow.Launcher.Plugin.SharedCommands } } - /// - /// Open the folder that contains - /// - /// - public static void OpenContainingFolder(string path) - { - Process.Start(FileExplorerProgramEXE, $" /select,\"{path}\""); - } - /// /// This checks whether a given string is a directory path or network location string. /// It does not check if location actually exists. diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index b2487693e..ec997f537 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -1,4 +1,4 @@ - using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -195,17 +195,20 @@ namespace Flow.Launcher ((PluginJsonStorage)_pluginJsonStorages[type]).Save(); } - public void OpenDirectory(string DirectoryPath, string FileName = null) + public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null) { using var explorer = new Process(); var explorerInfo = _settingsVM.Settings.CustomExplorer; explorer.StartInfo = new ProcessStartInfo { FileName = explorerInfo.Path, - Arguments = FileName is null ? - explorerInfo.DirectoryArgument.Replace("%d", DirectoryPath) : - explorerInfo.FileArgument.Replace("%d", DirectoryPath).Replace("%f", - Path.IsPathRooted(FileName) ? FileName : Path.Combine(DirectoryPath, FileName)) + Arguments = FileNameOrFilePath is null + ? explorerInfo.DirectoryArgument.Replace("%d", DirectoryPath) + : explorerInfo.FileArgument + .Replace("%d", DirectoryPath) + .Replace("%f", + Path.IsPathRooted(FileNameOrFilePath) ? FileNameOrFilePath : Path.Combine(DirectoryPath, FileNameOrFilePath) + ) }; explorer.Start(); } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs index 3efd09c4d..e618b5c36 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs @@ -17,7 +17,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything { private const int BufferSize = 4096; - private static SemaphoreSlim _semaphore = new(1, 1); + private static readonly SemaphoreSlim _semaphore = new(1, 1); // cached buffer to remove redundant allocations. private static readonly StringBuilder buffer = new(BufferSize); @@ -34,6 +34,9 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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. /// @@ -78,7 +81,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything if (option.MaxCount < 0) throw new ArgumentOutOfRangeException(nameof(option.MaxCount), option.MaxCount, "MaxCount must be greater than or equal to 0"); - + await _semaphore.WaitAsync(token); @@ -112,6 +115,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything EverythingApiDllImport.Everything_SetSort(option.SortOption); EverythingApiDllImport.Everything_SetMatchPath(option.IsFullPathSearch); + + if (option.SortOption == SortOption.RUN_COUNT_DESCENDING) + { + EverythingApiDllImport.Everything_SetRequestFlags(EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME | EVERYTHING_REQUEST_RUN_COUNT); + } + else + { + EverythingApiDllImport.Everything_SetRequestFlags(EVERYTHING_REQUEST_FULL_PATH_AND_FILE_NAME); + } + + if (token.IsCancellationRequested) yield break; @@ -132,10 +146,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything 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 + ResultType.Volume, + Score = (int)EverythingApiDllImport.Everything_GetResultRunCount( (uint)idx) }; yield return result; @@ -172,5 +188,19 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything throw new ArgumentOutOfRangeException(); } } + + public static async Task IncrementRunCounterAsync(string fileOrFolder) + { + await _semaphore.WaitAsync(TimeSpan.FromSeconds(1)); + try + { + _ = EverythingApiDllImport.Everything_IncRunCountFromFileName(fileOrFolder); + } + catch (Exception) + { + /*ignored*/ + } + 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 344707892..1ea23777c 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs @@ -39,6 +39,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything Main.Context.API.GetTranslation("flowlauncher_plugin_everything_sdk_issue")); } } + private async ValueTask ClickToInstallEverythingAsync(ActionContext _) { var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(Settings.EverythingInstalledPath, Main.Context.API); @@ -68,8 +69,8 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything await foreach (var result in EverythingApi.SearchAsync(option, token)) yield return result; } - public async IAsyncEnumerable ContentSearchAsync(string plainSearch, - string contentSearch, + + public async IAsyncEnumerable ContentSearchAsync(string plainSearch, string contentSearch, [EnumeratorCancellation] CancellationToken token) { await ThrowIfEverythingNotAvailableAsync(token); @@ -102,6 +103,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.Everything yield return result; } } + public async IAsyncEnumerable EnumerateAsync(string path, string search, bool recursive, [EnumeratorCancellation] CancellationToken token) { await ThrowIfEverythingNotAvailableAsync(token); diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs index 434afd1b4..c57e3fe4a 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs @@ -27,7 +27,6 @@ namespace Flow.Launcher.Plugin.Everything.Everything ATTRIBUTES_DESCENDING = 16u, FILE_LIST_FILENAME_ASCENDING = 17u, FILE_LIST_FILENAME_DESCENDING = 18u, - RUN_COUNT_ASCENDING = 19u, RUN_COUNT_DESCENDING = 20u, DATE_RECENTLY_CHANGED_ASCENDING = 21u, DATE_RECENTLY_CHANGED_DESCENDING = 22u, diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index ed4f39735..7147c348e 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; +using Flow.Launcher.Plugin.Explorer.Search.Everything; namespace Flow.Launcher.Plugin.Explorer.Search { @@ -37,13 +38,13 @@ namespace Flow.Launcher.Plugin.Explorer.Search var keyword = usePathSearchActionKeyword ? pathSearchActionKeyword : searchActionKeyword; - var formatted_path = path; + var formattedPath = path; if (type == ResultType.Folder) // the separator is needed so when navigating the folder structure contents of the folder are listed - formatted_path = path.EndsWith(Constants.DirectorySeparator) ? path : path + Constants.DirectorySeparator; + formattedPath = path.EndsWith(Constants.DirectorySeparator) ? path : path + Constants.DirectorySeparator; - return $"{keyword}{formatted_path}"; + return $"{keyword}{formattedPath}"; } public static string GetAutoCompleteText(string title, Query query, string path, ResultType resultType) @@ -57,10 +58,10 @@ namespace Flow.Launcher.Plugin.Explorer.Search { return result.Type switch { - ResultType.Folder or ResultType.Volume => CreateFolderResult(Path.GetFileName(result.FullPath), - result.FullPath, result.FullPath, query, 0, result.WindowsIndexed), - ResultType.File => CreateFileResult( - result.FullPath, query, 0, result.WindowsIndexed), + ResultType.Folder or ResultType.Volume => + CreateFolderResult(Path.GetFileName(result.FullPath), result.FullPath, result.FullPath, query, result.Score, result.WindowsIndexed), + ResultType.File => + CreateFileResult(result.FullPath, query, result.Score, result.WindowsIndexed), _ => throw new ArgumentOutOfRangeException() }; } @@ -77,11 +78,12 @@ namespace Flow.Launcher.Plugin.Explorer.Search CopyText = path, Action = c => { + // open folder if (c.SpecialKeyState.CtrlPressed || (!Settings.PathSearchKeywordEnabled && !Settings.SearchActionKeywordEnabled)) { try { - Context.API.OpenDirectory(path); + OpenFolder(path); return true; } catch (Exception ex) @@ -91,6 +93,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search } } + // or make this folder the current query Context.API.ChangeQuery(GetPathWithActionKeyword(path, ResultType.Folder, query.ActionKeyword)); return false; @@ -98,12 +101,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search Score = score, TitleToolTip = InternationalizationManager.Instance.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"), SubTitleToolTip = path, - ContextData = new SearchResult - { - Type = ResultType.Folder, - FullPath = path, - WindowsIndexed = windowsIndexed - } + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = path, WindowsIndexed = windowsIndexed } }; } @@ -112,7 +110,6 @@ namespace Flow.Launcher.Plugin.Explorer.Search var progressBarColor = "#26a0da"; var title = string.Empty; // hide title when use progress bar, var driveLetter = path[..1].ToUpper(); - var driveName = driveLetter + ":\\"; DriveInfo drv = new DriveInfo(driveLetter); var freespace = ToReadableSize(drv.AvailableFreeSpace, 2); var totalspace = ToReadableSize(drv.TotalSize, 2); @@ -133,19 +130,14 @@ namespace Flow.Launcher.Plugin.Explorer.Search Score = 500, ProgressBar = progressValue, ProgressBarColor = progressBarColor, - Action = c => + Action = _ => { - Context.API.OpenDirectory(path); + OpenFolder(path); return true; }, TitleToolTip = path, SubTitleToolTip = path, - ContextData = new SearchResult - { - Type = ResultType.Volume, - FullPath = path, - WindowsIndexed = windowsIndexed - } + ContextData = new SearchResult { Type = ResultType.Volume, FullPath = path, WindowsIndexed = windowsIndexed } }; } @@ -153,7 +145,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search { int mok = 0; double drvSize = pDrvSize; - string Space = "Byte"; + string uom = "Byte"; // Unit Of Measurement while (drvSize > 1024.0) { @@ -162,23 +154,23 @@ namespace Flow.Launcher.Plugin.Explorer.Search } if (mok == 1) - Space = "KB"; + uom = "KB"; else if (mok == 2) - Space = " MB"; + uom = " MB"; else if (mok == 3) - Space = " GB"; + uom = " GB"; else if (mok == 4) - Space = " TB"; + uom = " TB"; - var returnStr = $"{Convert.ToInt32(drvSize)}{Space}"; + var returnStr = $"{Convert.ToInt32(drvSize)}{uom}"; if (mok != 0) { returnStr = pi switch { - 1 => $"{drvSize:F1}{Space}", - 2 => $"{drvSize:F2}{Space}", - 3 => $"{drvSize:F3}{Space}", - _ => $"{Convert.ToInt32(drvSize)}{Space}" + 1 => $"{drvSize:F1}{uom}", + 2 => $"{drvSize:F2}{uom}", + 3 => $"{drvSize:F3}{uom}", + _ => $"{Convert.ToInt32(drvSize)}{uom}" }; } @@ -201,24 +193,18 @@ namespace Flow.Launcher.Plugin.Explorer.Search CopyText = folderPath, Action = _ => { - Context.API.OpenDirectory(folderPath); + OpenFolder(folderPath); return true; }, - ContextData = new SearchResult - { - Type = ResultType.Folder, - FullPath = folderPath, - WindowsIndexed = windowsIndexed - } + ContextData = new SearchResult { Type = ResultType.Folder, FullPath = folderPath, WindowsIndexed = windowsIndexed } }; } internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false) { - Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) ? new Result.PreviewInfo - { - IsMedia = true, PreviewImagePath = filePath, - } : Result.PreviewInfo.Default; + Result.PreviewInfo preview = IsMedia(Path.GetExtension(filePath)) + ? new Result.PreviewInfo { IsMedia = true, PreviewImagePath = filePath, } + : Result.PreviewInfo.Default; var title = Path.GetFileName(filePath); @@ -236,33 +222,17 @@ namespace Flow.Launcher.Plugin.Explorer.Search { try { - if (File.Exists(filePath) && c.SpecialKeyState.CtrlPressed && c.SpecialKeyState.ShiftPressed) + if (c.SpecialKeyState.CtrlPressed && c.SpecialKeyState.ShiftPressed) { - _ = Task.Run(() => - { - try - { - Process.Start(new ProcessStartInfo - { - FileName = filePath, - UseShellExecute = true, - WorkingDirectory = Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, - Verb = "runas", - }); - } - catch (Exception e) - { - MessageBox.Show(e.Message, "Could not start " + filePath); - } - }); + OpenFileAsAdmin(filePath); } else if (c.SpecialKeyState.CtrlPressed) { - Context.API.OpenDirectory(Path.GetDirectoryName(filePath), filePath); + OpenFolder(filePath, filePath); } else { - FilesFolders.OpenPath(filePath); + OpenFile(filePath); } } catch (Exception ex) @@ -274,32 +244,59 @@ namespace Flow.Launcher.Plugin.Explorer.Search }, TitleToolTip = InternationalizationManager.Instance.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"), SubTitleToolTip = filePath, - ContextData = new SearchResult - { - Type = ResultType.File, - FullPath = filePath, - WindowsIndexed = windowsIndexed - } + ContextData = new SearchResult { Type = ResultType.File, FullPath = filePath, WindowsIndexed = windowsIndexed } }; return result; } - public static bool IsMedia(string extension) + private static bool IsMedia(string extension) { - if (string.IsNullOrEmpty(extension)) - { - return false; - } - else - { - return MediaExtensions.Contains(extension.ToLowerInvariant()); - } + if (string.IsNullOrEmpty(extension)) { return false; } + + return MediaExtensions.Contains(extension.ToLowerInvariant()); } - public static readonly string[] MediaExtensions = + private static void OpenFile(string filePath) { - ".jpg", ".png", ".avi", ".mkv", ".bmp", ".gif", ".wmv", ".mp3", ".flac", ".mp4" - }; + IncrementEverythingRunCounterIfNeeded(filePath); + FilesFolders.OpenPath(filePath); + } + + private static void OpenFolder(string folderPath, string fileNameOrFilePath = null) + { + IncrementEverythingRunCounterIfNeeded(folderPath); + Context.API.OpenDirectory(Path.GetDirectoryName(folderPath), fileNameOrFilePath); + } + + private static void OpenFileAsAdmin(string filePath) + { + _ = Task.Run(() => + { + try + { + IncrementEverythingRunCounterIfNeeded(filePath); + Process.Start(new ProcessStartInfo + { + FileName = filePath, + UseShellExecute = true, + WorkingDirectory = Settings.UseLocationAsWorkingDir ? Path.GetDirectoryName(filePath) : string.Empty, + Verb = "runas", + }); + } + catch (Exception e) + { + MessageBox.Show(e.Message, "Could not start " + filePath); + } + }); + } + + private static void IncrementEverythingRunCounterIfNeeded(string fileOrFolder) + { + if (Settings.EverythingEnabled) + _ = Task.Run(() => EverythingApi.IncrementRunCounterAsync(fileOrFolder)); + } + + private static readonly string[] MediaExtensions = { ".jpg", ".png", ".avi", ".mkv", ".bmp", ".gif", ".wmv", ".mp3", ".flac", ".mp4" }; } public enum ResultType