diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index b97c096c363..1085cc83313 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -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); - /// /// 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. /// diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Views/PluginsManagerSettings.xaml.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Views/PluginsManagerSettings.xaml.cs index 26668cc0529..3b0ff07436e 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Views/PluginsManagerSettings.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Views/PluginsManagerSettings.xaml.cs @@ -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 /// public partial class PluginsManagerSettings { - private readonly SettingsViewModel viewModel; - internal PluginsManagerSettings(SettingsViewModel viewModel) { InitializeComponent(); - this.viewModel = viewModel; - - this.DataContext = viewModel; + DataContext = viewModel; } } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj index 4e216b7b26a..0c501b2d9c9 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj @@ -13,6 +13,7 @@ false false en + true @@ -58,7 +59,6 @@ - diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml index e7a1361147f..ea6e54fef7a 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Languages/en.xaml @@ -1,6 +1,7 @@ - + Process Killer Kill running processes from Flow Launcher @@ -9,4 +10,6 @@ kill {0} processes kill all instances + Put processes with visible windows on the top + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index be2a2dd6673..9ab1502a2c7 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -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(); + _viewModel = new SettingsViewModel(Settings); } public List Query(Query query) @@ -48,7 +57,7 @@ public List LoadContextMenus(Result result) { foreach (var p in similarProcesses) { - processHelper.TryKill(p); + processHelper.TryKill(_context, p); } return true; @@ -62,16 +71,72 @@ public List LoadContextMenus(Result result) private List 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(); + // Filter processes based on search term + var searchTerm = query.Search; + var processlist = new List(); + 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(); foreach (var pr in processlist) { var p = pr.Process; @@ -79,28 +144,29 @@ private List CreateResultsFromQuery(Query query) 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 @@ private List 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; } }); } return sortedResults; } + + public Control CreateSettingPanel() + { + return new SettingsControl(_viewModel); + } } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt index 7fa794755e1..13bf27932ae 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt @@ -1,2 +1,7 @@ QueryFullProcessImageName -OpenProcess \ No newline at end of file +OpenProcess +EnumWindows +GetWindowTextLength +GetWindowText +IsWindowVisible +GetWindowThreadProcessId \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 519e8a79297..4c07341ec2a 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -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 _systemProcessList = new HashSet() + private readonly HashSet _systemProcessList = new() { "conhost", "svchost", @@ -31,35 +30,85 @@ internal class ProcessHelper "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); /// - /// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm + /// Get title based on process name and id /// - public List GetMatchingProcesses(string searchTerm) + public static string GetProcessNameIdTitle(Process p) { - var processlist = new List(); + var sb = new StringBuilder(); + sb.Append(p.ProcessName); + sb.Append(" - "); + sb.Append(p.Id); + return sb.ToString(); + } + + /// + /// Returns a Process for evey running non-system process + /// + public List GetMatchingProcesses() + { + var processlist = new List(); 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 + processlist.Add(p); + } + + return processlist; + } + + /// + /// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title. + /// + public static unsafe Dictionary GetProcessesWithNonEmptyWindowTitle() + { + var processDict = new Dictionary(); + 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 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(); } /// @@ -67,10 +116,10 @@ public List GetMatchingProcesses(string searchTerm) /// public IEnumerable 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 @@ 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); } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs index 03856677e63..146c9c92cf8 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs new file mode 100644 index 00000000000..916bc6a3979 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs @@ -0,0 +1,7 @@ +namespace Flow.Launcher.Plugin.ProcessKiller +{ + public class Settings + { + public bool PutVisibleWindowProcessesTop { get; set; } = false; + } +} diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs new file mode 100644 index 00000000000..bacf1ba08c9 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs @@ -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; + } + } +} diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml new file mode 100644 index 00000000000..d15d6c3e084 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs new file mode 100644 index 00000000000..a066ab6a912 --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs @@ -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 +{ + /// + /// Interaction logic for SettingsControl.xaml + /// + public SettingsControl(SettingsViewModel viewModel) + { + InitializeComponent(); + + DataContext = viewModel; + } +}