From a3ae95df0907dad53ae1bbd5e294f943d16a9275 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 5 Feb 2026 13:22:49 -0500 Subject: [PATCH] launcher v2: general performance improvements --- .../Modals/DankLauncherV2/Controller.qml | 195 +++++++++++++----- .../Modals/DankLauncherV2/ResultsList.qml | 56 ++--- quickshell/Modals/DankLauncherV2/Scorer.js | 35 ++-- 3 files changed, 182 insertions(+), 104 deletions(-) diff --git a/quickshell/Modals/DankLauncherV2/Controller.qml b/quickshell/Modals/DankLauncherV2/Controller.qml index 21776e60..d861b1e3 100644 --- a/quickshell/Modals/DankLauncherV2/Controller.qml +++ b/quickshell/Modals/DankLauncherV2/Controller.qml @@ -257,13 +257,22 @@ Item { } property int _searchVersion: 0 + property bool _pluginPhasePending: false + property bool _pluginPhaseForceFirst: false + property var _phase1Items: [] Timer { id: searchDebounce - interval: searchMode === "all" && searchQuery.length > 0 ? 90 : 60 + interval: 60 onTriggered: root.performSearch() } + Timer { + id: pluginPhaseTimer + interval: 1 + onTriggered: root._performPluginPhase() + } + Timer { id: fileSearchDebounce interval: 200 @@ -277,6 +286,9 @@ Item { function setSearchQuery(query) { _searchVersion++; _queryDrivenSearch = true; + _pluginPhasePending = false; + _phase1Items = []; + pluginPhaseTimer.stop(); searchQuery = query; searchDebounce.restart(); @@ -337,6 +349,10 @@ Item { collapsedSections = {}; _clearModeCache(); _queryDrivenSearch = false; + _pluginPhasePending = false; + _pluginPhaseForceFirst = false; + _phase1Items = []; + pluginPhaseTimer.stop(); } function loadPluginCategories(pluginId) { @@ -418,7 +434,7 @@ Item { sections = modeCache.sections; flatModel = modeCache.flatModel; } else { - sections = cachedSections.map(function (s) { + var newSections = cachedSections.map(function (s) { var copy = Object.assign({}, s, { items: s.items ? s.items.slice() : [] }); @@ -426,7 +442,8 @@ Item { copy.collapsed = collapsedSections[s.id]; return copy; }); - flatModel = Scorer.flattenSections(sections); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; _setCachedModeData("all", sections, flatModel); } selectedFlatIndex = restoreSelection(flatModel); @@ -449,7 +466,8 @@ Item { loadPluginCategories(triggerMatch.pluginId); var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query); - allItems = allItems.concat(pluginItems); + for (var k = 0; k < pluginItems.length; k++) + allItems.push(pluginItems[k]); if (triggerMatch.isBuiltIn) { var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query); @@ -461,17 +479,18 @@ Item { var dynamicDefs = buildDynamicSectionDefs(allItems); var scoredItems = Scorer.scoreItems(allItems, triggerMatch.query, getFrecencyForItem); var sortAlpha = !triggerMatch.query && SettingsData.sortAppsAlphabetically; - sections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500); + var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500); for (var sid in collapsedSections) { - for (var i = 0; i < sections.length; i++) { - if (sections[i].id === sid) { - sections[i].collapsed = collapsedSections[sid]; + for (var i = 0; i < newSections.length; i++) { + if (newSections[i].id === sid) { + newSections[i].collapsed = collapsedSections[sid]; } } } - flatModel = Scorer.flattenSections(sections); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; selectedFlatIndex = restoreSelection(flatModel); updateSelectedItem(); @@ -507,7 +526,7 @@ Item { flatModel = modeCache.flatModel; } else { var appSectionIds = ["favorites", "apps"]; - sections = cachedSections.filter(function (s) { + var newSections = cachedSections.filter(function (s) { return appSectionIds.indexOf(s.id) !== -1; }).map(function (s) { var copy = Object.assign({}, s, { @@ -517,7 +536,8 @@ Item { copy.collapsed = collapsedSections[s.id]; return copy; }); - flatModel = Scorer.flattenSections(sections); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; _setCachedModeData("apps", sections, flatModel); } selectedFlatIndex = restoreSelection(flatModel); @@ -534,17 +554,18 @@ Item { var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; - sections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500); + var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500); for (var sid in collapsedSections) { - for (var i = 0; i < sections.length; i++) { - if (sections[i].id === sid) { - sections[i].collapsed = collapsedSections[sid]; + for (var i = 0; i < newSections.length; i++) { + if (newSections[i].id === sid) { + newSections[i].collapsed = collapsedSections[sid]; } } } - flatModel = Scorer.flattenSections(sections); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; selectedFlatIndex = restoreSelection(flatModel); updateSelectedItem(); @@ -556,13 +577,15 @@ Item { if (searchMode === "plugins") { if (!searchQuery && !pluginFilter) { var browseItems = getPluginBrowseItems(); - allItems = allItems.concat(browseItems); + for (var k = 0; k < browseItems.length; k++) + allItems.push(browseItems[k]); } else if (pluginFilter) { var isBuiltInFilter = !!AppSearchService.builtInPlugins[pluginFilter]; applyActivePluginViewPreference(pluginFilter, isBuiltInFilter); var filterItems = getPluginItems(pluginFilter, searchQuery); - allItems = allItems.concat(filterItems); + for (var k = 0; k < filterItems.length; k++) + allItems.push(filterItems[k]); var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery); for (var j = 0; j < builtInItems.length; j++) { @@ -573,7 +596,8 @@ Item { for (var i = 0; i < emptyTriggerPlugins.length; i++) { var pluginId = emptyTriggerPlugins[i]; var pItems = getPluginItems(pluginId, searchQuery); - allItems = allItems.concat(pItems); + for (var k = 0; k < pItems.length; k++) + allItems.push(pItems[k]); } var builtInLauncherPlugins = getBuiltInEmptyTriggerLaunchers(); @@ -589,17 +613,18 @@ Item { var dynamicDefs = buildDynamicSectionDefs(allItems); var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically; - sections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500); + var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500); for (var sid in collapsedSections) { - for (var i = 0; i < sections.length; i++) { - if (sections[i].id === sid) { - sections[i].collapsed = collapsedSections[sid]; + for (var i = 0; i < newSections.length; i++) { + if (newSections[i].id === sid) { + newSections[i].collapsed = collapsedSections[sid]; } } } - flatModel = Scorer.flattenSections(sections); + flatModel = Scorer.flattenSections(newSections); + sections = newSections; selectedFlatIndex = restoreSelection(flatModel); updateSelectedItem(); @@ -610,31 +635,26 @@ Item { var calculatorResult = evaluateCalculator(searchQuery); if (calculatorResult) { + calculatorResult._preScored = 12000; allItems.push(calculatorResult); } var apps = searchApps(searchQuery); - allItems = allItems.concat(apps); + for (var i = 0; i < apps.length; i++) { + if (searchQuery) + apps[i]._preScored = 11000 - i; + allItems.push(apps[i]); + } if (searchMode === "all") { - var includePlugins = !searchQuery || searchQuery.length >= 2; - if (searchQuery && includePlugins) { - var allPluginsOrdered = getAllVisiblePluginsOrdered(); - var maxPerPlugin = 10; - for (var i = 0; i < allPluginsOrdered.length; i++) { - var plugin = allPluginsOrdered[i]; - if (plugin.isBuiltIn) { - var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); - var blLimit = Math.min(blItems.length, maxPerPlugin); - for (var j = 0; j < blLimit; j++) - allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id)); - } else { - var pItems = getPluginItems(plugin.id, searchQuery); - if (pItems.length > maxPerPlugin) - pItems = pItems.slice(0, maxPerPlugin); - allItems = allItems.concat(pItems); - } - } + if (searchQuery && searchQuery.length >= 2) { + _pluginPhasePending = true; + _pluginPhaseForceFirst = shouldResetSelection; + _phase1Items = allItems; + pluginPhaseTimer.restart(); + isSearching = true; + searchCompleted(); + return; } else if (!searchQuery) { var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered(); for (var i = 0; i < emptyTriggerOrdered.length; i++) { @@ -645,12 +665,14 @@ Item { allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id)); } else { var pItems = getPluginItems(plugin.id, searchQuery); - allItems = allItems.concat(pItems); + for (var j = 0; j < pItems.length; j++) + allItems.push(pItems[j]); } } var browseItems = getPluginBrowseItems(); - allItems = allItems.concat(browseItems); + for (var i = 0; i < browseItems.length; i++) + allItems.push(browseItems[i]); } } @@ -677,8 +699,8 @@ Item { } } + flatModel = Scorer.flattenSections(newSections); sections = newSections; - flatModel = Scorer.flattenSections(sections); if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) { AppSearchService.setCachedDefaultSections(sections, flatModel); @@ -687,6 +709,63 @@ Item { selectedFlatIndex = restoreSelection(flatModel); updateSelectedItem(); + isSearching = _pluginPhasePending; + searchCompleted(); + } + + function _performPluginPhase() { + _pluginPhasePending = false; + if (!searchQuery || searchQuery.length < 2 || searchMode !== "all") + return; + + var currentVersion = _searchVersion; + var restoreSelection = preserveSelectionAfterUpdate(_pluginPhaseForceFirst); + var allItems = _phase1Items; + _phase1Items = []; + + var allPluginsOrdered = getAllVisiblePluginsOrdered(); + var maxPerPlugin = 10; + for (var i = 0; i < allPluginsOrdered.length; i++) { + if (currentVersion !== _searchVersion) + return; + var plugin = allPluginsOrdered[i]; + if (plugin.isBuiltIn) { + var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery); + var blLimit = Math.min(blItems.length, maxPerPlugin); + for (var j = 0; j < blLimit; j++) { + var item = transformBuiltInLauncherItem(blItems[j], plugin.id); + item._preScored = 900 - j; + allItems.push(item); + } + } else { + var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin); + for (var j = 0; j < pItems.length; j++) { + pItems[j]._preScored = 900 - j; + allItems.push(pItems[j]); + } + } + } + + if (currentVersion !== _searchVersion) + return; + + var dynamicDefs = buildDynamicSectionDefs(allItems); + var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem); + var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, false, 50); + + if (currentVersion !== _searchVersion) + return; + + for (var i = 0; i < newSections.length; i++) { + var sid = newSections[i].id; + if (collapsedSections[sid] !== undefined) + newSections[i].collapsed = collapsedSections[sid]; + } + + flatModel = Scorer.flattenSections(newSections); + sections = newSections; + selectedFlatIndex = restoreSelection(flatModel); + updateSelectedItem(); isSearching = false; searchCompleted(); } @@ -756,9 +835,8 @@ Item { newSections.sort(function (a, b) { return a.priority - b.priority; }); + flatModel = Scorer.flattenSections(newSections); sections = newSections; - - flatModel = Scorer.flattenSections(sections); if (selectedFlatIndex >= flatModel.length) { selectedFlatIndex = getFirstItemIndex(); } @@ -954,11 +1032,12 @@ Item { return sortPluginIdsByOrder(visible); } - function getPluginItems(pluginId, query) { + function getPluginItems(pluginId, query, limit) { var items = AppSearchService.getPluginItemsForPlugin(pluginId, query); + var count = limit > 0 && limit < items.length ? limit : items.length; var transformed = []; - for (var i = 0; i < items.length; i++) { + for (var i = 0; i < count; i++) { transformed.push(transformPluginItem(items[i], pluginId)); } @@ -1127,8 +1206,14 @@ Item { return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns); } + function _cancelPendingSelectionReset() { + _queryDrivenSearch = false; + _pluginPhaseForceFirst = false; + } + function selectNext() { keyboardNavigationActive = true; + _cancelPendingSelectionReset(); var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; @@ -1138,6 +1223,7 @@ Item { function selectPrevious() { keyboardNavigationActive = true; + _cancelPendingSelectionReset(); var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; @@ -1147,6 +1233,7 @@ Item { function selectRight() { keyboardNavigationActive = true; + _cancelPendingSelectionReset(); var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; @@ -1156,6 +1243,7 @@ Item { function selectLeft() { keyboardNavigationActive = true; + _cancelPendingSelectionReset(); var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; @@ -1165,6 +1253,7 @@ Item { function selectNextSection() { keyboardNavigationActive = true; + _cancelPendingSelectionReset(); var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; @@ -1174,6 +1263,7 @@ Item { function selectPreviousSection() { keyboardNavigationActive = true; + _cancelPendingSelectionReset(); var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; @@ -1183,6 +1273,7 @@ Item { function selectPageDown(visibleItems) { keyboardNavigationActive = true; + _cancelPendingSelectionReset(); var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; @@ -1192,6 +1283,7 @@ Item { function selectPageUp(visibleItems) { keyboardNavigationActive = true; + _cancelPendingSelectionReset(); var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems); if (newIndex !== selectedFlatIndex) { selectedFlatIndex = newIndex; @@ -1232,10 +1324,9 @@ Item { }); } } + flatModel = Scorer.flattenSections(newSections); sections = newSections; - flatModel = Scorer.flattenSections(sections); - if (selectedFlatIndex >= flatModel.length) { selectedFlatIndex = getFirstItemIndex(); } diff --git a/quickshell/Modals/DankLauncherV2/ResultsList.qml b/quickshell/Modals/DankLauncherV2/ResultsList.qml index 9b1777fb..afd4c51b 100644 --- a/quickshell/Modals/DankLauncherV2/ResultsList.qml +++ b/quickshell/Modals/DankLauncherV2/ResultsList.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import Quickshell import qs.Common import qs.Services import qs.Widgets @@ -173,7 +174,10 @@ Item { width: parent.width Repeater { - model: root.controller?.sections ?? [] + model: ScriptModel { + values: root.controller?.sections ?? [] + objectProp: "id" + } Column { id: sectionDelegate @@ -207,33 +211,23 @@ Item { visible: !sectionDelegate.isGridMode && !sectionDelegate.isCollapsed Repeater { - model: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? []) + model: ScriptModel { + values: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? []) + objectProp: "id" + } ResultItem { required property var modelData required property int index + readonly property int computedFlatIndex: (sectionDelegate.modelData?.flatStartIndex ?? 0) + index + width: listContent.width height: 52 item: modelData - isSelected: getFlatIndex() === root.controller?.selectedFlatIndex + isSelected: computedFlatIndex === root.controller?.selectedFlatIndex controller: root.controller - flatIndex: getFlatIndex() - - function getFlatIndex() { - if (!sectionDelegate?.sectionId) - return -1; - var flatIdx = 0; - var sections = root.controller?.sections ?? []; - for (var i = 0; i < sections.length; i++) { - flatIdx++; - if (sections[i].id === sectionDelegate.sectionId) - return flatIdx + index; - if (!sections[i].collapsed) - flatIdx += sections[i].items?.length ?? 0; - } - return -1; - } + flatIndex: computedFlatIndex onClicked: { if (root.controller) { @@ -242,7 +236,7 @@ Item { } onRightClicked: (mouseX, mouseY) => { - root.itemRightClicked(getFlatIndex(), modelData, mouseX, mouseY); + root.itemRightClicked(computedFlatIndex, modelData, mouseX, mouseY); } } } @@ -258,7 +252,10 @@ Item { readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24 Repeater { - model: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : [] + model: ScriptModel { + values: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : [] + objectProp: "id" + } Item { id: gridDelegateItem @@ -268,22 +265,7 @@ Item { width: gridContent.cellWidth height: gridContent.cellHeight - function getFlatIndex() { - if (!sectionDelegate?.sectionId) - return -1; - var flatIdx = 0; - var sections = root.controller?.sections ?? []; - for (var i = 0; i < sections.length; i++) { - flatIdx++; - if (sections[i].id === sectionDelegate.sectionId) - return flatIdx + index; - if (!sections[i].collapsed) - flatIdx += sections[i].items?.length ?? 0; - } - return -1; - } - - readonly property int cachedFlatIndex: getFlatIndex() + readonly property int cachedFlatIndex: (sectionDelegate.modelData?.flatStartIndex ?? 0) + index GridItem { width: parent.width - 4 diff --git a/quickshell/Modals/DankLauncherV2/Scorer.js b/quickshell/Modals/DankLauncherV2/Scorer.js index 6e2cfb23..bde47736 100644 --- a/quickshell/Modals/DankLauncherV2/Scorer.js +++ b/quickshell/Modals/DankLauncherV2/Scorer.js @@ -42,26 +42,23 @@ function hasWordBoundaryMatch(text, query) { function levenshteinDistance(s1, s2) { var len1 = s1.length var len2 = s2.length - var matrix = [] + var prev = new Array(len2 + 1) + var curr = new Array(len2 + 1) - for (var i = 0; i <= len1; i++) { - matrix[i] = [i] - } - for (var j = 0; j <= len2; j++) { - matrix[0][j] = j - } + for (var j = 0; j <= len2; j++) + prev[j] = j for (var i = 1; i <= len1; i++) { + curr[0] = i for (var j = 1; j <= len2; j++) { var cost = s1[i - 1] === s2[j - 1] ? 0 : 1 - matrix[i][j] = Math.min( - matrix[i - 1][j] + 1, - matrix[i][j - 1] + 1, - matrix[i - 1][j - 1] + cost - ) + curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost) } + var tmp = prev + prev = curr + curr = tmp } - return matrix[len1][len2] + return prev[len2] } function fuzzyScore(text, query) { @@ -153,8 +150,14 @@ function scoreItems(items, query, getFrecencyFn) { for (var i = 0; i < items.length; i++) { var item = items[i] - var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null - var itemScore = score(item, query, frecencyData) + var itemScore + + if (item._preScored !== undefined) { + itemScore = item._preScored + } else { + var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null + itemScore = score(item, query, frecencyData) + } if (itemScore > 0 || !query || query.length === 0) { scored.push({ @@ -228,6 +231,8 @@ function flattenSections(sections) { sectionIndex: i }) + section.flatStartIndex = flat.length + if (!section.collapsed) { for (var j = 0; j < section.items.length; j++) { flat.push({