diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 22f547a5a..0df853a5d 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -40,15 +40,7 @@ namespace Flow.Launcher.Core.Plugin public List LoadContextMenus(Result selectedResult) { var output = ExecuteContextMenu(selectedResult); - try - { - return DeserializedResult(output); - } - catch (Exception e) - { - Log.Exception($"|JsonRPCPlugin.LoadContextMenus|Exception on result <{selectedResult}>", e); - return null; - } + return DeserializedResult(output); } private static readonly JsonSerializerOptions options = new() @@ -65,23 +57,10 @@ namespace Flow.Launcher.Core.Plugin { if (output == Stream.Null) return null; - try - { - var queryResponseModel = - await JsonSerializer.DeserializeAsync(output, options); + var queryResponseModel = + await JsonSerializer.DeserializeAsync(output, options); - return ParseResults(queryResponseModel); - } - catch (JsonException e) - { - Log.Exception(GetType().FullName, "Unexpected Json Input", e); - } - finally - { - await output.DisposeAsync(); - } - - return null; + return ParseResults(queryResponseModel); } private List DeserializedResult(string output) @@ -249,7 +228,7 @@ namespace Flow.Launcher.Core.Plugin await using var source = process.StandardOutput.BaseStream; var buffer = BufferManager.GetStream(); - + token.Register(() => { // ReSharper disable once AccessToModifiedClosure @@ -274,6 +253,14 @@ namespace Flow.Launcher.Core.Plugin token.ThrowIfCancellationRequested(); + if (buffer.Length == 0) + { + var errorMessage = process.StandardError.EndOfStream ? + "Empty JSONRPC Response" : + await process.StandardError.ReadToEndAsync(); + throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}"); + } + if (!process.StandardError.EndOfStream) { using var standardError = process.StandardError; @@ -281,23 +268,12 @@ namespace Flow.Launcher.Core.Plugin if (!string.IsNullOrEmpty(error)) { - Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}"); - return Stream.Null; + Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}"); } - - Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error."); - return Stream.Null; } return buffer; } - catch (Exception e) - { - Log.Exception( - $"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", - e); - return Stream.Null; - } finally { process?.Dispose(); @@ -307,20 +283,8 @@ namespace Flow.Launcher.Core.Plugin public async Task> QueryAsync(Query query, CancellationToken token) { - try - { - var output = await ExecuteQueryAsync(query, token); - return await DeserializedResultAsync(output); - } - catch (OperationCanceledException) - { - return null; - } - catch (Exception e) - { - Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e); - return null; - } + var output = await ExecuteQueryAsync(query, token); + return await DeserializedResultAsync(output); } public virtual Task InitAsync(PluginInitContext context) @@ -329,4 +293,4 @@ namespace Flow.Launcher.Core.Plugin return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Infrastructure/Image/ImageCache.cs b/Flow.Launcher.Infrastructure/Image/ImageCache.cs index bb7ec6817..13277c7d9 100644 --- a/Flow.Launcher.Infrastructure/Image/ImageCache.cs +++ b/Flow.Launcher.Infrastructure/Image/ImageCache.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Media; @@ -26,7 +27,8 @@ namespace Flow.Launcher.Infrastructure.Image private const int MaxCached = 50; public ConcurrentDictionary Data { get; private set; } = new ConcurrentDictionary(); private const int permissibleFactor = 2; - + private SemaphoreSlim semaphore = new(1, 1); + public void Initialization(Dictionary usage) { foreach (var key in usage.Keys) @@ -60,20 +62,29 @@ namespace Flow.Launcher.Infrastructure.Image } ); - // To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size - // This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time - if (Data.Count > permissibleFactor * MaxCached) + SliceExtra(); + + async void SliceExtra() { - // To delete the images from the data dictionary based on the resizing of the Usage Dictionary. - foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key)) - Data.TryRemove(key, out _); + // To prevent the dictionary from drastically increasing in size by caching images, the dictionary size is not allowed to grow more than the permissibleFactor * maxCached size + // This is done so that we don't constantly perform this resizing operation and also maintain the image cache size at the same time + if (Data.Count > permissibleFactor * MaxCached) + { + await semaphore.WaitAsync().ConfigureAwait(false); + // To delete the images from the data dictionary based on the resizing of the Usage Dictionary + // Double Check to avoid concurrent remove + if (Data.Count > permissibleFactor * MaxCached) + foreach (var key in Data.OrderBy(x => x.Value.usage).Take(Data.Count - MaxCached).Select(x => x.Key).ToArray()) + Data.TryRemove(key, out _); + semaphore.Release(); + } } } } public bool ContainsKey(string key) { - return Data.ContainsKey(key) && Data[key].imageSource != null; + return key is not null && Data.ContainsKey(key) && Data[key].imageSource != null; } public int CacheSize() diff --git a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs index abce8b41e..fd2464f2e 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/PluginSettings.cs @@ -18,7 +18,7 @@ namespace Flow.Launcher.Infrastructure.UserSettings // TODO: Remove. This is backwards compatibility for 1.8.0 release. // Introduced two new action keywords in Explorer, so need to update plugin setting in the UserData folder. - if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count != settings.ActionKeywords.Count) + if (metadata.ID == "572be03c74c642baae319fc283e561a8" && metadata.ActionKeywords.Count > settings.ActionKeywords.Count) { settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for index search settings.ActionKeywords.Add(Query.GlobalPluginWildcardSign); // for path search diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index e7e7902d4..ebef2c631 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -39,7 +39,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings /// public bool ShouldUsePinyin { get; set; } = false; - internal SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular; + [JsonInclude, JsonConverter(typeof(JsonStringEnumConverter))] + public SearchPrecisionScore QuerySearchPrecision { get; private set; } = SearchPrecisionScore.Regular; [JsonIgnore] public string QuerySearchPrecisionString diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index 877b82a06..dfee89810 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -42,7 +42,7 @@ + Source="{Binding Image}" Visibility="{Binding ShowIcon}" /> diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ca6ffe845..6eee51bd8 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -108,9 +108,7 @@ namespace Flow.Launcher.ViewModel } Log.Error("MainViewModel", "Unexpected ResultViewUpdate ends"); - } - - ; + }; void continueAction(Task t) { diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 3eba63b22..176943e4c 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -12,71 +12,23 @@ namespace Flow.Launcher.ViewModel { public class ResultViewModel : BaseModel { - public class LazyAsync : Lazy> - { - private readonly T defaultValue; - - private readonly Action _updateCallback; - public new T Value - { - get - { - if (!IsValueCreated) - { - _ = Exercute(); // manually use callback strategy - - return defaultValue; - } - - if (!base.Value.IsCompletedSuccessfully) - return defaultValue; - - return base.Value.Result; - - // If none of the variables captured by the local function are captured by other lambdas, - // the compiler can avoid heap allocations. - async ValueTask Exercute() - { - await base.Value.ConfigureAwait(false); - _updateCallback(); - } - - } - } - public LazyAsync(Func> factory, T defaultValue, Action updateCallback) : base(factory) - { - if (defaultValue != null) - { - this.defaultValue = defaultValue; - } - - _updateCallback = updateCallback; - } - } - public ResultViewModel(Result result, Settings settings) { if (result != null) { Result = result; - Image = new LazyAsync( - SetImage, - ImageLoader.DefaultImage, - () => - { - OnPropertyChanged(nameof(Image)); - }); - if (Result.Glyph is { FontFamily: not null } glyph) { - if (glyph.FontFamily.Contains('/')) + if (glyph.FontFamily.EndsWith(".ttf") || glyph.FontFamily.EndsWith(".otf")) { var fontPath = Result.Glyph.FontFamily; - Glyph = Path.IsPathRooted(fontPath) ? Result.Glyph : Result.Glyph with - { - FontFamily = Path.Combine(Result.PluginDirectory, fontPath) - }; + Glyph = Path.IsPathRooted(fontPath) + ? Result.Glyph + : Result.Glyph with + { + FontFamily = Path.Combine(Result.PluginDirectory, fontPath) + }; } else { @@ -88,10 +40,15 @@ namespace Flow.Launcher.ViewModel Settings = settings; } - public Settings Settings { get; private set; } + private Settings Settings { get; } + + public Visibility ShowOpenResultHotkey => + Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Hidden; + + public Visibility ShowIcon => Result.IcoPath != null || Result.Icon is not null || Glyph == null + ? Visibility.Visible + : Visibility.Hidden; - public Visibility ShowOpenResultHotkey => Settings.ShowOpenResultHotkey ? Visibility.Visible : Visibility.Hidden; - public Visibility ShowIcon => Result.IcoPath != null || Result.Icon is not null || Glyph == null ? Visibility.Visible : Visibility.Hidden; public Visibility ShowGlyph => Glyph is not null ? Visibility.Visible : Visibility.Hidden; public string OpenResultModifiers => Settings.OpenResultModifiers; @@ -103,46 +60,61 @@ namespace Flow.Launcher.ViewModel ? Result.SubTitle : Result.SubTitleToolTip; - public LazyAsync Image { get; set; } + private volatile bool ImageLoaded; + + private ImageSource image = ImageLoader.DefaultImage; + + public ImageSource Image + { + get + { + if (!ImageLoaded) + { + ImageLoaded = true; + _ = LoadImageAsync(); + } + + return image; + } + private set => image = value; + } public GlyphInfo Glyph { get; set; } - private async ValueTask SetImage() + private async ValueTask LoadImageAsync() { var imagePath = Result.IcoPath; if (string.IsNullOrEmpty(imagePath) && Result.Icon != null) { try { - return Result.Icon(); + image = Result.Icon(); + return; } catch (Exception e) { - Log.Exception($"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", e); - return ImageLoader.DefaultImage; + Log.Exception( + $"|ResultViewModel.Image|IcoPath is empty and exception when calling Icon() for result <{Result.Title}> of plugin <{Result.PluginDirectory}>", + e); } } if (ImageLoader.CacheContainImage(imagePath)) + { // will get here either when icoPath has value\icon delegate is null\when had exception in delegate - return ImageLoader.Load(imagePath); + image = ImageLoader.Load(imagePath); + return; + } - return await Task.Run(() => ImageLoader.Load(imagePath)); + // We need to modify the property not field here to trigger the OnPropertyChanged event + Image = await Task.Run(() => ImageLoader.Load(imagePath)).ConfigureAwait(false); } public Result Result { get; } public override bool Equals(object obj) { - var r = obj as ResultViewModel; - if (r != null) - { - return Result.Equals(r.Result); - } - else - { - return false; - } + return obj is ResultViewModel r && Result.Equals(r.Result); } public override int GetHashCode() diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs index d0f78e14d..09315d9dd 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs @@ -52,7 +52,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search }, Score = score, TitleToolTip = Constants.ToolTipOpenDirectory, - SubTitleToolTip = Constants.ToolTipOpenDirectory, + SubTitleToolTip = path, ContextData = new SearchResult { Type = ResultType.Folder, @@ -144,7 +144,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search return true; }, TitleToolTip = Constants.ToolTipOpenContainingFolder, - SubTitleToolTip = Constants.ToolTipOpenContainingFolder, + SubTitleToolTip = filePath, ContextData = new SearchResult { Type = ResultType.File, diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs index 9995f45d3..d367eddca 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs @@ -143,7 +143,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search return await IndexSearch.WindowsIndexSearchAsync( querySearchString, - queryConstructor.CreateQueryHelper(), + queryConstructor.CreateQueryHelper, queryConstructor.QueryForFileContentSearch, Settings.IndexSearchExcludedSubdirectoryPaths, query, @@ -181,7 +181,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search return await IndexSearch.WindowsIndexSearchAsync( querySearchString, - queryConstructor.CreateQueryHelper(), + queryConstructor.CreateQueryHelper, queryConstructor.QueryForAllFilesAndFolders, Settings.IndexSearchExcludedSubdirectoryPaths, query, @@ -195,7 +195,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search return await IndexSearch.WindowsIndexSearchAsync( path, - queryConstructor.CreateQueryHelper(), + queryConstructor.CreateQueryHelper, queryConstructor.QueryForTopLevelDirectorySearch, Settings.IndexSearchExcludedSubdirectoryPaths, query, diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs index 010a19b58..2e90eadb6 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs +++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs @@ -88,7 +88,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex internal async static Task> WindowsIndexSearchAsync( string searchString, - CSearchQueryHelper queryHelper, + Func createQueryHelper, Func constructQuery, List exclusionList, Query query, @@ -104,7 +104,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex var constructedQuery = constructQuery(searchString); return RemoveResultsInExclusionList( - await ExecuteWindowsIndexSearchAsync(constructedQuery, queryHelper.ConnectionString, query, token).ConfigureAwait(false), + await ExecuteWindowsIndexSearchAsync(constructedQuery, createQueryHelper().ConnectionString, query, token).ConfigureAwait(false), exclusionList, token); } diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json index 59a31ec13..1881eff31 100644 --- a/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.Explorer/plugin.json @@ -9,7 +9,7 @@ "Name": "Explorer", "Description": "Search and manage files and folders. Explorer utilises Windows Index Search", "Author": "Jeremy Wu", - "Version": "1.8.2", + "Version": "1.8.4", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.Explorer.dll", diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs index 995ac0fed..c950288e0 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs @@ -120,7 +120,10 @@ namespace Flow.Launcher.Plugin.PluginsManager .ChangeQuery( $"{Context.CurrentPluginMetadata.ActionKeywords.FirstOrDefault()} {Settings.HotkeyUpdate} {plugin.Name}"); - Application.Current.MainWindow.Show(); + var mainWindow = Application.Current.MainWindow; + mainWindow.Visibility = Visibility.Visible; + mainWindow.Focus(); + shouldHideWindow = false; return; @@ -185,7 +188,6 @@ namespace Flow.Launcher.Plugin.PluginsManager var uninstallSearch = search.Replace(Settings.HotkeyUpdate, string.Empty).TrimStart(); - var resultsForUpdate = from existingPlugin in Context.API.GetAllPlugins() join pluginFromManifest in pluginsManifest.UserPlugins diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json index fa916a29d..08dab6fbf 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/plugin.json @@ -6,7 +6,7 @@ "Name": "Plugins Manager", "Description": "Management of installing, uninstalling or updating Flow Launcher plugins", "Author": "Jeremy Wu", - "Version": "1.8.4", + "Version": "1.8.5", "Language": "csharp", "Website": "https://github.com/Flow-Launcher/Flow.Launcher", "ExecuteFileName": "Flow.Launcher.Plugin.PluginsManager.dll", diff --git a/README.md b/README.md index e89558d57..9fda057c0 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Flow Launcher. Dedicated to make your workflow flow more seamlessly. Aimed at be ### Installation -| [Windows 7 and up installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest) | [Portable](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Portable.zip) | `WinGet install "Flow Launcher"` | +| [Windows 7+ installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Setup.exe) | [Portable](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Portable.zip) | `WinGet install "Flow Launcher"` | | --------------------------------- | --------------------------------- | --------------------------------- | Windows may complain about security due to code not being signed, this will be completed at a later stage. If you downloaded from this repo, you are good to continue the set up. @@ -59,10 +59,10 @@ Windows may complain about security due to code not being signed, this will be c - Open context menu: on the selected result, press Ctrl+O/Shift+Enter. - Cancel/Return to previous screen: Esc. - Install/Uninstall/Update plugins: in the search window, type `pm` `install`/`uninstall`/`update` + the plugin name. -- Saved user settings are located: +- Type `flow user data` to open your saved user settings folder. They are located at: - If using roaming: `%APPDATA%\FlowLauncher` - If using portable, by default: `%localappdata%\FlowLauncher\app-\UserData` -- Logs are saved along with your user settings folder. +- Type `open log location` to open your logs folder, they are saved along with your user settings folder. [More tips](https://flow-launcher.github.io/docs/#/usage-tips) @@ -74,7 +74,7 @@ If you are using Python plugins, flow will prompt to either select the location Vist [here](https://flow-launcher.github.io/docs/#/plugins) for our plugin portfolio. -If you are keen to write your own plugin for flow, please take a look at our plugin development documentation for [C#](https://flow-launcher.github.io/docs/#/develop-csharp-plugins) or [Python](https://flow-launcher.github.io/docs/#/develop-py-plugins) +If you are keen to write your own plugin for flow, please take a look at our plugin development documentation for [C#](https://flow-launcher.github.io/docs/#/develop-dotnet-plugins) or [Python](https://flow-launcher.github.io/docs/#/develop-py-plugins) ## Questions/Suggestions diff --git a/Scripts/post_build.ps1 b/Scripts/post_build.ps1 index b573b984b..139880d27 100644 --- a/Scripts/post_build.ps1 +++ b/Scripts/post_build.ps1 @@ -89,7 +89,7 @@ function Pack-Squirrel-Installer ($path, $version, $output) { Move-Item $temp\* $output -Force Remove-Item $temp - $file = "$output\Flow-Launcher-v$version.exe" + $file = "$output\Flow-Launcher-Setup.exe" Write-Host "Filename: $file" Move-Item "$output\Setup.exe" $file -Force @@ -109,7 +109,7 @@ function Publish-Self-Contained ($p) { function Publish-Portable ($outputLocation, $version) { - & $outputLocation\Flow-Launcher-v$v.exe --silent | Out-Null + & $outputLocation\Flow-Launcher-Setup.exe --silent | Out-Null mkdir "$env:LocalAppData\FlowLauncher\app-$version\UserData" Compress-Archive -Path $env:LocalAppData\FlowLauncher -DestinationPath $outputLocation\Flow-Launcher-Portable.zip }