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
2 changes: 0 additions & 2 deletions Flow.Launcher.Infrastructure/FileExplorerHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <summary>
/// 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.
/// </summary>
Expand Down
59 changes: 36 additions & 23 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,63 +60,76 @@ 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 searchTerm = query.Search;
var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle();
var processList = processHelper.GetMatchingProcesses(searchTerm, processWindowTitle);

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);
var title = p.ProcessName + " - " + p.Id;
var score = pr.Score;
if (processWindowTitle.TryGetValue(p.Id, out var mainWindowTitle))
{
title = mainWindowTitle;
if (string.IsNullOrWhiteSpace(searchTerm))
{
// Add score to prioritize processes with visible windows
score += 200;
}
}
results.Add(new Result()
{
IcoPath = path,
Title = p.ProcessName + " - " + p.Id,
Title = title,
SubTitle = path,
TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData,
Score = pr.Score,
ContextData = p.ProcessName,
TitleHighlightData = StringMatcher.FuzzySearch(searchTerm, p.ProcessName).MatchData,
Score = score,
ContextData = new RunningProcessInfo(p.ProcessName, mainWindowTitle),
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;
_context.API.ReQuery();
return false;
}
});
}

var sortedResults = results.OrderBy(x => x.Title).ToList();
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()
{
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);
}
// Re-query to refresh process list
_context.API.ChangeQuery(query.RawQuery, true);
return true;
_context.API.ReQuery();
return false;
}
});
}
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
75 changes: 68 additions & 7 deletions 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 All @@ -31,12 +31,15 @@ internal class ProcessHelper
"explorer"
};

private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower());
private const string FlowLauncherProcessName = "Flow.Launcher";

private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower()) ||
string.Compare(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase) == 0;

/// <summary>
/// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm
/// </summary>
public List<ProcessResult> GetMatchingProcesses(string searchTerm)
public List<ProcessResult> GetMatchingProcesses(string searchTerm, Dictionary<int, string> processWindowTitle)
{
var processlist = new List<ProcessResult>();

Expand All @@ -46,22 +49,80 @@ public List<ProcessResult> GetMatchingProcesses(string searchTerm)

if (string.IsNullOrWhiteSpace(searchTerm))
{
// show all non-system processes
// Show all non-system processes
processlist.Add(new ProcessResult(p, 0));
}
else
{
var score = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + p.Id).Score;
if (score > 0)
// Search window title first
if (processWindowTitle.TryGetValue(p.Id, out var windowTitle))
{
var score = StringMatcher.FuzzySearch(searchTerm, windowTitle).Score;
if (score > 0)
{
processlist.Add(new ProcessResult(p, score));
}
}

// Search process name and process id
var score1 = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + " - " + p.Id).Score;
if (score1 > 0)
{
processlist.Add(new ProcessResult(p, score));
processlist.Add(new ProcessResult(p, score1));
continue;
}
}
}

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, _) =>
{
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