diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj
index 1f979cbdf..c1fe421fb 100644
--- a/Flow.Launcher/Flow.Launcher.csproj
+++ b/Flow.Launcher/Flow.Launcher.csproj
@@ -89,7 +89,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml
index 2e3922d06..f393af119 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -92,6 +92,7 @@
Query time:
| Version
Website
+ Uninstall
diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
index 5cff7e2e5..4596acb4c 100644
--- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
+++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Flow.Launcher.Plugin.BrowserBookmark.csproj
@@ -3,7 +3,6 @@
Library
net6.0-windows
- win-x64
true
{9B130CC5-14FB-41FF-B310-0A95B6894C37}
Properties
@@ -65,4 +64,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml
index 39d5f8a4e..b1a8744fd 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Program/AddProgramSource.xaml
@@ -5,11 +5,12 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{DynamicResource flowlauncher_plugin_program_directory}"
- Width="530"
+ Width="Auto"
+ Height="276"
Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
ResizeMode="NoResize"
- SizeToContent="Height"
+ SizeToContent="Width"
WindowStartupLocation="CenterScreen">
@@ -69,9 +70,9 @@
TextAlignment="Left"
TextWrapping="WrapWithOverflow" />
-
-
-
+
+
+
@@ -88,21 +89,22 @@
VerticalAlignment="Center"
FontSize="14"
Text="{DynamicResource flowlauncher_plugin_program_directory}" />
-
+ Content="{DynamicResource flowlauncher_plugin_program_browse}"
+ DockPanel.Dock="Right" />
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Center" />
- bat;py)
- Insert protocols of .url files you want to index. Protocols should be separated by ';'. (ex>ftp;netflix)
+ Insert protocols of .url files you want to index. Protocols should be separated by ';', and should end with "://". (ex>ftp://;mailto://)
Run As Different User
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
index c212bf8f4..feeb8e78a 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs
@@ -102,7 +102,7 @@ namespace Flow.Launcher.Plugin.Program
await Task.WhenAll(a, b);
Win32.WatchProgramUpdate(_settings);
- UWP.WatchPackageChange();
+ _ = UWP.WatchPackageChange();
}
public static void IndexWin32Programs()
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
index 84aa93664..8a731f5b9 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWP.cs
@@ -277,7 +277,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
[Serializable]
public class Application : IProgram
{
- public string UniqueIdentifier { get; set; }
+ private string _uid = string.Empty;
+ public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); }
public string DisplayName { get; set; }
public string Description { get; set; }
public string UserModelId { get; set; }
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
index c13b32e80..25ac2b213 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
@@ -23,10 +23,21 @@ namespace Flow.Launcher.Plugin.Program.Programs
public class Win32 : IProgram, IEquatable
{
public string Name { get; set; }
- public string UniqueIdentifier { get => _uid; set => _uid = value.ToLowerInvariant(); } // For path comparison
+ public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison
public string IcoPath { get; set; }
+ ///
+ /// Path of the file. It's the path of .lnk or .url for .lnk and .url.
+ ///
public string FullPath { get; set; }
+ ///
+ /// Path of the excutable for .lnk, or the URL for .url.
+ ///
public string LnkResolvedPath { get; set; }
+ ///
+ /// Path of the actual executable file.
+ ///
+ public string ExecutablePath => LnkResolvedPath ?? FullPath;
+ public string WorkingDir => Directory.GetParent(ExecutablePath)?.FullName ?? string.Empty;
public string ParentDirectory { get; set; }
public string ExecutableName { get; set; }
public string Description { get; set; }
@@ -97,10 +108,23 @@ namespace Flow.Launcher.Plugin.Program.Programs
matchResult.MatchData = new List();
}
+ string subtitle = string.Empty;
+ if (!Main._settings.HideAppsPath)
+ {
+ if (Extension(FullPath) == UrlExtension)
+ {
+ subtitle = LnkResolvedPath;
+ }
+ else
+ {
+ subtitle = FullPath;
+ }
+ }
+
var result = new Result
{
Title = title,
- SubTitle = Main._settings.HideAppsPath ? string.Empty : LnkResolvedPath ?? FullPath,
+ SubTitle = subtitle,
IcoPath = IcoPath,
Score = matchResult.Score,
TitleHighlightData = matchResult.MatchData,
@@ -116,8 +140,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
var info = new ProcessStartInfo
{
- FileName = LnkResolvedPath ?? FullPath,
- WorkingDirectory = ParentDirectory,
+ FileName = ExecutablePath,
+ WorkingDirectory = WorkingDir,
UseShellExecute = true,
Verb = runAsAdmin ? "runas" : null
};
@@ -143,8 +167,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var info = new ProcessStartInfo
{
- FileName = FullPath,
- WorkingDirectory = ParentDirectory,
+ FileName = ExecutablePath,
+ WorkingDirectory = WorkingDir,
UseShellExecute = true
};
@@ -162,8 +186,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var info = new ProcessStartInfo
{
- FileName = FullPath,
- WorkingDirectory = ParentDirectory,
+ FileName = ExecutablePath,
+ WorkingDirectory = WorkingDir,
Verb = "runas",
UseShellExecute = true
};
@@ -224,6 +248,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
return Default;
}
+#if !DEBUG
+ catch (Exception e)
+ {
+ ProgramLogger.LogException($"|Win32|Win32Program|{path}" +
+ "|An unexpected error occurred in the calling method Win32Program", e);
+
+ return Default;
+ }
+#endif
}
private static Win32 LnkProgram(string path)
@@ -241,8 +274,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
var extension = Extension(target);
if (extension == ExeExtension && File.Exists(target))
{
- program.LnkResolvedPath = program.FullPath;
- program.FullPath = Path.GetFullPath(target).ToLowerInvariant();
+ program.LnkResolvedPath = Path.GetFullPath(target);
program.ExecutableName = Path.GetFileName(target);
var description = _helper.description;
@@ -270,16 +302,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
"|Error caused likely due to trying to get the description of the program",
e);
- program.Valid = false;
- return program;
+ return Default;
}
catch (FileNotFoundException e)
{
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
"|An unexpected error occurred in the calling method LnkProgram", e);
- program.Valid = false;
- return program;
+ return Default;
}
#if !DEBUG //Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
catch (Exception e)
@@ -287,8 +317,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
"|An unexpected error occurred in the calling method LnkProgram", e);
- program.Valid = false;
- return program;
+ return Default;
}
#endif
}
@@ -342,6 +371,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
program.Description = info.FileDescription;
return program;
}
+ catch (FileNotFoundException e)
+ {
+ ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
+ $"|File not found when trying to load the program from {path}", e);
+
+ return Default;
+ }
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
{
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
@@ -351,7 +387,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- private static IEnumerable ProgramPaths(string directory, string[] suffixes, bool recursive = true)
+ private static IEnumerable EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true)
{
if (!Directory.Exists(directory))
return Enumerable.Empty();
@@ -376,15 +412,14 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- private static IEnumerable UnregisteredPrograms(List sources, string[] suffixes, string[] protocols)
+ private static IEnumerable UnregisteredPrograms(List directories, string[] suffixes, string[] protocols)
{
// Disabled custom sources are not in DisabledProgramSources
- var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
- .AsParallel()
- .SelectMany(s => ProgramPaths(s.Location, suffixes)))
- .Distinct();
+ var paths = directories.AsParallel()
+ .SelectMany(s => EnumerateProgramsInDir(s, suffixes));
- var programs = paths.Select(x => GetProgramFromPath(x, protocols));
+ // Remove disabled programs in DisabledProgramSources
+ var programs = ExceptDisabledSource(paths).Select(x => GetProgramFromPath(x, protocols));
return programs;
}
@@ -392,8 +427,8 @@ namespace Flow.Launcher.Plugin.Program.Programs
{
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms);
- var paths1 = ProgramPaths(directory1, suffixes);
- var paths2 = ProgramPaths(directory2, suffixes);
+ var paths1 = EnumerateProgramsInDir(directory1, suffixes);
+ var paths2 = EnumerateProgramsInDir(directory2, suffixes);
var toFilter = paths1.Concat(paths2);
@@ -402,26 +437,22 @@ namespace Flow.Launcher.Plugin.Program.Programs
return programs;
}
- private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols)
+ private static IEnumerable PATHPrograms(string[] suffixes, string[] protocols, List commonParents)
{
var pathEnv = Environment.GetEnvironmentVariable("Path");
- if (String.IsNullOrEmpty(pathEnv))
- {
- return Array.Empty();
+ if (String.IsNullOrEmpty(pathEnv))
+ {
+ return Array.Empty();
}
var paths = pathEnv.Split(";", StringSplitOptions.RemoveEmptyEntries).DistinctBy(p => p.ToLowerInvariant());
- var toFilter = paths.AsParallel().SelectMany(p => ProgramPaths(p, suffixes, recursive: false));
+ paths = paths.Where(x => commonParents.All(parent => !x.StartsWith(parent, StringComparison.OrdinalIgnoreCase)));
+
+ var toFilter = paths.AsParallel().SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false));
var programs = ExceptDisabledSource(toFilter.Distinct())
- .Select(x => Extension(x) switch
- {
- ShortcutExtension => LnkProgram(x),
- UrlExtension => UrlProgram(x, protocols),
- ExeExtension => ExeProgram(x),
- _ => Win32Program(x)
- });
+ .Select(x => GetProgramFromPath(x, protocols));
return programs;
}
@@ -496,9 +527,6 @@ namespace Flow.Launcher.Plugin.Program.Programs
path = Environment.ExpandEnvironmentVariables(path);
- if (!File.Exists(path))
- return Default;
-
return Extension(path) switch
{
ShortcutExtension => LnkProgram(path),
@@ -545,7 +573,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
private static IEnumerable ProgramsHasher(IEnumerable programs)
{
- return programs.GroupBy(p => p.FullPath.ToLowerInvariant())
+ return programs.GroupBy(p => p.ExecutablePath.ToLowerInvariant())
.AsParallel()
.SelectMany(g =>
{
@@ -553,7 +581,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (temp.Any())
return DistinctBy(temp, x => x.Description);
return g.Take(1);
- }).ToArray();
+ });
}
@@ -565,11 +593,15 @@ namespace Flow.Launcher.Plugin.Program.Programs
var suffixes = settings.GetSuffixes();
var protocols = settings.GetProtocols();
- var unregistered = UnregisteredPrograms(settings.ProgramSources, suffixes, protocols);
+ // Disabled custom sources are not in DisabledProgramSources
+ var sources = settings.ProgramSources.Where(s => Directory.Exists(s.Location) && s.Enabled).Distinct();
+ var commonParents = GetCommonParents(sources);
+
+ var unregistered = UnregisteredPrograms(commonParents, suffixes, protocols);
programs = programs.Concat(unregistered);
- var autoIndexPrograms = Enumerable.Empty();
+ var autoIndexPrograms = Enumerable.Empty(); // for single programs, not folders
if (settings.EnableRegistrySource)
{
@@ -585,11 +617,11 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (settings.EnablePATHSource)
{
- var path = PATHPrograms(settings.GetSuffixes(), protocols);
- autoIndexPrograms = autoIndexPrograms.Concat(path);
+ var path = PATHPrograms(settings.GetSuffixes(), protocols, commonParents);
+ programs = programs.Concat(path);
}
- autoIndexPrograms = ProgramsHasher(autoIndexPrograms);
+ autoIndexPrograms = ProgramsHasher(autoIndexPrograms).ToArray();
return programs.Concat(autoIndexPrograms).Where(x => x.Valid).Distinct().ToArray();
}
@@ -651,11 +683,13 @@ namespace Flow.Launcher.Plugin.Program.Programs
if (settings.EnableStartMenuSource)
paths.AddRange(GetStartMenuPaths());
- paths.AddRange(from source in settings.ProgramSources where source.Enabled select source.Location);
+ var customSources = GetCommonParents(settings.ProgramSources);
+ paths.AddRange(customSources);
+ var fileExtensionToWatch = settings.GetSuffixes();
foreach (var directory in from path in paths where Directory.Exists(path) select path)
{
- WatchDirectory(directory);
+ WatchDirectory(directory, fileExtensionToWatch);
}
_ = Task.Run(MonitorDirectoryChangeAsync);
@@ -676,7 +710,7 @@ namespace Flow.Launcher.Plugin.Program.Programs
}
}
- public static void WatchDirectory(string directory)
+ public static void WatchDirectory(string directory, string[] extensions)
{
if (!Directory.Exists(directory))
{
@@ -688,6 +722,10 @@ namespace Flow.Launcher.Plugin.Program.Programs
watcher.Deleted += static (_, _) => indexQueue.Writer.TryWrite(default);
watcher.EnableRaisingEvents = true;
watcher.IncludeSubdirectories = true;
+ foreach (var extension in extensions)
+ {
+ watcher.Filters.Add($"*.{extension}");
+ }
Watchers.Add(watcher);
}
@@ -699,5 +737,38 @@ namespace Flow.Launcher.Plugin.Program.Programs
fileSystemWatcher.Dispose();
}
}
+
+ // https://stackoverflow.com/a/66877016
+ private static bool IsSubPathOf(string subPath, string basePath)
+ {
+ var rel = Path.GetRelativePath(basePath, subPath);
+ return rel != "."
+ && rel != ".."
+ && !rel.StartsWith("../")
+ && !rel.StartsWith(@"..\")
+ && !Path.IsPathRooted(rel);
+ }
+
+ private static List GetCommonParents(IEnumerable programSources)
+ {
+ // To avoid unnecessary io
+ // like c:\windows and c:\windows\system32
+ var grouped = programSources.GroupBy(p => p.Location.ToLowerInvariant()[0]); // group by disk
+ List result = new();
+ foreach (var group in grouped)
+ {
+ HashSet parents = group.ToHashSet();
+ foreach (var source in group)
+ {
+ if (parents.Any(p => IsSubPathOf(source.Location, p.Location) &&
+ source != p))
+ {
+ parents.Remove(source);
+ }
+ }
+ result.AddRange(parents.Select(x => x.Location));
+ }
+ return result.DistinctBy(x => x.ToLowerInvariant()).ToList();
+ }
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs b/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs
index fb32fb892..571dc0017 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Views/Models/ProgramSource.cs
@@ -15,37 +15,39 @@ namespace Flow.Launcher.Plugin.Program.Views.Models
///
public class ProgramSource
{
- private string name;
+ private string name = string.Empty;
+ private string loc = string.Empty;
+ private string uniqueIdentifier = string.Empty;
- private string loc;
public string Location
{
get => loc;
set
{
- loc = value;
- UniqueIdentifier = value.ToLowerInvariant();
+ loc = value ?? string.Empty;
+ UniqueIdentifier = value;
}
}
- public string Name { get => name ?? new DirectoryInfo(Location).Name; set => name = value; }
+
+ public string Name { get => name; set => name = value ?? string.Empty; }
public bool Enabled { get; set; } = true;
- public string UniqueIdentifier { get; private set; }
+ public string UniqueIdentifier
+ {
+ get => uniqueIdentifier;
+ private set
+ {
+ uniqueIdentifier = value == null ? string.Empty : value.ToLowerInvariant();
+ }
+ }
[JsonConstructor]
public ProgramSource(string name, string location, bool enabled, string uniqueIdentifier)
{
- loc = location;
- this.name = name;
+ loc = location ?? string.Empty;
+ Name = name;
Enabled = enabled;
- if (location.Equals(uniqueIdentifier, StringComparison.OrdinalIgnoreCase))
- {
- UniqueIdentifier = location.ToLowerInvariant(); // To make sure old config can be reset to case-insensitive
- }
- else
- {
- UniqueIdentifier = uniqueIdentifier; // For uwp apps
- }
+ UniqueIdentifier = uniqueIdentifier;
}
///
@@ -55,14 +57,15 @@ namespace Flow.Launcher.Plugin.Program.Views.Models
/// enabled
public ProgramSource(string location, bool enabled = true)
{
- loc = location;
+ loc = location ?? string.Empty;
Enabled = enabled;
- UniqueIdentifier = location.ToLowerInvariant(); // For path comparison
+ UniqueIdentifier = location; // For path comparison
+ Name = new DirectoryInfo(Location).Name;
}
public ProgramSource(IProgram source)
{
- loc = source.Location;
+ loc = source.Location ?? string.Empty;
Name = source.Name;
Enabled = source.Enabled;
UniqueIdentifier = source.UniqueIdentifier;
diff --git a/appveyor.yml b/appveyor.yml
index d467792d7..aa490fd25 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -4,6 +4,10 @@ init:
- ps: |
$version = new-object System.Version $env:APPVEYOR_BUILD_VERSION
$env:flowVersion = "{0}.{1}.{2}" -f $version.Major, $version.Minor, $version.Build
+ if ($env:APPVEYOR_REPO_BRANCH -eq "dev")
+ {
+ $env:prereleaseTag = "{0}.{1}.{2}.{3}" -f $version.Major, $version.Minor, $version.Build, $version.Revision
+ }
- sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest
- net start WSearch
@@ -18,12 +22,6 @@ assembly_info:
assembly_file_version: $(flowVersion)
assembly_informational_version: $(flowVersion)
-skip_branch_with_pr: true
-
-skip_commits:
- files:
- - '*.md'
-
image: Visual Studio 2022
platform: Any CPU
configuration: Release
@@ -57,6 +55,17 @@ deploy:
on:
APPVEYOR_REPO_TAG: true
+ - provider: GitHub
+ repository: Flow-Launcher/Prereleases
+ release: v$(prereleaseTag)
+ description: 'This is the early access build of our upcoming release. All changes contained here are reviewed, tested and stable to use.\n\nSee our [release](https://github.com/Flow-Launcher/Flow.Launcher/pulls?q=is%3Aopen+is%3Apr+label%3Arelease) Pull Request for details.\n\nFor latest production release visit [here](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest)\n\nPlease report any bugs or issues over at the [main repository](https://github.com/Flow-Launcher/Flow.Launcher/issues)'
+ auth_token:
+ secure: ij4UeXUYQBDJxn2YRAAhUOjklOGVKDB87Hn5J8tKIzj13yatoI7sLM666QDQFEgv
+ artifact: Squirrel Installer, Portable Version, Squirrel nupkg, Squirrel RELEASES
+ force_update: true
+ on:
+ branch: dev
+
- provider: GitHub
release: v$(flowVersion)
auth_token: