mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge pull request #3150 from PaulPSta/processkiller_orderby_windowtitle
Put Processes with Visible Window On the Top & Do not kill FL Process
This commit is contained in:
commit
773fb8d9eb
12 changed files with 255 additions and 61 deletions
|
|
@ -67,8 +67,6 @@ namespace Flow.Launcher.Infrastructure
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
public partial class PluginsManagerSettings
|
||||
{
|
||||
private readonly SettingsViewModel viewModel;
|
||||
|
||||
internal PluginsManagerSettings(SettingsViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
this.viewModel = viewModel;
|
||||
|
||||
this.DataContext = viewModel;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
|
|
@ -58,7 +59,6 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<system:String x:Key="flowlauncher_plugin_processkiller_plugin_name">Process Killer</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_processkiller_plugin_description">Kill running processes from Flow Launcher</system:String>
|
||||
|
|
@ -9,4 +10,6 @@
|
|||
<system:String x:Key="flowlauncher_plugin_processkiller_kill_all_count">kill {0} processes</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_processkiller_kill_instances">kill all instances</system:String>
|
||||
|
||||
<system:String x:Key="flowlauncher_plugin_processkiller_put_visible_window_process_top">Put processes with visible windows on the top</system:String>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
|
@ -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<Settings>();
|
||||
_viewModel = new SettingsViewModel(Settings);
|
||||
}
|
||||
|
||||
public List<Result> Query(Query query)
|
||||
|
|
@ -48,7 +57,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
{
|
||||
foreach (var p in similarProcesses)
|
||||
{
|
||||
processHelper.TryKill(p);
|
||||
processHelper.TryKill(_context, p);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -62,16 +71,72 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
|
||||
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, 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<Result>();
|
||||
foreach (var pr in processlist)
|
||||
{
|
||||
var p = pr.Process;
|
||||
|
|
@ -79,28 +144,29 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
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 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
QueryFullProcessImageName
|
||||
OpenProcess
|
||||
OpenProcess
|
||||
EnumWindows
|
||||
GetWindowTextLength
|
||||
GetWindowText
|
||||
IsWindowVisible
|
||||
GetWindowThreadProcessId
|
||||
|
|
@ -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<string> _systemProcessList = new HashSet<string>()
|
||||
private readonly HashSet<string> _systemProcessList = new()
|
||||
{
|
||||
"conhost",
|
||||
"svchost",
|
||||
|
|
@ -31,46 +30,96 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
"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);
|
||||
|
||||
/// <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>();
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(p.ProcessName);
|
||||
sb.Append(" - ");
|
||||
sb.Append(p.Id);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <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 (IsSystemProcessOrFlowLauncher(p)) continue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
processlist.Add(new ProcessResult(p, score));
|
||||
}
|
||||
}
|
||||
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))
|
||||
{
|
||||
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>
|
||||
public IEnumerable<Process> 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 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs
Normal file
7
Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Flow.Launcher.Plugin.ProcessKiller
|
||||
{
|
||||
public class Settings
|
||||
{
|
||||
public bool PutVisibleWindowProcessesTop { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<UserControl
|
||||
x:Class="Flow.Launcher.Plugin.ProcessKiller.Views.SettingsControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="500"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Margin="{StaticResource SettingPanelMargin}">
|
||||
<Grid.ColumnDefinitions />
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<CheckBox
|
||||
Grid.Row="0"
|
||||
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
|
||||
Content="{DynamicResource flowlauncher_plugin_processkiller_put_visible_window_process_top}"
|
||||
IsChecked="{Binding PutVisibleWindowProcessesTop}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SettingsControl.xaml
|
||||
/// </summary>
|
||||
public SettingsControl(SettingsViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue