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
Open
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