diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index d0fee9559..2d6fdb7f0 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -1,7 +1,6 @@
crowdin
DWM
workflows
-Wpf
wpf
actionkeyword
stackoverflow
@@ -20,9 +19,7 @@ Prioritise
Segoe
Google
Customise
-UWP
uwp
-Uwp
Bokmal
Bokm
uninstallation
@@ -61,7 +58,6 @@ popup
ptr
pluginindicator
TobiasSekan
-Img
img
resx
bak
@@ -78,7 +74,6 @@ WCA_ACCENT_POLICY
HGlobal
dopusrt
firefox
-Firefox
msedge
svgc
ime
@@ -87,7 +82,6 @@ txb
btn
otf
searchplugin
-Noresult
wpftk
mkv
flac
@@ -108,4 +102,7 @@ Preinstalled
errormetadatafile
noresult
pluginsmanager
-alreadyexists
\ No newline at end of file
+alreadyexists
+JsonRPC
+JsonRPCV2
+Softpedia
diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt
index 903714aef..f29f57ad5 100644
--- a/.github/actions/spelling/patterns.txt
+++ b/.github/actions/spelling/patterns.txt
@@ -118,3 +118,6 @@
# UWP
[Uu][Ww][Pp]
+
+# version suffix v#
+(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\d+(?:\b|(?=[a-zA-Z_]))
diff --git a/.github/workflows/default_plugins.yml b/.github/workflows/default_plugins.yml
index 8000c5456..a2283defe 100644
--- a/.github/workflows/default_plugins.yml
+++ b/.github/workflows/default_plugins.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml
index 97d3cccb3..7aaa9296a 100644
--- a/.github/workflows/spelling.yml
+++ b/.github/workflows/spelling.yml
@@ -73,7 +73,7 @@ jobs:
steps:
- name: check-spelling
id: spelling
- uses: check-spelling/check-spelling@v0.0.22
+ uses: check-spelling/check-spelling@prerelease
with:
suppress_push_for_open_pull_request: 1
checkout: true
@@ -91,10 +91,9 @@ jobs:
extra_dictionaries:
cspell:software-terms/dict/softwareTerms.txt
cspell:win32/src/win32.txt
- cspell:php/src/php.txt
cspell:filetypes/filetypes.txt
cspell:csharp/csharp.txt
- cspell:dotnet/src/dotnet.txt
+ cspell:dotnet/dict/dotnet.txt
cspell:python/src/common/extra.txt
cspell:python/src/python/python-lib.txt
cspell:aws/aws.txt
@@ -130,7 +129,7 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
- uses: check-spelling/check-spelling@v0.0.22
+ uses: check-spelling/check-spelling@prerelease
with:
checkout: true
spell_check_this: check-spelling/spell-check-this@main
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index caac10c93..dd3fb2fca 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -13,7 +13,7 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/stale@v8
+ - uses: actions/stale@v9
with:
stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
days-before-stale: 45
diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
index 312dfdd9e..5cd09d407 100644
--- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj
+++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj
@@ -57,7 +57,7 @@
-
+
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
index b0075c8f0..f6e5e5879 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs
@@ -126,14 +126,15 @@ namespace Flow.Launcher.Core.Plugin
private async Task InitSettingAsync()
{
- if (!File.Exists(SettingConfigurationPath))
- return;
+ JsonRpcConfigurationModel configuration = null;
+ if (File.Exists(SettingConfigurationPath))
+ {
+ var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build();
+ configuration =
+ deserializer.Deserialize(
+ await File.ReadAllTextAsync(SettingConfigurationPath));
+ }
- var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
- .Build();
- var configuration =
- deserializer.Deserialize(
- await File.ReadAllTextAsync(SettingConfigurationPath));
Settings ??= new JsonRPCPluginSettings
{
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
index fd73a44cd..3848af6a4 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
@@ -11,15 +11,15 @@ namespace Flow.Launcher.Core.Plugin
{
public class JsonRPCPluginSettings
{
- public required JsonRpcConfigurationModel Configuration { get; init; }
+ public required JsonRpcConfigurationModel? Configuration { get; init; }
public required string SettingPath { get; init; }
public Dictionary SettingControls { get; } = new();
-
+
public IReadOnlyDictionary Inner => Settings;
protected ConcurrentDictionary Settings { get; set; }
public required IPublicAPI API { get; init; }
-
+
private JsonStorage> _storage;
// maybe move to resource?
@@ -37,13 +37,18 @@ namespace Flow.Launcher.Core.Plugin
_storage = new JsonStorage>(SettingPath);
Settings = await _storage.LoadAsync();
- foreach (var (type, attributes) in Configuration.Body)
+ if (Settings != null || Configuration == null)
+ {
+ return;
+ }
+
+ foreach (var (type, attributes) in Configuration.Body)
{
if (attributes.Name == null)
{
continue;
}
-
+
if (!Settings.ContainsKey(attributes.Name))
{
Settings[attributes.Name] = attributes.DefaultValue;
@@ -51,7 +56,7 @@ namespace Flow.Launcher.Core.Plugin
}
}
-
+
public void UpdateSettings(IReadOnlyDictionary settings)
{
if (settings == null || settings.Count == 0)
@@ -59,10 +64,7 @@ namespace Flow.Launcher.Core.Plugin
foreach (var (key, value) in settings)
{
- if (Settings.ContainsKey(key))
- {
- Settings[key] = value;
- }
+ Settings[key] = value;
if (SettingControls.TryGetValue(key, out var control))
{
@@ -78,33 +80,35 @@ namespace Flow.Launcher.Core.Plugin
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
break;
case CheckBox checkBox:
- checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string ?? string.Empty));
+ checkBox.Dispatcher.Invoke(() =>
+ checkBox.IsChecked = value is bool isChecked
+ ? isChecked
+ : bool.Parse(value as string ?? string.Empty));
break;
}
}
}
+
+ Save();
}
-
+
public async Task SaveAsync()
{
await _storage.SaveAsync();
}
-
+
public void Save()
{
_storage.Save();
}
-
+
public Control CreateSettingPanel()
{
- if (Settings == null)
+ if (Settings == null || Settings.Count == 0)
return new();
var settingWindow = new UserControl();
- var mainPanel = new Grid
- {
- Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center
- };
+ var mainPanel = new Grid { Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center };
ColumnDefinition gridCol1 = new ColumnDefinition();
ColumnDefinition gridCol2 = new ColumnDefinition();
@@ -239,10 +243,7 @@ namespace Flow.Launcher.Core.Plugin
Margin = new Thickness(10, 0, 0, 0), Content = "Browse"
};
- var dockPanel = new DockPanel()
- {
- Margin = settingControlMargin
- };
+ var dockPanel = new DockPanel() { Margin = settingControlMargin };
DockPanel.SetDock(Btn, Dock.Right);
dockPanel.Children.Add(Btn);
@@ -349,7 +350,10 @@ namespace Flow.Launcher.Core.Plugin
case "checkbox":
var checkBox = new CheckBox
{
- IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue),
+ IsChecked =
+ Settings[attribute.Name] is bool isChecked
+ ? isChecked
+ : bool.Parse(attribute.DefaultValue),
Margin = settingCheckboxMargin,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
ToolTip = attribute.Description
@@ -372,14 +376,12 @@ namespace Flow.Launcher.Core.Plugin
break;
case "hyperlink":
- var hyperlink = new Hyperlink
- {
- ToolTip = attribute.Description, NavigateUri = attribute.url
- };
+ var hyperlink = new Hyperlink { ToolTip = attribute.Description, NavigateUri = attribute.url };
var linkbtn = new System.Windows.Controls.Button
{
- HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin
+ HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
+ Margin = settingControlMargin
};
linkbtn.Content = attribute.urlLabel;
@@ -405,12 +407,9 @@ namespace Flow.Launcher.Core.Plugin
mainPanel.Children.Add(panel);
mainPanel.Children.Add(contentControl);
rowCount++;
-
}
return settingWindow;
}
-
-
}
}
diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
index 60130843e..390da072b 100644
--- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
+++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs
@@ -91,7 +91,7 @@ namespace Flow.Launcher.Core.Plugin
private void SetupJsonRPC()
{
- var formatter = new JsonMessageFormatter();
+ var formatter = new SystemTextJsonFormatter { JsonSerializerOptions = RequestSerializeOption };
var handler = new NewLineDelimitedMessageHandler(ClientPipe,
formatter);
@@ -100,10 +100,8 @@ namespace Flow.Launcher.Core.Plugin
RPC.AddLocalRpcMethod("UpdateResults", new Action((rawQuery, response) =>
{
var results = ParseResults(response);
- ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs { Query = new Query()
- {
- RawQuery = rawQuery
- }, Results = results });
+ ResultsUpdated?.Invoke(this,
+ new ResultUpdatedEventArgs { Query = new Query() { RawQuery = rawQuery }, Results = results });
}));
RPC.SynchronizationContext = null;
RPC.StartListening();
diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs
index 47edc21c2..7f4d4ea35 100644
--- a/Flow.Launcher.Core/Plugin/PluginManager.cs
+++ b/Flow.Launcher.Core/Plugin/PluginManager.cs
@@ -212,7 +212,19 @@ namespace Flow.Launcher.Core.Plugin
}
catch (Exception e)
{
- throw new FlowPluginException(metadata, e);
+ Result r = new()
+ {
+ Title = $"{metadata.Name}: Failed to respond!",
+ SubTitle = "Select this result for more info",
+ IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon,
+ PluginDirectory = metadata.PluginDirectory,
+ ActionKeywordAssigned = query.ActionKeyword,
+ PluginID = metadata.ID,
+ OriginQuery = query,
+ Action = _ => { throw new FlowPluginException(metadata, e);},
+ Score = -100
+ };
+ results.Add(r);
}
return results;
}
diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
index 24d06d975..a476f06e9 100644
--- a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
+++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs
@@ -17,7 +17,7 @@ namespace Flow.Launcher.Core.Plugin
protected abstract ProcessStartInfo StartInfo { get; set; }
- public Process ClientProcess { get; set; }
+ protected Process ClientProcess { get; set; }
public override async Task InitAsync(PluginInitContext context)
{
@@ -33,6 +33,8 @@ namespace Flow.Launcher.Core.Plugin
SetupPipe(ClientProcess);
+ ErrorStream = ClientProcess.StandardError;
+
await base.InitAsync(context);
}
diff --git a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
index 2f5259039..b24f069c1 100644
--- a/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
+++ b/Flow.Launcher.Infrastructure/Flow.Launcher.Infrastructure.csproj
@@ -53,6 +53,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs
index 203c5646a..add6d4e92 100644
--- a/Flow.Launcher.Infrastructure/Image/ImageLoader.cs
+++ b/Flow.Launcher.Infrastructure/Image/ImageLoader.cs
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
@@ -15,6 +16,7 @@ namespace Flow.Launcher.Infrastructure.Image
public static class ImageLoader
{
private static readonly ImageCache ImageCache = new();
+ private static SemaphoreSlim storageLock { get; } = new SemaphoreSlim(1, 1);
private static BinaryStorage> _storage;
private static readonly ConcurrentDictionary GuidToKey = new();
private static IImageHashGenerator _hashGenerator;
@@ -25,24 +27,18 @@ namespace Flow.Launcher.Infrastructure.Image
public const int FullIconSize = 256;
- private static readonly string[] ImageExtensions =
- {
- ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico"
- };
+ private static readonly string[] ImageExtensions = { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico" };
- public static void Initialize()
+ public static async Task InitializeAsync()
{
_storage = new BinaryStorage>("Image");
_hashGenerator = new ImageHashGenerator();
- var usage = LoadStorageToConcurrentDictionary();
+ var usage = await LoadStorageToConcurrentDictionaryAsync();
ImageCache.Initialize(usage.ToDictionary(x => x.Key, x => x.Value));
- foreach (var icon in new[]
- {
- Constant.DefaultIcon, Constant.MissingImgIcon
- })
+ foreach (var icon in new[] { Constant.DefaultIcon, Constant.MissingImgIcon })
{
ImageSource img = new BitmapImage(new Uri(icon));
img.Freeze();
@@ -58,29 +54,41 @@ namespace Flow.Launcher.Infrastructure.Image
await LoadAsync(path, isFullImage);
}
});
- Log.Info($"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
+ Log.Info(
+ $"|ImageLoader.Initialize|Number of preload images is <{ImageCache.CacheSize()}>, Images Number: {ImageCache.CacheSize()}, Unique Items {ImageCache.UniqueImagesInCache()}");
});
}
- public static void Save()
+ public static async Task Save()
{
- lock (_storage)
+ await storageLock.WaitAsync();
+
+ try
{
- _storage.Save(ImageCache.Data
+ _storage.SaveAsync(ImageCache.Data
.ToDictionary(
x => x.Key,
x => x.Value.usage));
}
+ finally
+ {
+ storageLock.Release();
+ }
}
- private static ConcurrentDictionary<(string, bool), int> LoadStorageToConcurrentDictionary()
+ private static async Task> LoadStorageToConcurrentDictionaryAsync()
{
- lock (_storage)
+ await storageLock.WaitAsync();
+ try
{
- var loaded = _storage.TryLoad(new Dictionary<(string, bool), int>());
+ var loaded = await _storage.TryLoadAsync(new Dictionary<(string, bool), int>());
return new ConcurrentDictionary<(string, bool), int>(loaded);
}
+ finally
+ {
+ storageLock.Release();
+ }
}
private class ImageResult
@@ -129,6 +137,7 @@ namespace Flow.Launcher.Infrastructure.Image
ImageCache[path, loadFullImage] = image;
return new ImageResult(image, ImageType.ImageFile);
}
+
if (path.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
var imageSource = new BitmapImage(new Uri(path));
@@ -158,6 +167,7 @@ namespace Flow.Launcher.Infrastructure.Image
return imageResult;
}
+
private static async Task LoadRemoteImageAsync(bool loadFullImage, Uri uriResult)
{
// Download image from url
@@ -173,6 +183,7 @@ namespace Flow.Launcher.Infrastructure.Image
image.DecodePixelHeight = SmallIconSize;
image.DecodePixelWidth = SmallIconSize;
}
+
image.StreamSource = buffer;
image.EndInit();
image.StreamSource = null;
@@ -188,8 +199,8 @@ namespace Flow.Launcher.Infrastructure.Image
if (Directory.Exists(path))
{
/* Directories can also have thumbnails instead of shell icons.
- * Generating thumbnails for a bunch of folder results while scrolling
- * could have a big impact on performance and Flow.Launcher responsibility.
+ * Generating thumbnails for a bunch of folder results while scrolling
+ * could have a big impact on performance and Flow.Launcher responsibility.
* - Solution: just load the icon
*/
type = ImageType.Folder;
@@ -208,9 +219,9 @@ namespace Flow.Launcher.Infrastructure.Image
}
else
{
- /* Although the documentation for GetImage on MSDN indicates that
+ /* Although the documentation for GetImage on MSDN indicates that
* if a thumbnail is available it will return one, this has proved to not
- * be the case in many situations while testing.
+ * be the case in many situations while testing.
* - Solution: explicitly pass the ThumbnailOnly flag
*/
image = GetThumbnail(path, ThumbnailOptions.ThumbnailOnly);
@@ -236,7 +247,8 @@ namespace Flow.Launcher.Infrastructure.Image
return new ImageResult(image, type);
}
- private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly, int size = SmallIconSize)
+ private static BitmapSource GetThumbnail(string path, ThumbnailOptions option = ThumbnailOptions.ThumbnailOnly,
+ int size = SmallIconSize)
{
return WindowsThumbnailProvider.GetThumbnail(
path,
@@ -261,17 +273,19 @@ namespace Flow.Launcher.Infrastructure.Image
var img = imageResult.ImageSource;
if (imageResult.ImageType != ImageType.Error && imageResult.ImageType != ImageType.Cache)
- { // we need to get image hash
+ {
+ // we need to get image hash
string hash = EnableImageHash ? _hashGenerator.GetHashFromImage(img) : null;
if (hash != null)
{
-
if (GuidToKey.TryGetValue(hash, out string key))
- { // image already exists
+ {
+ // image already exists
img = ImageCache[key, loadFullImage] ?? img;
}
else
- { // new guid
+ {
+ // new guid
GuidToKey[hash] = path;
}
@@ -289,7 +303,7 @@ namespace Flow.Launcher.Infrastructure.Image
BitmapImage image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
- image.UriSource = new Uri(path);
+ image.UriSource = new Uri(path);
image.CreateOptions = BitmapCreateOptions.IgnoreColorProfile;
image.EndInit();
@@ -314,8 +328,10 @@ namespace Flow.Launcher.Infrastructure.Image
resizedHeight.EndInit();
return resizedHeight;
}
+
return resizedWidth;
}
+
return image;
}
}
diff --git a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs
index ea2d42773..a679643fd 100644
--- a/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs
+++ b/Flow.Launcher.Infrastructure/Storage/BinaryStorage.cs
@@ -4,67 +4,65 @@ using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;
+using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
+using MemoryPack;
namespace Flow.Launcher.Infrastructure.Storage
{
-#pragma warning disable SYSLIB0011 // BinaryFormatter is obsolete.
///
/// Stroage object using binary data
/// Normally, it has better performance, but not readable
///
+ ///
+ /// It utilize MemoryPack, which means the object must be MemoryPackSerializable
+ /// https://github.com/Cysharp/MemoryPack
+ ///
public class BinaryStorage
{
+ const string DirectoryName = "Cache";
+
+ const string FileSuffix = ".cache";
+
public BinaryStorage(string filename)
{
- const string directoryName = "Cache";
- var directoryPath = Path.Combine(DataLocation.DataDirectory(), directoryName);
+ var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
Helper.ValidateDirectory(directoryPath);
- const string fileSuffix = ".cache";
- FilePath = Path.Combine(directoryPath, $"{filename}{fileSuffix}");
+ FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
}
public string FilePath { get; }
- public T TryLoad(T defaultData)
+ public async ValueTask TryLoadAsync(T defaultData)
{
if (File.Exists(FilePath))
{
if (new FileInfo(FilePath).Length == 0)
{
Log.Error($"|BinaryStorage.TryLoad|Zero length cache file <{FilePath}>");
- Save(defaultData);
+ await SaveAsync(defaultData);
return defaultData;
}
- using (var stream = new FileStream(FilePath, FileMode.Open))
- {
- var d = Deserialize(stream, defaultData);
- return d;
- }
+ await using var stream = new FileStream(FilePath, FileMode.Open);
+ var d = await DeserializeAsync(stream, defaultData);
+ return d;
}
else
{
Log.Info("|BinaryStorage.TryLoad|Cache file not exist, load default data");
- Save(defaultData);
+ await SaveAsync(defaultData);
return defaultData;
}
}
- private T Deserialize(FileStream stream, T defaultData)
+ private async ValueTask DeserializeAsync(Stream stream, T defaultData)
{
- //http://stackoverflow.com/questions/2120055/binaryformatter-deserialize-gives-serializationexception
- AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
- BinaryFormatter binaryFormatter = new BinaryFormatter
- {
- AssemblyFormat = FormatterAssemblyStyle.Simple
- };
-
try
{
- var t = ((T)binaryFormatter.Deserialize(stream)).NonNull();
+ var t = await MemoryPackSerializer.DeserializeAsync(stream);
return t;
}
catch (System.Exception e)
@@ -72,47 +70,12 @@ namespace Flow.Launcher.Infrastructure.Storage
Log.Exception($"|BinaryStorage.Deserialize|Deserialize error for file <{FilePath}>", e);
return defaultData;
}
- finally
- {
- AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
- }
}
- private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
+ public async ValueTask SaveAsync(T data)
{
- Assembly ayResult = null;
- string sShortAssemblyName = args.Name.Split(',')[0];
- Assembly[] ayAssemblies = AppDomain.CurrentDomain.GetAssemblies();
- foreach (Assembly ayAssembly in ayAssemblies)
- {
- if (sShortAssemblyName == ayAssembly.FullName.Split(',')[0])
- {
- ayResult = ayAssembly;
- break;
- }
- }
- return ayResult;
- }
-
- public void Save(T data)
- {
- using (var stream = new FileStream(FilePath, FileMode.Create))
- {
- BinaryFormatter binaryFormatter = new BinaryFormatter
- {
- AssemblyFormat = FormatterAssemblyStyle.Simple
- };
-
- try
- {
- binaryFormatter.Serialize(stream, data);
- }
- catch (SerializationException e)
- {
- Log.Exception($"|BinaryStorage.Save|serialize error for file <{FilePath}>", e);
- }
- }
+ await using var stream = new FileStream(FilePath, FileMode.Create);
+ await MemoryPackSerializer.SerializeAsync(stream, data);
}
}
-#pragma warning restore SYSLIB0011
}
diff --git a/Flow.Launcher.Plugin/ActionContext.cs b/Flow.Launcher.Plugin/ActionContext.cs
index d6ba4894e..e31c8e31d 100644
--- a/Flow.Launcher.Plugin/ActionContext.cs
+++ b/Flow.Launcher.Plugin/ActionContext.cs
@@ -50,5 +50,12 @@ namespace Flow.Launcher.Plugin
(AltPressed ? ModifierKeys.Alt : ModifierKeys.None) |
(WinPressed ? ModifierKeys.Windows : ModifierKeys.None);
}
+
+ public static readonly SpecialKeyState Default = new () {
+ CtrlPressed = false,
+ ShiftPressed = false,
+ AltPressed = false,
+ WinPressed = false
+ };
}
}
diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj
index c662fdeff..29414baa6 100644
--- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj
+++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj
@@ -54,7 +54,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
\ No newline at end of file
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index 295dd3e7a..f4a17761f 100644
--- a/Flow.Launcher/App.xaml.cs
+++ b/Flow.Launcher/App.xaml.cs
@@ -52,12 +52,13 @@ namespace Flow.Launcher
{
_portable.PreStartCleanUpAfterPortabilityUpdate();
- Log.Info("|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
+ Log.Info(
+ "|App.OnStartup|Begin Flow Launcher startup ----------------------------------------------------");
Log.Info($"|App.OnStartup|Runtime info:{ErrorReporting.RuntimeInfo()}");
RegisterAppDomainExceptions();
RegisterDispatcherUnhandledException();
- ImageLoader.Initialize();
+ var imageLoadertask = ImageLoader.InitializeAsync();
_settingsVM = new SettingWindowViewModel(_updater, _portable);
_settings = _settingsVM.Settings;
@@ -78,6 +79,8 @@ namespace Flow.Launcher
Http.Proxy = _settings.Proxy;
await PluginManager.InitializePluginsAsync(API);
+ await imageLoadertask;
+
var window = new MainWindow(_settings, _mainVM);
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
@@ -103,7 +106,8 @@ namespace Flow.Launcher
AutoUpdates();
API.SaveAppAllSettings();
- Log.Info("|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
+ Log.Info(
+ "|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
});
}
@@ -122,7 +126,8 @@ namespace Flow.Launcher
// but if it fails (permissions, etc) then don't keep retrying
// this also gives the user a visual indication in the Settings widget
_settings.StartFlowLauncherOnSystemStartup = false;
- Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), e.Message);
+ Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"),
+ e.Message);
}
}
}
diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs
index c832c258d..61bf0c4dc 100644
--- a/Flow.Launcher/ViewModel/MainViewModel.cs
+++ b/Flow.Launcher/ViewModel/MainViewModel.cs
@@ -285,7 +285,8 @@ namespace Flow.Launcher.ViewModel
}
var hideWindow = await result.ExecuteAsync(new ActionContext
{
- SpecialKeyState = GlobalHotkey.CheckModifiers()
+ // not null means pressing modifier key + number, should ignore the modifier key
+ SpecialKeyState = index is not null ? SpecialKeyState.Default : GlobalHotkey.CheckModifiers()
})
.ConfigureAwait(false);
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
index edaa3dd29..4ce8584fc 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
@@ -56,7 +56,7 @@
-
+
\ No newline at end of file
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 1c0bdaad7..6d1497327 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
@@ -45,7 +45,7 @@
-
+
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs
index f23ff71f0..811bec50c 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Settings.cs
@@ -10,6 +10,6 @@
public bool WarnFromUnknownSource { get; set; } = true;
- public bool AutoRestartAfterChanging { get; set; } = false;
+ public bool AutoRestartAfterChanging { get; set; } = true;
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
index ac23534b1..c02573ed8 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
@@ -15,24 +15,22 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher.Plugin.Program
{
- public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable, IDisposable
+ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, ISavable, IAsyncReloadable,
+ IDisposable
{
internal static Win32[] _win32s { get; set; }
- internal static UWP.Application[] _uwps { get; set; }
+ internal static UWPApp[] _uwps { get; set; }
internal static Settings _settings { get; set; }
internal static PluginInitContext Context { get; private set; }
private static BinaryStorage _win32Storage;
- private static BinaryStorage _uwpStorage;
+ private static BinaryStorage _uwpStorage;
private static readonly List emptyResults = new();
- private static readonly MemoryCacheOptions cacheOptions = new()
- {
- SizeLimit = 1560
- };
+ private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 };
private static MemoryCache cache = new(cacheOptions);
static Main()
@@ -41,8 +39,8 @@ namespace Flow.Launcher.Plugin.Program
public void Save()
{
- _win32Storage.Save(_win32s);
- _uwpStorage.Save(_uwps);
+ _win32Storage.SaveAsync(_win32s);
+ _uwpStorage.SaveAsync(_uwps);
}
public async Task> QueryAsync(Query query, CancellationToken token)
@@ -76,12 +74,12 @@ namespace Flow.Launcher.Plugin.Program
_settings = context.API.LoadSettingJsonStorage();
- Stopwatch.Normal("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", () =>
+ await Stopwatch.NormalAsync("|Flow.Launcher.Plugin.Program.Main|Preload programs cost", async () =>
{
_win32Storage = new BinaryStorage("Win32");
- _win32s = _win32Storage.TryLoad(Array.Empty());
- _uwpStorage = new BinaryStorage("UWP");
- _uwps = _uwpStorage.TryLoad(Array.Empty());
+ _win32s = await _win32Storage.TryLoadAsync(Array.Empty());
+ _uwpStorage = new BinaryStorage("UWP");
+ _uwps = await _uwpStorage.TryLoadAsync(Array.Empty());
});
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload win32 programs <{_win32s.Length}>");
Log.Info($"|Flow.Launcher.Plugin.Program.Main|Number of preload uwps <{_uwps.Length}>");
@@ -104,7 +102,7 @@ namespace Flow.Launcher.Plugin.Program
static void WatchProgramUpdate()
{
Win32.WatchProgramUpdate(_settings);
- _ = UWP.WatchPackageChange();
+ _ = UWPPackage.WatchPackageChange();
}
}
@@ -113,16 +111,16 @@ namespace Flow.Launcher.Plugin.Program
var win32S = Win32.All(_settings);
_win32s = win32S;
ResetCache();
- _win32Storage.Save(_win32s);
+ _win32Storage.SaveAsync(_win32s);
_settings.LastIndexTime = DateTime.Now;
}
public static void IndexUwpPrograms()
{
- var applications = UWP.All(_settings);
+ var applications = UWPPackage.All(_settings);
_uwps = applications;
ResetCache();
- _uwpStorage.Save(_uwps);
+ _uwpStorage.SaveAsync(_uwps);
_settings.LastIndexTime = DateTime.Now;
}
@@ -228,7 +226,8 @@ namespace Flow.Launcher.Plugin.Program
catch (Exception)
{
var title = Context.API.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error");
- var message = string.Format(Context.API.GetTranslation("flowlauncher_plugin_program_run_failed"), info.FileName);
+ var message = string.Format(Context.API.GetTranslation("flowlauncher_plugin_program_run_failed"),
+ info.FileName);
Context.API.ShowMsg(title, string.Format(message, info.FileName), string.Empty);
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
deleted file mode 100644
index d5924ba28..000000000
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
+++ /dev/null
@@ -1,737 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Security.Principal;
-using System.Threading.Tasks;
-using System.Windows.Media.Imaging;
-using Windows.ApplicationModel;
-using Windows.Management.Deployment;
-using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Plugin.Program.Logger;
-using Flow.Launcher.Plugin.SharedModels;
-using System.Threading.Channels;
-using System.Xml;
-using Windows.ApplicationModel.Core;
-using System.Windows.Input;
-
-namespace Flow.Launcher.Plugin.Program.Programs
-{
- [Serializable]
- public class UWP
- {
- public string Name { get; }
- public string FullName { get; }
- public string FamilyName { get; }
- public string Location { get; set; }
-
- public Application[] Apps { get; set; } = Array.Empty();
-
-
- public UWP(Package package)
- {
- Location = package.InstalledLocation.Path;
- Name = package.Id.Name;
- FullName = package.Id.FullName;
- FamilyName = package.Id.FamilyName;
- }
-
- public void InitAppsInPackage(Package package)
- {
- var apps = new List();
- // WinRT
- var appListEntries = package.GetAppListEntries();
- foreach (var app in appListEntries)
- {
- try
- {
- var tmp = new Application(app, this);
- apps.Add(tmp);
- }
- catch (Exception e)
- {
- ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" +
- "|Unexpected exception occurs when trying to construct a Application from package"
- + $"{FullName} from location {Location}", e);
- }
- }
- Apps = apps.ToArray();
-
- try
- {
- var xmlDoc = GetManifestXml();
- if (xmlDoc == null)
- {
- return;
- }
-
- var xmlRoot = xmlDoc.DocumentElement;
- var packageVersion = GetPackageVersionFromManifest(xmlRoot);
- if (!smallLogoNameFromVersion.TryGetValue(packageVersion, out string logoName) ||
- !bigLogoNameFromVersion.TryGetValue(packageVersion, out string bigLogoName))
- {
- return;
- }
-
- var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
- namespaceManager.AddNamespace("d", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); // still need a name
- namespaceManager.AddNamespace("rescap", "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities");
- namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10");
-
- var allowElevationNode = xmlRoot.SelectSingleNode("//rescap:Capability[@Name='allowElevation']", namespaceManager);
- bool packageCanElevate = allowElevationNode != null;
-
- var appsNode = xmlRoot.SelectSingleNode("d:Applications", namespaceManager);
- foreach (var app in Apps)
- {
- // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package
- // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
- var id = app.UserModelId.Split('!')[1];
- var appNode = appsNode?.SelectSingleNode($"d:Application[@Id='{id}']", namespaceManager);
- if (appNode != null)
- {
- app.CanRunElevated = packageCanElevate || Application.IfAppCanRunElevated(appNode);
-
- // local name to fit all versions
- var visualElement = appNode.SelectSingleNode($"*[local-name()='VisualElements']", namespaceManager);
- var logoUri = visualElement?.Attributes[logoName]?.Value;
- app.LogoPath = app.LogoPathFromUri(logoUri, (64, 64));
- // use small logo or may have a big margin
- var previewUri = visualElement?.Attributes[logoName]?.Value;
- app.PreviewImagePath = app.LogoPathFromUri(previewUri, (256, 256));
- }
- }
- }
- catch (Exception e)
- {
- ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" +
- "|Unexpected exception occurs when trying to construct a Application from package"
- + $"{FullName} from location {Location}", e);
- }
- }
-
- private XmlDocument GetManifestXml()
- {
- var manifest = Path.Combine(Location, "AppxManifest.xml");
- try
- {
- var file = File.ReadAllText(manifest);
- var xmlDoc = new XmlDocument();
- xmlDoc.LoadXml(file);
- return xmlDoc;
- }
- catch (FileNotFoundException e)
- {
- ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "AppxManifest.xml not found.", e);
- return null;
- }
- catch (Exception e)
- {
- ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "An unexpected error occurred and unable to parse AppxManifest.xml", e);
- return null;
- }
- }
-
- private PackageVersion GetPackageVersionFromManifest(XmlNode xmlRoot)
- {
- if (xmlRoot != null)
- {
-
- var namespaces = xmlRoot.Attributes;
- foreach (XmlAttribute ns in namespaces)
- {
- if (versionFromNamespace.TryGetValue(ns.Value, out var packageVersion))
- {
- return packageVersion;
- }
- }
-
- ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" +
- "|Trying to get the package version of the UWP program, but an unknown UWP app-manifest version in package "
- + $"{FullName} from location {Location}", new FormatException());
- return PackageVersion.Unknown;
- }
- else
- {
- ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" +
- "|Can't parse AppManifest.xml of package "
- + $"{FullName} from location {Location}", new ArgumentNullException(nameof(xmlRoot)));
- return PackageVersion.Unknown;
- }
- }
-
- private static readonly Dictionary versionFromNamespace = new()
- {
- {
- "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10
- },
- {
- "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81
- },
- {
- "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8
- },
- };
-
- private static readonly Dictionary smallLogoNameFromVersion = new()
- {
- {
- PackageVersion.Windows10, "Square44x44Logo"
- },
- {
- PackageVersion.Windows81, "Square30x30Logo"
- },
- {
- PackageVersion.Windows8, "SmallLogo"
- },
- };
-
- private static readonly Dictionary bigLogoNameFromVersion = new()
- {
- {
- PackageVersion.Windows10, "Square150x150Logo"
- },
- {
- PackageVersion.Windows81, "Square150x150Logo"
- },
- {
- PackageVersion.Windows8, "Logo"
- },
- };
-
- public static Application[] All(Settings settings)
- {
- var support = SupportUWP();
- if (support && settings.EnableUWP)
- {
- var applications = CurrentUserPackages().AsParallel().SelectMany(p =>
- {
- UWP u;
- try
- {
- u = new UWP(p);
- u.InitAppsInPackage(p);
- }
-#if !DEBUG
- catch (Exception e)
- {
- ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and unable to convert Package to UWP for {p.Id.FullName}", e);
- return Array.Empty();
- }
-#endif
-#if DEBUG //make developer aware and implement handling
- catch
- {
- throw;
- }
-#endif
- return u.Apps;
- }).ToArray();
-
- var updatedListWithoutDisabledApps = applications
- .Where(t1 => !Main._settings.DisabledProgramSources
- .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier));
-
- return updatedListWithoutDisabledApps.ToArray();
- }
- else
- {
- return Array.Empty();
- }
- }
-
- public static bool SupportUWP()
- {
- var windows10 = new Version(10, 0);
- var support = Environment.OSVersion.Version.Major >= windows10.Major;
- return support;
- }
-
- private static IEnumerable CurrentUserPackages()
- {
- var user = WindowsIdentity.GetCurrent().User;
-
- if (user != null)
- {
- var userId = user.Value;
- PackageManager packageManager;
- try
- {
- packageManager = new PackageManager();
- }
- catch
- {
- // Bug from https://github.com/microsoft/CsWinRT, using Microsoft.Windows.SDK.NET.Ref 10.0.19041.0.
- // Only happens on the first time, so a try catch can fix it.
- packageManager = new PackageManager();
- }
- var packages = packageManager.FindPackagesForUser(userId);
- packages = packages.Where(p =>
- {
- try
- {
- var f = p.IsFramework;
- var d = p.IsDevelopmentMode;
- var path = p.InstalledLocation.Path;
- return !f && !d && !string.IsNullOrEmpty(path);
- }
- catch (Exception e)
- {
- ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{p.Id.FullName}", "An unexpected error occurred and "
- + $"unable to verify if package is valid", e);
- return false;
- }
- });
- return packages;
- }
- else
- {
- return Array.Empty();
- }
- }
-
- private static Channel PackageChangeChannel = Channel.CreateBounded(1);
-
- public static async Task WatchPackageChange()
- {
- if (Environment.OSVersion.Version.Major >= 10)
- {
- var catalog = PackageCatalog.OpenForCurrentUser();
- catalog.PackageInstalling += (_, args) =>
- {
- if (args.IsComplete)
- PackageChangeChannel.Writer.TryWrite(default);
- };
- catalog.PackageUninstalling += (_, args) =>
- {
- if (args.IsComplete)
- PackageChangeChannel.Writer.TryWrite(default);
- };
- catalog.PackageUpdating += (_, args) =>
- {
- if (args.IsComplete)
- PackageChangeChannel.Writer.TryWrite(default);
- };
-
- while (await PackageChangeChannel.Reader.WaitToReadAsync().ConfigureAwait(false))
- {
- await Task.Delay(3000).ConfigureAwait(false);
- PackageChangeChannel.Reader.TryRead(out _);
- await Task.Run(Main.IndexUwpPrograms);
- }
-
- }
- }
-
- public override string ToString()
- {
- return FamilyName;
- }
-
- public override bool Equals(object obj)
- {
- if (obj is UWP uwp)
- {
- return FamilyName.Equals(uwp.FamilyName);
- }
- else
- {
- return false;
- }
- }
-
- public override int GetHashCode()
- {
- return FamilyName.GetHashCode();
- }
-
- [Serializable]
- public class Application : IProgram
- {
- private string _uid = string.Empty;
- public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); }
- public string DisplayName { get; set; } = string.Empty;
- public string Description { get; set; } = string.Empty;
- public string UserModelId { get; set; } = string.Empty;
- //public string BackgroundColor { get; set; } = string.Empty; // preserve for future use
- public string Name => DisplayName;
- public string Location { get; set; } = string.Empty;
-
- public bool Enabled { get; set; } = false;
- public bool CanRunElevated { get; set; } = false;
- public string LogoPath { get; set; } = string.Empty;
- public string PreviewImagePath { get; set; } = string.Empty;
-
- public Application(AppListEntry appListEntry, UWP package)
- {
- UserModelId = appListEntry.AppUserModelId;
- UniqueIdentifier = appListEntry.AppUserModelId;
- DisplayName = appListEntry.DisplayInfo.DisplayName;
- Description = appListEntry.DisplayInfo.Description;
- Location = package.Location;
- Enabled = true;
- }
-
- public Result Result(string query, IPublicAPI api)
- {
- string title;
- MatchResult matchResult;
-
- // We suppose Name won't be null
- if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description))
- {
- title = Name;
- matchResult = StringMatcher.FuzzySearch(query, Name);
- }
- else
- {
- title = $"{Name}: {Description}";
- var nameMatch = StringMatcher.FuzzySearch(query, Name);
- var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
- if (descriptionMatch.Score > nameMatch.Score)
- {
- for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
- {
- descriptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": "
- }
- matchResult = descriptionMatch;
- }
- else
- {
- matchResult = nameMatch;
- }
- }
-
- if (!matchResult.IsSearchPrecisionScoreMet())
- return null;
-
- var result = new Result
- {
- Title = title,
- AutoCompleteText = Name,
- SubTitle = Main._settings.HideAppsPath ? string.Empty : Location,
- IcoPath = LogoPath,
- Preview = new Result.PreviewInfo
- {
- IsMedia = false,
- PreviewImagePath = PreviewImagePath,
- Description = Description
- },
- Score = matchResult.Score,
- TitleHighlightData = matchResult.MatchData,
- ContextData = this,
- Action = e =>
- {
- // Ctrl + Enter to open containing folder
- bool openFolder = e.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control;
- if (openFolder)
- {
- Main.Context.API.OpenDirectory(Location);
- return true;
- }
-
- // Ctrl + Shift + Enter to run elevated
- bool elevated = e.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift);
-
- bool shouldRunElevated = elevated && CanRunElevated;
- _ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false);
- if (elevated && !shouldRunElevated)
- {
- var title = api.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error");
- var message = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator_not_supported_message");
- api.ShowMsg(title, message, string.Empty);
- }
-
- return true;
- }
- };
-
-
- return result;
- }
-
- public List ContextMenus(IPublicAPI api)
- {
- var contextMenus = new List
- {
- new Result
- {
- Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
- Action = _ =>
- {
- Main.Context.API.OpenDirectory(Location);
-
- return true;
- },
- IcoPath = "Images/folder.png",
- Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe838"),
- }
- };
-
- if (CanRunElevated)
- {
- contextMenus.Add(new Result
- {
- Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"),
- Action = _ =>
- {
- Task.Run(() => Launch(true)).ConfigureAwait(false);
- return true;
- },
- IcoPath = "Images/cmd.png",
- Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ef")
- });
- }
-
- return contextMenus;
- }
-
- private void Launch(bool elevated = false)
- {
- string command = "shell:AppsFolder\\" + UserModelId;
- command = Environment.ExpandEnvironmentVariables(command.Trim());
-
- var info = new ProcessStartInfo(command)
- {
- UseShellExecute = true,
- Verb = elevated ? "runas" : ""
- };
-
- Main.StartProcess(Process.Start, info);
- }
-
- internal static bool IfAppCanRunElevated(XmlNode appNode)
- {
- // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package
- // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
-
- return appNode?.Attributes["EntryPoint"]?.Value == "Windows.FullTrustApplication" ||
- appNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL";
- }
-
- internal string LogoPathFromUri(string uri, (int, int) desiredSize)
- {
- // all https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
- // windows 10 https://msdn.microsoft.com/en-us/library/windows/apps/dn934817.aspx
- // windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size
- // windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx
-
- if (string.IsNullOrWhiteSpace(uri))
- {
- ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
- $"|{UserModelId} 's logo uri is null or empty: {Location}", new ArgumentException("uri"));
- return string.Empty;
- }
-
- string path = Path.Combine(Location, uri);
-
- var pxCount = desiredSize.Item1 * desiredSize.Item2;
- var logoPath = TryToFindLogo(uri, path, pxCount);
- if (logoPath == string.Empty)
- {
- var tmp = Path.Combine(Location, "Assets", uri);
- if (!path.Equals(tmp, StringComparison.OrdinalIgnoreCase))
- {
- // TODO: Don't know why, just keep it at the moment
- // Maybe on older version of Windows 10?
- // for C:\Windows\MiracastView etc
- return TryToFindLogo(uri, tmp, pxCount);
- }
- }
- return logoPath;
-
- string TryToFindLogo(string uri, string path, int px)
- {
- var extension = Path.GetExtension(path);
- if (extension != null)
- {
- //if (File.Exists(path))
- //{
- // return path; // shortcut, avoid enumerating files
- //}
-
- var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44
- var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets
- if (String.IsNullOrEmpty(logoNamePrefix) || !Directory.Exists(logoDir))
- {
- // Known issue: Edge always triggers it since logo is not at uri
- ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
- $"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Location}", new FileNotFoundException());
- return string.Empty;
- }
-
- var logos = Directory.EnumerateFiles(logoDir, $"{logoNamePrefix}*{extension}");
-
- // Currently we don't care which one to choose
- // Just ignore all qualifiers
- // select like logo.[xxx_yyy].png
- // https://learn.microsoft.com/en-us/windows/uwp/app-resources/tailor-resources-lang-scale-contrast
-
- // todo select from file name like pt run
- var selected = logos.FirstOrDefault();
- var closest = selected;
- int min = int.MaxValue;
- foreach (var logo in logos)
- {
-
- var imageStream = File.OpenRead(logo);
- var decoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.None);
- var height = decoder.Frames[0].PixelHeight;
- var width = decoder.Frames[0].PixelWidth;
- int pixelCountDiff = Math.Abs(height * width - px);
- if (pixelCountDiff < min)
- {
- // try to find the closest to desired size
- closest = logo;
- if (pixelCountDiff == 0)
- break; // found
- min = pixelCountDiff;
- }
- }
-
- selected = closest;
- if (!string.IsNullOrEmpty(selected))
- {
- return selected;
- }
- else
- {
- ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
- $"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Location}", new FileNotFoundException());
- return string.Empty;
- }
- }
- else
- {
- ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
- $"|Unable to find extension from {uri} for {UserModelId} " +
- $"in package location {Location}", new FileNotFoundException());
- return string.Empty;
- }
- }
- }
-
-
- #region logo legacy
- // preserve for potential future use
-
- //public ImageSource Logo()
- //{
- // var logo = ImageFromPath(LogoPath);
- // var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package?
-
- // // todo magic! temp fix for cross thread object
- // plated.Freeze();
- // return plated;
- //}
- //private BitmapImage ImageFromPath(string path)
- //{
- // if (File.Exists(path))
- // {
- // var image = new BitmapImage();
- // image.BeginInit();
- // image.UriSource = new Uri(path);
- // image.CacheOption = BitmapCacheOption.OnLoad;
- // image.EndInit();
- // image.Freeze();
- // return image;
- // }
- // else
- // {
- // ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Available" : path)}" +
- // $"|Unable to get logo for {UserModelId} from {path} and" +
- // $" located in {Location}", new FileNotFoundException());
- // return new BitmapImage(new Uri(Constant.MissingImgIcon));
- // }
- //}
-
- //private ImageSource PlatedImage(BitmapImage image)
- //{
- // if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent")
- // {
- // var width = image.Width;
- // var height = image.Height;
- // var x = 0;
- // var y = 0;
-
- // var group = new DrawingGroup();
-
- // var converted = ColorConverter.ConvertFromString(BackgroundColor);
- // if (converted != null)
- // {
- // var color = (Color)converted;
- // var brush = new SolidColorBrush(color);
- // var pen = new Pen(brush, 1);
- // var backgroundArea = new Rect(0, 0, width, width);
- // var rectangle = new RectangleGeometry(backgroundArea);
- // var rectDrawing = new GeometryDrawing(brush, pen, rectangle);
- // group.Children.Add(rectDrawing);
-
- // var imageArea = new Rect(x, y, image.Width, image.Height);
- // var imageDrawing = new ImageDrawing(image, imageArea);
- // group.Children.Add(imageDrawing);
-
- // // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
- // var visual = new DrawingVisual();
- // var context = visual.RenderOpen();
- // context.DrawDrawing(group);
- // context.Close();
- // const int dpiScale100 = 96;
- // var bitmap = new RenderTargetBitmap(
- // Convert.ToInt32(width), Convert.ToInt32(height),
- // dpiScale100, dpiScale100,
- // PixelFormats.Pbgra32
- // );
- // bitmap.Render(visual);
- // return bitmap;
- // }
- // else
- // {
- // ProgramLogger.LogException($"|UWP|PlatedImage|{Location}" +
- // $"|Unable to convert background string {BackgroundColor} " +
- // $"to color for {Location}", new InvalidOperationException());
-
- // return new BitmapImage(new Uri(Constant.MissingImgIcon));
- // }
- // }
- // else
- // {
- // // todo use windows theme as background
- // return image;
- // }
- //}
-
- #endregion
- public override string ToString()
- {
- return $"{DisplayName}: {Description}";
- }
-
- public override bool Equals(object obj)
- {
- if (obj is Application other)
- {
- return UniqueIdentifier == other.UniqueIdentifier;
- }
- else
- {
- return false;
- }
- }
-
- public override int GetHashCode()
- {
- return UniqueIdentifier.GetHashCode();
- }
- }
-
- public enum PackageVersion
- {
- Windows10,
- Windows81,
- Windows8,
- Unknown
- }
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs
new file mode 100644
index 000000000..654897cc5
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs
@@ -0,0 +1,752 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Security.Principal;
+using System.Threading.Tasks;
+using System.Windows.Media.Imaging;
+using Windows.ApplicationModel;
+using Windows.Management.Deployment;
+using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Plugin.Program.Logger;
+using Flow.Launcher.Plugin.SharedModels;
+using System.Threading.Channels;
+using System.Xml;
+using Windows.ApplicationModel.Core;
+using System.Windows.Input;
+using MemoryPack;
+
+namespace Flow.Launcher.Plugin.Program.Programs
+{
+ [MemoryPackable]
+ public partial class UWPPackage
+ {
+ public string Name { get; }
+ public string FullName { get; }
+ public string FamilyName { get; }
+ public string Location { get; set; }
+
+ public UWPApp[] Apps { get; set; } = Array.Empty();
+
+
+ ///
+ /// For serialization
+ ///
+ [MemoryPackConstructor]
+ private UWPPackage()
+ {
+ }
+
+ public UWPPackage(Package package)
+ {
+ Location = package.InstalledLocation.Path;
+ Name = package.Id.Name;
+ FullName = package.Id.FullName;
+ FamilyName = package.Id.FamilyName;
+ }
+
+ public void InitAppsInPackage(Package package)
+ {
+ var apps = new List();
+ // WinRT
+ var appListEntries = package.GetAppListEntries();
+ foreach (var app in appListEntries)
+ {
+ try
+ {
+ var tmp = new UWPApp(app, this);
+ apps.Add(tmp);
+ }
+ catch (Exception e)
+ {
+ ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" +
+ "|Unexpected exception occurs when trying to construct a Application from package"
+ + $"{FullName} from location {Location}", e);
+ }
+ }
+
+ Apps = apps.ToArray();
+
+ try
+ {
+ var xmlDoc = GetManifestXml();
+ if (xmlDoc == null)
+ {
+ return;
+ }
+
+ var xmlRoot = xmlDoc.DocumentElement;
+ var packageVersion = GetPackageVersionFromManifest(xmlRoot);
+ if (!smallLogoNameFromVersion.TryGetValue(packageVersion, out string logoName) ||
+ !bigLogoNameFromVersion.TryGetValue(packageVersion, out string bigLogoName))
+ {
+ return;
+ }
+
+ var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
+ namespaceManager.AddNamespace("d",
+ "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); // still need a name
+ namespaceManager.AddNamespace("rescap",
+ "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities");
+ namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10");
+
+ var allowElevationNode =
+ xmlRoot.SelectSingleNode("//rescap:Capability[@Name='allowElevation']", namespaceManager);
+ bool packageCanElevate = allowElevationNode != null;
+
+ var appsNode = xmlRoot.SelectSingleNode("d:Applications", namespaceManager);
+ foreach (var app in Apps)
+ {
+ // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package
+ // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
+ var id = app.UserModelId.Split('!')[1];
+ var appNode = appsNode?.SelectSingleNode($"d:Application[@Id='{id}']", namespaceManager);
+ if (appNode != null)
+ {
+ app.CanRunElevated = packageCanElevate || UWPApp.IfAppCanRunElevated(appNode);
+
+ // local name to fit all versions
+ var visualElement =
+ appNode.SelectSingleNode($"*[local-name()='VisualElements']", namespaceManager);
+ var logoUri = visualElement?.Attributes[logoName]?.Value;
+ app.LogoPath = app.LogoPathFromUri(logoUri, (64, 64));
+ // use small logo or may have a big margin
+ var previewUri = visualElement?.Attributes[logoName]?.Value;
+ app.PreviewImagePath = app.LogoPathFromUri(previewUri, (256, 256));
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ ProgramLogger.LogException($"|UWP|InitAppsInPackage|{Location}" +
+ "|Unexpected exception occurs when trying to construct a Application from package"
+ + $"{FullName} from location {Location}", e);
+ }
+ }
+
+ private XmlDocument GetManifestXml()
+ {
+ var manifest = Path.Combine(Location, "AppxManifest.xml");
+ try
+ {
+ var file = File.ReadAllText(manifest);
+ var xmlDoc = new XmlDocument();
+ xmlDoc.LoadXml(file);
+ return xmlDoc;
+ }
+ catch (FileNotFoundException e)
+ {
+ ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}", "AppxManifest.xml not found.", e);
+ return null;
+ }
+ catch (Exception e)
+ {
+ ProgramLogger.LogException("UWP", "GetManifestXml", $"{Location}",
+ "An unexpected error occurred and unable to parse AppxManifest.xml", e);
+ return null;
+ }
+ }
+
+ private PackageVersion GetPackageVersionFromManifest(XmlNode xmlRoot)
+ {
+ if (xmlRoot != null)
+ {
+ var namespaces = xmlRoot.Attributes;
+ foreach (XmlAttribute ns in namespaces)
+ {
+ if (versionFromNamespace.TryGetValue(ns.Value, out var packageVersion))
+ {
+ return packageVersion;
+ }
+ }
+
+ ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" +
+ "|Trying to get the package version of the UWP program, but an unknown UWP app-manifest version in package "
+ + $"{FullName} from location {Location}", new FormatException());
+ return PackageVersion.Unknown;
+ }
+ else
+ {
+ ProgramLogger.LogException($"|UWP|GetPackageVersionFromManifest|{Location}" +
+ "|Can't parse AppManifest.xml of package "
+ + $"{FullName} from location {Location}",
+ new ArgumentNullException(nameof(xmlRoot)));
+ return PackageVersion.Unknown;
+ }
+ }
+
+ private static readonly Dictionary versionFromNamespace = new()
+ {
+ { "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 },
+ { "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81 },
+ { "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 },
+ };
+
+ private static readonly Dictionary smallLogoNameFromVersion = new()
+ {
+ { PackageVersion.Windows10, "Square44x44Logo" },
+ { PackageVersion.Windows81, "Square30x30Logo" },
+ { PackageVersion.Windows8, "SmallLogo" },
+ };
+
+ private static readonly Dictionary bigLogoNameFromVersion = new()
+ {
+ { PackageVersion.Windows10, "Square150x150Logo" },
+ { PackageVersion.Windows81, "Square150x150Logo" },
+ { PackageVersion.Windows8, "Logo" },
+ };
+
+ public static UWPApp[] All(Settings settings)
+ {
+ var support = SupportUWP();
+ if (support && settings.EnableUWP)
+ {
+ var applications = CurrentUserPackages().AsParallel().SelectMany(p =>
+ {
+ UWPPackage u;
+ try
+ {
+ u = new UWPPackage(p);
+ u.InitAppsInPackage(p);
+ }
+#if !DEBUG
+ catch (Exception e)
+ {
+ ProgramLogger.LogException($"|UWP|All|{p.InstalledLocation}|An unexpected error occurred and unable to convert Package to UWP for {p.Id.FullName}", e);
+ return Array.Empty();
+ }
+#endif
+#if DEBUG //make developer aware and implement handling
+ catch
+ {
+ throw;
+ }
+#endif
+ return u.Apps;
+ }).ToArray();
+
+ var updatedListWithoutDisabledApps = applications
+ .Where(t1 => !Main._settings.DisabledProgramSources
+ .Any(x => x.UniqueIdentifier == t1.UniqueIdentifier));
+
+ return updatedListWithoutDisabledApps.ToArray();
+ }
+ else
+ {
+ return Array.Empty();
+ }
+ }
+
+ public static bool SupportUWP()
+ {
+ var windows10 = new Version(10, 0);
+ var support = Environment.OSVersion.Version.Major >= windows10.Major;
+ return support;
+ }
+
+ private static IEnumerable CurrentUserPackages()
+ {
+ var user = WindowsIdentity.GetCurrent().User;
+
+ if (user != null)
+ {
+ var userId = user.Value;
+ PackageManager packageManager;
+ try
+ {
+ packageManager = new PackageManager();
+ }
+ catch
+ {
+ // Bug from https://github.com/microsoft/CsWinRT, using Microsoft.Windows.SDK.NET.Ref 10.0.19041.0.
+ // Only happens on the first time, so a try catch can fix it.
+ packageManager = new PackageManager();
+ }
+
+ var packages = packageManager.FindPackagesForUser(userId);
+ packages = packages.Where(p =>
+ {
+ try
+ {
+ var f = p.IsFramework;
+ var d = p.IsDevelopmentMode;
+ var path = p.InstalledLocation.Path;
+ return !f && !d && !string.IsNullOrEmpty(path);
+ }
+ catch (Exception e)
+ {
+ ProgramLogger.LogException("UWP", "CurrentUserPackages", $"{p.Id.FullName}",
+ "An unexpected error occurred and "
+ + $"unable to verify if package is valid", e);
+ return false;
+ }
+ });
+ return packages;
+ }
+ else
+ {
+ return Array.Empty();
+ }
+ }
+
+ private static Channel PackageChangeChannel = Channel.CreateBounded(1);
+
+ public static async Task WatchPackageChange()
+ {
+ if (Environment.OSVersion.Version.Major >= 10)
+ {
+ var catalog = PackageCatalog.OpenForCurrentUser();
+ catalog.PackageInstalling += (_, args) =>
+ {
+ if (args.IsComplete)
+ PackageChangeChannel.Writer.TryWrite(default);
+ };
+ catalog.PackageUninstalling += (_, args) =>
+ {
+ if (args.IsComplete)
+ PackageChangeChannel.Writer.TryWrite(default);
+ };
+ catalog.PackageUpdating += (_, args) =>
+ {
+ if (args.IsComplete)
+ PackageChangeChannel.Writer.TryWrite(default);
+ };
+
+ while (await PackageChangeChannel.Reader.WaitToReadAsync().ConfigureAwait(false))
+ {
+ await Task.Delay(3000).ConfigureAwait(false);
+ PackageChangeChannel.Reader.TryRead(out _);
+ await Task.Run(Main.IndexUwpPrograms);
+ }
+ }
+ }
+
+ public override string ToString()
+ {
+ return FamilyName;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is UWPPackage uwp)
+ {
+ return FamilyName.Equals(uwp.FamilyName);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public override int GetHashCode()
+ {
+ return FamilyName.GetHashCode();
+ }
+
+
+ public enum PackageVersion
+ {
+ Windows10,
+ Windows81,
+ Windows8,
+ Unknown
+ }
+ }
+
+ [MemoryPackable]
+ public partial class UWPApp : IProgram
+ {
+ private string _uid = string.Empty;
+
+ public string UniqueIdentifier
+ {
+ get => _uid;
+ set => _uid = value == null ? string.Empty : value.ToLowerInvariant();
+ }
+
+ public string DisplayName { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+
+ public string UserModelId { get; set; } = string.Empty;
+
+ //public string BackgroundColor { get; set; } = string.Empty; // preserve for future use
+ public string Name => DisplayName;
+ public string Location { get; set; } = string.Empty;
+
+ public bool Enabled { get; set; } = false;
+ public bool CanRunElevated { get; set; } = false;
+ public string LogoPath { get; set; } = string.Empty;
+ public string PreviewImagePath { get; set; } = string.Empty;
+
+ [MemoryPackConstructor]
+ private UWPApp()
+ {
+ }
+
+ public UWPApp(AppListEntry appListEntry, UWPPackage package)
+ {
+ UserModelId = appListEntry.AppUserModelId;
+ UniqueIdentifier = appListEntry.AppUserModelId;
+ DisplayName = appListEntry.DisplayInfo.DisplayName;
+ Description = appListEntry.DisplayInfo.Description;
+ Location = package.Location;
+ Enabled = true;
+ }
+
+ public Result Result(string query, IPublicAPI api)
+ {
+ string title;
+ MatchResult matchResult;
+
+ // We suppose Name won't be null
+ if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || Name.Equals(Description))
+ {
+ title = Name;
+ matchResult = StringMatcher.FuzzySearch(query, Name);
+ }
+ else
+ {
+ title = $"{Name}: {Description}";
+ var nameMatch = StringMatcher.FuzzySearch(query, Name);
+ var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
+ if (descriptionMatch.Score > nameMatch.Score)
+ {
+ for (int i = 0; i < descriptionMatch.MatchData.Count; i++)
+ {
+ descriptionMatch.MatchData[i] += Name.Length + 2; // 2 is ": "
+ }
+
+ matchResult = descriptionMatch;
+ }
+ else
+ {
+ matchResult = nameMatch;
+ }
+ }
+
+ if (!matchResult.IsSearchPrecisionScoreMet())
+ return null;
+
+ var result = new Result
+ {
+ Title = title,
+ AutoCompleteText = Name,
+ SubTitle = Main._settings.HideAppsPath ? string.Empty : Location,
+ IcoPath = LogoPath,
+ Preview = new Result.PreviewInfo
+ {
+ IsMedia = false, PreviewImagePath = PreviewImagePath, Description = Description
+ },
+ Score = matchResult.Score,
+ TitleHighlightData = matchResult.MatchData,
+ ContextData = this,
+ Action = e =>
+ {
+ // Ctrl + Enter to open containing folder
+ bool openFolder = e.SpecialKeyState.ToModifierKeys() == ModifierKeys.Control;
+ if (openFolder)
+ {
+ Main.Context.API.OpenDirectory(Location);
+ return true;
+ }
+
+ // Ctrl + Shift + Enter to run elevated
+ bool elevated = e.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift);
+
+ bool shouldRunElevated = elevated && CanRunElevated;
+ _ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false);
+ if (elevated && !shouldRunElevated)
+ {
+ var title = api.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error");
+ var message =
+ api.GetTranslation(
+ "flowlauncher_plugin_program_run_as_administrator_not_supported_message");
+ api.ShowMsg(title, message, string.Empty);
+ }
+
+ return true;
+ }
+ };
+
+
+ return result;
+ }
+
+ public List ContextMenus(IPublicAPI api)
+ {
+ var contextMenus = new List
+ {
+ new Result
+ {
+ Title = api.GetTranslation("flowlauncher_plugin_program_open_containing_folder"),
+ Action = _ =>
+ {
+ Main.Context.API.OpenDirectory(Location);
+
+ return true;
+ },
+ IcoPath = "Images/folder.png",
+ Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe838"),
+ }
+ };
+
+ if (CanRunElevated)
+ {
+ contextMenus.Add(new Result
+ {
+ Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"),
+ Action = _ =>
+ {
+ Task.Run(() => Launch(true)).ConfigureAwait(false);
+ return true;
+ },
+ IcoPath = "Images/cmd.png",
+ Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\xe7ef")
+ });
+ }
+
+ return contextMenus;
+ }
+
+ private void Launch(bool elevated = false)
+ {
+ string command = "shell:AppsFolder\\" + UserModelId;
+ command = Environment.ExpandEnvironmentVariables(command.Trim());
+
+ var info = new ProcessStartInfo(command) { UseShellExecute = true, Verb = elevated ? "runas" : "" };
+
+ Main.StartProcess(Process.Start, info);
+ }
+
+ internal static bool IfAppCanRunElevated(XmlNode appNode)
+ {
+ // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package
+ // and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
+
+ return appNode?.Attributes["EntryPoint"]?.Value == "Windows.FullTrustApplication" ||
+ appNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL";
+ }
+
+ internal string LogoPathFromUri(string uri, (int, int) desiredSize)
+ {
+ // all https://msdn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
+ // windows 10 https://msdn.microsoft.com/en-us/library/windows/apps/dn934817.aspx
+ // windows 8.1 https://msdn.microsoft.com/en-us/library/windows/apps/hh965372.aspx#target_size
+ // windows 8 https://msdn.microsoft.com/en-us/library/windows/apps/br211475.aspx
+
+ if (string.IsNullOrWhiteSpace(uri))
+ {
+ ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
+ $"|{UserModelId} 's logo uri is null or empty: {Location}",
+ new ArgumentException("uri"));
+ return string.Empty;
+ }
+
+ string path = Path.Combine(Location, uri);
+
+ var pxCount = desiredSize.Item1 * desiredSize.Item2;
+ var logoPath = TryToFindLogo(uri, path, pxCount);
+ if (logoPath == string.Empty)
+ {
+ var tmp = Path.Combine(Location, "Assets", uri);
+ if (!path.Equals(tmp, StringComparison.OrdinalIgnoreCase))
+ {
+ // TODO: Don't know why, just keep it at the moment
+ // Maybe on older version of Windows 10?
+ // for C:\Windows\MiracastView etc
+ return TryToFindLogo(uri, tmp, pxCount);
+ }
+ }
+
+ return logoPath;
+
+ string TryToFindLogo(string uri, string path, int px)
+ {
+ var extension = Path.GetExtension(path);
+ if (extension != null)
+ {
+ //if (File.Exists(path))
+ //{
+ // return path; // shortcut, avoid enumerating files
+ //}
+
+ var logoNamePrefix = Path.GetFileNameWithoutExtension(uri); // e.g Square44x44
+ var logoDir = Path.GetDirectoryName(path); // e.g ..\..\Assets
+ if (String.IsNullOrEmpty(logoNamePrefix) || !Directory.Exists(logoDir))
+ {
+ // Known issue: Edge always triggers it since logo is not at uri
+ ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
+ $"|{UserModelId} can't find logo uri for {uri} in package location (logo name or directory not found): {Location}",
+ new FileNotFoundException());
+ return string.Empty;
+ }
+
+ var logos = Directory.EnumerateFiles(logoDir, $"{logoNamePrefix}*{extension}");
+
+ // Currently we don't care which one to choose
+ // Just ignore all qualifiers
+ // select like logo.[xxx_yyy].png
+ // https://learn.microsoft.com/en-us/windows/uwp/app-resources/tailor-resources-lang-scale-contrast
+
+ // todo select from file name like pt run
+ var selected = logos.FirstOrDefault();
+ var closest = selected;
+ int min = int.MaxValue;
+ foreach (var logo in logos)
+ {
+ var imageStream = File.OpenRead(logo);
+ var decoder = BitmapDecoder.Create(imageStream, BitmapCreateOptions.IgnoreColorProfile,
+ BitmapCacheOption.None);
+ var height = decoder.Frames[0].PixelHeight;
+ var width = decoder.Frames[0].PixelWidth;
+ int pixelCountDiff = Math.Abs(height * width - px);
+ if (pixelCountDiff < min)
+ {
+ // try to find the closest to desired size
+ closest = logo;
+ if (pixelCountDiff == 0)
+ break; // found
+ min = pixelCountDiff;
+ }
+ }
+
+ selected = closest;
+ if (!string.IsNullOrEmpty(selected))
+ {
+ return selected;
+ }
+ else
+ {
+ ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
+ $"|{UserModelId} can't find logo uri for {uri} in package location (can't find specified logo): {Location}",
+ new FileNotFoundException());
+ return string.Empty;
+ }
+ }
+ else
+ {
+ ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Location}" +
+ $"|Unable to find extension from {uri} for {UserModelId} " +
+ $"in package location {Location}", new FileNotFoundException());
+ return string.Empty;
+ }
+ }
+ }
+
+
+ #region logo legacy
+
+ // preserve for potential future use
+
+ //public ImageSource Logo()
+ //{
+ // var logo = ImageFromPath(LogoPath);
+ // var plated = PlatedImage(logo); // TODO: maybe get plated directly from app package?
+
+ // // todo magic! temp fix for cross thread object
+ // plated.Freeze();
+ // return plated;
+ //}
+ //private BitmapImage ImageFromPath(string path)
+ //{
+ // if (File.Exists(path))
+ // {
+ // var image = new BitmapImage();
+ // image.BeginInit();
+ // image.UriSource = new Uri(path);
+ // image.CacheOption = BitmapCacheOption.OnLoad;
+ // image.EndInit();
+ // image.Freeze();
+ // return image;
+ // }
+ // else
+ // {
+ // ProgramLogger.LogException($"|UWP|ImageFromPath|{(string.IsNullOrEmpty(path) ? "Not Available" : path)}" +
+ // $"|Unable to get logo for {UserModelId} from {path} and" +
+ // $" located in {Location}", new FileNotFoundException());
+ // return new BitmapImage(new Uri(Constant.MissingImgIcon));
+ // }
+ //}
+
+ //private ImageSource PlatedImage(BitmapImage image)
+ //{
+ // if (!string.IsNullOrEmpty(BackgroundColor) && BackgroundColor != "transparent")
+ // {
+ // var width = image.Width;
+ // var height = image.Height;
+ // var x = 0;
+ // var y = 0;
+
+ // var group = new DrawingGroup();
+
+ // var converted = ColorConverter.ConvertFromString(BackgroundColor);
+ // if (converted != null)
+ // {
+ // var color = (Color)converted;
+ // var brush = new SolidColorBrush(color);
+ // var pen = new Pen(brush, 1);
+ // var backgroundArea = new Rect(0, 0, width, width);
+ // var rectangle = new RectangleGeometry(backgroundArea);
+ // var rectDrawing = new GeometryDrawing(brush, pen, rectangle);
+ // group.Children.Add(rectDrawing);
+
+ // var imageArea = new Rect(x, y, image.Width, image.Height);
+ // var imageDrawing = new ImageDrawing(image, imageArea);
+ // group.Children.Add(imageDrawing);
+
+ // // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
+ // var visual = new DrawingVisual();
+ // var context = visual.RenderOpen();
+ // context.DrawDrawing(group);
+ // context.Close();
+ // const int dpiScale100 = 96;
+ // var bitmap = new RenderTargetBitmap(
+ // Convert.ToInt32(width), Convert.ToInt32(height),
+ // dpiScale100, dpiScale100,
+ // PixelFormats.Pbgra32
+ // );
+ // bitmap.Render(visual);
+ // return bitmap;
+ // }
+ // else
+ // {
+ // ProgramLogger.LogException($"|UWP|PlatedImage|{Location}" +
+ // $"|Unable to convert background string {BackgroundColor} " +
+ // $"to color for {Location}", new InvalidOperationException());
+
+ // return new BitmapImage(new Uri(Constant.MissingImgIcon));
+ // }
+ // }
+ // else
+ // {
+ // // todo use windows theme as background
+ // return image;
+ // }
+ //}
+
+ #endregion
+
+ public override string ToString()
+ {
+ return $"{DisplayName}: {Description}";
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj is UWPApp other)
+ {
+ return UniqueIdentifier == other.UniqueIdentifier;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public override int GetHashCode()
+ {
+ return UniqueIdentifier.GetHashCode();
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
index 20f489c30..7d08d3670 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
@@ -16,14 +16,21 @@ using System.Threading.Channels;
using Flow.Launcher.Plugin.Program.Views.Models;
using IniParser;
using System.Windows.Input;
+using MemoryPack;
namespace Flow.Launcher.Plugin.Program.Programs
{
- [Serializable]
- public class Win32 : IProgram, IEquatable
+ [MemoryPackable]
+ public partial class Win32 : IProgram, IEquatable
{
public string Name { get; set; }
- public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison
+
+ public string UniqueIdentifier
+ {
+ get => _uid;
+ set => _uid = value == null ? string.Empty : value.ToLowerInvariant();
+ } // For path comparison
+
public string IcoPath { get; set; }
///
@@ -96,7 +103,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
bool useLocalizedName = !string.IsNullOrEmpty(LocalizedName) && !Name.Equals(LocalizedName);
string resultName = useLocalizedName ? LocalizedName : Name;
- if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) || resultName.Equals(Description))
+ if (!Main._settings.EnableDescription || string.IsNullOrWhiteSpace(Description) ||
+ resultName.Equals(Description))
{
title = resultName;
matchResult = StringMatcher.FuzzySearch(query, resultName);
@@ -113,6 +121,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
descriptionMatch.MatchData[i] += resultName.Length + 2; // 2 is ": "
}
+
matchResult = descriptionMatch;
}
else
@@ -129,10 +138,12 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
candidates.Add(ExecutableName);
}
+
if (useLocalizedName)
{
candidates.Add(Name);
}
+
matchResult = Match(query, candidates);
if (matchResult == null)
{
@@ -209,9 +220,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var info = new ProcessStartInfo
{
- FileName = FullPath,
- WorkingDirectory = ParentDirectory,
- UseShellExecute = true
+ FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true
};
Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info));
@@ -424,7 +433,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true)
+ private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes,
+ bool recursive = true)
{
if (!Directory.Exists(directory))
return Enumerable.Empty();
@@ -448,7 +458,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes, string[] protocols)
+ private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes,
+ string[] protocols)
{
// Disabled custom sources are not in DisabledProgramSources
var paths = directories.AsParallel()
@@ -466,14 +477,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
.Distinct();
var startupPaths = GetStartupPaths();
-
+
var programs = ExceptDisabledSource(allPrograms)
.Where(x => !startupPaths.Any(startup => FilesFolders.PathContains(startup, x)))
.Select(x => GetProgramFromPath(x, protocols));
return programs;
}
- private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols, List commonParents)
+ private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols,
+ List commonParents)
{
var pathEnv = Environment.GetEnvironmentVariable("Path");
if (String.IsNullOrEmpty(pathEnv))
@@ -515,7 +527,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
toFilter = toFilter.Distinct().Where(p => suffixes.Contains(Extension(p)));
var programs = ExceptDisabledSource(toFilter)
- .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid).ToList(); // ToList due to disposing issue
+ .Select(x => GetProgramFromPath(x, protocols)).Where(x => x.Valid)
+ .ToList(); // ToList due to disposing issue
return programs;
}
@@ -616,7 +629,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
.SelectMany(g =>
{
// is shortcut and in start menu
- var startMenu = g.Where(g => g.LnkResolvedPath != null && startMenuPaths.Any(x => FilesFolders.PathContains(x, g.FullPath))).ToList();
+ var startMenu = g.Where(g =>
+ g.LnkResolvedPath != null &&
+ startMenuPaths.Any(x => FilesFolders.PathContains(x, g.FullPath)))
+ .ToList();
if (startMenu.Any())
return startMenu.Take(1);
@@ -756,6 +772,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
while (reader.TryRead(out _))
{
}
+
await Task.Run(Main.IndexWin32Programs);
}
}
@@ -766,6 +783,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
throw new ArgumentException("Path Not Exist");
}
+
var watcher = new FileSystemWatcher(directory);
watcher.Created += static (_, _) => indexQueue.Writer.TryWrite(default);
@@ -804,8 +822,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
parents.Remove(source);
}
}
+
result.AddRange(parents.Select(x => x.Location));
}
+
return result.DistinctBy(x => x.ToLowerInvariant()).ToList();
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
index 156f33ebc..9879993fe 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Views/ProgramSetting.xaml.cs
@@ -87,9 +87,9 @@ namespace Flow.Launcher.Plugin.Program.Views
}
}
- public bool ShowUWPCheckbox => UWP.SupportUWP();
+ public bool ShowUWPCheckbox => UWPPackage.SupportUWP();
- public ProgramSetting(PluginInitContext context, Settings settings, Win32[] win32s, UWP.Application[] uwps)
+ public ProgramSetting(PluginInitContext context, Settings settings, Win32[] win32s, UWPApp[] uwps)
{
this.context = context;
_settings = settings;
@@ -149,9 +149,9 @@ namespace Flow.Launcher.Plugin.Program.Views
private void DeleteProgramSources(List itemsToDelete)
{
itemsToDelete.ForEach(t1 => _settings.ProgramSources
- .Remove(_settings.ProgramSources
- .Where(x => x.UniqueIdentifier == t1.UniqueIdentifier)
- .FirstOrDefault()));
+ .Remove(_settings.ProgramSources
+ .Where(x => x.UniqueIdentifier == t1.UniqueIdentifier)
+ .FirstOrDefault()));
itemsToDelete.ForEach(x => ProgramSettingDisplayList.Remove(x));
ReIndexing();
@@ -182,16 +182,20 @@ namespace Flow.Launcher.Plugin.Program.Views
{
if (selectedProgramSource.Enabled)
{
- ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, true); // sync status in win32, uwp and disabled
+ ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource },
+ true); // sync status in win32, uwp and disabled
ProgramSettingDisplay.RemoveDisabledFromSettings();
}
else
{
- ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource }, false);
+ ProgramSettingDisplay.SetProgramSourcesStatus(new List { selectedProgramSource },
+ false);
ProgramSettingDisplay.StoreDisabledInSettings();
}
+
ReIndexing();
}
+
programSourceView.SelectedIndex = selectedIndex;
}
}
@@ -233,7 +237,8 @@ namespace Flow.Launcher.Plugin.Program.Views
foreach (string directory in directories)
{
if (Directory.Exists(directory)
- && !ProgramSettingDisplayList.Any(x => x.UniqueIdentifier.Equals(directory, System.StringComparison.OrdinalIgnoreCase)))
+ && !ProgramSettingDisplayList.Any(x =>
+ x.UniqueIdentifier.Equals(directory, System.StringComparison.OrdinalIgnoreCase)))
{
var source = new ProgramSource(directory);
@@ -262,8 +267,8 @@ namespace Flow.Launcher.Plugin.Program.Views
private void btnProgramSourceStatus_OnClick(object sender, RoutedEventArgs e)
{
var selectedItems = programSourceView
- .SelectedItems.Cast()
- .ToList();
+ .SelectedItems.Cast()
+ .ToList();
if (selectedItems.Count == 0)
{
@@ -274,7 +279,8 @@ namespace Flow.Launcher.Plugin.Program.Views
if (IsAllItemsUserAdded(selectedItems))
{
- var msg = string.Format(context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source"));
+ var msg = string.Format(
+ context.API.GetTranslation("flowlauncher_plugin_program_delete_program_source"));
if (MessageBox.Show(msg, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
{
@@ -364,8 +370,8 @@ namespace Flow.Launcher.Plugin.Program.Views
private void programSourceView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedItems = programSourceView
- .SelectedItems.Cast()
- .ToList();
+ .SelectedItems.Cast()
+ .ToList();
if (IsAllItemsUserAdded(selectedItems))
{
@@ -400,7 +406,8 @@ namespace Flow.Launcher.Plugin.Program.Views
ListView listView = sender as ListView;
GridView gView = listView.View as GridView;
- var workingWidth = listView.ActualWidth - SystemParameters.VerticalScrollBarWidth; // take into account vertical scrollbar
+ var workingWidth =
+ listView.ActualWidth - SystemParameters.VerticalScrollBarWidth; // take into account vertical scrollbar
var col1 = 0.25;
var col2 = 0.15;
var col3 = 0.60;
diff --git a/README.md b/README.md
index f121f2b75..1b415b0a2 100644
--- a/README.md
+++ b/README.md
@@ -222,28 +222,27 @@ And you can download
-### SpotifyPremium
+### [SpotifyPremium](https://github.com/fow5040/Flow.Launcher.Plugin.SpotifyPremium)
-
-### Steam Search
+### [Steam Search](https://github.com/Garulf/Steam-Search)
-### Clipboard History
+### [Clipboard History](https://github.com/liberize/Flow.Launcher.Plugin.ClipboardHistory)
-### Home Assistant Commander
+### [Home Assistant Commander](https://github.com/Garulf/HA-Commander)
-### Colors
+### [Colors](https://github.com/Flow-Launcher/Flow.Launcher.Plugin.Color)
-### GitHub
+### [GitHub](https://github.com/JohnTheGr8/Flow.Plugin.Github)
-### Window Walker
+### [Window Walker](https://github.com/taooceros/Flow.Plugin.WindowWalker)
......and more!