Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
</ItemGroup>

Expand Down
91 changes: 72 additions & 19 deletions Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System;
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 @@ -48,7 +48,7 @@ public List<Result> LoadContextMenus(Result result)
{
foreach (var p in similarProcesses)
{
processHelper.TryKill(p);
processHelper.TryKill(_context, p);
}

return true;
Expand All @@ -62,45 +62,99 @@ public List<Result> LoadContextMenus(Result result)

private List<Result> 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<Result>();
// Filter processes based on search term
var searchTerm = query.Search;
var processlist = new List<ProcessResult>();
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, 200, 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
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<Result>();
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 = 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()
{
Expand All @@ -112,11 +166,10 @@ private List<Result> 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;
}
});
}
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
83 changes: 63 additions & 20 deletions Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
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;
Expand All @@ -13,7 +11,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,35 +29,80 @@ 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
/// Get title based on process name and id
/// </summary>
public List<ProcessResult> GetMatchingProcesses(string searchTerm)
public static string GetProcessNameIdTitle(Process p)
{
var processlist = new List<ProcessResult>();
return p.ProcessName + " - " + p.Id;
}

/// <summary>
/// Returns a Process for evey running non-system process
/// </summary>
public List<Process> GetMatchingProcesses()
{
var processlist = new List<Process>();

foreach (var p in Process.GetProcesses())
{
if (IsSystemProcess(p)) continue;

if (string.IsNullOrWhiteSpace(searchTerm))
{
// show all non-system processes
processlist.Add(new ProcessResult(p, 0));
}
else
processlist.Add(p);
}

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))
{
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<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 processlist;
return buffer[..length].ToString();
}

/// <summary>
Expand All @@ -70,7 +113,7 @@ public IEnumerable<Process> GetSimilarProcesses(string processPath)
return Process.GetProcesses().Where(p => !IsSystemProcess(p) && TryGetProcessFilename(p) == processPath);
}

public void TryKill(Process p)
public void TryKill(PluginInitContext context, Process p)
{
try
{
Expand All @@ -82,7 +125,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);
}
}

Expand Down
14 changes: 12 additions & 2 deletions Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
}
Loading