Skip to content

Commit 27d7ef5

Browse files
authored
Merge branch 'dev' into back_query_results_before_change_query
2 parents ddbbd69 + 735cc14 commit 27d7ef5

File tree

32 files changed

+717
-156
lines changed

32 files changed

+717
-156
lines changed

Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Flow.Launcher.Infrastructure.Logger;
1+
using Flow.Launcher.Infrastructure.Logger;
22
using System;
33
using System.Collections.Generic;
44
using System.Threading;
@@ -21,7 +21,7 @@ public static class PluginsManifest
2121

2222
public static List<UserPlugin> UserPlugins { get; private set; }
2323

24-
public static async Task UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
24+
public static async Task<bool> UpdateManifestAsync(CancellationToken token = default, bool usePrimaryUrlOnly = false)
2525
{
2626
try
2727
{
@@ -31,8 +31,14 @@ public static async Task UpdateManifestAsync(CancellationToken token = default,
3131
{
3232
var results = await mainPluginStore.FetchAsync(token, usePrimaryUrlOnly).ConfigureAwait(false);
3333

34-
UserPlugins = results;
35-
lastFetchedAt = DateTime.Now;
34+
// If the results are empty, we shouldn't update the manifest because the results are invalid.
35+
if (results.Count != 0)
36+
{
37+
UserPlugins = results;
38+
lastFetchedAt = DateTime.Now;
39+
40+
return true;
41+
}
3642
}
3743
}
3844
catch (Exception e)
@@ -43,6 +49,8 @@ public static async Task UpdateManifestAsync(CancellationToken token = default,
4349
{
4450
manifestUpdateLock.Release();
4551
}
52+
53+
return false;
4654
}
4755
}
4856
}

Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.ComponentModel;
43
using System.Diagnostics.CodeAnalysis;
54
using System.IO;
65
using System.Runtime.CompilerServices;
@@ -121,10 +120,10 @@ public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = def
121120
return _api.HttpGetStreamAsync(url, token);
122121
}
123122

124-
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
123+
public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null,
125124
CancellationToken token = default)
126125
{
127-
return _api.HttpDownloadAsync(url, filePath, token);
126+
return _api.HttpDownloadAsync(url, filePath, reportProgress, token);
128127
}
129128

130129
public void AddActionKeyword(string pluginId, string newActionKeyword)
@@ -162,13 +161,11 @@ public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null
162161
_api.OpenDirectory(DirectoryPath, FileNameOrFilePath);
163162
}
164163

165-
166164
public void OpenUrl(string url, bool? inPrivate = null)
167165
{
168166
_api.OpenUrl(url, inPrivate);
169167
}
170168

171-
172169
public void OpenAppUri(string appUri)
173170
{
174171
_api.OpenAppUri(appUri);

Flow.Launcher.Core/Plugin/PythonPlugin.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Diagnostics;
1+
using System;
2+
using System.Diagnostics;
23
using System.IO;
34
using System.Text.Json;
45
using System.Threading;
@@ -25,14 +26,13 @@ public PythonPlugin(string filename)
2526

2627
var path = Path.Combine(Constant.ProgramDirectory, JsonRPC);
2728
_startInfo.EnvironmentVariables["PYTHONPATH"] = path;
29+
// Prevent Python from writing .py[co] files.
30+
// Because .pyc contains location infos which will prevent python portable.
31+
_startInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
2832

2933
_startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version;
3034
_startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory;
3135
_startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory;
32-
33-
34-
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
35-
_startInfo.ArgumentList.Add("-B");
3636
}
3737

3838
protected override Task<Stream> RequestAsync(JsonRPCRequestModel request, CancellationToken token = default)
@@ -50,10 +50,53 @@ protected override string Request(JsonRPCRequestModel rpcRequest, CancellationTo
5050
// TODO: Async Action
5151
return Execute(_startInfo);
5252
}
53+
5354
public override async Task InitAsync(PluginInitContext context)
5455
{
55-
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
56-
_startInfo.ArgumentList.Add("");
56+
// Run .py files via `-c <code>`
57+
if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
58+
{
59+
var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
60+
var libDirectory = Path.Combine(rootDirectory, "lib");
61+
var libPyWin32Directory = Path.Combine(libDirectory, "win32");
62+
var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
63+
var pluginDirectory = Path.Combine(rootDirectory, "plugin");
64+
65+
// This makes it easier for plugin authors to import their own modules.
66+
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
67+
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
68+
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
69+
_startInfo.ArgumentList.Add("-c");
70+
_startInfo.ArgumentList.Add(
71+
$"""
72+
import sys
73+
sys.path.append(r'{rootDirectory}')
74+
sys.path.append(r'{libDirectory}')
75+
sys.path.append(r'{libPyWin32LibDirectory}')
76+
sys.path.append(r'{libPyWin32Directory}')
77+
sys.path.append(r'{pluginDirectory}')
78+
79+
import runpy
80+
runpy.run_path(r'{context.CurrentPluginMetadata.ExecuteFilePath}', None, '__main__')
81+
"""
82+
);
83+
// Plugins always expect the JSON data to be in the third argument
84+
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
85+
_startInfo.ArgumentList.Add("");
86+
}
87+
// Run .pyz files as is
88+
else
89+
{
90+
// No need for -B flag because we're using PYTHONDONTWRITEBYTECODE env variable now,
91+
// but the plugins still expect data to be sent as the third argument, so we're keeping
92+
// the flag here, even though it's not necessary anymore.
93+
_startInfo.ArgumentList.Add("-B");
94+
_startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
95+
// Plugins always expect the JSON data to be in the third argument
96+
// (we're always setting it as _startInfo.ArgumentList[2] = ...).
97+
_startInfo.ArgumentList.Add("");
98+
}
99+
57100
await base.InitAsync(context);
58101
_startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory;
59102
}

Flow.Launcher.Core/Plugin/PythonPluginV2.cs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,45 @@ public PythonPluginV2(string filename)
2626

2727
var path = Path.Combine(Constant.ProgramDirectory, JsonRpc);
2828
StartInfo.EnvironmentVariables["PYTHONPATH"] = path;
29-
30-
//Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable
31-
StartInfo.ArgumentList.Add("-B");
29+
StartInfo.EnvironmentVariables["PYTHONDONTWRITEBYTECODE"] = "1";
3230
}
3331

3432
public override async Task InitAsync(PluginInitContext context)
3533
{
36-
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
34+
// Run .py files via `-c <code>`
35+
if (context.CurrentPluginMetadata.ExecuteFilePath.EndsWith(".py", StringComparison.OrdinalIgnoreCase))
36+
{
37+
var rootDirectory = context.CurrentPluginMetadata.PluginDirectory;
38+
var libDirectory = Path.Combine(rootDirectory, "lib");
39+
var libPyWin32Directory = Path.Combine(libDirectory, "win32");
40+
var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
41+
var pluginDirectory = Path.Combine(rootDirectory, "plugin");
42+
var filePath = context.CurrentPluginMetadata.ExecuteFilePath;
43+
44+
// This makes it easier for plugin authors to import their own modules.
45+
// They won't have to add `.`, `./lib`, or `./plugin` to their sys.path manually.
46+
// Instead of running the .py file directly, we pass the code we want to run as a CLI argument.
47+
// This code sets sys.path for the plugin author and then runs the .py file via runpy.
48+
StartInfo.ArgumentList.Add("-c");
49+
StartInfo.ArgumentList.Add(
50+
$"""
51+
import sys
52+
sys.path.append(r'{rootDirectory}')
53+
sys.path.append(r'{libDirectory}')
54+
sys.path.append(r'{libPyWin32LibDirectory}')
55+
sys.path.append(r'{libPyWin32Directory}')
56+
sys.path.append(r'{pluginDirectory}')
57+
58+
import runpy
59+
runpy.run_path(r'{filePath}', None, '__main__')
60+
"""
61+
);
62+
}
63+
// Run .pyz files as is
64+
else
65+
{
66+
StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
67+
}
3768
await base.InitAsync(context);
3869
}
3970

Flow.Launcher.Infrastructure/FileExplorerHelper.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,20 @@ public static string GetActiveExplorerPath()
1515
{
1616
var explorerWindow = GetActiveExplorer();
1717
string locationUrl = explorerWindow?.LocationURL;
18-
return !string.IsNullOrEmpty(locationUrl) ? new Uri(locationUrl).LocalPath + "\\" : null;
18+
return !string.IsNullOrEmpty(locationUrl) ? GetDirectoryPath(new Uri(locationUrl).LocalPath) : null;
19+
}
20+
21+
/// <summary>
22+
/// Get directory path from a file path
23+
/// </summary>
24+
private static string GetDirectoryPath(string path)
25+
{
26+
if (!path.EndsWith("\\"))
27+
{
28+
return path + "\\";
29+
}
30+
31+
return path;
1932
}
2033

2134
/// <summary>

Flow.Launcher.Infrastructure/Http/Http.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,50 @@ var userName when string.IsNullOrEmpty(userName) =>
8383
}
8484
}
8585

86-
public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default)
86+
public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default)
8787
{
8888
try
8989
{
9090
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
91+
9192
if (response.StatusCode == HttpStatusCode.OK)
9293
{
93-
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
94-
await response.Content.CopyToAsync(fileStream, token);
94+
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
95+
var canReportProgress = totalBytes != -1;
96+
97+
if (canReportProgress && reportProgress != null)
98+
{
99+
await using var contentStream = await response.Content.ReadAsStreamAsync(token);
100+
await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true);
101+
102+
var buffer = new byte[8192];
103+
long totalRead = 0;
104+
int read;
105+
double progressValue = 0;
106+
107+
reportProgress(0);
108+
109+
while ((read = await contentStream.ReadAsync(buffer, token)) > 0)
110+
{
111+
await fileStream.WriteAsync(buffer.AsMemory(0, read), token);
112+
totalRead += read;
113+
114+
progressValue = totalRead * 100.0 / totalBytes;
115+
116+
if (token.IsCancellationRequested)
117+
return;
118+
else
119+
reportProgress(progressValue);
120+
}
121+
122+
if (progressValue < 100)
123+
reportProgress(100);
124+
}
125+
else
126+
{
127+
await using var fileStream = new FileStream(filePath, FileMode.CreateNew);
128+
await response.Content.CopyToAsync(fileStream, token);
129+
}
95130
}
96131
else
97132
{

Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Flow.Launcher.Plugin.SharedModels;
1+
using Flow.Launcher.Plugin.SharedModels;
22
using JetBrains.Annotations;
33
using System;
44
using System.Collections.Generic;
@@ -181,9 +181,13 @@ public interface IPublicAPI
181181
/// </summary>
182182
/// <param name="url">URL to download file</param>
183183
/// <param name="filePath">path to save downloaded file</param>
184+
/// <param name="reportProgress">
185+
/// Action to report progress. The input of the action is the progress value which is a double value between 0 and 100.
186+
/// It will be called if url support range request and the reportProgress is not null.
187+
/// </param>
184188
/// <param name="token">place to store file</param>
185189
/// <returns>Task showing the progress</returns>
186-
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default);
190+
Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default);
187191

188192
/// <summary>
189193
/// Add ActionKeyword for specific plugin
@@ -316,5 +320,19 @@ public interface IPublicAPI
316320
/// <param name="defaultResult">Specifies the default result of the message box.</param>
317321
/// <returns>Specifies which message box button is clicked by the user.</returns>
318322
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK);
323+
324+
/// <summary>
325+
/// Displays a standardised Flow message box.
326+
/// If there is issue when showing the message box, it will return null.
327+
/// </summary>
328+
/// <param name="caption">The caption of the message box.</param>
329+
/// <param name="reportProgressAsync">
330+
/// Time-consuming task function, whose input is the action to report progress.
331+
/// The input of the action is the progress value which is a double value between 0 and 100.
332+
/// If there are any exceptions, this action will be null.
333+
/// </param>
334+
/// <param name="forceClosed">When user closes the progress box manually by button or esc key, this action will be called.</param>
335+
/// <returns>A progress box interface.</returns>
336+
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action forceClosed = null);
319337
}
320338
}

Flow.Launcher.Plugin/Query.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text.Json.Serialization;
1+
using System.Text.Json.Serialization;
52

63
namespace Flow.Launcher.Plugin
74
{
85
public class Query
96
{
107
public Query() { }
118

12-
[Obsolete("Use the default Query constructor.")]
13-
public Query(string rawQuery, string search, string[] terms, string[] searchTerms, string actionKeyword = "")
14-
{
15-
Search = search;
16-
RawQuery = rawQuery;
17-
SearchTerms = searchTerms;
18-
ActionKeyword = actionKeyword;
19-
}
20-
219
/// <summary>
2210
/// Raw query, this includes action keyword if it has
2311
/// We didn't recommend use this property directly. You should always use Search property.

Flow.Launcher.Plugin/Result.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,16 @@ public Result Clone()
185185
TitleHighlightData = TitleHighlightData,
186186
OriginQuery = OriginQuery,
187187
PluginDirectory = PluginDirectory,
188+
ContextData = ContextData,
189+
PluginID = PluginID,
190+
TitleToolTip = TitleToolTip,
191+
SubTitleToolTip = SubTitleToolTip,
192+
PreviewPanel = PreviewPanel,
193+
ProgressBar = ProgressBar,
194+
ProgressBarColor = ProgressBarColor,
195+
Preview = Preview,
196+
AddSelectedCount = AddSelectedCount,
197+
RecordKey = RecordKey
188198
};
189199
}
190200

@@ -252,6 +262,13 @@ public ValueTask<bool> ExecuteAsync(ActionContext context)
252262
/// </summary>
253263
public const int MaxScore = int.MaxValue;
254264

265+
/// <summary>
266+
/// The key to identify the record. This is used when FL checks whether the result is the topmost record. Or FL calculates the hashcode of the result for user selected records.
267+
/// This can be useful when your plugin will change the Title or SubTitle of the result dynamically.
268+
/// If the plugin does not specific this, FL just uses Title and SubTitle to identify this result.
269+
/// </summary>
270+
public string RecordKey { get; set; } = string.Empty;
271+
255272
/// <summary>
256273
/// Info of the preview section of a <see cref="Result"/>
257274
/// </summary>

Flow.Launcher/Flow.Launcher.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
101101
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
102102
<PackageReference Include="SemanticVersioning" Version="3.0.0" />
103-
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.0" />
103+
<PackageReference Include="VirtualizingWrapPanel" Version="2.1.1" />
104104
</ItemGroup>
105105

106106
<ItemGroup>

0 commit comments

Comments
 (0)