diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
index 41d062570f8..79d106ef24e 100644
--- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
@@ -183,6 +183,13 @@ public interface IPublicAPI
/// The actionkeyword that is supposed to be removed
void RemoveActionKeyword(string pluginId, string oldActionKeyword);
+ ///
+ /// Check whether specific ActionKeyword is assigned to any of the plugin
+ ///
+ /// The actionkeyword for checking
+ /// True if the actionkeyword is already assigned, False otherwise
+ bool ActionKeywordAssigned(string actionKeyword);
+
///
/// Log debug message
/// Message will only be logged in Debug mode
diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
index 78be463e4da..a5e3ec3df40 100644
--- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs
+++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs
@@ -7,6 +7,7 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
@@ -32,12 +33,11 @@ private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummy
{
new Result
{
- Title="Result 1"
+ Title = "Result 1"
},
-
new Result
{
- Title="Result 2"
+ Title = "Result 2"
}
};
}
@@ -46,15 +46,13 @@ private List MethodDirectoryInfoClassSearchReturnsTwoResults(Query dummy
private bool PreviousLocationNotExistReturnsFalse(string dummyString) => false;
+ [SupportedOSPlatform("windows7.0")]
[TestCase("C:\\SomeFolder\\", "directory='file:C:\\SomeFolder\\'")]
public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestrictionsShouldUseDirectoryString(string path, string expectedString)
{
- // Given
- var queryConstructor = new QueryConstructor(new Settings());
-
// When
var folderPath = path;
- var result = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(folderPath);
+ var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath);
// Then
Assert.IsTrue(result == expectedString,
@@ -62,6 +60,7 @@ public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestric
$"Actual: {result}{Environment.NewLine}");
}
+ [SupportedOSPlatform("windows7.0")]
[TestCase("C:\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\' ORDER BY System.FileName")]
[TestCase("C:\\SomeFolder\\", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType FROM SystemIndex WHERE directory='file:C:\\SomeFolder\\' ORDER BY System.FileName")]
public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_ThenQueryShouldUseExpectedString(string folderPath, string expectedString)
@@ -70,130 +69,68 @@ public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_Then
var queryConstructor = new QueryConstructor(new Settings());
//When
- var queryString = queryConstructor.QueryForTopLevelDirectorySearch(folderPath);
+ var queryString = queryConstructor.Directory(folderPath);
// Then
- Assert.IsTrue(queryString == expectedString,
+ Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "),
$"Expected string: {expectedString}{Environment.NewLine} " +
$"Actual string was: {queryString}{Environment.NewLine}");
}
- [TestCase("C:\\SomeFolder\\flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
- "FROM SystemIndex WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
- "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033))" +
- " AND directory='file:C:\\SomeFolder' ORDER BY System.FileName")]
+ [SupportedOSPlatform("windows7.0")]
+ [TestCase("C:\\SomeFolder", "flow.launcher.sln", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType" +
+ " FROM SystemIndex WHERE directory='file:C:\\SomeFolder'" +
+ " AND (System.FileName LIKE 'flow.launcher.sln%' OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"'))" +
+ " ORDER BY System.FileName")]
public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryShouldUseExpectedString(
- string userSearchString, string expectedString)
- {
- // Given
- var queryConstructor = new QueryConstructor(new Settings());
-
- //When
- var queryString = queryConstructor.QueryForTopLevelDirectorySearch(userSearchString);
-
- // Then
- Assert.IsTrue(queryString == expectedString,
- $"Expected string: {expectedString}{Environment.NewLine} " +
- $"Actual string was: {queryString}{Environment.NewLine}");
- }
-
- [TestCase("C:\\SomeFolder\\SomeApp", "(System.FileName LIKE 'SomeApp%' " +
- "OR CONTAINS(System.FileName,'\"SomeApp*\"',1033))" +
- " AND directory='file:C:\\SomeFolder'")]
- public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificItem_ThenQueryWhereRestrictionsShouldUseDirectoryString(
- string userSearchString, string expectedString)
+ string folderPath, string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
//When
- var queryString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectorySearch(userSearchString);
+ var queryString = queryConstructor.Directory(folderPath, userSearchString);
// Then
- Assert.IsTrue(queryString == expectedString,
- $"Expected string: {expectedString}{Environment.NewLine} " +
- $"Actual string was: {queryString}{Environment.NewLine}");
+ Assert.AreEqual(expectedString, queryString);
}
+ [SupportedOSPlatform("windows7.0")]
[TestCase("scope='file:'")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereRestrictionsShouldUseScopeString(string expectedString)
{
//When
- var resultString = QueryConstructor.QueryWhereRestrictionsForAllFilesAndFoldersSearch;
+ const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch;
// Then
- Assert.IsTrue(resultString == expectedString,
- $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
- $"Actual string was: {resultString}{Environment.NewLine}");
+ Assert.AreEqual(expectedString, resultString);
}
+ [SupportedOSPlatform("windows7.0")]
[TestCase("flow.launcher.sln", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" " +
- "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
- "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
+ "FROM \"SystemIndex\" WHERE (System.FileName LIKE 'flow.launcher.sln%' " +
+ "OR CONTAINS(System.FileName,'\"flow.launcher.sln*\"',1033)) AND scope='file:' ORDER BY System.FileName")]
+ [TestCase("", "SELECT TOP 100 \"System.FileName\", \"System.ItemUrl\", \"System.ItemType\" FROM \"SystemIndex\" WHERE WorkId IS NOT NULL AND scope='file:' ORDER BY System.FileName")]
public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
// Given
var queryConstructor = new QueryConstructor(new Settings());
- var baseQuery = queryConstructor.CreateBaseQuery();
-
+ var baseQuery = queryConstructor.BaseQueryHelper;
+
// system running this test could have different locale than the hard-coded 1033 LCID en-US.
var queryKeywordLocale = baseQuery.QueryKeywordLocale;
expectedString = expectedString.Replace("1033", queryKeywordLocale.ToString());
-
-
//When
- var resultString = queryConstructor.QueryForAllFilesAndFolders(userSearchString);
-
- // Then
- Assert.IsTrue(resultString == expectedString,
- $"Expected query string: {expectedString}{Environment.NewLine} " +
- $"Actual string was: {resultString}{Environment.NewLine}");
- }
-
- [TestCase]
- public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldContinueDirectoryInfoClassSearchAsync()
- {
- // Given
- var searchManager = new SearchManager(new Settings(), new PluginInitContext());
-
- // When
- var results = await searchManager.TopLevelDirectorySearchBehaviourAsync(
- MethodWindowsIndexSearchReturnsZeroResultsAsync,
- MethodDirectoryInfoClassSearchReturnsTwoResults,
- false,
- new Query(),
- "string not used",
- default);
+ var resultString = queryConstructor.FilesAndFolders(userSearchString);
// Then
- Assert.IsTrue(results.Count == 2,
- $"Expected to have 2 results from DirectoryInfoClassSearch {Environment.NewLine} " +
- $"Actual number of results is {results.Count} {Environment.NewLine}");
+ Assert.AreEqual(expectedString, resultString);
}
- [TestCase]
- public async Task GivenTopLevelDirectorySearch_WhenIndexSearchNotRequired_ThenSearchMethodShouldNotContinueDirectoryInfoClassSearchAsync()
- {
- // Given
- var searchManager = new SearchManager(new Settings(), new PluginInitContext());
-
- // When
- var results = await searchManager.TopLevelDirectorySearchBehaviourAsync(
- MethodWindowsIndexSearchReturnsZeroResultsAsync,
- MethodDirectoryInfoClassSearchReturnsTwoResults,
- true,
- new Query(),
- "string not used",
- default);
-
- // Then
- Assert.IsTrue(results.Count == 0,
- $"Expected to have 0 results because location is indexed {Environment.NewLine} " +
- $"Actual number of results is {results.Count} {Environment.NewLine}");
- }
+ [SupportedOSPlatform("windows7.0")]
[TestCase(@"some words", @"FREETEXT('some words')")]
public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSearch_ThenShouldReturnFreeTextString(
string querySearchString, string expectedString)
@@ -202,7 +139,7 @@ public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSe
var queryConstructor = new QueryConstructor(new Settings());
//When
- var resultString = queryConstructor.QueryWhereRestrictionsForFileContentSearch(querySearchString);
+ var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString);
// Then
Assert.IsTrue(resultString == expectedString,
@@ -210,8 +147,9 @@ public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSe
$"Actual string was: {resultString}{Environment.NewLine}");
}
+ [SupportedOSPlatform("windows7.0")]
[TestCase("some words", "SELECT TOP 100 System.FileName, System.ItemUrl, System.ItemType " +
- "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
+ "FROM SystemIndex WHERE FREETEXT('some words') AND scope='file:' ORDER BY System.FileName")]
public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseExpectedString(
string userSearchString, string expectedString)
{
@@ -219,7 +157,7 @@ public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseE
var queryConstructor = new QueryConstructor(new Settings());
//When
- var resultString = queryConstructor.QueryForFileContentSearch(userSearchString);
+ var resultString = queryConstructor.FileContent(userSearchString);
// Then
Assert.IsTrue(resultString == expectedString,
@@ -230,12 +168,15 @@ public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseE
public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileContentSearchRequiredShouldReturnTrue()
{
// Given
- var query = new Query { ActionKeyword = "doc:", Search = "search term" };
+ var query = new Query
+ {
+ ActionKeyword = "doc:", Search = "search term"
+ };
var searchManager = new SearchManager(new Settings(), new PluginInitContext());
// When
- var result = searchManager.IsFileContentSearch(query.ActionKeyword);
+ var result = SearchManager.IsFileContentSearch(query.ActionKeyword);
// Then
Assert.IsTrue(result,
@@ -303,24 +244,19 @@ public void WhenGivenAPath_ThenShouldReturnThePreviousDirectoryPathIfIncompleteO
$"Actual path string is {returnedPath} {Environment.NewLine}");
}
- [TestCase("c:\\SomeFolder\\>", "scope='file:c:\\SomeFolder'")]
- [TestCase("c:\\SomeFolder\\>SomeName", "(System.FileName LIKE 'SomeName%' "
- + "OR CONTAINS(System.FileName,'\"SomeName*\"',1033)) AND "
- + "scope='file:c:\\SomeFolder'")]
- public void GivenWindowsIndexSearch_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
+ [SupportedOSPlatform("windows7.0")]
+ [TestCase("c:\\SomeFolder", "scope='file:c:\\SomeFolder'")]
+ [TestCase("c:\\OtherFolder", "scope='file:c:\\OtherFolder'")]
+ public void GivenFilePath_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestrictionsShouldUseScopeString(string path, string expectedString)
{
- // Given
- var queryConstructor = new QueryConstructor(new Settings());
-
//When
- var resultString = queryConstructor.QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path);
+ var resultString = QueryConstructor.RecursiveDirectoryConstraint(path);
// Then
- Assert.IsTrue(resultString == expectedString,
- $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " +
- $"Actual string was: {resultString}{Environment.NewLine}");
+ Assert.AreEqual(expectedString, resultString);
}
+ [SupportedOSPlatform("windows7.0")]
[TestCase("c:\\somefolder\\>somefile", "*somefile*")]
[TestCase("c:\\somefolder\\somefile", "somefile*")]
[TestCase("c:\\somefolder\\", "*")]
@@ -331,9 +267,7 @@ public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSear
var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path);
// Then
- Assert.IsTrue(resultString == expectedString,
- $"Expected criteria string: {expectedString}{Environment.NewLine} " +
- $"Actual criteria string was: {resultString}{Environment.NewLine}");
+ Assert.AreEqual(expectedString, resultString);
}
}
}
diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs
index 6d65492507f..b802cb4579d 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -608,7 +608,7 @@ private void OnKeyDown(object sender, KeyEventArgs e)
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{
var queryWithoutActionKeyword =
- QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins).Search;
+ QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search;
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
{
diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs
index 5fef5499b84..92705597107 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -141,6 +141,8 @@ public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath,
public void AddActionKeyword(string pluginId, string newActionKeyword) =>
PluginManager.AddActionKeyword(pluginId, newActionKeyword);
+ public bool ActionKeywordAssigned(string actionKeyword) => PluginManager.ActionKeywordRegistered(actionKeyword);
+
public void RemoveActionKeyword(string pluginId, string oldActionKeyword) =>
PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
index 4bc6705f429..1a6159073e2 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ContextMenu.cs
@@ -9,6 +9,8 @@
using Flow.Launcher.Plugin.Explorer.Search;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
using System.Linq;
+using System.Windows.Controls;
+using System.Windows.Input;
using MessageBox = System.Windows.Forms.MessageBox;
using MessageBoxIcon = System.Windows.Forms.MessageBoxIcon;
using MessageBoxButton = System.Windows.Forms.MessageBoxButtons;
@@ -37,25 +39,25 @@ public List LoadContextMenus(Result selectedResult)
var contextMenus = new List();
if (selectedResult.ContextData is SearchResult record)
{
- if (record.Type == ResultType.File)
+ if (record.Type == ResultType.File && !string.IsNullOrEmpty(Settings.EditorPath))
contextMenus.Add(CreateOpenWithEditorResult(record));
if (record.Type == ResultType.Folder && record.WindowsIndexed)
+ {
contextMenus.Add(CreateAddToIndexSearchExclusionListResult(record));
-
+ contextMenus.Add(CreateOpenWithShellResult(record));
+ }
contextMenus.Add(CreateOpenContainingFolderResult(record));
- contextMenus.Add(CreateOpenWindowsIndexingOptions());
-
- if (record.ShowIndexState)
- contextMenus.Add(new Result {Title = "From index search: " + (record.WindowsIndexed ? "Yes" : "No"),
- SubTitle = "Location: " + record.FullPath,
- Score = 501, IcoPath = Constants.IndexImagePath});
+ if (record.WindowsIndexed)
+ {
+ contextMenus.Add(CreateOpenWindowsIndexingOptions());
+ }
var icoPath = (record.Type == ResultType.File) ? Constants.FileImagePath : Constants.FolderImagePath;
var fileOrFolder = (record.Type == ResultType.File) ? "file" : "folder";
- if (!Settings.QuickAccessLinks.Any(x => x.Path == record.FullPath))
+ if (Settings.QuickAccessLinks.All(x => x.Path != record.FullPath))
{
contextMenus.Add(new Result
{
@@ -63,13 +65,16 @@ public List LoadContextMenus(Result selectedResult)
SubTitle = string.Format(Context.API.GetTranslation("plugin_explorer_add_to_quickaccess_subtitle"), fileOrFolder),
Action = (context) =>
{
- Settings.QuickAccessLinks.Add(new AccessLink { Path = record.FullPath, Type = record.Type });
+ Settings.QuickAccessLinks.Add(new AccessLink
+ {
+ Path = record.FullPath, Type = record.Type
+ });
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess"),
- string.Format(
- Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess_detail"),
- fileOrFolder),
- Constants.ExplorerIconImageFullPath);
+ string.Format(
+ Context.API.GetTranslation("plugin_explorer_addfilefoldersuccess_detail"),
+ fileOrFolder),
+ Constants.ExplorerIconImageFullPath);
ViewModel.Save();
@@ -91,10 +96,10 @@ public List LoadContextMenus(Result selectedResult)
Settings.QuickAccessLinks.Remove(Settings.QuickAccessLinks.FirstOrDefault(x => x.Path == record.FullPath));
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess"),
- string.Format(
- Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess_detail"),
- fileOrFolder),
- Constants.ExplorerIconImageFullPath);
+ string.Format(
+ Context.API.GetTranslation("plugin_explorer_removefilefoldersuccess_detail"),
+ fileOrFolder),
+ Constants.ExplorerIconImageFullPath);
ViewModel.Save();
@@ -105,12 +110,12 @@ public List LoadContextMenus(Result selectedResult)
IcoPath = Constants.RemoveQuickAccessImagePath
});
}
-
+
contextMenus.Add(new Result
{
Title = Context.API.GetTranslation("plugin_explorer_copypath"),
SubTitle = $"Copy the current {fileOrFolder} path to clipboard",
- Action = (context) =>
+ Action = _ =>
{
try
{
@@ -132,11 +137,14 @@ public List LoadContextMenus(Result selectedResult)
{
Title = Context.API.GetTranslation("plugin_explorer_copyfilefolder") + $" {fileOrFolder}",
SubTitle = $"Copy the {fileOrFolder} to clipboard",
- Action = (context) =>
+ Action = _ =>
{
try
{
- Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection { record.FullPath });
+ Clipboard.SetFileDropList(new System.Collections.Specialized.StringCollection
+ {
+ record.FullPath
+ });
return true;
}
catch (Exception e)
@@ -151,7 +159,8 @@ public List LoadContextMenus(Result selectedResult)
IcoPath = icoPath
});
- if (record.Type == ResultType.File || record.Type == ResultType.Folder)
+
+ if (record.Type is ResultType.File or ResultType.Folder)
contextMenus.Add(new Result
{
Title = Context.API.GetTranslation("plugin_explorer_deletefilefolder") + $" {fileOrFolder}",
@@ -161,10 +170,10 @@ public List LoadContextMenus(Result selectedResult)
try
{
if (MessageBox.Show(
- string.Format(Context.API.GetTranslation("plugin_explorer_deletefilefolderconfirm"),fileOrFolder),
- string.Empty,
- MessageBoxButton.YesNo,
- MessageBoxIcon.Warning)
+ string.Format(Context.API.GetTranslation("plugin_explorer_deletefilefolderconfirm"), fileOrFolder),
+ string.Empty,
+ MessageBoxButton.YesNo,
+ MessageBoxIcon.Warning)
== DialogResult.No)
return false;
@@ -173,11 +182,11 @@ public List LoadContextMenus(Result selectedResult)
else
Directory.Delete(record.FullPath, true);
- Task.Run(() =>
+ _ = Task.Run(() =>
{
Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_deletefilefoldersuccess"),
- string.Format(Context.API.GetTranslation("plugin_explorer_deletefilefoldersuccess_detail"), fileOrFolder),
- Constants.ExplorerIconImageFullPath);
+ string.Format(Context.API.GetTranslation("plugin_explorer_deletefilefoldersuccess_detail"), fileOrFolder),
+ Constants.ExplorerIconImageFullPath);
});
}
catch (Exception e)
@@ -193,6 +202,52 @@ public List LoadContextMenus(Result selectedResult)
IcoPath = Constants.DeleteFileFolderImagePath
});
+ if (record.Type is not ResultType.Volume)
+ {
+ contextMenus.Add(new Result()
+ {
+ Title = Context.API.GetTranslation("plugin_explorer_show_contextmenu_title"),
+ IcoPath = Constants.ShowContextMenuImagePath,
+ Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue700"),
+ Action = _ =>
+ {
+ if (record.Type is ResultType.Volume)
+ return false;
+
+ var screenWithMouseCursor = System.Windows.Forms.Screen.FromPoint(System.Windows.Forms.Cursor.Position);
+ var xOfScreenCenter = screenWithMouseCursor.WorkingArea.Left + screenWithMouseCursor.WorkingArea.Width / 2;
+ var yOfScreenCenter = screenWithMouseCursor.WorkingArea.Top + screenWithMouseCursor.WorkingArea.Height / 2;
+ var showPosition = new System.Drawing.Point(xOfScreenCenter, yOfScreenCenter);
+
+ switch (record.Type)
+ {
+ case ResultType.File:
+ {
+ var fileInfos = new FileInfo[]
+ {
+ new(record.FullPath)
+ };
+
+ new Peter.ShellContextMenu().ShowContextMenu(fileInfos, showPosition);
+ break;
+ }
+ case ResultType.Folder:
+ {
+ var directoryInfos = new DirectoryInfo[]
+ {
+ new(record.FullPath)
+ };
+
+ new Peter.ShellContextMenu().ShowContextMenu(directoryInfos, showPosition);
+ break;
+ }
+ }
+
+ return false;
+ },
+ });
+ }
+
if (record.Type == ResultType.File && CanRunAsDifferentUser(record.FullPath))
contextMenus.Add(new Result
{
@@ -246,12 +301,13 @@ private Result CreateOpenContainingFolderResult(SearchResult record)
};
}
+
+
private Result CreateOpenWithEditorResult(SearchResult record)
{
- string editorPath = "Notepad.exe"; // TODO add the ability to create a custom editor
+ string editorPath = Settings.EditorPath;
- var name = Context.API.GetTranslation("plugin_explorer_openwitheditor")
- + " " + Path.GetFileNameWithoutExtension(editorPath);
+ var name = $"{Context.API.GetTranslation("plugin_explorer_openwitheditor")} {Path.GetFileNameWithoutExtension(editorPath)}";
return new Result
{
@@ -265,7 +321,38 @@ private Result CreateOpenWithEditorResult(SearchResult record)
}
catch (Exception e)
{
- var message = $"Failed to open editor for file at {record.FullPath}";
+ var message = $"Failed to open editor for file at {record.FullPath} with Editor {Path.GetFileNameWithoutExtension(editorPath)} at {editorPath}";
+ LogException(message, e);
+ Context.API.ShowMsgError(message);
+ return false;
+ }
+ },
+ IcoPath = Constants.FileImagePath
+ };
+ }
+
+ private Result CreateOpenWithShellResult(SearchResult record)
+ {
+ string shellPath = Settings.ShellPath;
+
+ var name = $"{Context.API.GetTranslation("plugin_explorer_openwithshell")} {Path.GetFileNameWithoutExtension(shellPath)}";
+
+ return new Result
+ {
+ Title = name,
+ Action = _ =>
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo()
+ {
+ FileName = shellPath, WorkingDirectory = record.FullPath
+ });
+ return true;
+ }
+ catch (Exception e)
+ {
+ var message = $"Failed to open editor for file at {record.FullPath} with Shell {Path.GetFileNameWithoutExtension(shellPath)} at {shellPath}";
LogException(message, e);
Context.API.ShowMsgError(message);
return false;
@@ -283,14 +370,17 @@ private Result CreateAddToIndexSearchExclusionListResult(SearchResult record)
SubTitle = Context.API.GetTranslation("plugin_explorer_path") + " " + record.FullPath,
Action = _ =>
{
- if(!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath))
- Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink { Path = record.FullPath });
+ if (!Settings.IndexSearchExcludedSubdirectoryPaths.Any(x => x.Path == record.FullPath))
+ Settings.IndexSearchExcludedSubdirectoryPaths.Add(new AccessLink
+ {
+ Path = record.FullPath
+ });
Task.Run(() =>
{
- Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"),
- Context.API.GetTranslation("plugin_explorer_path") +
- " " + record.FullPath, Constants.ExplorerIconImageFullPath);
+ Context.API.ShowMsg(Context.API.GetTranslation("plugin_explorer_excludedfromindexsearch_msg"),
+ Context.API.GetTranslation("plugin_explorer_path") +
+ " " + record.FullPath, Constants.ExplorerIconImageFullPath);
// so the new path can be persisted to storage and not wait till next ViewModel save.
Context.API.SaveAppAllSettings();
@@ -313,11 +403,11 @@ private Result CreateOpenWindowsIndexingOptions()
try
{
var psi = new ProcessStartInfo
- {
- FileName = "control.exe",
- UseShellExecute = true,
- Arguments = "srchadmin.dll"
- };
+ {
+ FileName = "control.exe",
+ UseShellExecute = true,
+ Arguments = "srchadmin.dll"
+ };
Process.Start(psi);
return true;
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x64/Everything.dll b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x64/Everything.dll
new file mode 100644
index 00000000000..6d093b79316
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x64/Everything.dll differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x86/Everything.dll b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x86/Everything.dll
new file mode 100644
index 00000000000..de73b87d109
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/EverythingSDK/x86/Everything.dll differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Exceptions/EngineNotAvailableException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Exceptions/EngineNotAvailableException.cs
new file mode 100644
index 00000000000..1a48892f597
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Exceptions/EngineNotAvailableException.cs
@@ -0,0 +1,48 @@
+#nullable enable
+
+using System;
+using System.Threading.Tasks;
+using System.Windows;
+using Flow.Launcher.Plugin.Explorer.Search.IProvider;
+using JetBrains.Annotations;
+
+namespace Flow.Launcher.Plugin.Explorer.Exceptions;
+
+public class EngineNotAvailableException : Exception
+{
+ public string EngineName { get; }
+ public string Resolution { get; }
+ public Func>? Action { get; }
+
+ public string? ErrorIcon { get; init; }
+
+ public EngineNotAvailableException(
+ string engineName,
+ string resolution,
+ string message,
+ Func> action = null) : base(message)
+ {
+ EngineName = engineName;
+ Resolution = resolution;
+ Action = action ?? (_ =>
+ {
+ Clipboard.SetDataObject(this.ToString());
+ return ValueTask.FromResult(true);
+ });
+ }
+
+ public EngineNotAvailableException(
+ string engineName,
+ string resolution,
+ string message,
+ Exception innerException) : base(message, innerException)
+ {
+ EngineName = engineName;
+ Resolution = resolution;
+ }
+
+ public override string ToString()
+ {
+ return $"Engine {EngineName} is not available.\n Try to {Resolution}\n {base.ToString()}";
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Exceptions/SearchException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Exceptions/SearchException.cs
new file mode 100644
index 00000000000..eef81a92169
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Exceptions/SearchException.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Exceptions
+{
+ public class SearchException : Exception
+ {
+ public string EngineName { get; }
+ public SearchException(string engineName, string message) : base(message)
+ {
+ EngineName = engineName;
+ }
+
+ public SearchException(string engineName, string message, Exception innerException) : base(message, innerException)
+ {
+ EngineName = engineName;
+ }
+
+ public override string ToString()
+ {
+ return $"{EngineName} Search Exception:\n {base.ToString()}";
+ }
+ }
+}
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 b4ab89a361d..62cb599a18f 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Flow.Launcher.Plugin.Explorer.csproj
@@ -8,6 +8,7 @@
true
false
en
+ warnings
@@ -24,6 +25,12 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
@@ -39,6 +46,7 @@
+
@@ -47,5 +55,5 @@
-
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenu.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenu.cs
new file mode 100644
index 00000000000..3870c487671
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/ShellContextMenu.cs
@@ -0,0 +1,1615 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Runtime.InteropServices;
+using System.Drawing;
+using System.Windows.Forms;
+using System.IO;
+using System.Security.Permissions;
+
+namespace Peter
+{
+ // Code from https://www.codeproject.com/Articles/22012/Explorer-Shell-Context-Menu:
+ ///
+ /// "Stand-alone" shell context menu
+ ///
+ /// It isn't really debugged but is mostly working.
+ /// Create an instance and call ShowContextMenu with a list of FileInfo for the files.
+ /// Limitation is that it only handles files in the same directory but it can be fixed
+ /// by changing the way files are translated into PIDLs.
+ ///
+ /// Based on FileBrowser in C# from CodeProject
+ /// http://www.codeproject.com/useritems/FileBrowser.asp
+ ///
+ /// Hooking class taken from MSDN Magazine Cutting Edge column
+ /// http://msdn.microsoft.com/msdnmag/issues/02/10/CuttingEdge/
+ ///
+ /// Andreas Johansson
+ /// afjohansson@hotmail.com
+ /// http://afjohansson.spaces.live.com
+ ///
+ ///
+ /// ShellContextMenu scm = new ShellContextMenu();
+ /// FileInfo[] files = new FileInfo[1];
+ /// files[0] = new FileInfo(@"c:\windows\notepad.exe");
+ /// scm.ShowContextMenu(this.Handle, files, Cursor.Position);
+ ///
+ public class ShellContextMenu : NativeWindow
+ {
+ #region Constructor
+
+ /// Default constructor
+ public ShellContextMenu()
+ {
+ this.CreateHandle(new CreateParams());
+ }
+
+ #endregion
+
+ #region Destructor
+
+ /// Ensure all resources get released
+ ~ShellContextMenu()
+ {
+ ReleaseAll();
+ }
+
+ #endregion
+
+ #region GetContextMenuInterfaces()
+
+ /// Gets the interfaces to the context menu
+ /// Parent folder
+ /// PIDLs
+ /// true if it got the interfaces, otherwise false
+ private bool GetContextMenuInterfaces(IShellFolder oParentFolder, IntPtr[] arrPIDLs, out IntPtr ctxMenuPtr)
+ {
+ int nResult = oParentFolder.GetUIObjectOf(
+ IntPtr.Zero,
+ (uint)arrPIDLs.Length,
+ arrPIDLs,
+ ref IID_IContextMenu,
+ IntPtr.Zero,
+ out ctxMenuPtr);
+
+ if (S_OK == nResult)
+ {
+ _oContextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(ctxMenuPtr, typeof(IContextMenu));
+
+ return true;
+ }
+ else
+ {
+ ctxMenuPtr = IntPtr.Zero;
+ _oContextMenu = null;
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Override
+
+ ///
+ /// This method receives WindowMessages. It will make the "Open With" and "Send To" work
+ /// by calling HandleMenuMsg and HandleMenuMsg2. It will also call the OnContextMenuMouseHover
+ /// method of Browser when hovering over a ContextMenu item.
+ ///
+ /// the Message of the Browser's WndProc
+ /// true if the message has been handled, false otherwise
+ protected override void WndProc(ref Message m)
+ {
+ #region IContextMenu
+
+ if (_oContextMenu != null &&
+ m.Msg == (int)WM.MENUSELECT &&
+ (ShellHelper.HiWord(m.WParam) & (nint)MFT.SEPARATOR) == 0 &&
+ (ShellHelper.HiWord(m.WParam) & (nint)MFT.POPUP) == 0)
+ {
+ string info = string.Empty;
+
+ if (ShellHelper.LoWord(m.WParam) == (nint)CMD_CUSTOM.ExpandCollapse)
+ info = "Expands or collapses the current selected item";
+ else
+ {
+ info = "";
+ }
+ }
+
+ #endregion
+
+ #region IContextMenu2
+
+ if (_oContextMenu2 != null &&
+ (m.Msg == (int)WM.INITMENUPOPUP ||
+ m.Msg == (int)WM.MEASUREITEM ||
+ m.Msg == (int)WM.DRAWITEM))
+ {
+ if (_oContextMenu2.HandleMenuMsg(
+ (uint)m.Msg, m.WParam, m.LParam) == S_OK)
+ return;
+ }
+
+ #endregion
+
+ #region IContextMenu3
+
+ if (_oContextMenu3 != null &&
+ m.Msg == (int)WM.MENUCHAR)
+ {
+ if (_oContextMenu3.HandleMenuMsg2(
+ (uint)m.Msg, m.WParam, m.LParam, IntPtr.Zero) == S_OK)
+ return;
+ }
+
+ #endregion
+
+ base.WndProc(ref m);
+ }
+
+ #endregion
+
+ #region InvokeCommand
+
+ private void InvokeCommand(IContextMenu oContextMenu, uint nCmd, string strFolder, Point pointInvoke)
+ {
+ CMINVOKECOMMANDINFOEX invoke = new CMINVOKECOMMANDINFOEX();
+ invoke.cbSize = cbInvokeCommand;
+ invoke.lpVerb = (IntPtr)(nCmd - CMD_FIRST);
+ invoke.lpDirectory = strFolder;
+ invoke.lpVerbW = (IntPtr)(nCmd - CMD_FIRST);
+ invoke.lpDirectoryW = strFolder;
+ invoke.fMask = CMIC.UNICODE | CMIC.PTINVOKE |
+ ((Control.ModifierKeys & Keys.Control) != 0 ? CMIC.CONTROL_DOWN : 0) |
+ ((Control.ModifierKeys & Keys.Shift) != 0 ? CMIC.SHIFT_DOWN : 0);
+ invoke.ptInvoke = new POINT(pointInvoke.X, pointInvoke.Y);
+ invoke.nShow = SW.SHOWNORMAL;
+
+ oContextMenu.InvokeCommand(ref invoke);
+ }
+
+ #endregion
+
+ #region ReleaseAll()
+
+ ///
+ /// Release all allocated interfaces, PIDLs
+ ///
+ private void ReleaseAll()
+ {
+ if (null != _oContextMenu)
+ {
+ Marshal.ReleaseComObject(_oContextMenu);
+ _oContextMenu = null;
+ }
+ if (null != _oContextMenu2)
+ {
+ Marshal.ReleaseComObject(_oContextMenu2);
+ _oContextMenu2 = null;
+ }
+ if (null != _oContextMenu3)
+ {
+ Marshal.ReleaseComObject(_oContextMenu3);
+ _oContextMenu3 = null;
+ }
+ if (null != _oDesktopFolder)
+ {
+ Marshal.ReleaseComObject(_oDesktopFolder);
+ _oDesktopFolder = null;
+ }
+ if (null != _oParentFolder)
+ {
+ Marshal.ReleaseComObject(_oParentFolder);
+ _oParentFolder = null;
+ }
+ if (null != _arrPIDLs)
+ {
+ FreePIDLs(_arrPIDLs);
+ _arrPIDLs = null;
+ }
+ }
+
+ #endregion
+
+ #region GetDesktopFolder()
+
+ ///
+ /// Gets the desktop folder
+ ///
+ /// IShellFolder for desktop folder
+ private IShellFolder GetDesktopFolder()
+ {
+ IntPtr pUnkownDesktopFolder = IntPtr.Zero;
+
+ if (null == _oDesktopFolder)
+ {
+ // Get desktop IShellFolder
+ int nResult = SHGetDesktopFolder(out pUnkownDesktopFolder);
+ if (S_OK != nResult)
+ {
+ throw new ShellContextMenuException("Failed to get the desktop shell folder");
+ }
+ _oDesktopFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pUnkownDesktopFolder, typeof(IShellFolder));
+ }
+
+ return _oDesktopFolder;
+ }
+
+ #endregion
+
+ #region GetParentFolder()
+
+ ///
+ /// Gets the parent folder
+ ///
+ /// Folder path
+ /// IShellFolder for the folder (relative from the desktop)
+ private IShellFolder GetParentFolder(string folderName)
+ {
+ if (null == _oParentFolder)
+ {
+ IShellFolder oDesktopFolder = GetDesktopFolder();
+ if (null == oDesktopFolder)
+ {
+ return null;
+ }
+
+ // Get the PIDL for the folder file is in
+ IntPtr pPIDL = IntPtr.Zero;
+ uint pchEaten = 0;
+ SFGAO pdwAttributes = 0;
+ int nResult = oDesktopFolder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, folderName, ref pchEaten, out pPIDL, ref pdwAttributes);
+ if (S_OK != nResult)
+ {
+ return null;
+ }
+
+ IntPtr pStrRet = Marshal.AllocCoTaskMem(MAX_PATH * 2 + 4);
+ Marshal.WriteInt32(pStrRet, 0, 0);
+ nResult = _oDesktopFolder.GetDisplayNameOf(pPIDL, SHGNO.FORPARSING, pStrRet);
+ StringBuilder strFolder = new StringBuilder(MAX_PATH);
+ StrRetToBuf(pStrRet, pPIDL, strFolder, MAX_PATH);
+ Marshal.FreeCoTaskMem(pStrRet);
+ pStrRet = IntPtr.Zero;
+ _strParentFolder = strFolder.ToString();
+
+ // Get the IShellFolder for folder
+ IntPtr pUnknownParentFolder = IntPtr.Zero;
+ nResult = oDesktopFolder.BindToObject(pPIDL, IntPtr.Zero, ref IID_IShellFolder, out pUnknownParentFolder);
+ // Free the PIDL first
+ Marshal.FreeCoTaskMem(pPIDL);
+ if (S_OK != nResult)
+ {
+ return null;
+ }
+ _oParentFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pUnknownParentFolder, typeof(IShellFolder));
+ }
+
+ return _oParentFolder;
+ }
+
+ #endregion
+
+ #region GetPIDLs()
+
+ ///
+ /// Get the PIDLs
+ ///
+ /// Array of FileInfo
+ /// Array of PIDLs
+ protected IntPtr[] GetPIDLs(FileInfo[] arrFI)
+ {
+ if (null == arrFI || 0 == arrFI.Length)
+ {
+ return null;
+ }
+
+ IShellFolder oParentFolder = GetParentFolder(arrFI[0].DirectoryName);
+ if (null == oParentFolder)
+ {
+ return null;
+ }
+
+ IntPtr[] arrPIDLs = new IntPtr[arrFI.Length];
+ int n = 0;
+ foreach (FileInfo fi in arrFI)
+ {
+ // Get the file relative to folder
+ uint pchEaten = 0;
+ SFGAO pdwAttributes = 0;
+ IntPtr pPIDL = IntPtr.Zero;
+ int nResult = oParentFolder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, fi.Name, ref pchEaten, out pPIDL, ref pdwAttributes);
+ if (S_OK != nResult)
+ {
+ FreePIDLs(arrPIDLs);
+ return null;
+ }
+ arrPIDLs[n] = pPIDL;
+ n++;
+ }
+
+ return arrPIDLs;
+ }
+
+ ///
+ /// Get the PIDLs
+ ///
+ /// Array of DirectoryInfo
+ /// Array of PIDLs
+ protected IntPtr[] GetPIDLs(DirectoryInfo[] arrFI)
+ {
+ if (null == arrFI || 0 == arrFI.Length)
+ {
+ return null;
+ }
+
+ IShellFolder oParentFolder = GetParentFolder(arrFI[0].Parent.FullName);
+ if (null == oParentFolder)
+ {
+ return null;
+ }
+
+ IntPtr[] arrPIDLs = new IntPtr[arrFI.Length];
+ int n = 0;
+ foreach (DirectoryInfo fi in arrFI)
+ {
+ // Get the file relative to folder
+ uint pchEaten = 0;
+ SFGAO pdwAttributes = 0;
+ IntPtr pPIDL = IntPtr.Zero;
+ int nResult = oParentFolder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, fi.Name, ref pchEaten, out pPIDL, ref pdwAttributes);
+ if (S_OK != nResult)
+ {
+ FreePIDLs(arrPIDLs);
+ return null;
+ }
+ arrPIDLs[n] = pPIDL;
+ n++;
+ }
+
+ return arrPIDLs;
+ }
+
+ #endregion
+
+ #region FreePIDLs()
+
+ ///
+ /// Free the PIDLs
+ ///
+ /// Array of PIDLs (IntPtr)
+ protected void FreePIDLs(IntPtr[] arrPIDLs)
+ {
+ if (null != arrPIDLs)
+ {
+ for (int n = 0; n < arrPIDLs.Length; n++)
+ {
+ if (arrPIDLs[n] != IntPtr.Zero)
+ {
+ Marshal.FreeCoTaskMem(arrPIDLs[n]);
+ arrPIDLs[n] = IntPtr.Zero;
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region InvokeContextMenuDefault
+
+ private void InvokeContextMenuDefault(FileInfo[] arrFI)
+ {
+ // Release all resources first.
+ ReleaseAll();
+
+ IntPtr pMenu = IntPtr.Zero,
+ iContextMenuPtr = IntPtr.Zero;
+
+ try
+ {
+ _arrPIDLs = GetPIDLs(arrFI);
+ if (null == _arrPIDLs)
+ {
+ ReleaseAll();
+ return;
+ }
+
+ if (false == GetContextMenuInterfaces(_oParentFolder, _arrPIDLs, out iContextMenuPtr))
+ {
+ ReleaseAll();
+ return;
+ }
+
+ pMenu = CreatePopupMenu();
+
+ int nResult = _oContextMenu.QueryContextMenu(
+ pMenu,
+ 0,
+ CMD_FIRST,
+ CMD_LAST,
+ CMF.DEFAULTONLY |
+ ((Control.ModifierKeys & Keys.Shift) != 0 ? CMF.EXTENDEDVERBS : 0));
+
+ uint nDefaultCmd = (uint)GetMenuDefaultItem(pMenu, false, 0);
+ if (nDefaultCmd >= CMD_FIRST)
+ {
+ InvokeCommand(_oContextMenu, nDefaultCmd, arrFI[0].DirectoryName, Control.MousePosition);
+ }
+
+ DestroyMenu(pMenu);
+ pMenu = IntPtr.Zero;
+ }
+ catch
+ {
+ throw;
+ }
+ finally
+ {
+ if (pMenu != IntPtr.Zero)
+ {
+ DestroyMenu(pMenu);
+ }
+ ReleaseAll();
+ }
+ }
+
+ #endregion
+
+ #region ShowContextMenu()
+
+ ///
+ /// Shows the context menu
+ ///
+ /// FileInfos (should all be in same directory)
+ /// Where to show the menu
+ public void ShowContextMenu(FileInfo[] files, Point pointScreen)
+ {
+ // Release all resources first.
+ ReleaseAll();
+ _arrPIDLs = GetPIDLs(files);
+ this.ShowContextMenu(pointScreen);
+ }
+
+ ///
+ /// Shows the context menu
+ ///
+ /// DirectoryInfos (should all be in same directory)
+ /// Where to show the menu
+ public void ShowContextMenu(DirectoryInfo[] dirs, Point pointScreen)
+ {
+ // Release all resources first.
+ ReleaseAll();
+ _arrPIDLs = GetPIDLs(dirs);
+ this.ShowContextMenu(pointScreen);
+ }
+
+ ///
+ /// Shows the context menu
+ ///
+ /// FileInfos (should all be in same directory)
+ /// Where to show the menu
+ private void ShowContextMenu(Point pointScreen)
+ {
+ IntPtr pMenu = IntPtr.Zero,
+ iContextMenuPtr = IntPtr.Zero,
+ iContextMenuPtr2 = IntPtr.Zero,
+ iContextMenuPtr3 = IntPtr.Zero;
+
+ try
+ {
+ if (null == _arrPIDLs)
+ {
+ ReleaseAll();
+ return;
+ }
+
+ if (false == GetContextMenuInterfaces(_oParentFolder, _arrPIDLs, out iContextMenuPtr))
+ {
+ ReleaseAll();
+ return;
+ }
+
+ pMenu = CreatePopupMenu();
+
+ int nResult = _oContextMenu.QueryContextMenu(
+ pMenu,
+ 0,
+ CMD_FIRST,
+ CMD_LAST,
+ CMF.EXPLORE |
+ CMF.NORMAL |
+ ((Control.ModifierKeys & Keys.Shift) != 0 ? CMF.EXTENDEDVERBS : 0));
+
+ Marshal.QueryInterface(iContextMenuPtr, ref IID_IContextMenu2, out iContextMenuPtr2);
+ Marshal.QueryInterface(iContextMenuPtr, ref IID_IContextMenu3, out iContextMenuPtr3);
+
+ _oContextMenu2 = (IContextMenu2)Marshal.GetTypedObjectForIUnknown(iContextMenuPtr2, typeof(IContextMenu2));
+ _oContextMenu3 = (IContextMenu3)Marshal.GetTypedObjectForIUnknown(iContextMenuPtr3, typeof(IContextMenu3));
+
+ uint nSelected = TrackPopupMenuEx(
+ pMenu,
+ TPM.RETURNCMD,
+ pointScreen.X,
+ pointScreen.Y,
+ this.Handle,
+ IntPtr.Zero);
+
+ DestroyMenu(pMenu);
+ pMenu = IntPtr.Zero;
+
+ if (nSelected != 0)
+ {
+ InvokeCommand(_oContextMenu, nSelected, _strParentFolder, pointScreen);
+ }
+ }
+ catch
+ {
+ throw;
+ }
+ finally
+ {
+ //hook.Uninstall();
+ if (pMenu != IntPtr.Zero)
+ {
+ DestroyMenu(pMenu);
+ }
+
+ if (iContextMenuPtr != IntPtr.Zero)
+ Marshal.Release(iContextMenuPtr);
+
+ if (iContextMenuPtr2 != IntPtr.Zero)
+ Marshal.Release(iContextMenuPtr2);
+
+ if (iContextMenuPtr3 != IntPtr.Zero)
+ Marshal.Release(iContextMenuPtr3);
+
+ ReleaseAll();
+ }
+ }
+
+ #endregion
+
+ #region Local variabled
+
+ private IContextMenu _oContextMenu;
+ private IContextMenu2 _oContextMenu2;
+ private IContextMenu3 _oContextMenu3;
+ private IShellFolder _oDesktopFolder;
+ private IShellFolder _oParentFolder;
+ private IntPtr[] _arrPIDLs;
+ private string _strParentFolder;
+
+ #endregion
+
+ #region Variables and Constants
+
+ private const int MAX_PATH = 260;
+ private const uint CMD_FIRST = 1;
+ private const uint CMD_LAST = 30000;
+
+ private const int S_OK = 0;
+ private const int S_FALSE = 1;
+
+ private static int cbMenuItemInfo = Marshal.SizeOf(typeof(MENUITEMINFO));
+ private static int cbInvokeCommand = Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX));
+
+ #endregion
+
+ #region DLL Import
+
+ // Retrieves the IShellFolder interface for the desktop folder, which is the root of the Shell's namespace.
+ [DllImport("shell32.dll")]
+ private static extern Int32 SHGetDesktopFolder(out IntPtr ppshf);
+
+ // Takes a STRRET structure returned by IShellFolder::GetDisplayNameOf, converts it to a string, and places the result in a buffer.
+ [DllImport("shlwapi.dll", EntryPoint = "StrRetToBuf", ExactSpelling = false, CharSet = CharSet.Auto, SetLastError = true)]
+ private static extern Int32 StrRetToBuf(IntPtr pstr, IntPtr pidl, StringBuilder pszBuf, int cchBuf);
+
+ // The TrackPopupMenuEx function displays a shortcut menu at the specified location and tracks the selection of items on the shortcut menu. The shortcut menu can appear anywhere on the screen.
+ [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
+ private static extern uint TrackPopupMenuEx(IntPtr hmenu, TPM flags, int x, int y, IntPtr hwnd, IntPtr lptpm);
+
+ // The CreatePopupMenu function creates a drop-down menu, submenu, or shortcut menu. The menu is initially empty. You can insert or append menu items by using the InsertMenuItem function. You can also use the InsertMenu function to insert menu items and the AppendMenu function to append menu items.
+ [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern IntPtr CreatePopupMenu();
+
+ // The DestroyMenu function destroys the specified menu and frees any memory that the menu occupies.
+ [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern bool DestroyMenu(IntPtr hMenu);
+
+ // Determines the default menu item on the specified menu
+ [DllImport("user32", SetLastError = true, CharSet = CharSet.Auto)]
+ private static extern int GetMenuDefaultItem(IntPtr hMenu, bool fByPos, uint gmdiFlags);
+
+ #endregion
+
+ #region Shell GUIDs
+
+ private static Guid IID_IShellFolder = new Guid("{000214E6-0000-0000-C000-000000000046}");
+ private static Guid IID_IContextMenu = new Guid("{000214e4-0000-0000-c000-000000000046}");
+ private static Guid IID_IContextMenu2 = new Guid("{000214f4-0000-0000-c000-000000000046}");
+ private static Guid IID_IContextMenu3 = new Guid("{bcfce0a0-ec17-11d0-8d10-00a0c90f2719}");
+
+ #endregion
+
+ #region Structs
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct CWPSTRUCT
+ {
+ public IntPtr lparam;
+ public IntPtr wparam;
+ public int message;
+ public IntPtr hwnd;
+ }
+
+ // Contains extended information about a shortcut menu command
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct CMINVOKECOMMANDINFOEX
+ {
+ public int cbSize;
+ public CMIC fMask;
+ public IntPtr hwnd;
+ public IntPtr lpVerb;
+ [MarshalAs(UnmanagedType.LPStr)]
+ public string lpParameters;
+ [MarshalAs(UnmanagedType.LPStr)]
+ public string lpDirectory;
+ public SW nShow;
+ public int dwHotKey;
+ public IntPtr hIcon;
+ [MarshalAs(UnmanagedType.LPStr)]
+ public string lpTitle;
+ public IntPtr lpVerbW;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string lpParametersW;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string lpDirectoryW;
+ [MarshalAs(UnmanagedType.LPWStr)]
+ public string lpTitleW;
+ public POINT ptInvoke;
+ }
+
+ // Contains information about a menu item
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ private struct MENUITEMINFO
+ {
+ public MENUITEMINFO(string text)
+ {
+ cbSize = cbMenuItemInfo;
+ dwTypeData = text;
+ cch = text.Length;
+ fMask = 0;
+ fType = 0;
+ fState = 0;
+ wID = 0;
+ hSubMenu = IntPtr.Zero;
+ hbmpChecked = IntPtr.Zero;
+ hbmpUnchecked = IntPtr.Zero;
+ dwItemData = IntPtr.Zero;
+ hbmpItem = IntPtr.Zero;
+ }
+
+ public int cbSize;
+ public MIIM fMask;
+ public MFT fType;
+ public MFS fState;
+ public uint wID;
+ public IntPtr hSubMenu;
+ public IntPtr hbmpChecked;
+ public IntPtr hbmpUnchecked;
+ public IntPtr dwItemData;
+ [MarshalAs(UnmanagedType.LPTStr)]
+ public string dwTypeData;
+ public int cch;
+ public IntPtr hbmpItem;
+ }
+
+ // A generalized global memory handle used for data transfer operations by the
+ // IAdviseSink, IDataObject, and IOleCache interfaces
+ [StructLayout(LayoutKind.Sequential)]
+ private struct STGMEDIUM
+ {
+ public TYMED tymed;
+ public IntPtr hBitmap;
+ public IntPtr hMetaFilePict;
+ public IntPtr hEnhMetaFile;
+ public IntPtr hGlobal;
+ public IntPtr lpszFileName;
+ public IntPtr pstm;
+ public IntPtr pstg;
+ public IntPtr pUnkForRelease;
+ }
+
+ // Defines the x- and y-coordinates of a point
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ private struct POINT
+ {
+ public POINT(int x, int y)
+ {
+ this.x = x;
+ this.y = y;
+ }
+
+ public int x;
+ public int y;
+ }
+
+ #endregion
+
+ #region Enums
+
+ // Defines the values used with the IShellFolder::GetDisplayNameOf and IShellFolder::SetNameOf
+ // methods to specify the type of file or folder names used by those methods
+ [Flags]
+ private enum SHGNO
+ {
+ NORMAL = 0x0000,
+ INFOLDER = 0x0001,
+ FOREDITING = 0x1000,
+ FORADDRESSBAR = 0x4000,
+ FORPARSING = 0x8000
+ }
+
+ // The attributes that the caller is requesting, when calling IShellFolder::GetAttributesOf
+ [Flags]
+ private enum SFGAO : uint
+ {
+ BROWSABLE = 0x8000000,
+ CANCOPY = 1,
+ CANDELETE = 0x20,
+ CANLINK = 4,
+ CANMONIKER = 0x400000,
+ CANMOVE = 2,
+ CANRENAME = 0x10,
+ CAPABILITYMASK = 0x177,
+ COMPRESSED = 0x4000000,
+ CONTENTSMASK = 0x80000000,
+ DISPLAYATTRMASK = 0xfc000,
+ DROPTARGET = 0x100,
+ ENCRYPTED = 0x2000,
+ FILESYSANCESTOR = 0x10000000,
+ FILESYSTEM = 0x40000000,
+ FOLDER = 0x20000000,
+ GHOSTED = 0x8000,
+ HASPROPSHEET = 0x40,
+ HASSTORAGE = 0x400000,
+ HASSUBFOLDER = 0x80000000,
+ HIDDEN = 0x80000,
+ ISSLOW = 0x4000,
+ LINK = 0x10000,
+ NEWCONTENT = 0x200000,
+ NONENUMERATED = 0x100000,
+ READONLY = 0x40000,
+ REMOVABLE = 0x2000000,
+ SHARE = 0x20000,
+ STORAGE = 8,
+ STORAGEANCESTOR = 0x800000,
+ STORAGECAPMASK = 0x70c50008,
+ STREAM = 0x400000,
+ VALIDATE = 0x1000000
+ }
+
+ // Determines the type of items included in an enumeration.
+ // These values are used with the IShellFolder::EnumObjects method
+ [Flags]
+ private enum SHCONTF
+ {
+ FOLDERS = 0x0020,
+ NONFOLDERS = 0x0040,
+ INCLUDEHIDDEN = 0x0080,
+ INIT_ON_FIRST_NEXT = 0x0100,
+ NETPRINTERSRCH = 0x0200,
+ SHAREABLE = 0x0400,
+ STORAGE = 0x0800,
+ }
+
+ // Specifies how the shortcut menu can be changed when calling IContextMenu::QueryContextMenu
+ [Flags]
+ private enum CMF : uint
+ {
+ NORMAL = 0x00000000,
+ DEFAULTONLY = 0x00000001,
+ VERBSONLY = 0x00000002,
+ EXPLORE = 0x00000004,
+ NOVERBS = 0x00000008,
+ CANRENAME = 0x00000010,
+ NODEFAULT = 0x00000020,
+ INCLUDESTATIC = 0x00000040,
+ EXTENDEDVERBS = 0x00000100,
+ RESERVED = 0xffff0000
+ }
+
+ // Flags specifying the information to return when calling IContextMenu::GetCommandString
+ [Flags]
+ private enum GCS : uint
+ {
+ VERBA = 0,
+ HELPTEXTA = 1,
+ VALIDATEA = 2,
+ VERBW = 4,
+ HELPTEXTW = 5,
+ VALIDATEW = 6
+ }
+
+ // Specifies how TrackPopupMenuEx positions the shortcut menu horizontally
+ [Flags]
+ private enum TPM : uint
+ {
+ LEFTBUTTON = 0x0000,
+ RIGHTBUTTON = 0x0002,
+ LEFTALIGN = 0x0000,
+ CENTERALIGN = 0x0004,
+ RIGHTALIGN = 0x0008,
+ TOPALIGN = 0x0000,
+ VCENTERALIGN = 0x0010,
+ BOTTOMALIGN = 0x0020,
+ HORIZONTAL = 0x0000,
+ VERTICAL = 0x0040,
+ NONOTIFY = 0x0080,
+ RETURNCMD = 0x0100,
+ RECURSE = 0x0001,
+ HORPOSANIMATION = 0x0400,
+ HORNEGANIMATION = 0x0800,
+ VERPOSANIMATION = 0x1000,
+ VERNEGANIMATION = 0x2000,
+ NOANIMATION = 0x4000,
+ LAYOUTRTL = 0x8000
+ }
+
+ // The cmd for a custom added menu item
+ private enum CMD_CUSTOM
+ {
+ ExpandCollapse = (int)CMD_LAST + 1
+ }
+
+ // Flags used with the CMINVOKECOMMANDINFOEX structure
+ [Flags]
+ private enum CMIC : uint
+ {
+ HOTKEY = 0x00000020,
+ ICON = 0x00000010,
+ FLAG_NO_UI = 0x00000400,
+ UNICODE = 0x00004000,
+ NO_CONSOLE = 0x00008000,
+ ASYNCOK = 0x00100000,
+ NOZONECHECKS = 0x00800000,
+ SHIFT_DOWN = 0x10000000,
+ CONTROL_DOWN = 0x40000000,
+ FLAG_LOG_USAGE = 0x04000000,
+ PTINVOKE = 0x20000000
+ }
+
+ // Specifies how the window is to be shown
+ [Flags]
+ private enum SW
+ {
+ HIDE = 0,
+ SHOWNORMAL = 1,
+ NORMAL = 1,
+ SHOWMINIMIZED = 2,
+ SHOWMAXIMIZED = 3,
+ MAXIMIZE = 3,
+ SHOWNOACTIVATE = 4,
+ SHOW = 5,
+ MINIMIZE = 6,
+ SHOWMINNOACTIVE = 7,
+ SHOWNA = 8,
+ RESTORE = 9,
+ SHOWDEFAULT = 10,
+ }
+
+ // Window message flags
+ [Flags]
+ private enum WM : uint
+ {
+ ACTIVATE = 0x6,
+ ACTIVATEAPP = 0x1C,
+ AFXFIRST = 0x360,
+ AFXLAST = 0x37F,
+ APP = 0x8000,
+ ASKCBFORMATNAME = 0x30C,
+ CANCELJOURNAL = 0x4B,
+ CANCELMODE = 0x1F,
+ CAPTURECHANGED = 0x215,
+ CHANGECBCHAIN = 0x30D,
+ CHAR = 0x102,
+ CHARTOITEM = 0x2F,
+ CHILDACTIVATE = 0x22,
+ CLEAR = 0x303,
+ CLOSE = 0x10,
+ COMMAND = 0x111,
+ COMPACTING = 0x41,
+ COMPAREITEM = 0x39,
+ CONTEXTMENU = 0x7B,
+ COPY = 0x301,
+ COPYDATA = 0x4A,
+ CREATE = 0x1,
+ CTLCOLORBTN = 0x135,
+ CTLCOLORDLG = 0x136,
+ CTLCOLOREDIT = 0x133,
+ CTLCOLORLISTBOX = 0x134,
+ CTLCOLORMSGBOX = 0x132,
+ CTLCOLORSCROLLBAR = 0x137,
+ CTLCOLORSTATIC = 0x138,
+ CUT = 0x300,
+ DEADCHAR = 0x103,
+ DELETEITEM = 0x2D,
+ DESTROY = 0x2,
+ DESTROYCLIPBOARD = 0x307,
+ DEVICECHANGE = 0x219,
+ DEVMODECHANGE = 0x1B,
+ DISPLAYCHANGE = 0x7E,
+ DRAWCLIPBOARD = 0x308,
+ DRAWITEM = 0x2B,
+ DROPFILES = 0x233,
+ ENABLE = 0xA,
+ ENDSESSION = 0x16,
+ ENTERIDLE = 0x121,
+ ENTERMENULOOP = 0x211,
+ ENTERSIZEMOVE = 0x231,
+ ERASEBKGND = 0x14,
+ EXITMENULOOP = 0x212,
+ EXITSIZEMOVE = 0x232,
+ FONTCHANGE = 0x1D,
+ GETDLGCODE = 0x87,
+ GETFONT = 0x31,
+ GETHOTKEY = 0x33,
+ GETICON = 0x7F,
+ GETMINMAXINFO = 0x24,
+ GETOBJECT = 0x3D,
+ GETSYSMENU = 0x313,
+ GETTEXT = 0xD,
+ GETTEXTLENGTH = 0xE,
+ HANDHELDFIRST = 0x358,
+ HANDHELDLAST = 0x35F,
+ HELP = 0x53,
+ HOTKEY = 0x312,
+ HSCROLL = 0x114,
+ HSCROLLCLIPBOARD = 0x30E,
+ ICONERASEBKGND = 0x27,
+ IME_CHAR = 0x286,
+ IME_COMPOSITION = 0x10F,
+ IME_COMPOSITIONFULL = 0x284,
+ IME_CONTROL = 0x283,
+ IME_ENDCOMPOSITION = 0x10E,
+ IME_KEYDOWN = 0x290,
+ IME_KEYLAST = 0x10F,
+ IME_KEYUP = 0x291,
+ IME_NOTIFY = 0x282,
+ IME_REQUEST = 0x288,
+ IME_SELECT = 0x285,
+ IME_SETCONTEXT = 0x281,
+ IME_STARTCOMPOSITION = 0x10D,
+ INITDIALOG = 0x110,
+ INITMENU = 0x116,
+ INITMENUPOPUP = 0x117,
+ INPUTLANGCHANGE = 0x51,
+ INPUTLANGCHANGEREQUEST = 0x50,
+ KEYDOWN = 0x100,
+ KEYFIRST = 0x100,
+ KEYLAST = 0x108,
+ KEYUP = 0x101,
+ KILLFOCUS = 0x8,
+ LBUTTONDBLCLK = 0x203,
+ LBUTTONDOWN = 0x201,
+ LBUTTONUP = 0x202,
+ LVM_GETEDITCONTROL = 0x1018,
+ LVM_SETIMAGELIST = 0x1003,
+ MBUTTONDBLCLK = 0x209,
+ MBUTTONDOWN = 0x207,
+ MBUTTONUP = 0x208,
+ MDIACTIVATE = 0x222,
+ MDICASCADE = 0x227,
+ MDICREATE = 0x220,
+ MDIDESTROY = 0x221,
+ MDIGETACTIVE = 0x229,
+ MDIICONARRANGE = 0x228,
+ MDIMAXIMIZE = 0x225,
+ MDINEXT = 0x224,
+ MDIREFRESHMENU = 0x234,
+ MDIRESTORE = 0x223,
+ MDISETMENU = 0x230,
+ MDITILE = 0x226,
+ MEASUREITEM = 0x2C,
+ MENUCHAR = 0x120,
+ MENUCOMMAND = 0x126,
+ MENUDRAG = 0x123,
+ MENUGETOBJECT = 0x124,
+ MENURBUTTONUP = 0x122,
+ MENUSELECT = 0x11F,
+ MOUSEACTIVATE = 0x21,
+ MOUSEFIRST = 0x200,
+ MOUSEHOVER = 0x2A1,
+ MOUSELAST = 0x20A,
+ MOUSELEAVE = 0x2A3,
+ MOUSEMOVE = 0x200,
+ MOUSEWHEEL = 0x20A,
+ MOVE = 0x3,
+ MOVING = 0x216,
+ NCACTIVATE = 0x86,
+ NCCALCSIZE = 0x83,
+ NCCREATE = 0x81,
+ NCDESTROY = 0x82,
+ NCHITTEST = 0x84,
+ NCLBUTTONDBLCLK = 0xA3,
+ NCLBUTTONDOWN = 0xA1,
+ NCLBUTTONUP = 0xA2,
+ NCMBUTTONDBLCLK = 0xA9,
+ NCMBUTTONDOWN = 0xA7,
+ NCMBUTTONUP = 0xA8,
+ NCMOUSEHOVER = 0x2A0,
+ NCMOUSELEAVE = 0x2A2,
+ NCMOUSEMOVE = 0xA0,
+ NCPAINT = 0x85,
+ NCRBUTTONDBLCLK = 0xA6,
+ NCRBUTTONDOWN = 0xA4,
+ NCRBUTTONUP = 0xA5,
+ NEXTDLGCTL = 0x28,
+ NEXTMENU = 0x213,
+ NOTIFY = 0x4E,
+ NOTIFYFORMAT = 0x55,
+ NULL = 0x0,
+ PAINT = 0xF,
+ PAINTCLIPBOARD = 0x309,
+ PAINTICON = 0x26,
+ PALETTECHANGED = 0x311,
+ PALETTEISCHANGING = 0x310,
+ PARENTNOTIFY = 0x210,
+ PASTE = 0x302,
+ PENWINFIRST = 0x380,
+ PENWINLAST = 0x38F,
+ POWER = 0x48,
+ PRINT = 0x317,
+ PRINTCLIENT = 0x318,
+ QUERYDRAGICON = 0x37,
+ QUERYENDSESSION = 0x11,
+ QUERYNEWPALETTE = 0x30F,
+ QUERYOPEN = 0x13,
+ QUEUESYNC = 0x23,
+ QUIT = 0x12,
+ RBUTTONDBLCLK = 0x206,
+ RBUTTONDOWN = 0x204,
+ RBUTTONUP = 0x205,
+ RENDERALLFORMATS = 0x306,
+ RENDERFORMAT = 0x305,
+ SETCURSOR = 0x20,
+ SETFOCUS = 0x7,
+ SETFONT = 0x30,
+ SETHOTKEY = 0x32,
+ SETICON = 0x80,
+ SETMARGINS = 0xD3,
+ SETREDRAW = 0xB,
+ SETTEXT = 0xC,
+ SETTINGCHANGE = 0x1A,
+ SHOWWINDOW = 0x18,
+ SIZE = 0x5,
+ SIZECLIPBOARD = 0x30B,
+ SIZING = 0x214,
+ SPOOLERSTATUS = 0x2A,
+ STYLECHANGED = 0x7D,
+ STYLECHANGING = 0x7C,
+ SYNCPAINT = 0x88,
+ SYSCHAR = 0x106,
+ SYSCOLORCHANGE = 0x15,
+ SYSCOMMAND = 0x112,
+ SYSDEADCHAR = 0x107,
+ SYSKEYDOWN = 0x104,
+ SYSKEYUP = 0x105,
+ TCARD = 0x52,
+ TIMECHANGE = 0x1E,
+ TIMER = 0x113,
+ TVM_GETEDITCONTROL = 0x110F,
+ TVM_SETIMAGELIST = 0x1109,
+ UNDO = 0x304,
+ UNINITMENUPOPUP = 0x125,
+ USER = 0x400,
+ USERCHANGED = 0x54,
+ VKEYTOITEM = 0x2E,
+ VSCROLL = 0x115,
+ VSCROLLCLIPBOARD = 0x30A,
+ WINDOWPOSCHANGED = 0x47,
+ WINDOWPOSCHANGING = 0x46,
+ WININICHANGE = 0x1A,
+ SH_NOTIFY = 0x0401
+ }
+
+ // Specifies the content of the new menu item
+ [Flags]
+ private enum MFT : uint
+ {
+ GRAYED = 0x00000003,
+ DISABLED = 0x00000003,
+ CHECKED = 0x00000008,
+ SEPARATOR = 0x00000800,
+ RADIOCHECK = 0x00000200,
+ BITMAP = 0x00000004,
+ OWNERDRAW = 0x00000100,
+ MENUBARBREAK = 0x00000020,
+ MENUBREAK = 0x00000040,
+ RIGHTORDER = 0x00002000,
+ BYCOMMAND = 0x00000000,
+ BYPOSITION = 0x00000400,
+ POPUP = 0x00000010
+ }
+
+ // Specifies the state of the new menu item
+ [Flags]
+ private enum MFS : uint
+ {
+ GRAYED = 0x00000003,
+ DISABLED = 0x00000003,
+ CHECKED = 0x00000008,
+ HILITE = 0x00000080,
+ ENABLED = 0x00000000,
+ UNCHECKED = 0x00000000,
+ UNHILITE = 0x00000000,
+ DEFAULT = 0x00001000
+ }
+
+ // Specifies the content of the new menu item
+ [Flags]
+ private enum MIIM : uint
+ {
+ BITMAP = 0x80,
+ CHECKMARKS = 0x08,
+ DATA = 0x20,
+ FTYPE = 0x100,
+ ID = 0x02,
+ STATE = 0x01,
+ STRING = 0x40,
+ SUBMENU = 0x04,
+ TYPE = 0x10
+ }
+
+ // Indicates the type of storage medium being used in a data transfer
+ [Flags]
+ private enum TYMED
+ {
+ ENHMF = 0x40,
+ FILE = 2,
+ GDI = 0x10,
+ HGLOBAL = 1,
+ ISTORAGE = 8,
+ ISTREAM = 4,
+ MFPICT = 0x20,
+ NULL = 0
+ }
+
+ #endregion
+
+ #region IShellFolder
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("000214E6-0000-0000-C000-000000000046")]
+ private interface IShellFolder
+ {
+ // Translates a file object's or folder's display name into an item identifier list.
+ // Return value: error code, if any
+ [PreserveSig]
+ Int32 ParseDisplayName(
+ IntPtr hwnd,
+ IntPtr pbc,
+ [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName,
+ ref uint pchEaten,
+ out IntPtr ppidl,
+ ref SFGAO pdwAttributes);
+
+ // Allows a client to determine the contents of a folder by creating an item
+ // identifier enumeration object and returning its IEnumIDList interface.
+ // Return value: error code, if any
+ [PreserveSig]
+ Int32 EnumObjects(
+ IntPtr hwnd,
+ SHCONTF grfFlags,
+ out IntPtr enumIDList);
+
+ // Retrieves an IShellFolder object for a subfolder.
+ // Return value: error code, if any
+ [PreserveSig]
+ Int32 BindToObject(
+ IntPtr pidl,
+ IntPtr pbc,
+ ref Guid riid,
+ out IntPtr ppv);
+
+ // Requests a pointer to an object's storage interface.
+ // Return value: error code, if any
+ [PreserveSig]
+ Int32 BindToStorage(
+ IntPtr pidl,
+ IntPtr pbc,
+ ref Guid riid,
+ out IntPtr ppv);
+
+ // Determines the relative order of two file objects or folders, given their
+ // item identifier lists. Return value: If this method is successful, the
+ // CODE field of the HRESULT contains one of the following values (the code
+ // can be retrived using the helper function GetHResultCode): Negative A
+ // negative return value indicates that the first item should precede
+ // the second (pidl1 < pidl2).
+
+ // Positive A positive return value indicates that the first item should
+ // follow the second (pidl1 > pidl2). Zero A return value of zero
+ // indicates that the two items are the same (pidl1 = pidl2).
+ [PreserveSig]
+ Int32 CompareIDs(
+ IntPtr lParam,
+ IntPtr pidl1,
+ IntPtr pidl2);
+
+ // Requests an object that can be used to obtain information from or interact
+ // with a folder object.
+ // Return value: error code, if any
+ [PreserveSig]
+ Int32 CreateViewObject(
+ IntPtr hwndOwner,
+ Guid riid,
+ out IntPtr ppv);
+
+ // Retrieves the attributes of one or more file objects or subfolders.
+ // Return value: error code, if any
+ [PreserveSig]
+ Int32 GetAttributesOf(
+ uint cidl,
+ [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl,
+ ref SFGAO rgfInOut);
+
+ // Retrieves an OLE interface that can be used to carry out actions on the
+ // specified file objects or folders.
+ // Return value: error code, if any
+ [PreserveSig]
+ Int32 GetUIObjectOf(
+ IntPtr hwndOwner,
+ uint cidl,
+ [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl,
+ ref Guid riid,
+ IntPtr rgfReserved,
+ out IntPtr ppv);
+
+ // Retrieves the display name for the specified file object or subfolder.
+ // Return value: error code, if any
+ [PreserveSig()]
+ Int32 GetDisplayNameOf(
+ IntPtr pidl,
+ SHGNO uFlags,
+ IntPtr lpName);
+
+ // Sets the display name of a file object or subfolder, changing the item
+ // identifier in the process.
+ // Return value: error code, if any
+ [PreserveSig]
+ Int32 SetNameOf(
+ IntPtr hwnd,
+ IntPtr pidl,
+ [MarshalAs(UnmanagedType.LPWStr)] string pszName,
+ SHGNO uFlags,
+ out IntPtr ppidlOut);
+ }
+
+ #endregion
+
+ #region IContextMenu
+
+ [ComImport()]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [GuidAttribute("000214e4-0000-0000-c000-000000000046")]
+ private interface IContextMenu
+ {
+ // Adds commands to a shortcut menu
+ [PreserveSig()]
+ Int32 QueryContextMenu(
+ IntPtr hmenu,
+ uint iMenu,
+ uint idCmdFirst,
+ uint idCmdLast,
+ CMF uFlags);
+
+ // Carries out the command associated with a shortcut menu item
+ [PreserveSig()]
+ Int32 InvokeCommand(
+ ref CMINVOKECOMMANDINFOEX info);
+
+ // Retrieves information about a shortcut menu command,
+ // including the help string and the language-independent,
+ // or canonical, name for the command
+ [PreserveSig()]
+ Int32 GetCommandString(
+ uint idcmd,
+ GCS uflags,
+ uint reserved,
+ [MarshalAs(UnmanagedType.LPArray)] byte[] commandstring,
+ int cch);
+ }
+
+ [ComImport, Guid("000214f4-0000-0000-c000-000000000046")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface IContextMenu2
+ {
+ // Adds commands to a shortcut menu
+ [PreserveSig()]
+ Int32 QueryContextMenu(
+ IntPtr hmenu,
+ uint iMenu,
+ uint idCmdFirst,
+ uint idCmdLast,
+ CMF uFlags);
+
+ // Carries out the command associated with a shortcut menu item
+ [PreserveSig()]
+ Int32 InvokeCommand(
+ ref CMINVOKECOMMANDINFOEX info);
+
+ // Retrieves information about a shortcut menu command,
+ // including the help string and the language-independent,
+ // or canonical, name for the command
+ [PreserveSig()]
+ Int32 GetCommandString(
+ uint idcmd,
+ GCS uflags,
+ uint reserved,
+ [MarshalAs(UnmanagedType.LPWStr)] StringBuilder commandstring,
+ int cch);
+
+ // Allows client objects of the IContextMenu interface to
+ // handle messages associated with owner-drawn menu items
+ [PreserveSig]
+ Int32 HandleMenuMsg(
+ uint uMsg,
+ IntPtr wParam,
+ IntPtr lParam);
+ }
+
+ [ComImport, Guid("bcfce0a0-ec17-11d0-8d10-00a0c90f2719")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ private interface IContextMenu3
+ {
+ // Adds commands to a shortcut menu
+ [PreserveSig()]
+ Int32 QueryContextMenu(
+ IntPtr hmenu,
+ uint iMenu,
+ uint idCmdFirst,
+ uint idCmdLast,
+ CMF uFlags);
+
+ // Carries out the command associated with a shortcut menu item
+ [PreserveSig()]
+ Int32 InvokeCommand(
+ ref CMINVOKECOMMANDINFOEX info);
+
+ // Retrieves information about a shortcut menu command,
+ // including the help string and the language-independent,
+ // or canonical, name for the command
+ [PreserveSig()]
+ Int32 GetCommandString(
+ uint idcmd,
+ GCS uflags,
+ uint reserved,
+ [MarshalAs(UnmanagedType.LPWStr)] StringBuilder commandstring,
+ int cch);
+
+ // Allows client objects of the IContextMenu interface to
+ // handle messages associated with owner-drawn menu items
+ [PreserveSig]
+ Int32 HandleMenuMsg(
+ uint uMsg,
+ IntPtr wParam,
+ IntPtr lParam);
+
+ // Allows client objects of the IContextMenu3 interface to
+ // handle messages associated with owner-drawn menu items
+ [PreserveSig]
+ Int32 HandleMenuMsg2(
+ uint uMsg,
+ IntPtr wParam,
+ IntPtr lParam,
+ IntPtr plResult);
+ }
+
+ #endregion
+ }
+
+ #region ShellContextMenuException
+
+ public class ShellContextMenuException : Exception
+ {
+ /// Default contructor
+ public ShellContextMenuException()
+ {
+ }
+
+ /// Constructor with message
+ /// Message
+ public ShellContextMenuException(string message)
+ : base(message)
+ {
+ }
+ }
+
+ #endregion
+
+ #region Class HookEventArgs
+
+ public class HookEventArgs : EventArgs
+ {
+ public int HookCode; // Hook code
+ public IntPtr wParam; // WPARAM argument
+ public IntPtr lParam; // LPARAM argument
+ }
+
+ #endregion
+
+ #region Enum HookType
+
+ // Hook Types
+ public enum HookType : int
+ {
+ WH_JOURNALRECORD = 0,
+ WH_JOURNALPLAYBACK = 1,
+ WH_KEYBOARD = 2,
+ WH_GETMESSAGE = 3,
+ WH_CALLWNDPROC = 4,
+ WH_CBT = 5,
+ WH_SYSMSGFILTER = 6,
+ WH_MOUSE = 7,
+ WH_HARDWARE = 8,
+ WH_DEBUG = 9,
+ WH_SHELL = 10,
+ WH_FOREGROUNDIDLE = 11,
+ WH_CALLWNDPROCRET = 12,
+ WH_KEYBOARD_LL = 13,
+ WH_MOUSE_LL = 14
+ }
+
+ #endregion
+
+ #region Class LocalWindowsHook
+
+ public class LocalWindowsHook
+ {
+ // ************************************************************************
+ // Filter function delegate
+ public delegate int HookProc(int code, IntPtr wParam, IntPtr lParam);
+ // ************************************************************************
+
+ // ************************************************************************
+ // Internal properties
+ protected IntPtr m_hhook = IntPtr.Zero;
+ protected HookProc m_filterFunc = null;
+ protected HookType m_hookType;
+ // ************************************************************************
+
+ // ************************************************************************
+ // Event delegate
+ public delegate void HookEventHandler(object sender, HookEventArgs e);
+ // ************************************************************************
+
+ // ************************************************************************
+ // Event: HookInvoked
+ public event HookEventHandler HookInvoked;
+ protected void OnHookInvoked(HookEventArgs e)
+ {
+ if (HookInvoked != null)
+ HookInvoked(this, e);
+ }
+ // ************************************************************************
+
+ // ************************************************************************
+ // Class constructor(s)
+ public LocalWindowsHook(HookType hook)
+ {
+ m_hookType = hook;
+ m_filterFunc = new HookProc(this.CoreHookProc);
+ }
+ public LocalWindowsHook(HookType hook, HookProc func)
+ {
+ m_hookType = hook;
+ m_filterFunc = func;
+ }
+ // ************************************************************************
+
+ // ************************************************************************
+ // Default filter function
+ protected int CoreHookProc(int code, IntPtr wParam, IntPtr lParam)
+ {
+ if (code < 0)
+ return CallNextHookEx(m_hhook, code, wParam, lParam);
+
+ // Let clients determine what to do
+ HookEventArgs e = new HookEventArgs();
+ e.HookCode = code;
+ e.wParam = wParam;
+ e.lParam = lParam;
+ OnHookInvoked(e);
+
+ // Yield to the next hook in the chain
+ return CallNextHookEx(m_hhook, code, wParam, lParam);
+ }
+ // ************************************************************************
+
+ // ************************************************************************
+ // Install the hook
+ public void Install()
+ {
+ m_hhook = SetWindowsHookEx(
+ m_hookType,
+ m_filterFunc,
+ IntPtr.Zero,
+ (int)AppDomain.GetCurrentThreadId());
+ }
+ // ************************************************************************
+
+ // ************************************************************************
+ // Uninstall the hook
+ public void Uninstall()
+ {
+ UnhookWindowsHookEx(m_hhook);
+ }
+ // ************************************************************************
+
+
+ #region Win32 Imports
+
+ // ************************************************************************
+ // Win32: SetWindowsHookEx()
+ [DllImport("user32.dll")]
+ protected static extern IntPtr SetWindowsHookEx(HookType code,
+ HookProc func,
+ IntPtr hInstance,
+ int threadID);
+ // ************************************************************************
+
+ // ************************************************************************
+ // Win32: UnhookWindowsHookEx()
+ [DllImport("user32.dll")]
+ protected static extern int UnhookWindowsHookEx(IntPtr hhook);
+ // ************************************************************************
+
+ // ************************************************************************
+ // Win32: CallNextHookEx()
+ [DllImport("user32.dll")]
+ protected static extern int CallNextHookEx(IntPtr hhook,
+ int code, IntPtr wParam, IntPtr lParam);
+ // ************************************************************************
+
+ #endregion
+ }
+
+ #endregion
+
+ #region ShellHelper
+
+ internal static class ShellHelper
+ {
+ #region Low/High Word
+
+ ///
+ /// Retrieves the High Word of a WParam of a WindowMessage
+ ///
+ /// The pointer to the WParam
+ /// The unsigned integer for the High Word
+ public static nint HiWord(IntPtr ptr)
+ {
+ if (((nint)ptr & 0x80000000) == 0x80000000)
+ return ((nint)ptr >> 16);
+ else
+ return (((nint)ptr >> 16) & 0xffff);
+ }
+
+ ///
+ /// Retrieves the Low Word of a WParam of a WindowMessage
+ ///
+ /// The pointer to the WParam
+ /// The unsigned integer for the Low Word
+ public static nint LoWord(IntPtr ptr)
+ {
+ return (nint)ptr & 0xffff;
+ }
+
+ #endregion
+ }
+
+ #endregion
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Helper/SortOptionTranlationHelper.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/SortOptionTranlationHelper.cs
new file mode 100644
index 00000000000..d3a6552d9e5
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Helper/SortOptionTranlationHelper.cs
@@ -0,0 +1,25 @@
+using Flow.Launcher.Plugin.Everything.Everything;
+using JetBrains.Annotations;
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Helper;
+
+public static class SortOptionTranslationHelper
+{
+ [CanBeNull]
+ public static IPublicAPI API { get; internal set; }
+
+ public static string GetTranslatedName(this SortOption sortOption)
+ {
+ const string prefix = "flowlauncher_plugin_everything_sort_by_";
+
+ ArgumentNullException.ThrowIfNull(API);
+
+ var enumName = Enum.GetName(sortOption);
+ var splited = enumName.Split('_');
+ var name = string.Join('_', splited[..^1]);
+ var direction = splited[^1];
+
+ return $"{API.GetTranslation(prefix + name.ToLower())} {API.GetTranslation(prefix + direction.ToLower())}";
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/context_menu.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/context_menu.png
new file mode 100644
index 00000000000..c6138b765ba
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/context_menu.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/error.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/error.png
new file mode 100644
index 00000000000..3b17d925b3f
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/error.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/everything_error.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/everything_error.png
new file mode 100644
index 00000000000..ad3eab17973
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/everything_error.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png
deleted file mode 100644
index a671dac21e7..00000000000
Binary files a/Plugins/Flow.Launcher.Plugin.Explorer/Images/index.png and /dev/null differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/index_error.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index_error.png
new file mode 100644
index 00000000000..518d1d75d21
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index_error.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/index_error2.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index_error2.png
new file mode 100644
index 00000000000..13a3d34fe23
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/index_error2.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Images/robot_error.png b/Plugins/Flow.Launcher.Plugin.Explorer/Images/robot_error.png
new file mode 100644
index 00000000000..6b4f83b42f2
Binary files /dev/null and b/Plugins/Flow.Launcher.Plugin.Explorer/Images/robot_error.png differ
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
index e703d8545bd..4a6ada4fac4 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Languages/en.xaml
@@ -1,8 +1,8 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:system="clr-namespace:System;assembly=mscorlib">
-
+
Please make a selection first
Please select a folder link
Are you sure you want to delete {0}?
@@ -17,15 +17,22 @@
Explorer Alternative
Error occurred during search: {0}
-
+
Delete
Edit
Add
+ General Setting
Customise Action Keywords
Quick Access Links
+ Everything Setting
+ Sort Option:
+ Everything Path:
+ Launch Hidden
+ Editor Path
+ Shell Path
Index Search Excluded Paths
+ Use search result's location as executable working directory
Use Index Search For Path Search
- Turning this on will return indexed directories/files faster, but if a directory/file is not indexed it will not show up. If a directory/file has been added to Index Search Excluded Path then it will still show up even if this option is on
Indexing Options
Search:
Path Search:
@@ -36,12 +43,24 @@
Done
Enabled
When disabled Flow will not execute this search option, and will additionally revert back to '*' to free up the action keyword
+ Everything
+ Windows Index
+ Direct Enumeration
-
+ Content Search Engine
+ Directory Recursive Search Engine
+ Index Search Engine
+ Open Window Index Option
+
+
Explorer
Search and manage files and folders. Explorer utilises Windows Index Search
-
+
+ Ctrl + Enter to open the directory
+ Ctrl + Enter to open the containing folder
+
+
Copy path
Copy
Delete
@@ -52,6 +71,7 @@
Open containing folder
Opens the location that contains the file or folder
Open With Editor:
+ Open With Shell:
Exclude current and sub-directories from Index Search
Excluded from Index Search
Open Windows Indexing Options
@@ -67,5 +87,36 @@
Remove from Quick Access
Remove from Quick Access
Remove the current {0} from Quick Access
+ Show Windows Context Menu
+
+
+ Everything SDK Loaded Fail
+ Warning: Everything service is not running
+ Error while querying Everything
+ Sort By
+ Name
+ Path
+ Size
+ Extension
+ Type Name
+ Date Created
+ Date Modified
+ Attributes
+ File List FileName
+ Run Count
+ Date Recently Changed
+ Date Accessed
+ Date Run
+ ↑
+ ↓
+ Warning: This is not a Fast Sort option, searches may be slow
+
+ Click to Launch or Install Everything
+ Everything Installation
+ Installing Everything service. Please wait...
+ Successfully installed Everything service
+ Failed to automatically install Everything service. Please manually install it from https://www.voidtools.com
+ Click here to start it
+ Unable to find an Everything installation, would you like to manually select a location?{0}{0}Click no and Everything will be automatically installed for you
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
index 60208759ed0..82a5d544122 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Main.cs
@@ -1,19 +1,22 @@
-using Flow.Launcher.Infrastructure.Storage;
+using Flow.Launcher.Plugin.Explorer.Helper;
using Flow.Launcher.Plugin.Explorer.Search;
-using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
+using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.ViewModels;
using Flow.Launcher.Plugin.Explorer.Views;
+using System;
using System.Collections.Generic;
-using System.Linq;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using System.Windows;
using System.Windows.Controls;
+using Flow.Launcher.Plugin.Explorer.Exceptions;
namespace Flow.Launcher.Plugin.Explorer
{
public class Main : ISettingProvider, IAsyncPlugin, IContextMenu, IPluginI18n
{
- internal PluginInitContext Context { get; set; }
+ internal static PluginInitContext Context { get; set; }
internal Settings Settings;
@@ -31,23 +34,19 @@ public Control CreateSettingPanel()
public Task InitAsync(PluginInitContext context)
{
Context = context;
-
+
Settings = context.API.LoadSettingJsonStorage();
viewModel = new SettingsViewModel(context, Settings);
-
-
- // as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards.
- if (Settings.QuickFolderAccessLinks.Any())
- {
- Settings.QuickAccessLinks = Settings.QuickFolderAccessLinks;
- Settings.QuickFolderAccessLinks = new List();
- }
contextMenu = new ContextMenu(Context, Settings, viewModel);
searchManager = new SearchManager(Settings, Context);
ResultManager.Init(Context, Settings);
+
+ SortOptionTranslationHelper.API = context.API;
+ EverythingApiDllImport.Load(Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "EverythingSDK",
+ Environment.Is64BitProcess ? "x64" : "x86"));
return Task.CompletedTask;
}
@@ -58,7 +57,34 @@ public List LoadContextMenus(Result selectedResult)
public async Task> QueryAsync(Query query, CancellationToken token)
{
- return await searchManager.SearchAsync(query, token);
+ try
+ {
+ return await searchManager.SearchAsync(query, token);
+ }
+ catch (Exception e) when (e is SearchException or EngineNotAvailableException)
+ {
+ return new List
+ {
+ new()
+ {
+ Title = e.Message,
+ SubTitle = e is EngineNotAvailableException { Resolution: { } resolution }
+ ? resolution
+ : "Enter to copy the message to clipboard",
+ Score = 501,
+ IcoPath = e is EngineNotAvailableException { ErrorIcon: { } iconPath }
+ ? iconPath
+ : Constants.GeneralSearchErrorImagePath,
+ AsyncAction = e is EngineNotAvailableException {Action: { } action}
+ ? action
+ : _ =>
+ {
+ Clipboard.SetDataObject(e.ToString());
+ return new ValueTask(true);
+ }
+ }
+ };
+ }
}
public string GetTranslatedPluginTitle()
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
index 78c7c98a5ec..2918cb61f21 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Constants.cs
@@ -17,6 +17,12 @@ internal static class Constants
internal const string IndexingOptionsIconImagePath = "Images\\windowsindexingoptions.png";
internal const string QuickAccessImagePath = "Images\\quickaccess.png";
internal const string RemoveQuickAccessImagePath = "Images\\removequickaccess.png";
+ internal const string ShowContextMenuImagePath = "Images\\context_menu.png";
+ internal const string EverythingErrorImagePath = "Images\\everything_error.png";
+ internal const string IndexSearchWarningImagePath = "Images\\index_error.png";
+ internal const string WindowsIndexErrorImagePath = "Images\\index_error2.png";
+ internal const string GeneralSearchErrorImagePath = "Images\\robot_error.png";
+
internal const string ToolTipOpenDirectory = "Ctrl + Enter to open the directory";
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs
index 93b68675fb8..d24ad898100 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/DirectoryInfo/DirectoryInfoSearch.cs
@@ -10,7 +10,7 @@ namespace Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo
{
public static class DirectoryInfoSearch
{
- internal static List TopLevelDirectorySearch(Query query, string search, CancellationToken token)
+ internal static IEnumerable TopLevelDirectorySearch(Query query, string search, CancellationToken token)
{
var criteria = ConstructSearchCriteria(search);
@@ -19,9 +19,9 @@ internal static List TopLevelDirectorySearch(Query query, string search,
return DirectorySearch(new EnumerationOptions
{
RecurseSubdirectories = true
- }, query, search, criteria, token);
+ }, search, criteria, token);
- return DirectorySearch(new EnumerationOptions(), query, search, criteria,
+ return DirectorySearch(new EnumerationOptions(), search, criteria,
token); // null will be passed as default
}
@@ -33,10 +33,10 @@ public static string ConstructSearchCriteria(string search)
{
var indexOfSeparator = search.LastIndexOf(Constants.DirectorySeperator);
- incompleteName = search.Substring(indexOfSeparator + 1).ToLower();
+ incompleteName = search[(indexOfSeparator + 1)..].ToLower();
if (incompleteName.StartsWith(Constants.AllFilesFolderSearchWildcard))
- incompleteName = "*" + incompleteName.Substring(1);
+ incompleteName = string.Concat("*", incompleteName.AsSpan(1));
}
incompleteName += "*";
@@ -44,54 +44,45 @@ public static string ConstructSearchCriteria(string search)
return incompleteName;
}
- private static List DirectorySearch(EnumerationOptions enumerationOption, Query query, string search,
+ private static IEnumerable DirectorySearch(EnumerationOptions enumerationOption, string search,
string searchCriteria, CancellationToken token)
{
- var results = new List();
+ var results = new List();
var path = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(search);
- var folderList = new List();
- var fileList = new List();
-
try
{
var directoryInfo = new System.IO.DirectoryInfo(path);
foreach (var fileSystemInfo in directoryInfo.EnumerateFileSystemInfos(searchCriteria, enumerationOption))
{
- if (fileSystemInfo is System.IO.DirectoryInfo)
- {
- folderList.Add(ResultManager.CreateFolderResult(fileSystemInfo.Name, fileSystemInfo.FullName,
- fileSystemInfo.FullName, query, 0, true, false));
- }
- else
+ results.Add(new SearchResult
{
- fileList.Add(ResultManager.CreateFileResult(fileSystemInfo.FullName, query, 0, true, false));
- }
+ FullPath = fileSystemInfo.FullName,
+ Type = fileSystemInfo switch
+ {
+ System.IO.DirectoryInfo {Parent: null} => ResultType.Volume,
+ System.IO.DirectoryInfo => ResultType.Folder,
+ FileInfo => ResultType.File,
+ _ => throw new ArgumentOutOfRangeException(nameof(fileSystemInfo))
+ },
+ WindowsIndexed = false
+ });
- token.ThrowIfCancellationRequested();
+ if (token.IsCancellationRequested)
+ return results;
}
}
catch (Exception e)
{
Log.Exception(nameof(DirectoryInfoSearch), "Error occured while searching path", e);
- results.Add(
- new Result
- {
- Title = string.Format(SearchManager.Context.API.GetTranslation(
- "plugin_explorer_directoryinfosearch_error"),
- e.Message),
- Score = 501,
- IcoPath = Constants.ExplorerIconImagePath
- });
-
- return results;
+ throw;
}
// Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
- return results.Concat(folderList.OrderBy(x => x.Title)).Concat(fileList.OrderBy(x => x.Title)).ToList();
+ return results.OrderBy(r=>r.Type).ThenBy(r=>r.FullPath);
}
}
-}
\ No newline at end of file
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs
new file mode 100644
index 00000000000..5381d729d5b
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingAPI.cs
@@ -0,0 +1,216 @@
+using Flow.Launcher.Plugin.Everything.Everything;
+using Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms.Design;
+using Flow.Launcher.Plugin.Explorer.Exceptions;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything
+{
+
+ public static class EverythingApi
+ {
+
+ private const int BufferSize = 4096;
+
+ private static SemaphoreSlim _semaphore = new(1, 1);
+ // cached buffer to remove redundant allocations.
+ private static readonly StringBuilder buffer = new(BufferSize);
+
+ public enum StateCode
+ {
+ OK,
+ MemoryError,
+ IPCError,
+ RegisterClassExError,
+ CreateWindowError,
+ CreateThreadError,
+ InvalidIndexError,
+ InvalidCallError
+ }
+
+ ///
+ /// Gets or sets a value indicating whether [match path].
+ ///
+ /// true if [match path]; otherwise, false.
+ public static bool MatchPath
+ {
+ get => EverythingApiDllImport.Everything_GetMatchPath();
+ set => EverythingApiDllImport.Everything_SetMatchPath(value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether [match case].
+ ///
+ /// true if [match case]; otherwise, false.
+ public static bool MatchCase
+ {
+ get => EverythingApiDllImport.Everything_GetMatchCase();
+ set => EverythingApiDllImport.Everything_SetMatchCase(value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether [match whole word].
+ ///
+ /// true if [match whole word]; otherwise, false.
+ public static bool MatchWholeWord
+ {
+ get => EverythingApiDllImport.Everything_GetMatchWholeWord();
+ set => EverythingApiDllImport.Everything_SetMatchWholeWord(value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether [enable regex].
+ ///
+ /// true if [enable regex]; otherwise, false.
+ public static bool EnableRegex
+ {
+ get => EverythingApiDllImport.Everything_GetRegex();
+ set => EverythingApiDllImport.Everything_SetRegex(value);
+ }
+
+ ///
+ /// Checks whether the sort option is Fast Sort.
+ ///
+ public static bool IsFastSortOption(SortOption sortOption)
+ {
+ var fastSortOptionEnabled = EverythingApiDllImport.Everything_IsFastSort(sortOption);
+
+ // If the Everything service is not running, then this call will incorrectly report
+ // the state as false. This checks for errors thrown by the api and up to the caller to handle.
+ CheckAndThrowExceptionOnError();
+
+ return fastSortOptionEnabled;
+ }
+
+ public static async ValueTask IsEverythingRunningAsync(CancellationToken token = default)
+ {
+ await _semaphore.WaitAsync(token);
+
+ try
+ {
+ EverythingApiDllImport.Everything_GetMajorVersion();
+ var result = EverythingApiDllImport.Everything_GetLastError() != StateCode.IPCError;
+ return result;
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ ///
+ /// Searches the specified key word and reset the everything API afterwards
+ ///
+ /// Search Criteria
+ /// when cancelled the current search will stop and exit (and would not reset)
+ /// An IAsyncEnumerable that will enumerate all results searched by the specific query and option
+ public static async IAsyncEnumerable SearchAsync(EverythingSearchOption option,
+ [EnumeratorCancellation] CancellationToken token)
+ {
+ if (option.Offset < 0)
+ throw new ArgumentOutOfRangeException(nameof(option.Offset), option.Offset, "Offset must be greater than or equal to 0");
+
+ if (option.MaxCount < 0)
+ throw new ArgumentOutOfRangeException(nameof(option.MaxCount), option.MaxCount, "MaxCount must be greater than or equal to 0");
+
+ await _semaphore.WaitAsync(token);
+
+
+ try
+ {
+ if (token.IsCancellationRequested)
+ yield break;
+
+ if (option.Keyword.StartsWith("@"))
+ {
+ EverythingApiDllImport.Everything_SetRegex(true);
+ option.Keyword = option.Keyword[1..];
+ }
+
+ var builder = new StringBuilder();
+ builder.Append(option.Keyword);
+
+ if (!string.IsNullOrWhiteSpace(option.ParentPath))
+ {
+ builder.Append($" {(option.IsRecursive ? "" : "parent:")}\"{option.ParentPath}\"");
+ }
+
+ if (option.IsContentSearch)
+ {
+ builder.Append($" content:\"{option.ContentSearchKeyword}\"");
+ }
+
+ EverythingApiDllImport.Everything_SetSearchW(builder.ToString());
+ EverythingApiDllImport.Everything_SetOffset(option.Offset);
+ EverythingApiDllImport.Everything_SetMax(option.MaxCount);
+
+ EverythingApiDllImport.Everything_SetSort(option.SortOption);
+
+ if (token.IsCancellationRequested) yield break;
+
+ if (!EverythingApiDllImport.Everything_QueryW(true))
+ {
+ CheckAndThrowExceptionOnError();
+ yield break;
+ }
+
+ for (var idx = 0; idx < EverythingApiDllImport.Everything_GetNumResults(); ++idx)
+ {
+ if (token.IsCancellationRequested)
+ {
+ yield break;
+ }
+
+ EverythingApiDllImport.Everything_GetResultFullPathNameW(idx, buffer, BufferSize);
+
+ var result = new SearchResult
+ {
+ FullPath = buffer.ToString(),
+ Type = EverythingApiDllImport.Everything_IsFolderResult(idx) ? ResultType.Folder :
+ EverythingApiDllImport.Everything_IsFileResult(idx) ? ResultType.File :
+ ResultType.Volume
+ };
+
+ yield return result;
+ }
+ }
+ finally
+ {
+ EverythingApiDllImport.Everything_Reset();
+ _semaphore.Release();
+ }
+ }
+
+ private static void CheckAndThrowExceptionOnError()
+ {
+ switch (EverythingApiDllImport.Everything_GetLastError())
+ {
+ case StateCode.CreateThreadError:
+ throw new CreateThreadException();
+ case StateCode.CreateWindowError:
+ throw new CreateWindowException();
+ case StateCode.InvalidCallError:
+ throw new InvalidCallException();
+ case StateCode.InvalidIndexError:
+ throw new InvalidIndexException();
+ case StateCode.IPCError:
+ throw new IPCErrorException();
+ case StateCode.MemoryError:
+ throw new MemoryErrorException();
+ case StateCode.RegisterClassExError:
+ throw new RegisterClassExException();
+ case StateCode.OK:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiDllImport.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiDllImport.cs
new file mode 100644
index 00000000000..5b80819faed
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingApiDllImport.cs
@@ -0,0 +1,163 @@
+using Flow.Launcher.Plugin.Everything.Everything;
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything
+{
+ public static class EverythingApiDllImport
+ {
+ public static void Load(string directory)
+ {
+ var path = Path.Combine(directory, DLL);
+ int code = LoadLibrary(path);
+ if (code == 0)
+ {
+ int err = Marshal.GetLastPInvokeError();
+ Marshal.ThrowExceptionForHR(err);
+ }
+ }
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ private static extern int LoadLibrary(string name);
+
+ private const string DLL = "Everything.dll";
+
+ [DllImport(DLL, CharSet = CharSet.Unicode)]
+ internal static extern int Everything_SetSearchW(string lpSearchString);
+
+ [DllImport(DLL)]
+ internal static extern void Everything_SetMatchPath(bool bEnable);
+
+ [DllImport(DLL)]
+ internal static extern void Everything_SetMatchCase(bool bEnable);
+
+ [DllImport(DLL)]
+ internal static extern void Everything_SetMatchWholeWord(bool bEnable);
+
+ [DllImport(DLL)]
+ internal static extern void Everything_SetRegex(bool bEnable);
+
+ [DllImport(DLL)]
+ internal static extern void Everything_SetMax(int dwMax);
+
+ [DllImport(DLL)]
+ internal static extern void Everything_SetOffset(int dwOffset);
+
+ [DllImport(DLL)]
+ internal static extern bool Everything_GetMatchPath();
+
+ [DllImport(DLL)]
+ internal static extern bool Everything_GetMatchCase();
+
+ [DllImport(DLL)]
+ internal static extern bool Everything_GetMatchWholeWord();
+
+ [DllImport(DLL)]
+ internal static extern bool Everything_GetRegex();
+
+ [DllImport(DLL)]
+ internal static extern uint Everything_GetMax();
+
+ [DllImport(DLL)]
+ internal static extern uint Everything_GetOffset();
+
+ [DllImport(DLL, CharSet = CharSet.Unicode)]
+ internal static extern string Everything_GetSearchW();
+
+ [DllImport(DLL)]
+ internal static extern EverythingApi.StateCode Everything_GetLastError();
+
+ [DllImport(DLL, CharSet = CharSet.Unicode)]
+ internal static extern bool Everything_QueryW(bool bWait);
+
+ [DllImport(DLL)]
+ internal static extern void Everything_SortResultsByPath();
+
+ [DllImport(DLL)]
+ internal static extern int Everything_GetNumFileResults();
+
+ [DllImport(DLL)]
+ internal static extern int Everything_GetMajorVersion();
+
+ [DllImport(DLL)]
+ internal static extern int Everything_GetNumFolderResults();
+
+ [DllImport(DLL)]
+ internal static extern int Everything_GetNumResults();
+
+ [DllImport(DLL)]
+ internal static extern int Everything_GetTotFileResults();
+
+ [DllImport(DLL)]
+ internal static extern int Everything_GetTotFolderResults();
+
+ [DllImport(DLL)]
+ internal static extern int Everything_GetTotResults();
+
+ [DllImport(DLL)]
+ internal static extern bool Everything_IsVolumeResult(int nIndex);
+
+ [DllImport(DLL)]
+ internal static extern bool Everything_IsFolderResult(int nIndex);
+
+ [DllImport(DLL)]
+ internal static extern bool Everything_IsFileResult(int nIndex);
+
+ [DllImport(DLL, CharSet = CharSet.Unicode)]
+ internal static extern void Everything_GetResultFullPathNameW(int nIndex, StringBuilder lpString, int nMaxCount);
+
+ [DllImport(DLL)]
+ internal static extern void Everything_Reset();
+
+ // Everything 1.4
+
+ [DllImport(DLL)]
+ public static extern void Everything_SetSort(SortOption dwSortType);
+ [DllImport(DLL)]
+ public static extern bool Everything_IsFastSort(SortOption dwSortType);
+ [DllImport(DLL)]
+ public static extern SortOption Everything_GetSort();
+ [DllImport(DLL)]
+ public static extern uint Everything_GetResultListSort();
+ [DllImport(DLL)]
+ public static extern void Everything_SetRequestFlags(uint dwRequestFlags);
+ [DllImport(DLL)]
+ public static extern uint Everything_GetRequestFlags();
+ [DllImport(DLL)]
+ public static extern uint Everything_GetResultListRequestFlags();
+ [DllImport("Everything64.dll", CharSet = CharSet.Unicode)]
+ public static extern IntPtr Everything_GetResultExtension(uint nIndex);
+ [DllImport(DLL)]
+ public static extern bool Everything_GetResultSize(uint nIndex, out long lpFileSize);
+ [DllImport(DLL)]
+ public static extern bool Everything_GetResultDateCreated(uint nIndex, out long lpFileTime);
+ [DllImport(DLL)]
+ public static extern bool Everything_GetResultDateModified(uint nIndex, out long lpFileTime);
+ [DllImport(DLL)]
+ public static extern bool Everything_GetResultDateAccessed(uint nIndex, out long lpFileTime);
+ [DllImport(DLL)]
+ public static extern uint Everything_GetResultAttributes(uint nIndex);
+ [DllImport(DLL, CharSet = CharSet.Unicode)]
+ public static extern IntPtr Everything_GetResultFileListFileName(uint nIndex);
+ [DllImport(DLL)]
+ public static extern uint Everything_GetResultRunCount(uint nIndex);
+ [DllImport(DLL)]
+ public static extern bool Everything_GetResultDateRun(uint nIndex, out long lpFileTime);
+ [DllImport(DLL)]
+ public static extern bool Everything_GetResultDateRecentlyChanged(uint nIndex, out long lpFileTime);
+ [DllImport(DLL, CharSet = CharSet.Unicode)]
+ public static extern IntPtr Everything_GetResultHighlightedFileName(uint nIndex);
+ [DllImport(DLL, CharSet = CharSet.Unicode)]
+ public static extern IntPtr Everything_GetResultHighlightedPath(uint nIndex);
+ [DllImport(DLL, CharSet = CharSet.Unicode)]
+ public static extern IntPtr Everything_GetResultHighlightedFullPathAndFileName(uint nIndex);
+ [DllImport(DLL)]
+ public static extern uint Everything_GetRunCountFromFileName(string lpFileName);
+ [DllImport(DLL)]
+ public static extern bool Everything_SetRunCountFromFileName(string lpFileName, uint dwRunCount);
+ [DllImport(DLL)]
+ public static extern uint Everything_IncRunCountFromFileName(string lpFileName);
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingDownloadHelper.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingDownloadHelper.cs
new file mode 100644
index 00000000000..ce774281c94
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingDownloadHelper.cs
@@ -0,0 +1,88 @@
+using Droplex;
+using Flow.Launcher.Plugin.SharedCommands;
+using Microsoft.Win32;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything;
+
+public static class EverythingDownloadHelper
+{
+ public static async Task PromptDownloadIfNotInstallAsync(string installedLocation, IPublicAPI api)
+ {
+ if (!string.IsNullOrEmpty(installedLocation) && installedLocation.FileExists())
+ return installedLocation;
+
+ installedLocation = GetInstalledPath();
+
+ if (string.IsNullOrEmpty(installedLocation))
+ {
+ if (System.Windows.Forms.MessageBox.Show(
+ string.Format(api.GetTranslation("flowlauncher_plugin_everything_installing_select"), Environment.NewLine),
+ api.GetTranslation("flowlauncher_plugin_everything_installing_title"),
+ System.Windows.Forms.MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes)
+ {
+ var dlg = new System.Windows.Forms.OpenFileDialog
+ {
+ InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
+ };
+
+ var result = dlg.ShowDialog();
+ if (result == System.Windows.Forms.DialogResult.OK && !string.IsNullOrEmpty(dlg.FileName))
+ installedLocation = dlg.FileName;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(installedLocation))
+ {
+ return installedLocation;
+ }
+
+ api.ShowMsg(api.GetTranslation("flowlauncher_plugin_everything_installing_title"),
+ api.GetTranslation("flowlauncher_plugin_everything_installing_subtitle"), "", useMainWindowAsOwner: false);
+
+ await DroplexPackage.Drop(App.Everything1_4_1_1009).ConfigureAwait(false);
+
+ api.ShowMsg(api.GetTranslation("flowlauncher_plugin_everything_installing_title"),
+ api.GetTranslation("flowlauncher_plugin_everything_installationsuccess_subtitle"), "", useMainWindowAsOwner: false);
+
+ installedLocation = "C:\\Program Files\\Everything\\Everything.exe";
+
+ FilesFolders.OpenPath(installedLocation);
+
+ return installedLocation;
+
+ }
+
+ internal static string GetInstalledPath()
+ {
+ using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall");
+ if (key is not null)
+ {
+ foreach (var subKey in key.GetSubKeyNames().Select(keyName => key.OpenSubKey(keyName)))
+ {
+ if (subKey?.GetValue("DisplayName") is not string displayName || !displayName.Contains("Everything"))
+ {
+ continue;
+ }
+ if (subKey.GetValue("UninstallString") is not string uninstallString)
+ {
+ continue;
+ }
+
+ if (Path.GetDirectoryName(uninstallString) is not { } uninstallDirectory)
+ {
+ continue;
+ }
+ return Path.Combine(uninstallDirectory, "Everything.exe");
+ }
+ }
+
+ var scoopInstalledPath = Environment.ExpandEnvironmentVariables(@"%userprofile%\scoop\apps\everything\current\Everything.exe");
+ return File.Exists(scoopInstalledPath) ? scoopInstalledPath : string.Empty;
+
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs
new file mode 100644
index 00000000000..ffd22d9f5eb
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Flow.Launcher.Plugin.Explorer.Exceptions;
+using Flow.Launcher.Plugin.Explorer.Search.IProvider;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything
+{
+ public class EverythingSearchManager : IIndexProvider, IContentIndexProvider, IPathIndexProvider
+ {
+ private Settings Settings { get; }
+
+ public EverythingSearchManager(Settings settings)
+ {
+ Settings = settings;
+ }
+
+ private async ValueTask ThrowIfEverythingNotAvailableAsync(CancellationToken token = default)
+ {
+ try
+ {
+ if (!await EverythingApi.IsEverythingRunningAsync(token))
+ throw new EngineNotAvailableException(
+ Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
+ Main.Context.API.GetTranslation("flowlauncher_plugin_everything_click_to_launch_or_install"),
+ Main.Context.API.GetTranslation("flowlauncher_plugin_everything_is_not_running"),
+ ClickToInstallEverythingAsync)
+ {
+ ErrorIcon = Constants.EverythingErrorImagePath
+ };
+ }
+ catch (DllNotFoundException)
+ {
+ throw new EngineNotAvailableException(
+ Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
+ "Please check whether your system is x86 or x64",
+ Main.Context.API.GetTranslation("flowlauncher_plugin_everything_sdk_issue"))
+ {
+ ErrorIcon = Constants.GeneralSearchErrorImagePath
+ };
+ }
+ }
+ private async ValueTask ClickToInstallEverythingAsync(ActionContext _)
+ {
+ var installedPath = await EverythingDownloadHelper.PromptDownloadIfNotInstallAsync(Settings.EverythingInstalledPath, Main.Context.API);
+ if (installedPath == null)
+ {
+ Main.Context.API.ShowMsgError("Unable to find Everything.exe");
+ return false;
+ }
+ Settings.EverythingInstalledPath = installedPath;
+ Process.Start(installedPath, "-startup");
+ return true;
+ }
+
+ public async IAsyncEnumerable SearchAsync(string search, [EnumeratorCancellation] CancellationToken token)
+ {
+ await ThrowIfEverythingNotAvailableAsync(token);
+ if (token.IsCancellationRequested)
+ yield break;
+ var option = new EverythingSearchOption(search, Settings.SortOption);
+ await foreach (var result in EverythingApi.SearchAsync(option, token))
+ yield return result;
+ }
+ public async IAsyncEnumerable ContentSearchAsync(string plainSearch,
+ string contentSearch, [EnumeratorCancellation] CancellationToken token)
+ {
+ await ThrowIfEverythingNotAvailableAsync(token);
+ if (!Settings.EnableEverythingContentSearch)
+ {
+ throw new EngineNotAvailableException(Enum.GetName(Settings.IndexSearchEngineOption.Everything)!,
+ "Click to Enable Everything Content Search (only applicable to Everything 1.5+ with indexed content)",
+ "Everything Content Search is not enabled.",
+ _ =>
+ {
+ Settings.EnableEverythingContentSearch = true;
+ return ValueTask.FromResult(true);
+ })
+ {
+ ErrorIcon = Constants.EverythingErrorImagePath
+ };
+ }
+ if (token.IsCancellationRequested)
+ yield break;
+
+ var option = new EverythingSearchOption(plainSearch,
+ Settings.SortOption,
+ true,
+ contentSearch);
+
+ await foreach (var result in EverythingApi.SearchAsync(option, token))
+ {
+ yield return result;
+ }
+ }
+ public async IAsyncEnumerable EnumerateAsync(string path, string search, bool recursive, [EnumeratorCancellation] CancellationToken token)
+ {
+ await ThrowIfEverythingNotAvailableAsync(token);
+ if (token.IsCancellationRequested)
+ yield break;
+
+ var option = new EverythingSearchOption(search,
+ Settings.SortOption,
+ ParentPath: path,
+ IsRecursive: recursive);
+
+ await foreach (var result in EverythingApi.SearchAsync(option, token))
+ {
+ yield return result;
+ }
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs
new file mode 100644
index 00000000000..6839822a485
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchOption.cs
@@ -0,0 +1,14 @@
+using System;
+using Flow.Launcher.Plugin.Everything.Everything;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything
+{
+ public record struct EverythingSearchOption(string Keyword,
+ SortOption SortOption,
+ bool IsContentSearch = false,
+ string ContentSearchKeyword = default,
+ string ParentPath = default,
+ bool IsRecursive = true,
+ int Offset = 0,
+ int MaxCount = 100);
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/CreateThreadException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/CreateThreadException.cs
new file mode 100644
index 00000000000..32163057bc1
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/CreateThreadException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
+{
+ ///
+ ///
+ ///
+ public class CreateThreadException : ApplicationException
+ {
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/CreateWindowException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/CreateWindowException.cs
new file mode 100644
index 00000000000..9704226d78b
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/CreateWindowException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
+{
+ ///
+ ///
+ ///
+ public class CreateWindowException : ApplicationException
+ {
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/IPCErrorException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/IPCErrorException.cs
new file mode 100644
index 00000000000..41629d2e429
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/IPCErrorException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
+{
+ ///
+ ///
+ ///
+ public class IPCErrorException : ApplicationException
+ {
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/InvalidCallException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/InvalidCallException.cs
new file mode 100644
index 00000000000..f84dc1ab8c9
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/InvalidCallException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
+{
+ ///
+ ///
+ ///
+ public class InvalidCallException : ApplicationException
+ {
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/InvalidIndexException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/InvalidIndexException.cs
new file mode 100644
index 00000000000..cbf75e5a383
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/InvalidIndexException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
+{
+ ///
+ ///
+ ///
+ public class InvalidIndexException : ApplicationException
+ {
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/MemoryErrorException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/MemoryErrorException.cs
new file mode 100644
index 00000000000..c632cd53070
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/MemoryErrorException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
+{
+ ///
+ ///
+ ///
+ public class MemoryErrorException : ApplicationException
+ {
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/RegisterClassExException.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/RegisterClassExException.cs
new file mode 100644
index 00000000000..2ebdbb6899b
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/Exceptions/RegisterClassExException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.Everything.Exceptions
+{
+ ///
+ ///
+ ///
+ public class RegisterClassExException : ApplicationException
+ {
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs
new file mode 100644
index 00000000000..434afd1b465
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/SortOption.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Flow.Launcher.Plugin.Everything.Everything
+{
+ public enum SortOption : uint
+ {
+ NAME_ASCENDING = 1u,
+ NAME_DESCENDING = 2u,
+ PATH_ASCENDING = 3u,
+ PATH_DESCENDING = 4u,
+ SIZE_ASCENDING = 5u,
+ SIZE_DESCENDING = 6u,
+ EXTENSION_ASCENDING = 7u,
+ EXTENSION_DESCENDING = 8u,
+ TYPE_NAME_ASCENDING = 9u,
+ TYPE_NAME_DESCENDING = 10u,
+ DATE_CREATED_ASCENDING = 11u,
+ DATE_CREATED_DESCENDING = 12u,
+ DATE_MODIFIED_ASCENDING = 13u,
+ DATE_MODIFIED_DESCENDING = 14u,
+ ATTRIBUTES_ASCENDING = 15u,
+ ATTRIBUTES_DESCENDING = 16u,
+ FILE_LIST_FILENAME_ASCENDING = 17u,
+ FILE_LIST_FILENAME_DESCENDING = 18u,
+ RUN_COUNT_ASCENDING = 19u,
+ RUN_COUNT_DESCENDING = 20u,
+ DATE_RECENTLY_CHANGED_ASCENDING = 21u,
+ DATE_RECENTLY_CHANGED_DESCENDING = 22u,
+ DATE_ACCESSED_ASCENDING = 23u,
+ DATE_ACCESSED_DESCENDING = 24u,
+ DATE_RUN_ASCENDING = 25u,
+ DATE_RUN_DESCENDING = 26u
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IContentIndexProvider.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IContentIndexProvider.cs
new file mode 100644
index 00000000000..6e036e05846
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IContentIndexProvider.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
+{
+ public interface IContentIndexProvider
+ {
+ public IAsyncEnumerable ContentSearchAsync(string plainSearch, string contentSearch, CancellationToken token = default);
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IIndexProvider.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IIndexProvider.cs
new file mode 100644
index 00000000000..d43dd7df383
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IIndexProvider.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
+{
+ public interface IIndexProvider
+ {
+ public IAsyncEnumerable SearchAsync(string search, CancellationToken token);
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IPathIndexProvider.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IPathIndexProvider.cs
new file mode 100644
index 00000000000..4622df5f91b
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/IProvider/IPathIndexProvider.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.IProvider
+{
+ public interface IPathIndexProvider
+ {
+ public IAsyncEnumerable EnumerateAsync(string path, string search, bool recursive, CancellationToken token);
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs
index 55975c2a59d..cdd2c93e69c 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/QuickAccessLinks/QuickAccess.cs
@@ -8,7 +8,7 @@ internal static class QuickAccess
{
private const int quickAccessResultScore = 100;
- internal static List AccessLinkListMatched(Query query, List accessLinks)
+ internal static List AccessLinkListMatched(Query query, IEnumerable accessLinks)
{
if (string.IsNullOrEmpty(query.Search))
return new List();
@@ -17,7 +17,7 @@ internal static List AccessLinkListMatched(Query query, List
var queriedAccessLinks =
accessLinks
- .Where(x => x.Name.Contains(search, StringComparison.OrdinalIgnoreCase))
+ .Where(x => x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || x.Path.Contains(search, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.Type)
.ThenBy(x => x.Name);
@@ -29,7 +29,7 @@ internal static List AccessLinkListMatched(Query query, List
}).ToList();
}
- internal static List AccessLinkListAll(Query query, List accessLinks)
+ internal static List AccessLinkListAll(Query query, IEnumerable accessLinks)
=> accessLinks
.OrderBy(x => x.Type)
.ThenBy(x => x.Name)
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs
index d6d382e9a9d..e9cbfc2bd10 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/ResultManager.cs
@@ -1,7 +1,9 @@
-using Flow.Launcher.Infrastructure;
+using Flow.Launcher.Core.Resource;
+using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -35,13 +37,25 @@ private static string GetPathWithActionKeyword(string path, ResultType type)
return $"{keyword}{formatted_path}";
}
- internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool showIndexState = false, bool windowsIndexed = false)
+ public static Result CreateResult(Query query, SearchResult result)
+ {
+ return result.Type switch
+ {
+ ResultType.Folder or ResultType.Volume => CreateFolderResult(Path.GetFileName(result.FullPath),
+ result.FullPath, result.FullPath, query, 0, result.WindowsIndexed),
+ ResultType.File => CreateFileResult(
+ result.FullPath, query, 0, result.WindowsIndexed),
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ }
+
+ internal static Result CreateFolderResult(string title, string subtitle, string path, Query query, int score = 0, bool windowsIndexed = false)
{
return new Result
{
Title = title,
IcoPath = path,
- SubTitle = subtitle,
+ SubTitle = Path.GetDirectoryName(path),
AutoCompleteText = GetPathWithActionKeyword(path, ResultType.Folder),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, title).MatchData,
Action = c =>
@@ -61,17 +75,16 @@ internal static Result CreateFolderResult(string title, string subtitle, string
}
Context.API.ChangeQuery(GetPathWithActionKeyword(path, ResultType.Folder));
-
+
return false;
},
Score = score,
- TitleToolTip = Constants.ToolTipOpenDirectory,
+ TitleToolTip = InternationalizationManager.Instance.GetTranslation("plugin_explorer_plugin_ToolTipOpenDirectory"),
SubTitleToolTip = path,
ContextData = new SearchResult
{
Type = ResultType.Folder,
FullPath = path,
- ShowIndexState = showIndexState,
WindowsIndexed = windowsIndexed
}
};
@@ -80,15 +93,14 @@ internal static Result CreateFolderResult(string title, string subtitle, string
internal static Result CreateDriveSpaceDisplayResult(string path, bool windowsIndexed = false)
{
var progressBarColor = "#26a0da";
- int progressValue = 0;
var title = string.Empty; // hide title when use progress bar,
- var driveLetter = path.Substring(0, 1).ToUpper();
+ var driveLetter = path[..1].ToUpper();
var driveName = driveLetter + ":\\";
DriveInfo drv = new DriveInfo(driveLetter);
- var subtitle = toReadableSize(drv.AvailableFreeSpace, 2) + " free of " + toReadableSize(drv.TotalSize, 2);
- double UsingSize = (Convert.ToDouble(drv.TotalSize) - Convert.ToDouble(drv.AvailableFreeSpace)) / Convert.ToDouble(drv.TotalSize) * 100;
+ var subtitle = ToReadableSize(drv.AvailableFreeSpace, 2) + " free of " + ToReadableSize(drv.TotalSize, 2);
+ double usingSize = (Convert.ToDouble(drv.TotalSize) - Convert.ToDouble(drv.AvailableFreeSpace)) / Convert.ToDouble(drv.TotalSize) * 100;
- progressValue = Convert.ToInt32(UsingSize);
+ int? progressValue = Convert.ToInt32(usingSize);
if (progressValue >= 90)
progressBarColor = "#da2626";
@@ -111,15 +123,14 @@ internal static Result CreateDriveSpaceDisplayResult(string path, bool windowsIn
SubTitleToolTip = path,
ContextData = new SearchResult
{
- Type = ResultType.Folder,
+ Type = ResultType.Volume,
FullPath = path,
- ShowIndexState = true,
WindowsIndexed = windowsIndexed
}
};
}
- private static string toReadableSize(long pDrvSize, int pi)
+ private static string ToReadableSize(long pDrvSize, int pi)
{
int mok = 0;
double drvSize = pDrvSize;
@@ -140,24 +151,16 @@ private static string toReadableSize(long pDrvSize, int pi)
else if (mok == 4)
Space = " TB";
- var returnStr = string.Format("{0}{1}", Convert.ToInt32(drvSize), Space);
+ var returnStr = $"{Convert.ToInt32(drvSize)}{Space}";
if (mok != 0)
{
- switch (pi)
+ returnStr = pi switch
{
- case 1:
- returnStr = string.Format("{0:F1}{1}", drvSize, Space);
- break;
- case 2:
- returnStr = string.Format("{0:F2}{1}", drvSize, Space);
- break;
- case 3:
- returnStr = string.Format("{0:F3}{1}", drvSize, Space);
- break;
- default:
- returnStr = string.Format("{0}{1}", Convert.ToInt32(drvSize), Space);
- break;
- }
+ 1 => $"{drvSize:F1}{Space}",
+ 2 => $"{drvSize:F2}{Space}",
+ 3 => $"{drvSize:F3}{Space}",
+ _ => $"{Convert.ToInt32(drvSize)}{Space}"
+ };
}
return returnStr;
@@ -165,25 +168,13 @@ private static string toReadableSize(long pDrvSize, int pi)
internal static Result CreateOpenCurrentFolderResult(string path, bool windowsIndexed = false)
{
- var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path);
-
- var folderName = retrievedDirectoryPath.TrimEnd(Constants.DirectorySeperator).Split(new[]
+ var folderName = path.TrimEnd(Constants.DirectorySeperator).Split(new[]
{
Path.DirectorySeparatorChar
}, StringSplitOptions.None).Last();
- if (retrievedDirectoryPath.EndsWith(":\\"))
- {
- var driveLetter = path.Substring(0, 1).ToUpper();
- folderName = driveLetter + " drive";
- }
-
- var title = "Open current directory";
-
- if (retrievedDirectoryPath != path)
- title = "Open " + folderName;
-
-
+ var title = $"Open {folderName}";
+
var subtitleFolderName = folderName;
// ie. max characters can be displayed without subtitle cutting off: "Program Files (x86)"
@@ -195,32 +186,29 @@ internal static Result CreateOpenCurrentFolderResult(string path, bool windowsIn
Title = title,
SubTitle = $"Use > to search within {subtitleFolderName}, " +
$"* to search for file extensions or >* to combine both searches.",
- AutoCompleteText = GetPathWithActionKeyword(retrievedDirectoryPath, ResultType.Folder),
- IcoPath = retrievedDirectoryPath,
+ AutoCompleteText = GetPathWithActionKeyword(path, ResultType.Folder),
+ IcoPath = path,
Score = 500,
- Action = c =>
+ Action = _ =>
{
- Context.API.OpenDirectory(retrievedDirectoryPath);
+ Context.API.OpenDirectory(path);
return true;
},
- TitleToolTip = retrievedDirectoryPath,
- SubTitleToolTip = retrievedDirectoryPath,
ContextData = new SearchResult
{
Type = ResultType.Folder,
- FullPath = retrievedDirectoryPath,
- ShowIndexState = true,
+ FullPath = path,
WindowsIndexed = windowsIndexed
}
};
}
- internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool showIndexState = false, bool windowsIndexed = false)
+ internal static Result CreateFileResult(string filePath, Query query, int score = 0, bool windowsIndexed = false)
{
var result = new Result
{
Title = Path.GetFileName(filePath),
- SubTitle = filePath,
+ SubTitle = Path.GetDirectoryName(filePath),
IcoPath = filePath,
AutoCompleteText = GetPathWithActionKeyword(filePath, ResultType.File),
TitleHighlightData = StringMatcher.FuzzySearch(query.Search, Path.GetFileName(filePath)).MatchData,
@@ -231,7 +219,7 @@ internal static Result CreateFileResult(string filePath, Query query, int score
{
if (File.Exists(filePath) && c.SpecialKeyState.CtrlPressed && c.SpecialKeyState.ShiftPressed)
{
- Task.Run(() =>
+ _ = Task.Run(() =>
{
try
{
@@ -264,13 +252,12 @@ internal static Result CreateFileResult(string filePath, Query query, int score
return true;
},
- TitleToolTip = Constants.ToolTipOpenContainingFolder,
+ TitleToolTip = InternationalizationManager.Instance.GetTranslation("plugin_explorer_plugin_ToolTipOpenContainingFolder"),
SubTitleToolTip = filePath,
ContextData = new SearchResult
{
Type = ResultType.File,
FullPath = filePath,
- ShowIndexState = showIndexState,
WindowsIndexed = windowsIndexed
}
};
@@ -278,16 +265,6 @@ internal static Result CreateFileResult(string filePath, Query query, int score
}
}
- internal class SearchResult
- {
- public string FullPath { get; set; }
- public ResultType Type { get; set; }
-
- public bool WindowsIndexed { get; set; }
-
- public bool ShowIndexState { get; set; }
- }
-
public enum ResultType
{
Volume,
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
index 4bddbda5770..8d477d8e9e4 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchManager.cs
@@ -1,12 +1,13 @@
using Flow.Launcher.Plugin.Explorer.Search.DirectoryInfo;
+using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
-using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Flow.Launcher.Plugin.Explorer.Exceptions;
namespace Flow.Launcher.Plugin.Explorer.Search
{
@@ -29,52 +30,81 @@ private class PathEqualityComparator : IEqualityComparer
public bool Equals(Result x, Result y)
{
- return x.SubTitle == y.SubTitle;
+ return x.Title == y.Title && x.SubTitle == y.SubTitle;
}
public int GetHashCode(Result obj)
{
- return obj.SubTitle.GetHashCode();
+ return HashCode.Combine(obj.Title.GetHashCode(), obj.SubTitle?.GetHashCode() ?? 0);
}
}
internal async Task> SearchAsync(Query query, CancellationToken token)
{
- var querySearch = query.Search;
-
var results = new HashSet(PathEqualityComparator.Instance);
// This allows the user to type the below action keywords and see/search the list of quick folder links
if (ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)
|| ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword)
- || ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword))
+ || ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword)
+ || ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword)
+ || ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword))
{
- if (string.IsNullOrEmpty(query.Search))
+ if (string.IsNullOrEmpty(query.Search) && ActionKeywordMatch(query, Settings.ActionKeyword.QuickAccessActionKeyword))
return QuickAccess.AccessLinkListAll(query, Settings.QuickAccessLinks);
- var quickaccessLinks = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
+ var quickAccessLinks = QuickAccess.AccessLinkListMatched(query, Settings.QuickAccessLinks);
- results.UnionWith(quickaccessLinks);
+ results.UnionWith(quickAccessLinks);
}
-
- if (IsFileContentSearch(query.ActionKeyword))
- return await WindowsIndexFileContentSearchAsync(query, querySearch, token).ConfigureAwait(false);
-
- if (ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword) ||
- ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword))
+ else
{
- results.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false));
+ return new List();
}
- if ((ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword) ||
- ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword)) &&
- querySearch.Length > 0 &&
- !querySearch.IsLocationPathString())
+ IAsyncEnumerable searchResults = null;
+
+ bool isPathSearch = query.Search.IsLocationPathString();
+
+ switch (isPathSearch)
{
- results.UnionWith(await WindowsIndexFilesAndFoldersSearchAsync(query, querySearch, token)
- .ConfigureAwait(false));
+ case true
+ when ActionKeywordMatch(query, Settings.ActionKeyword.PathSearchActionKeyword)
+ || ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword):
+
+ results.UnionWith(await PathSearchAsync(query, token).ConfigureAwait(false));
+
+ return results.ToList();
+
+ case false
+ when ActionKeywordMatch(query, Settings.ActionKeyword.FileContentSearchActionKeyword):
+
+ // Intentionally require enabling of Everything's content search due to its slowness
+ if (Settings.ContentIndexProvider is EverythingSearchManager && !Settings.EnableEverythingContentSearch)
+ return EverythingContentSearchResult(query);
+
+ searchResults = Settings.ContentIndexProvider.ContentSearchAsync("", query.Search, token);
+
+ break;
+
+ case false
+ when ActionKeywordMatch(query, Settings.ActionKeyword.IndexSearchActionKeyword)
+ || ActionKeywordMatch(query, Settings.ActionKeyword.SearchActionKeyword):
+
+ searchResults = Settings.IndexProvider.SearchAsync(query.Search, token);
+
+ break;
}
+ if (searchResults == null)
+ return results.ToList();
+
+ await foreach (var search in searchResults.WithCancellation(token).ConfigureAwait(false))
+ results.Add(ResultManager.CreateResult(query, search));
+
+ results.RemoveWhere(r => Settings.IndexSearchExcludedSubdirectoryPaths.Any(
+ excludedPath => r.SubTitle.StartsWith(excludedPath.Path, StringComparison.OrdinalIgnoreCase)));
+
return results.ToList();
}
@@ -93,12 +123,31 @@ private bool ActionKeywordMatch(Query query, Settings.ActionKeyword allowedActio
Settings.ActionKeyword.IndexSearchActionKeyword => Settings.IndexSearchKeywordEnabled &&
keyword == Settings.IndexSearchActionKeyword,
Settings.ActionKeyword.QuickAccessActionKeyword => Settings.QuickAccessKeywordEnabled &&
- keyword == Settings.QuickAccessActionKeyword,
- _ => throw new NotImplementedException()
+ keyword == Settings.QuickAccessActionKeyword,
+ _ => throw new ArgumentOutOfRangeException(nameof(allowedActionKeyword), allowedActionKeyword, "actionKeyword out of range")
};
}
- public async Task> PathSearchAsync(Query query, CancellationToken token = default)
+ private static List EverythingContentSearchResult(Query query)
+ {
+ return new List()
+ {
+ new()
+ {
+ Title = "Do you want to enable content search for Everything?",
+ SubTitle = "It can be very slow without index (which is only supported in Everything v1.5+)",
+ IcoPath = "Images/index_error.png",
+ Action = c =>
+ {
+ Settings.EnableEverythingContentSearch = true;
+ Context.API.ChangeQuery(query.RawQuery, true);
+ return false;
+ }
+ }
+ };
+ }
+
+ private async Task> PathSearchAsync(Query query, CancellationToken token = default)
{
var querySearch = query.Search;
@@ -118,118 +167,68 @@ public async Task> PathSearchAsync(Query query, CancellationToken t
locationPath = EnvironmentVariables.TranslateEnvironmentVariablePath(locationPath);
// Check that actual location exists, otherwise directory search will throw directory not found exception
- if (!FilesFolders.LocationExists(FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath)))
+ if (!FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath).LocationExists())
return results.ToList();
- var useIndexSearch = UseWindowsIndexForDirectorySearch(locationPath);
+ var useIndexSearch = Settings.IndexSearchEngine is Settings.IndexSearchEngineOption.WindowsIndex
+ && UseWindowsIndexForDirectorySearch(locationPath);
+
+ var retrievedDirectoryPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath);
- if (locationPath.EndsWith(":\\"))
+ results.Add(retrievedDirectoryPath.EndsWith(":\\")
+ ? ResultManager.CreateDriveSpaceDisplayResult(retrievedDirectoryPath, useIndexSearch)
+ : ResultManager.CreateOpenCurrentFolderResult(retrievedDirectoryPath, useIndexSearch));
+
+ if (token.IsCancellationRequested)
+ return new List();
+
+ IEnumerable directoryResult;
+
+ var recursiveIndicatorIndex = query.Search.IndexOf('>');
+
+ if (recursiveIndicatorIndex > 0 && Settings.PathEnumerationEngine != Settings.PathEnumerationEngineOption.DirectEnumeration)
{
- results.Add(ResultManager.CreateDriveSpaceDisplayResult(locationPath, useIndexSearch));
+ directoryResult =
+ await Settings.PathEnumerator.EnumerateAsync(
+ query.Search[..recursiveIndicatorIndex],
+ query.Search[(recursiveIndicatorIndex + 1)..],
+ true,
+ token)
+ .ToListAsync(cancellationToken: token)
+ .ConfigureAwait(false);
+
}
else
{
- results.Add(ResultManager.CreateOpenCurrentFolderResult(locationPath, useIndexSearch));
+ try
+ {
+ directoryResult = DirectoryInfoSearch.TopLevelDirectorySearch(query, query.Search, token);
+ }
+ catch (Exception e)
+ {
+ throw new SearchException("DirectoryInfoSearch", e.Message, e);
+ }
}
- token.ThrowIfCancellationRequested();
- var directoryResult = await TopLevelDirectorySearchBehaviourAsync(WindowsIndexTopLevelFolderSearchAsync,
- DirectoryInfoClassSearch,
- useIndexSearch,
- query,
- locationPath,
- token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
- results.UnionWith(directoryResult);
+ results.UnionWith(directoryResult.Select(searchResult => ResultManager.CreateResult(query, searchResult)));
return results.ToList();
}
- private async Task> WindowsIndexFileContentSearchAsync(Query query, string querySearchString,
- CancellationToken token)
- {
- var queryConstructor = new QueryConstructor(Settings);
-
- if (string.IsNullOrEmpty(querySearchString))
- return new List();
-
- return await IndexSearch.WindowsIndexSearchAsync(
- querySearchString,
- queryConstructor.CreateQueryHelper,
- queryConstructor.QueryForFileContentSearch,
- Settings.IndexSearchExcludedSubdirectoryPaths,
- query,
- token).ConfigureAwait(false);
- }
-
- public bool IsFileContentSearch(string actionKeyword)
- {
- return actionKeyword == Settings.FileContentSearchActionKeyword;
- }
+ public static bool IsFileContentSearch(string actionKeyword) => actionKeyword == Settings.FileContentSearchActionKeyword;
- private List DirectoryInfoClassSearch(Query query, string querySearch, CancellationToken token)
- {
- return DirectoryInfoSearch.TopLevelDirectorySearch(query, querySearch, token);
- }
-
- public async Task> TopLevelDirectorySearchBehaviourAsync(
- Func>> windowsIndexSearch,
- Func> directoryInfoClassSearch,
- bool useIndexSearch,
- Query query,
- string querySearchString,
- CancellationToken token)
- {
- if (!useIndexSearch)
- return directoryInfoClassSearch(query, querySearchString, token);
-
- return await windowsIndexSearch(query, querySearchString, token);
- }
-
- private async Task> WindowsIndexFilesAndFoldersSearchAsync(Query query, string querySearchString,
- CancellationToken token)
- {
- var queryConstructor = new QueryConstructor(Settings);
-
- return await IndexSearch.WindowsIndexSearchAsync(
- querySearchString,
- queryConstructor.CreateQueryHelper,
- queryConstructor.QueryForAllFilesAndFolders,
- Settings.IndexSearchExcludedSubdirectoryPaths,
- query,
- token).ConfigureAwait(false);
- }
-
- private async Task> WindowsIndexTopLevelFolderSearchAsync(Query query, string path,
- CancellationToken token)
- {
- var queryConstructor = new QueryConstructor(Settings);
-
- return await IndexSearch.WindowsIndexSearchAsync(
- path,
- queryConstructor.CreateQueryHelper,
- queryConstructor.QueryForTopLevelDirectorySearch,
- Settings.IndexSearchExcludedSubdirectoryPaths,
- query,
- token).ConfigureAwait(false);
- }
private bool UseWindowsIndexForDirectorySearch(string locationPath)
{
var pathToDirectory = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(locationPath);
- if (!Settings.UseWindowsIndexForDirectorySearch)
- return false;
-
- if (Settings.IndexSearchExcludedSubdirectoryPaths
- .Any(x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory)
- .StartsWith(x.Path, StringComparison.OrdinalIgnoreCase)))
- return false;
-
- return IndexSearch.PathIsIndexed(pathToDirectory);
+ return !Settings.IndexSearchExcludedSubdirectoryPaths.Any(
+ x => FilesFolders.ReturnPreviousDirectoryIfIncompleteString(pathToDirectory).StartsWith(x.Path, StringComparison.OrdinalIgnoreCase))
+ && WindowsIndex.WindowsIndex.PathIsIndexed(pathToDirectory);
}
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs
new file mode 100644
index 00000000000..92c24559d6e
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/SearchResult.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Flow.Launcher.Plugin.Explorer.Search
+{
+ public record struct SearchResult
+ {
+ public string FullPath { get; init; }
+ public ResultType Type { get; init; }
+ public int Score { get; init; }
+
+ public bool WindowsIndexed { get; init; }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs
deleted file mode 100644
index 318a9bde958..00000000000
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/IndexSearch.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using Flow.Launcher.Infrastructure.Logger;
-using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
-using Microsoft.Search.Interop;
-using System;
-using System.Collections.Generic;
-using System.Data.OleDb;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Windows;
-
-namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
-{
- internal static class IndexSearch
- {
-
- // Reserved keywords in oleDB
- private const string reservedStringPattern = @"^[`\@\@\#\#\*\^,\&\&\/\\\$\%_;\[\]]+$";
-
- internal static async Task> ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, Query query, CancellationToken token)
- {
- var results = new List();
- var fileResults = new List();
-
- try
- {
- await using var conn = new OleDbConnection(connectionString);
- await conn.OpenAsync(token);
- token.ThrowIfCancellationRequested();
-
- await using var command = new OleDbCommand(indexQueryString, conn);
- // Results return as an OleDbDataReader.
- await using var dataReaderResults = await command.ExecuteReaderAsync(token) as OleDbDataReader;
- token.ThrowIfCancellationRequested();
-
- if (dataReaderResults.HasRows)
- {
- while (await dataReaderResults.ReadAsync(token))
- {
- token.ThrowIfCancellationRequested();
- if (dataReaderResults.GetValue(0) != DBNull.Value && dataReaderResults.GetValue(1) != DBNull.Value)
- {
- // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path
- var encodedFragmentPath = dataReaderResults
- .GetString(1)
- .Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
-
- var path = new Uri(encodedFragmentPath).LocalPath;
-
- if (dataReaderResults.GetString(2) == "Directory")
- {
- results.Add(ResultManager.CreateFolderResult(
- dataReaderResults.GetString(0),
- path,
- path,
- query, 0, true, true));
- }
- else
- {
- fileResults.Add(ResultManager.CreateFileResult(path, query, 0, true, true));
- }
- }
- }
- }
- }
- catch (OperationCanceledException)
- {
- // return empty result when cancelled
- return results;
- }
- catch (InvalidOperationException e)
- {
- // Internal error from ExecuteReader(): Connection closed.
- LogException("Internal error from ExecuteReader()", e);
- }
- catch (Exception e)
- {
- LogException("General error from performing index search", e);
- }
-
- results.AddRange(fileResults);
-
- // Intial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
- return results;
- }
-
- internal async static Task> WindowsIndexSearchAsync(
- string searchString,
- Func createQueryHelper,
- Func constructQuery,
- List exclusionList,
- Query query,
- CancellationToken token)
- {
- var regexMatch = Regex.Match(searchString, reservedStringPattern);
-
- if (regexMatch.Success)
- return new List();
-
- try
- {
- var constructedQuery = constructQuery(searchString);
-
- return RemoveResultsInExclusionList(
- await ExecuteWindowsIndexSearchAsync(constructedQuery, createQueryHelper().ConnectionString, query, token).ConfigureAwait(false),
- exclusionList,
- token);
- }
- catch (COMException)
- {
- // Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
- if (!SearchManager.Settings.WarnWindowsSearchServiceOff)
- return new List();
-
- return ResultForWindexSearchOff(query.RawQuery);
- }
- }
-
- private static List RemoveResultsInExclusionList(List results, List exclusionList, CancellationToken token)
- {
- var indexExclusionListCount = exclusionList.Count;
-
- if (indexExclusionListCount == 0)
- return results;
-
- var filteredResults = new List();
-
- for (var index = 0; index < results.Count; index++)
- {
- token.ThrowIfCancellationRequested();
-
- var excludeResult = false;
-
- for (var i = 0; i < indexExclusionListCount; i++)
- {
- token.ThrowIfCancellationRequested();
-
- if (results[index].SubTitle.StartsWith(exclusionList[i].Path, StringComparison.OrdinalIgnoreCase))
- {
- excludeResult = true;
- break;
- }
- }
-
- if (!excludeResult)
- filteredResults.Add(results[index]);
- }
-
- return filteredResults;
- }
-
- internal static bool PathIsIndexed(string path)
- {
- try
- {
- var csm = new CSearchManager();
- var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
- return indexManager.IncludedInCrawlScope(path) > 0;
- }
- catch(COMException)
- {
- // Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
- return false;
- }
- }
-
- private static List ResultForWindexSearchOff(string rawQuery)
- {
- var api = SearchManager.Context.API;
-
- return new List
- {
- new Result
- {
- Title = api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
- SubTitle = api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
- Action = c =>
- {
- SearchManager.Settings.WarnWindowsSearchServiceOff = false;
-
- var pluginsManagerPlugin= api.GetAllPlugins().FirstOrDefault(x => x.Metadata.ID == "9f8f9b14-2518-4907-b211-35ab6290dee7");
-
- var actionKeywordCount = pluginsManagerPlugin.Metadata.ActionKeywords.Count;
-
- if (actionKeywordCount > 1)
- LogException("PluginsManager's action keyword has increased to more than 1, this does not allow for determining the " +
- "right action keyword. Explorer's code for managing Windows Search service not running exception needs to be updated",
- new InvalidOperationException());
-
- if (MessageBox.Show(string.Format(api.GetTranslation("plugin_explorer_alternative"), Environment.NewLine),
- api.GetTranslation("plugin_explorer_alternative_title"),
- MessageBoxButton.YesNo) == MessageBoxResult.Yes
- && actionKeywordCount == 1)
- {
- api.ChangeQuery(string.Format("{0} install everything", pluginsManagerPlugin.Metadata.ActionKeywords[0]));
- }
- else
- {
- // Clears the warning message because same query string will not alter the displayed result list
- api.ChangeQuery(string.Empty);
-
- api.ChangeQuery(rawQuery);
- }
-
- var mainWindow = Application.Current.MainWindow;
- mainWindow.Show();
- mainWindow.Focus();
-
- return false;
- },
- IcoPath = Constants.ExplorerIconImagePath
- }
- };
- }
-
- private static void LogException(string message, Exception e)
- {
-#if DEBUG // Please investigate and handle error from index search
- throw e;
-#else
- Log.Exception($"|Flow.Launcher.Plugin.Explorer.IndexSearch|{message}", e);
-#endif
- }
- }
-}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
index 20e85bbb598..6501c16579b 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/QueryConstructor.cs
@@ -1,24 +1,30 @@
+using System;
+using System.Buffers;
using Microsoft.Search.Interop;
namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
{
public class QueryConstructor
{
- private readonly Settings settings;
+ private Settings Settings { get; }
private const string SystemIndex = "SystemIndex";
+ public CSearchQueryHelper BaseQueryHelper { get; }
+
public QueryConstructor(Settings settings)
{
- this.settings = settings;
+ Settings = settings;
+ BaseQueryHelper = CreateBaseQuery();
}
- public CSearchQueryHelper CreateBaseQuery()
+
+ private CSearchQueryHelper CreateBaseQuery()
{
var baseQuery = CreateQueryHelper();
// Set the number of results we want. Don't set this property if all results are needed.
- baseQuery.QueryMaxResults = settings.MaxResult;
+ baseQuery.QueryMaxResults = Settings.MaxResult;
// Set list of columns we want to display, getting the path presently
baseQuery.QuerySelectColumns = "System.FileName, System.ItemUrl, System.ItemType";
@@ -32,7 +38,7 @@ public CSearchQueryHelper CreateBaseQuery()
return baseQuery;
}
- internal CSearchQueryHelper CreateQueryHelper()
+ internal static CSearchQueryHelper CreateQueryHelper()
{
// This uses the Microsoft.Search.Interop assembly
var manager = new CSearchManager();
@@ -42,98 +48,67 @@ internal CSearchQueryHelper CreateQueryHelper()
// Get the ISearchQueryHelper which will help us to translate AQS --> SQL necessary to query the indexer
var queryHelper = catalogManager.GetQueryHelper();
-
- return queryHelper;
- }
-
- ///
- /// Set the required WHERE clause restriction to search on the first level of a specified directory.
- ///
- public string QueryWhereRestrictionsForTopLevelDirectorySearch(string path)
- {
- var searchDepth = $"directory='file:";
-
- return QueryWhereRestrictionsFromLocationPath(path, searchDepth);
- }
-
- ///
- /// Set the required WHERE clause restriction to search all files and subfolders of a specified directory.
- ///
- public string QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(string path)
- {
- var searchDepth = $"scope='file:";
- return QueryWhereRestrictionsFromLocationPath(path, searchDepth);
+ return queryHelper;
}
- private string QueryWhereRestrictionsFromLocationPath(string path, string searchDepth)
- {
- if (path.EndsWith(Constants.DirectorySeperator))
- return searchDepth + $"{path}'";
-
- var indexOfSeparator = path.LastIndexOf(Constants.DirectorySeperator);
-
- var itemName = path.Substring(indexOfSeparator + 1);
-
- if (itemName.StartsWith(Constants.AllFilesFolderSearchWildcard))
- itemName = itemName.Substring(1);
-
- var previousLevelDirectory = path.Substring(0, indexOfSeparator);
-
- if (string.IsNullOrEmpty(itemName))
- return $"{searchDepth}{previousLevelDirectory}'";
-
- return $"(System.FileName LIKE '{itemName}%' OR CONTAINS(System.FileName,'\"{itemName}*\"',1033)) AND {searchDepth}{previousLevelDirectory}'";
- }
+ public static string TopLevelDirectoryConstraint(ReadOnlySpan path) => $"directory='file:{path}'";
+ public static string RecursiveDirectoryConstraint(ReadOnlySpan path) => $"scope='file:{path}'";
+
///
/// Search will be performed on all folders and files on the first level of a specified directory.
///
- public string QueryForTopLevelDirectorySearch(string path)
+ public string Directory(ReadOnlySpan path, ReadOnlySpan searchString = default, bool recursive = false)
{
- string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE ";
+ var queryConstraint = searchString.IsWhiteSpace() ? "" : $"AND ({FileName} LIKE '{searchString}%' OR CONTAINS({FileName},'\"{searchString}*\"'))";
+
+ var scopeConstraint = recursive
+ ? RecursiveDirectoryConstraint(path)
+ : TopLevelDirectoryConstraint(path);
- if (path.LastIndexOf(Constants.AllFilesFolderSearchWildcard) > path.LastIndexOf(Constants.DirectorySeperator))
- return query + QueryWhereRestrictionsForTopLevelDirectoryAllFilesAndFoldersSearch(path) + QueryOrderByFileNameRestriction;
+ var query = $"SELECT TOP {Settings.MaxResult} {BaseQueryHelper.QuerySelectColumns} FROM {SystemIndex} WHERE {scopeConstraint} {queryConstraint} ORDER BY {FileName}";
- return query + QueryWhereRestrictionsForTopLevelDirectorySearch(path) + QueryOrderByFileNameRestriction;
+ return query;
}
///
/// Search will be performed on all folders and files based on user's search keywords.
///
- public string QueryForAllFilesAndFolders(string userSearchString)
+ public string FilesAndFolders(ReadOnlySpan userSearchString)
{
+ if (userSearchString.IsWhiteSpace())
+ userSearchString = "*";
+
// Generate SQL from constructed parameters, converting the userSearchString from AQS->WHERE clause
- return CreateBaseQuery().GenerateSQLFromUserQuery(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch
- + QueryOrderByFileNameRestriction;
+ return $"{BaseQueryHelper.GenerateSQLFromUserQuery(userSearchString.ToString())} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
}
///
/// Set the required WHERE clause restriction to search for all files and folders.
///
- public const string QueryWhereRestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
+ public const string RestrictionsForAllFilesAndFoldersSearch = "scope='file:'";
- public const string QueryOrderByFileNameRestriction = " ORDER BY System.FileName";
+ ///
+ /// Order identifier: file name
+ ///
+ public const string FileName = "System.FileName";
///
/// Search will be performed on all indexed file contents for the specified search keywords.
///
- public string QueryForFileContentSearch(string userSearchString)
+ public string FileContent(ReadOnlySpan userSearchString)
{
- string query = "SELECT TOP " + settings.MaxResult + $" {CreateBaseQuery().QuerySelectColumns} FROM {SystemIndex} WHERE ";
+ string query =
+ $"SELECT TOP {Settings.MaxResult} {BaseQueryHelper.QuerySelectColumns} FROM {SystemIndex} WHERE {RestrictionsForFileContentSearch(userSearchString)} AND {RestrictionsForAllFilesAndFoldersSearch} ORDER BY {FileName}";
- return query + QueryWhereRestrictionsForFileContentSearch(userSearchString) + " AND " + QueryWhereRestrictionsForAllFilesAndFoldersSearch
- + QueryOrderByFileNameRestriction;
+ return query;
}
///
/// Set the required WHERE clause restriction to search within file content.
///
- public string QueryWhereRestrictionsForFileContentSearch(string searchQuery)
- {
- return $"FREETEXT('{searchQuery}')";
- }
+ public static string RestrictionsForFileContentSearch(ReadOnlySpan searchQuery) => $"FREETEXT('{searchQuery}')";
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndex.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndex.cs
new file mode 100644
index 00000000000..4396951d647
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndex.cs
@@ -0,0 +1,132 @@
+using Flow.Launcher.Infrastructure.Logger;
+using Microsoft.Search.Interop;
+using System;
+using System.Collections.Generic;
+using System.Data.OleDb;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Flow.Launcher.Plugin.Explorer.Exceptions;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
+{
+ internal static class WindowsIndex
+ {
+
+ // Reserved keywords in oleDB
+ private static Regex _reservedPatternMatcher = new(@"^[`\@\@\#\#\*\^,\&\&\/\\\$\%_;\[\]]+$", RegexOptions.Compiled);
+
+ private static async IAsyncEnumerable ExecuteWindowsIndexSearchAsync(string indexQueryString, string connectionString, [EnumeratorCancellation] CancellationToken token)
+ {
+ await using var conn = new OleDbConnection(connectionString);
+ await conn.OpenAsync(token);
+ token.ThrowIfCancellationRequested();
+
+ await using var command = new OleDbCommand(indexQueryString, conn);
+ // Results return as an OleDbDataReader.
+ OleDbDataReader dataReaderAttempt;
+ try
+ {
+ dataReaderAttempt = await command.ExecuteReaderAsync(token) as OleDbDataReader;
+ }
+ catch (OleDbException e)
+ {
+ Log.Exception($"|WindowsIndex.ExecuteWindowsIndexSearchAsync|Failed to execute windows index search query: {indexQueryString}", e);
+ yield break;
+ }
+ await using var dataReader = dataReaderAttempt;
+ token.ThrowIfCancellationRequested();
+
+ if (dataReader is not { HasRows: true })
+ {
+ yield break;
+ }
+
+ while (await dataReader.ReadAsync(token))
+ {
+ token.ThrowIfCancellationRequested();
+ if (dataReader.GetValue(0) == DBNull.Value || dataReader.GetValue(1) == DBNull.Value)
+ {
+ continue;
+ }
+ // # is URI syntax for the fragment component, need to be encoded so LocalPath returns complete path
+ var encodedFragmentPath = dataReader
+ .GetString(1)
+ .Replace("#", "%23", StringComparison.OrdinalIgnoreCase);
+
+ var path = new Uri(encodedFragmentPath).LocalPath;
+
+ yield return new SearchResult
+ {
+ FullPath = path,
+ Type = dataReader.GetString(2) == "Directory" ? ResultType.Folder : ResultType.File,
+ WindowsIndexed = true
+ };
+ }
+
+ // Initial ordering, this order can be updated later by UpdateResultView.MainViewModel based on history of user selection.
+ }
+
+ internal static IAsyncEnumerable WindowsIndexSearchAsync(
+ string connectionString,
+ string search,
+ CancellationToken token)
+ {
+ try
+ {
+
+ return _reservedPatternMatcher.IsMatch(search)
+ ? AsyncEnumerable.Empty()
+ : ExecuteWindowsIndexSearchAsync(search, connectionString, token);
+ }
+ catch (InvalidOperationException e)
+ {
+ throw new SearchException("Windows Index", e.Message, e);
+ }
+ catch (COMException)
+ {
+ // Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
+
+ if (!SearchManager.Settings.WarnWindowsSearchServiceOff)
+ return AsyncEnumerable.Empty();
+
+ var api = SearchManager.Context.API;
+
+ throw new EngineNotAvailableException(
+ "Windows Index",
+ api.GetTranslation("plugin_explorer_windowsSearchServiceFix"),
+ api.GetTranslation("plugin_explorer_windowsSearchServiceNotRunning"),
+ c =>
+ {
+ SearchManager.Settings.WarnWindowsSearchServiceOff = false;
+
+ // Clears the warning message so user is not mistaken that it has not worked
+ api.ChangeQuery(string.Empty);
+
+ return ValueTask.FromResult(false);
+ })
+ {
+ ErrorIcon = Constants.WindowsIndexErrorImagePath
+ };
+ }
+ }
+
+ internal static bool PathIsIndexed(string path)
+ {
+ try
+ {
+ var csm = new CSearchManager();
+ var indexManager = csm.GetCatalog("SystemIndex").GetCrawlScopeManager();
+ return indexManager.IncludedInCrawlScope(path) > 0;
+ }
+ catch (COMException)
+ {
+ // Occurs because the Windows Indexing (WSearch) is turned off in services and unable to be used by Explorer plugin
+ return false;
+ }
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndexSearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndexSearchManager.cs
new file mode 100644
index 00000000000..91ba2a8582e
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/WindowsIndex/WindowsIndexSearchManager.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Flow.Launcher.Plugin.Explorer.Search.IProvider;
+using Microsoft.Search.Interop;
+
+namespace Flow.Launcher.Plugin.Explorer.Search.WindowsIndex
+{
+ public class WindowsIndexSearchManager : IIndexProvider, IContentIndexProvider, IPathIndexProvider
+ {
+ private Settings Settings { get; }
+ private QueryConstructor QueryConstructor { get; }
+
+ private CSearchQueryHelper QueryHelper { get; }
+ public WindowsIndexSearchManager(Settings settings)
+ {
+ Settings = settings;
+ QueryConstructor = new QueryConstructor(Settings);
+ QueryHelper = QueryConstructor.CreateQueryHelper();
+ }
+
+ private IAsyncEnumerable WindowsIndexFileContentSearchAsync(
+ ReadOnlySpan querySearchString,
+ CancellationToken token)
+ {
+ if (querySearchString.IsEmpty)
+ return AsyncEnumerable.Empty();
+
+ return WindowsIndex.WindowsIndexSearchAsync(
+ QueryHelper.ConnectionString,
+ QueryConstructor.FileContent(querySearchString),
+ token);
+ }
+
+ private IAsyncEnumerable WindowsIndexFilesAndFoldersSearchAsync(
+ ReadOnlySpan querySearchString,
+ CancellationToken token = default)
+ {
+ return WindowsIndex.WindowsIndexSearchAsync(
+ QueryHelper.ConnectionString,
+ QueryConstructor.FilesAndFolders(querySearchString),
+ token);
+ }
+
+ private IAsyncEnumerable WindowsIndexTopLevelFolderSearchAsync(
+ ReadOnlySpan search,
+ ReadOnlySpan path,
+ bool recursive,
+ CancellationToken token)
+ {
+ var queryConstructor = new QueryConstructor(Settings);
+
+ return WindowsIndex.WindowsIndexSearchAsync(
+ QueryConstructor.CreateQueryHelper().ConnectionString,
+ queryConstructor.Directory(path, search, recursive),
+ token);
+ }
+ public IAsyncEnumerable SearchAsync(string search, CancellationToken token)
+ {
+ return WindowsIndexFilesAndFoldersSearchAsync(search, token: token);
+ }
+ public IAsyncEnumerable ContentSearchAsync(string plainSearch, string contentSearch, CancellationToken token)
+ {
+ return WindowsIndexFileContentSearchAsync(contentSearch, token);
+ }
+ public IAsyncEnumerable EnumerateAsync(string path, string search, bool recursive, CancellationToken token)
+ {
+ return WindowsIndexTopLevelFolderSearchAsync(search, path, recursive, token);
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs
index 90b85d18750..67c4061d4c5 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Settings.cs
@@ -1,7 +1,15 @@
+using Flow.Launcher.Plugin.Everything.Everything;
using Flow.Launcher.Plugin.Explorer.Search;
+using Flow.Launcher.Plugin.Explorer.Search.Everything;
using Flow.Launcher.Plugin.Explorer.Search.QuickAccessLinks;
+using Flow.Launcher.Plugin.Explorer.Search.WindowsIndex;
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Flow.Launcher.Plugin.Explorer.Search.IProvider;
namespace Flow.Launcher.Plugin.Explorer
{
@@ -9,14 +17,19 @@ public class Settings
{
public int MaxResult { get; set; } = 100;
- public List QuickAccessLinks { get; set; } = new List();
+ public ObservableCollection QuickAccessLinks { get; set; } = new();
- // as at v1.7.0 this is to maintain backwards compatibility, need to be removed afterwards.
- public List QuickFolderAccessLinks { get; set; } = new List();
+ public ObservableCollection IndexSearchExcludedSubdirectoryPaths { get; set; } = new ObservableCollection();
- public bool UseWindowsIndexForDirectorySearch { get; set; } = false;
+ public string EditorPath { get; set; } = "";
+
+ public string ShellPath { get; set; } = "cmd";
+
+
+ public bool UseLocationAsWorkingDir { get; set; } = false;
+
+ public bool ShowWindowsContextMenu { get; set; } = true;
- public List IndexSearchExcludedSubdirectoryPaths { get; set; } = new List();
public string SearchActionKeyword { get; set; } = Query.GlobalPluginWildcardSign;
@@ -38,8 +51,92 @@ public class Settings
public bool QuickAccessKeywordEnabled { get; set; }
+
public bool WarnWindowsSearchServiceOff { get; set; } = true;
+ private EverythingSearchManager _everythingManagerInstance;
+ private WindowsIndexSearchManager _windowsIndexSearchManager;
+
+ #region SearchEngine
+
+ private EverythingSearchManager EverythingManagerInstance => _everythingManagerInstance ??= new EverythingSearchManager(this);
+ private WindowsIndexSearchManager WindowsIndexSearchManager => _windowsIndexSearchManager ??= new WindowsIndexSearchManager(this);
+
+
+ public IndexSearchEngineOption IndexSearchEngine { get; set; } = IndexSearchEngineOption.WindowsIndex;
+ [JsonIgnore]
+ public IIndexProvider IndexProvider => IndexSearchEngine switch
+ {
+ IndexSearchEngineOption.Everything => EverythingManagerInstance,
+ IndexSearchEngineOption.WindowsIndex => WindowsIndexSearchManager,
+ _ => throw new ArgumentOutOfRangeException(nameof(IndexSearchEngine))
+ };
+
+ public PathEnumerationEngineOption PathEnumerationEngine { get; set; } = PathEnumerationEngineOption.WindowsIndex;
+
+ [JsonIgnore]
+ public IPathIndexProvider PathEnumerator => PathEnumerationEngine switch
+ {
+ PathEnumerationEngineOption.Everything => EverythingManagerInstance,
+ PathEnumerationEngineOption.WindowsIndex => WindowsIndexSearchManager,
+ _ => throw new ArgumentOutOfRangeException(nameof(PathEnumerationEngine))
+ };
+
+ public ContentIndexSearchEngineOption ContentSearchEngine { get; set; } = ContentIndexSearchEngineOption.WindowsIndex;
+ [JsonIgnore]
+ public IContentIndexProvider ContentIndexProvider => ContentSearchEngine switch
+ {
+ ContentIndexSearchEngineOption.Everything => EverythingManagerInstance,
+ ContentIndexSearchEngineOption.WindowsIndex => WindowsIndexSearchManager,
+ _ => throw new ArgumentOutOfRangeException(nameof(ContentSearchEngine))
+ };
+
+ public enum PathEnumerationEngineOption
+ {
+ [Description("plugin_explorer_engine_windows_index")]
+ WindowsIndex,
+ [Description("plugin_explorer_engine_everything")]
+ Everything,
+ [Description("plugin_explorer_path_enumeration_engine_none")]
+ DirectEnumeration
+ }
+
+ public enum IndexSearchEngineOption
+ {
+ [Description("plugin_explorer_engine_windows_index")]
+ WindowsIndex,
+ [Description("plugin_explorer_engine_everything")]
+ Everything,
+ }
+
+ public enum ContentIndexSearchEngineOption
+ {
+ [Description("plugin_explorer_engine_windows_index")]
+ WindowsIndex,
+ [Description("plugin_explorer_engine_everything")]
+ Everything,
+ }
+
+ #endregion
+
+
+ #region Everything Settings
+
+ public string EverythingInstalledPath { get; set; }
+
+ [JsonIgnore]
+ public SortOption[] SortOptions { get; set; } = Enum.GetValues();
+
+ public SortOption SortOption { get; set; } = SortOption.NAME_ASCENDING;
+
+ public bool EnableEverythingContentSearch { get; set; } = false;
+
+ public bool EverythingEnabled => IndexSearchEngine == IndexSearchEngineOption.Everything ||
+ PathEnumerationEngine == PathEnumerationEngineOption.Everything ||
+ ContentSearchEngine == ContentIndexSearchEngineOption.Everything;
+
+ #endregion
+
internal enum ActionKeyword
{
SearchActionKeyword,
@@ -89,4 +186,4 @@ internal enum ActionKeyword
_ => throw new ArgumentOutOfRangeException(nameof(actionKeyword), actionKeyword, "ActionKeyword enabled status not defined")
};
}
-}
\ No newline at end of file
+}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/ActionKeywordModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/ActionKeywordModel.cs
new file mode 100644
index 00000000000..2f614ead80d
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/ActionKeywordModel.cs
@@ -0,0 +1,57 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Flow.Launcher.Plugin.Explorer.Views
+{
+ public class ActionKeywordModel : INotifyPropertyChanged
+ {
+ private static Settings _settings;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public static void Init(Settings settings)
+ {
+ _settings = settings;
+ }
+
+ internal ActionKeywordModel(Settings.ActionKeyword actionKeyword, string description)
+ {
+ KeywordProperty = actionKeyword;
+ Description = description;
+ }
+
+ public string Description { get; private init; }
+
+ internal Settings.ActionKeyword KeywordProperty { get; }
+
+ private void OnPropertyChanged([CallerMemberName] string propertyName = "")
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ private string? keyword;
+
+ public string Keyword
+ {
+ get => keyword ??= _settings.GetActionKeyword(KeywordProperty);
+ set
+ {
+ keyword = value;
+ _settings.SetActionKeyword(KeywordProperty, value);
+ OnPropertyChanged();
+ }
+ }
+ private bool? enabled;
+
+ public bool Enabled
+ {
+ get => enabled ??= _settings.GetActionKeywordEnabled(KeywordProperty);
+ set
+ {
+ enabled = value;
+ _settings.SetActionKeywordEnabled(KeywordProperty, value);
+ OnPropertyChanged();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/EnumBindingModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/EnumBindingModel.cs
new file mode 100644
index 00000000000..29c81a46525
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/EnumBindingModel.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace Flow.Launcher.Plugin.Explorer.ViewModels;
+
+public class EnumBindingModel where T : struct, Enum
+{
+ public static IReadOnlyList> CreateList()
+ {
+ return Enum.GetValues()
+ .Select(value => new EnumBindingModel
+ {
+ Value = value, LocalizationKey = GetDescriptionAttr(value)
+ })
+ .ToArray();
+ }
+
+ public EnumBindingModel From(T value)
+ {
+ var name = value.ToString();
+ var description = GetDescriptionAttr(value);
+
+ return new EnumBindingModel
+ {
+ Name = name,
+ LocalizationKey = description,
+ Value = value
+ };
+ }
+
+ private static string GetDescriptionAttr(T source)
+ {
+ var fi = source.GetType().GetField(source.ToString());
+
+ var attributes = (DescriptionAttribute[])fi?.GetCustomAttributes(
+ typeof(DescriptionAttribute), false);
+
+ return attributes is { Length: > 0 } ? attributes[0].Description : source.ToString();
+
+ }
+
+ public string Name { get; set; }
+ private string LocalizationKey { get; set; }
+ public string Description => Main.Context.API.GetTranslation(LocalizationKey);
+ public T Value { get; set; }
+}
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/RelayCommand.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/RelayCommand.cs
new file mode 100644
index 00000000000..ff704b67979
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/RelayCommand.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Windows.Input;
+
+namespace Flow.Launcher.Plugin.Explorer.ViewModels
+{
+ internal class RelayCommand : ICommand
+ {
+ private Action