diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs
index b97c096c363..1085cc83313 100644
--- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs
+++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs
@@ -67,8 +67,6 @@ private static dynamic GetActiveExplorer()
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
}
- private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
-
///
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
///
diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Views/PluginsManagerSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Views/PluginsManagerSettings.xaml.cs
index 26668cc0529..3b0ff07436e 100644
--- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Views/PluginsManagerSettings.xaml.cs
+++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Views/PluginsManagerSettings.xaml.cs
@@ -1,5 +1,4 @@
-
-using Flow.Launcher.Plugin.PluginsManager.ViewModels;
+using Flow.Launcher.Plugin.PluginsManager.ViewModels;
namespace Flow.Launcher.Plugin.PluginsManager.Views
{
@@ -8,15 +7,11 @@ namespace Flow.Launcher.Plugin.PluginsManager.Views
///
public partial class PluginsManagerSettings
{
- private readonly SettingsViewModel viewModel;
-
internal PluginsManagerSettings(SettingsViewModel viewModel)
{
InitializeComponent();
- this.viewModel = viewModel;
-
- this.DataContext = viewModel;
+ DataContext = viewModel;
}
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
index 4e216b7b26a..0c501b2d9c9 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj
@@ -13,6 +13,7 @@
false
false
en
+ true
@@ -58,7 +59,6 @@
-
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml
index e7a1361147f..ea6e54fef7a 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml
@@ -1,6 +1,7 @@
-
+
Process Killer
Kill running processes from Flow Launcher
@@ -9,4 +10,6 @@
kill {0} processes
kill all instances
+ Put processes with visible windows on the top
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs
index be2a2dd6673..9ab1502a2c7 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs
@@ -1,18 +1,27 @@
+using System;
using System.Collections.Generic;
using System.Linq;
-using Flow.Launcher.Infrastructure;
+using System.Windows.Controls;
+using Flow.Launcher.Plugin.ProcessKiller.ViewModels;
+using Flow.Launcher.Plugin.ProcessKiller.Views;
namespace Flow.Launcher.Plugin.ProcessKiller
{
- public class Main : IPlugin, IPluginI18n, IContextMenu
+ public class Main : IPlugin, IPluginI18n, IContextMenu, ISettingProvider
{
- private ProcessHelper processHelper = new ProcessHelper();
+ private readonly ProcessHelper processHelper = new();
private static PluginInitContext _context;
+ internal Settings Settings;
+
+ private SettingsViewModel _viewModel;
+
public void Init(PluginInitContext context)
{
_context = context;
+ Settings = context.API.LoadSettingJsonStorage();
+ _viewModel = new SettingsViewModel(Settings);
}
public List Query(Query query)
@@ -48,7 +57,7 @@ public List LoadContextMenus(Result result)
{
foreach (var p in similarProcesses)
{
- processHelper.TryKill(p);
+ processHelper.TryKill(_context, p);
}
return true;
@@ -62,16 +71,72 @@ public List LoadContextMenus(Result result)
private List CreateResultsFromQuery(Query query)
{
- string termToSearch = query.Search;
- var processlist = processHelper.GetMatchingProcesses(termToSearch);
-
- if (!processlist.Any())
+ // Get all non-system processes
+ var allPocessList = processHelper.GetMatchingProcesses();
+ if (!allPocessList.Any())
{
return null;
}
- var results = new List();
+ // Filter processes based on search term
+ var searchTerm = query.Search;
+ var processlist = new List();
+ var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle();
+ if (string.IsNullOrWhiteSpace(searchTerm))
+ {
+ foreach (var p in allPocessList)
+ {
+ var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p);
+ if (processWindowTitle.TryGetValue(p.Id, out var windowTitle))
+ {
+ // Add score to prioritize processes with visible windows
+ // And use window title for those processes
+ processlist.Add(new ProcessResult(p, Settings.PutVisibleWindowProcessesTop ? 200 : 0, windowTitle, null, progressNameIdTitle));
+ }
+ else
+ {
+ processlist.Add(new ProcessResult(p, 0, progressNameIdTitle, null, progressNameIdTitle));
+ }
+ }
+ }
+ else
+ {
+ foreach (var p in allPocessList)
+ {
+ var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p);
+
+ if (processWindowTitle.TryGetValue(p.Id, out var windowTitle))
+ {
+ // Get max score from searching process name, window title and process id
+ var windowTitleMatch = _context.API.FuzzySearch(searchTerm, windowTitle);
+ var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle);
+ var score = Math.Max(windowTitleMatch.Score, processNameIdMatch.Score);
+ if (score > 0)
+ {
+ // Add score to prioritize processes with visible windows
+ // And use window title for those processes
+ if (Settings.PutVisibleWindowProcessesTop)
+ {
+ score += 200;
+ }
+ processlist.Add(new ProcessResult(p, score, windowTitle,
+ score == windowTitleMatch.Score ? windowTitleMatch : null, progressNameIdTitle));
+ }
+ }
+ else
+ {
+ var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle);
+ var score = processNameIdMatch.Score;
+ if (score > 0)
+ {
+ processlist.Add(new ProcessResult(p, score, progressNameIdTitle, processNameIdMatch, progressNameIdTitle));
+ }
+ }
+ }
+ }
+
+ var results = new List();
foreach (var pr in processlist)
{
var p = pr.Process;
@@ -79,28 +144,29 @@ private List CreateResultsFromQuery(Query query)
results.Add(new Result()
{
IcoPath = path,
- Title = p.ProcessName + " - " + p.Id,
+ Title = pr.Title,
+ TitleToolTip = pr.Tooltip,
SubTitle = path,
- TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData,
+ TitleHighlightData = pr.TitleMatch?.MatchData,
Score = pr.Score,
ContextData = p.ProcessName,
AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}",
Action = (c) =>
{
- processHelper.TryKill(p);
- // Re-query to refresh process list
- _context.API.ChangeQuery(query.RawQuery, true);
- return true;
+ processHelper.TryKill(_context, p);
+ _context.API.ReQuery();
+ return false;
}
});
}
+ // Order results by process name for processes without visible windows
var sortedResults = results.OrderBy(x => x.Title).ToList();
// When there are multiple results AND all of them are instances of the same executable
// add a quick option to kill them all at the top of the results.
var firstResult = sortedResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.SubTitle));
- if (processlist.Count > 1 && !string.IsNullOrEmpty(termToSearch) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle))
+ if (processlist.Count > 1 && !string.IsNullOrEmpty(searchTerm) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle))
{
sortedResults.Insert(1, new Result()
{
@@ -112,16 +178,20 @@ private List CreateResultsFromQuery(Query query)
{
foreach (var p in processlist)
{
- processHelper.TryKill(p.Process);
+ processHelper.TryKill(_context, p.Process);
}
- // Re-query to refresh process list
- _context.API.ChangeQuery(query.RawQuery, true);
- return true;
+ _context.API.ReQuery();
+ return false;
}
});
}
return sortedResults;
}
+
+ public Control CreateSettingPanel()
+ {
+ return new SettingsControl(_viewModel);
+ }
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt
index 7fa794755e1..13bf27932ae 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt
@@ -1,2 +1,7 @@
QueryFullProcessImageName
-OpenProcess
\ No newline at end of file
+OpenProcess
+EnumWindows
+GetWindowTextLength
+GetWindowText
+IsWindowVisible
+GetWindowThreadProcessId
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs
index 519e8a79297..4c07341ec2a 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs
@@ -1,10 +1,9 @@
-using Flow.Launcher.Infrastructure;
-using Flow.Launcher.Infrastructure.Logger;
-using Microsoft.Win32.SafeHandles;
+using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.System.Threading;
@@ -13,7 +12,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
{
internal class ProcessHelper
{
- private readonly HashSet _systemProcessList = new HashSet()
+ private readonly HashSet _systemProcessList = new()
{
"conhost",
"svchost",
@@ -31,35 +30,85 @@ internal class ProcessHelper
"explorer"
};
- private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower());
+ private const string FlowLauncherProcessName = "Flow.Launcher";
+
+ private bool IsSystemProcessOrFlowLauncher(Process p) =>
+ _systemProcessList.Contains(p.ProcessName.ToLower()) ||
+ string.Equals(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase);
///
- /// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm
+ /// Get title based on process name and id
///
- public List GetMatchingProcesses(string searchTerm)
+ public static string GetProcessNameIdTitle(Process p)
{
- var processlist = new List();
+ var sb = new StringBuilder();
+ sb.Append(p.ProcessName);
+ sb.Append(" - ");
+ sb.Append(p.Id);
+ return sb.ToString();
+ }
+
+ ///
+ /// Returns a Process for evey running non-system process
+ ///
+ public List GetMatchingProcesses()
+ {
+ var processlist = new List();
foreach (var p in Process.GetProcesses())
{
- if (IsSystemProcess(p)) continue;
+ if (IsSystemProcessOrFlowLauncher(p)) continue;
- if (string.IsNullOrWhiteSpace(searchTerm))
- {
- // show all non-system processes
- processlist.Add(new ProcessResult(p, 0));
- }
- else
+ processlist.Add(p);
+ }
+
+ return processlist;
+ }
+
+ ///
+ /// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title.
+ ///
+ public static unsafe Dictionary GetProcessesWithNonEmptyWindowTitle()
+ {
+ var processDict = new Dictionary();
+ PInvoke.EnumWindows((hWnd, _) =>
+ {
+ var windowTitle = GetWindowTitle(hWnd);
+ if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd))
{
- var score = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + p.Id).Score;
- if (score > 0)
+ uint processId = 0;
+ var result = PInvoke.GetWindowThreadProcessId(hWnd, &processId);
+ if (result == 0u || processId == 0u)
{
- processlist.Add(new ProcessResult(p, score));
+ return false;
+ }
+
+ var process = Process.GetProcessById((int)processId);
+ if (!processDict.ContainsKey((int)processId))
+ {
+ processDict.Add((int)processId, windowTitle);
}
}
+
+ return true;
+ }, IntPtr.Zero);
+
+ return processDict;
+ }
+
+ private static unsafe string GetWindowTitle(HWND hwnd)
+ {
+ var capacity = PInvoke.GetWindowTextLength(hwnd) + 1;
+ int length;
+ Span buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity];
+ fixed (char* pBuffer = buffer)
+ {
+ // If the window has no title bar or text, if the title bar is empty,
+ // or if the window or control handle is invalid, the return value is zero.
+ length = PInvoke.GetWindowText(hwnd, pBuffer, capacity);
}
- return processlist;
+ return buffer[..length].ToString();
}
///
@@ -67,10 +116,10 @@ public List GetMatchingProcesses(string searchTerm)
///
public IEnumerable GetSimilarProcesses(string processPath)
{
- return Process.GetProcesses().Where(p => !IsSystemProcess(p) && TryGetProcessFilename(p) == processPath);
+ return Process.GetProcesses().Where(p => !IsSystemProcessOrFlowLauncher(p) && TryGetProcessFilename(p) == processPath);
}
- public void TryKill(Process p)
+ public void TryKill(PluginInitContext context, Process p)
{
try
{
@@ -82,7 +131,7 @@ public void TryKill(Process p)
}
catch (Exception e)
{
- Log.Exception($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e);
+ context.API.LogException($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e);
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs
index 03856677e63..146c9c92cf8 100644
--- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs
@@ -1,17 +1,27 @@
using System.Diagnostics;
+using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Plugin.ProcessKiller
{
internal class ProcessResult
{
- public ProcessResult(Process process, int score)
+ public ProcessResult(Process process, int score, string title, MatchResult match, string tooltip)
{
Process = process;
Score = score;
+ Title = title;
+ TitleMatch = match;
+ Tooltip = tooltip;
}
public Process Process { get; }
public int Score { get; }
+
+ public string Title { get; }
+
+ public MatchResult TitleMatch { get; }
+
+ public string Tooltip { get; }
}
-}
\ No newline at end of file
+}
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs
new file mode 100644
index 00000000000..916bc6a3979
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs
@@ -0,0 +1,7 @@
+namespace Flow.Launcher.Plugin.ProcessKiller
+{
+ public class Settings
+ {
+ public bool PutVisibleWindowProcessesTop { get; set; } = false;
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs
new file mode 100644
index 00000000000..bacf1ba08c9
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs
@@ -0,0 +1,18 @@
+namespace Flow.Launcher.Plugin.ProcessKiller.ViewModels
+{
+ public class SettingsViewModel
+ {
+ public Settings Settings { get; set; }
+
+ public SettingsViewModel(Settings settings)
+ {
+ Settings = settings;
+ }
+
+ public bool PutVisibleWindowProcessesTop
+ {
+ get => Settings.PutVisibleWindowProcessesTop;
+ set => Settings.PutVisibleWindowProcessesTop = value;
+ }
+ }
+}
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml
new file mode 100644
index 00000000000..d15d6c3e084
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs
new file mode 100644
index 00000000000..a066ab6a912
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs
@@ -0,0 +1,17 @@
+using System.Windows.Controls;
+using Flow.Launcher.Plugin.ProcessKiller.ViewModels;
+
+namespace Flow.Launcher.Plugin.ProcessKiller.Views;
+
+public partial class SettingsControl : UserControl
+{
+ ///
+ /// Interaction logic for SettingsControl.xaml
+ ///
+ public SettingsControl(SettingsViewModel viewModel)
+ {
+ InitializeComponent();
+
+ DataContext = viewModel;
+ }
+}