Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Put Processes with Visible Window On the Top & Do not kill FL Process #3150

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
32 changes: 19 additions & 13 deletions Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Flow.Launcher.Infrastructure;

namespace Flow.Launcher.Plugin.ProcessKiller
{
public class Main : IPlugin, IPluginI18n, IContextMenu
{
private ProcessHelper processHelper = new ProcessHelper();
private readonly ProcessHelper processHelper = new();

private static PluginInitContext _context;

Expand Down Expand Up @@ -60,30 +60,33 @@ public List<Result> LoadContextMenus(Result result)
return menuOptions;
}

private record RunningProcessInfo(string ProcessName, string MainWindowTitle);

private List<Result> CreateResultsFromQuery(Query query)
{
string termToSearch = query.Search;
var processlist = processHelper.GetMatchingProcesses(termToSearch);

if (!processlist.Any())
var processList = processHelper.GetMatchingProcesses(termToSearch);
var processWithNonEmptyMainWindowTitleList = ProcessHelper.GetProcessesWithNonEmptyWindowTitle();

if (!processList.Any())
{
return null;
}

var results = new List<Result>();

foreach (var pr in processlist)
foreach (var pr in processList)
{
var p = pr.Process;
var path = processHelper.TryGetProcessFilename(p);
results.Add(new Result()
{
IcoPath = path,
Title = p.ProcessName + " - " + p.Id,
Title = processWithNonEmptyMainWindowTitleList.TryGetValue(p.Id, out var mainWindowTitle) ? mainWindowTitle : p.ProcessName + " - " + p.Id,
SubTitle = path,
TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData,
Score = pr.Score,
ContextData = p.ProcessName,
ContextData = new RunningProcessInfo(p.ProcessName, mainWindowTitle),
AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}",
Action = (c) =>
{
Expand All @@ -95,22 +98,25 @@ private List<Result> CreateResultsFromQuery(Query query)
});
}

var sortedResults = results.OrderBy(x => x.Title).ToList();
var sortedResults = results
.OrderBy(x => string.IsNullOrEmpty(((RunningProcessInfo)x.ContextData).MainWindowTitle))
.ThenBy(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(termToSearch) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle))
{
sortedResults.Insert(1, new Result()
{
IcoPath = firstResult?.IcoPath,
Title = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all"), firstResult?.ContextData),
SubTitle = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all_count"), processlist.Count),
Title = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all"), ((RunningProcessInfo)firstResult?.ContextData).ProcessName),
SubTitle = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all_count"), processList.Count),
Score = 200,
Action = (c) =>
{
foreach (var p in processlist)
foreach (var p in processList)
{
processHelper.TryKill(p.Process);
}
Expand Down
7 changes: 6 additions & 1 deletion Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
QueryFullProcessImageName
OpenProcess
OpenProcess
EnumWindows
GetWindowTextLength
GetWindowText
IsWindowVisible
GetWindowThreadProcessId
48 changes: 47 additions & 1 deletion Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
{
internal class ProcessHelper
{
private readonly HashSet<string> _systemProcessList = new HashSet<string>()
private readonly HashSet<string> _systemProcessList = new()
{
"conhost",
"svchost",
Expand Down Expand Up @@ -62,6 +62,52 @@ public List<ProcessResult> GetMatchingProcesses(string searchTerm)
return processlist;
}

/// <summary>
/// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title.
/// </summary>
public static unsafe Dictionary<int, string> GetProcessesWithNonEmptyWindowTitle()
{
var processDict = new Dictionary<int, string>();
PInvoke.EnumWindows((hWnd, lParam) =>
{
var windowTitle = GetWindowTitle(hWnd);
if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd))
{
uint processId = 0;
var result = PInvoke.GetWindowThreadProcessId(hWnd, &processId);
if (result == 0u || processId == 0u)
{
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<char> 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 buffer[..length].ToString();
}

/// <summary>
/// Returns all non-system processes whose file path matches the given processPath
/// </summary>
Expand Down
Loading