From b2532fc88e09977b2c62d4eec3b8ec1ec0dd43d6 Mon Sep 17 00:00:00 2001 From: "pp.stabrawa" Date: Fri, 27 Dec 2024 12:43:43 +0100 Subject: [PATCH 01/10] Order search result by window title --- .../Main.cs | 30 ++++++++----- .../ProcessHelper.cs | 44 ++++++++++++++++++- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index be2a2dd6673..03b563c9bf1 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Flow.Launcher.Infrastructure; @@ -60,30 +60,33 @@ public List LoadContextMenus(Result result) return menuOptions; } + private record RunningProcessInfo(string ProcessName, string MainWindowTitle); + private List CreateResultsFromQuery(Query query) { string termToSearch = query.Search; - var processlist = processHelper.GetMatchingProcesses(termToSearch); - - if (!processlist.Any()) + var processList = processHelper.GetMatchingProcesses(termToSearch); + var processWithNonEmptyMainWindowTitleList = processHelper.GetProcessesWithNonEmptyWindowTitle(); + + if (!processList.Any()) { return null; } var results = new List(); - foreach (var pr in processlist) + 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 = processWithNonEmptyMainWindowTitleList.TryGetValue(p.Id, out var mainWindowTitle) ? mainWindowTitle : p.ProcessName + " - " + p.Id, SubTitle = path, TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData, Score = pr.Score, - ContextData = p.ProcessName, + ContextData = new RunningProcessInfo(p.ProcessName, mainWindowTitle), AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}", Action = (c) => { @@ -95,22 +98,25 @@ private List CreateResultsFromQuery(Query query) }); } - var sortedResults = results.OrderBy(x => x.Title).ToList(); + var sortedResults = results + .OrderBy(x => string.IsNullOrEmpty(((RunningProcessInfo)x.ContextData).MainWindowTitle)) + .ThenBy(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(termToSearch) && 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); } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 0acc39fbb1c..5cc94dfdf48 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using System; using System.Collections.Generic; @@ -11,6 +11,20 @@ namespace Flow.Launcher.Plugin.ProcessKiller { internal class ProcessHelper { + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); + + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + private readonly HashSet _systemProcessList = new HashSet() { "conhost", @@ -60,6 +74,34 @@ public List GetMatchingProcesses(string searchTerm) 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 Dictionary GetProcessesWithNonEmptyWindowTitle() + { + var processDict = new Dictionary(); + EnumWindows((hWnd, lParam) => + { + StringBuilder windowTitle = new StringBuilder(); + GetWindowText(hWnd, windowTitle, windowTitle.Capacity); + + if (!string.IsNullOrWhiteSpace(windowTitle.ToString()) && IsWindowVisible(hWnd)) + { + GetWindowThreadProcessId(hWnd, out var processId); + var process = Process.GetProcessById((int)processId); + + if (!processDict.ContainsKey((int)processId)) + { + processDict.Add((int)processId, windowTitle.ToString()); + } + } + + return true; + }, IntPtr.Zero); + + return processDict; + } + /// /// Returns all non-system processes whose file path matches the given processPath /// From fa465130c6141af48345a30b12cb5e11c1d4b841 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 13 Mar 2025 14:01:02 +0800 Subject: [PATCH 02/10] Use PInvoke to replace DllImport --- .../Main.cs | 4 +- .../NativeMethods.txt | 7 ++- .../ProcessHelper.cs | 52 ++++++++++--------- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index 03b563c9bf1..098e668cb9f 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -6,7 +6,7 @@ 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; @@ -66,7 +66,7 @@ private List CreateResultsFromQuery(Query query) { string termToSearch = query.Search; var processList = processHelper.GetMatchingProcesses(termToSearch); - var processWithNonEmptyMainWindowTitleList = processHelper.GetProcessesWithNonEmptyWindowTitle(); + var processWithNonEmptyMainWindowTitleList = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); if (!processList.Any()) { 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 d8873bc20f3..5f58dc6b11e 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -13,21 +13,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller { internal class ProcessHelper { - [DllImport("user32.dll")] - private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); - - private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - private static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - private readonly HashSet _systemProcessList = new HashSet() + private readonly HashSet _systemProcessList = new() { "conhost", "svchost", @@ -79,22 +65,25 @@ public List GetMatchingProcesses(string searchTerm) /// /// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title. /// - public Dictionary GetProcessesWithNonEmptyWindowTitle() + public static unsafe Dictionary GetProcessesWithNonEmptyWindowTitle() { var processDict = new Dictionary(); - EnumWindows((hWnd, lParam) => + PInvoke.EnumWindows((hWnd, lParam) => { - StringBuilder windowTitle = new StringBuilder(); - GetWindowText(hWnd, windowTitle, windowTitle.Capacity); - - if (!string.IsNullOrWhiteSpace(windowTitle.ToString()) && IsWindowVisible(hWnd)) + var windowTitle = GetWindowTitle(hWnd); + if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd)) { - GetWindowThreadProcessId(hWnd, out var processId); - var process = Process.GetProcessById((int)processId); + 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.ToString()); + processDict.Add((int)processId, windowTitle); } } @@ -104,6 +93,21 @@ public Dictionary GetProcessesWithNonEmptyWindowTitle() 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 buffer[..length].ToString(); + } + /// /// Returns all non-system processes whose file path matches the given processPath /// From b7694d31300a3b62a298d605e52a813e0e04f5bf Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 13 Mar 2025 14:09:13 +0800 Subject: [PATCH 03/10] Clean up codes --- Flow.Launcher.Infrastructure/FileExplorerHelper.cs | 2 -- Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) 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.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 5f58dc6b11e..c603fba09fb 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -68,7 +68,7 @@ public List GetMatchingProcesses(string searchTerm) public static unsafe Dictionary GetProcessesWithNonEmptyWindowTitle() { var processDict = new Dictionary(); - PInvoke.EnumWindows((hWnd, lParam) => + PInvoke.EnumWindows((hWnd, _) => { var windowTitle = GetWindowTitle(hWnd); if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd)) From d12cde7c44dc37e54d42fd35c42c958702122e2a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 13 Mar 2025 14:16:38 +0800 Subject: [PATCH 04/10] Do not close window when killed processes --- Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index 098e668cb9f..aec60757543 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -91,9 +91,8 @@ private List CreateResultsFromQuery(Query query) Action = (c) => { processHelper.TryKill(p); - // Re-query to refresh process list - _context.API.ChangeQuery(query.RawQuery, true); - return true; + _context.API.ReQuery(); + return false; } }); } @@ -120,9 +119,8 @@ private List CreateResultsFromQuery(Query query) { processHelper.TryKill(p.Process); } - // Re-query to refresh process list - _context.API.ChangeQuery(query.RawQuery, true); - return true; + _context.API.ReQuery(); + return false; } }); } From 6bc69b17cf619d9054fea4012ada1ff42bc414ea Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 14 Mar 2025 11:21:07 +0800 Subject: [PATCH 05/10] Do not allow kill FL process --- Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index c603fba09fb..1538f71dd6f 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -31,7 +31,10 @@ 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; /// /// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm From 8ee2d4852e24525f8206270a2d290cef2c3f80e4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 14 Mar 2025 11:39:16 +0800 Subject: [PATCH 06/10] Use score bump & Search window title --- .../Main.cs | 29 ++++++++++++------- .../ProcessHelper.cs | 22 ++++++++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index aec60757543..3b88f874b9a 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -64,9 +64,9 @@ private record RunningProcessInfo(string ProcessName, string MainWindowTitle); private List CreateResultsFromQuery(Query query) { - string termToSearch = query.Search; - var processList = processHelper.GetMatchingProcesses(termToSearch); - var processWithNonEmptyMainWindowTitleList = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); + var searchTerm = query.Search; + var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); + var processList = processHelper.GetMatchingProcesses(searchTerm, processWindowTitle); if (!processList.Any()) { @@ -74,18 +74,28 @@ private List CreateResultsFromQuery(Query query) } var results = new List(); - 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 = processWithNonEmptyMainWindowTitleList.TryGetValue(p.Id, out var mainWindowTitle) ? mainWindowTitle : p.ProcessName + " - " + p.Id, + Title = title, SubTitle = path, - TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData, - Score = pr.Score, + 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) => @@ -98,14 +108,13 @@ private List CreateResultsFromQuery(Query query) } var sortedResults = results - .OrderBy(x => string.IsNullOrEmpty(((RunningProcessInfo)x.ContextData).MainWindowTitle)) - .ThenBy(x => x.Title) + .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() { diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 1538f71dd6f..d4760e98d03 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -39,7 +39,7 @@ private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.Process /// /// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm /// - public List GetMatchingProcesses(string searchTerm) + public List GetMatchingProcesses(string searchTerm, Dictionary processWindowTitle) { var processlist = new List(); @@ -49,15 +49,27 @@ public List 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)) { - processlist.Add(new ProcessResult(p, score)); + 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, score1)); + continue; } } } From f25c5b9b0530e1fbce507c58e939629026f1fcb9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 14 Mar 2025 16:12:15 +0800 Subject: [PATCH 07/10] Improve code quality & Use progress name and id as title tooltip --- .../Flow.Launcher.Plugin.ProcessKiller.csproj | 1 - .../Main.cs | 106 ++++++++++++------ .../ProcessHelper.cs | 48 +++----- .../ProcessResult.cs | 14 ++- 4 files changed, 100 insertions(+), 69 deletions(-) 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..c4864dc5732 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj @@ -58,7 +58,6 @@ - diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index 3b88f874b9a..b44ecd99cf4 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using Flow.Launcher.Infrastructure; namespace Flow.Launcher.Plugin.ProcessKiller { @@ -48,7 +48,7 @@ public List LoadContextMenus(Result result) { foreach (var p in similarProcesses) { - processHelper.TryKill(p); + processHelper.TryKill(_context, p); } return true; @@ -60,73 +60,113 @@ public List LoadContextMenus(Result result) return menuOptions; } - private record RunningProcessInfo(string ProcessName, string MainWindowTitle); - private List CreateResultsFromQuery(Query query) { - var searchTerm = query.Search; - var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); - var processList = processHelper.GetMatchingProcesses(searchTerm, processWindowTitle); - - if (!processList.Any()) + // Get all non-system processes + var allPocessList = processHelper.GetMatchingProcesses(); + if (!allPocessList.Any()) { return null; } - var results = new List(); - foreach (var pr in processList) + // Filter processes based on search term + var searchTerm = query.Search; + var processlist = new List(); + var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); + if (string.IsNullOrWhiteSpace(searchTerm)) { - 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)) + foreach (var p in allPocessList) { - title = mainWindowTitle; - if (string.IsNullOrWhiteSpace(searchTerm)) + var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p); + + if (processWindowTitle.TryGetValue(p.Id, out var windowTitle)) { // Add score to prioritize processes with visible windows - score += 200; + // 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(); + foreach (var pr in processlist) + { + var p = pr.Process; + var path = processHelper.TryGetProcessFilename(p); results.Add(new Result() { IcoPath = path, - Title = title, + Title = pr.Title, + TitleToolTip = pr.Tooltip, SubTitle = path, - TitleHighlightData = StringMatcher.FuzzySearch(searchTerm, p.ProcessName).MatchData, - Score = score, - ContextData = new RunningProcessInfo(p.ProcessName, mainWindowTitle), + TitleHighlightData = pr.TitleMatch?.MatchData, + Score = pr.Score, + ContextData = p.ProcessName, AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}", Action = (c) => { - processHelper.TryKill(p); + processHelper.TryKill(_context, p); _context.API.ReQuery(); return false; } }); } - var sortedResults = results - .OrderBy(x => x.Title) - .ToList(); + // 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(searchTerm) && 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"), ((RunningProcessInfo)firstResult?.ContextData).ProcessName), - 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"), firstResult?.ContextData), + 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); + processHelper.TryKill(_context, p.Process); } _context.API.ReQuery(); return false; diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index d4760e98d03..b409df2afc9 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -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; @@ -37,41 +35,25 @@ private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.Process string.Compare(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase) == 0; /// - /// 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, Dictionary processWindowTitle) + public static string GetProcessNameIdTitle(Process p) { - var processlist = new List(); + return p.ProcessName + " - " + p.Id; + } + + /// + /// 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 (string.IsNullOrWhiteSpace(searchTerm)) - { - // Show all non-system processes - processlist.Add(new ProcessResult(p, 0)); - } - else - { - // 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, score1)); - continue; - } - } + processlist.Add(p); } return processlist; @@ -131,7 +113,7 @@ public IEnumerable 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 { @@ -143,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); } } 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 +} From aa6c9d91cf08b0adb7c3f61cd9d1d335ae755031 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 14 Mar 2025 16:42:54 +0800 Subject: [PATCH 08/10] Use string builder --- .../ProcessHelper.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index b409df2afc9..4c07341ec2a 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -3,6 +3,7 @@ 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; @@ -31,15 +32,20 @@ internal class ProcessHelper private const string FlowLauncherProcessName = "Flow.Launcher"; - private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower()) || - string.Compare(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase) == 0; + private bool IsSystemProcessOrFlowLauncher(Process p) => + _systemProcessList.Contains(p.ProcessName.ToLower()) || + string.Equals(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase); /// /// Get title based on process name and id /// public static string GetProcessNameIdTitle(Process p) { - return p.ProcessName + " - " + p.Id; + var sb = new StringBuilder(); + sb.Append(p.ProcessName); + sb.Append(" - "); + sb.Append(p.Id); + return sb.ToString(); } /// @@ -51,7 +57,7 @@ public List GetMatchingProcesses() foreach (var p in Process.GetProcesses()) { - if (IsSystemProcess(p)) continue; + if (IsSystemProcessOrFlowLauncher(p)) continue; processlist.Add(p); } @@ -110,7 +116,7 @@ private static unsafe string GetWindowTitle(HWND hwnd) /// 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(PluginInitContext context, Process p) From 3f0641a58334fd5394d319b4e8d3ca1b25de102b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 26 Mar 2025 15:33:49 +0800 Subject: [PATCH 09/10] Add settings control --- .../Languages/en.xaml | 9 +++++--- .../Main.cs | 23 ++++++++++++++++--- .../Settings.cs | 7 ++++++ .../ViewModels/SettingsViewModel.cs | 18 +++++++++++++++ .../Views/SettingsControl.xaml | 22 ++++++++++++++++++ .../Views/SettingsControl.xaml.cs | 18 +++++++++++++++ 6 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs create mode 100644 Plugins/Flow.Launcher.Plugin.ProcessKiller/ViewModels/SettingsViewModel.cs create mode 100644 Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml create mode 100644 Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs 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 b44ecd99cf4..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 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 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) @@ -83,7 +92,7 @@ private List CreateResultsFromQuery(Query query) { // Add score to prioritize processes with visible windows // And use window title for those processes - processlist.Add(new ProcessResult(p, 200, windowTitle, null, progressNameIdTitle)); + processlist.Add(new ProcessResult(p, Settings.PutVisibleWindowProcessesTop ? 200 : 0, windowTitle, null, progressNameIdTitle)); } else { @@ -107,7 +116,10 @@ private List CreateResultsFromQuery(Query query) { // Add score to prioritize processes with visible windows // And use window title for those processes - score += 200; + if (Settings.PutVisibleWindowProcessesTop) + { + score += 200; + } processlist.Add(new ProcessResult(p, score, windowTitle, score == windowTitleMatch.Score ? windowTitleMatch : null, progressNameIdTitle)); } @@ -176,5 +188,10 @@ private List CreateResultsFromQuery(Query query) return sortedResults; } + + public Control CreateSettingPanel() + { + return new SettingsControl(_viewModel); + } } } 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..ad7229417bd --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs @@ -0,0 +1,18 @@ +using System.Windows.Controls; +using Flow.Launcher.Plugin.ProcessKiller.ViewModels; + +namespace Flow.Launcher.Plugin.ProcessKiller.Views; + +public partial class SettingsControl : UserControl +{ + private readonly SettingsViewModel _viewModel; + + public SettingsControl(SettingsViewModel viewModel) + { + InitializeComponent(); + + _viewModel = viewModel; + + DataContext = viewModel; + } +} From f2e5006fe54fa0839f7665a7c1f1640ed821b6ec Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 26 Mar 2025 15:44:59 +0800 Subject: [PATCH 10/10] Code quality & Fix build issue --- .../Views/PluginsManagerSettings.xaml.cs | 9 ++------- .../Flow.Launcher.Plugin.ProcessKiller.csproj | 1 + .../Views/SettingsControl.xaml.cs | 7 +++---- 3 files changed, 6 insertions(+), 11 deletions(-) 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 c4864dc5732..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 diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs index ad7229417bd..a066ab6a912 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Views/SettingsControl.xaml.cs @@ -5,14 +5,13 @@ namespace Flow.Launcher.Plugin.ProcessKiller.Views; public partial class SettingsControl : UserControl { - private readonly SettingsViewModel _viewModel; - + /// + /// Interaction logic for SettingsControl.xaml + /// public SettingsControl(SettingsViewModel viewModel) { InitializeComponent(); - _viewModel = viewModel; - DataContext = viewModel; } }