mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-21 18:54:01 -04:00
674 lines
23 KiB
QML
674 lines
23 KiB
QML
pragma Singleton
|
||
pragma ComponentBehavior: Bound
|
||
|
||
import QtQuick
|
||
import Quickshell
|
||
import Quickshell.Io
|
||
import qs.Common
|
||
import qs.Services
|
||
import qs.Modules.DDash
|
||
|
||
Singleton {
|
||
id: root
|
||
readonly property var log: Log.scoped("SettingsSearchService")
|
||
|
||
property string query: ""
|
||
property var results: []
|
||
property string targetSection: ""
|
||
property string highlightSection: ""
|
||
property var registeredCards: ({})
|
||
property var settingsIndex: []
|
||
property bool indexLoaded: false
|
||
property var _translatedCache: []
|
||
property bool _cacheDirty: false
|
||
property int _scrollPass: 0
|
||
property int _stablePasses: 0
|
||
property real _appliedY: 0
|
||
property string _lastGeometry: ""
|
||
readonly property int settleInterval: 250
|
||
readonly property int stablePassesNeeded: 4
|
||
readonly property int maxScrollPasses: 12
|
||
|
||
Connections {
|
||
target: I18n
|
||
|
||
function onTranslationsChanged() {
|
||
root._refreshTranslatedCache();
|
||
}
|
||
|
||
function onTranslationsLoadedChanged() {
|
||
root._refreshTranslatedCache();
|
||
}
|
||
}
|
||
|
||
Connections {
|
||
target: PluginService
|
||
|
||
function onPluginListUpdated() {
|
||
root._refreshTranslatedCache();
|
||
}
|
||
}
|
||
|
||
Connections {
|
||
target: SettingsTabs
|
||
|
||
function onPageMapChanged() {
|
||
root._refreshTranslatedCache();
|
||
}
|
||
}
|
||
|
||
readonly property var conditionMap: ({
|
||
"isNiri": () => CompositorService.isNiri,
|
||
"pointerCapable": () => CompositorService.supportsPointerConfig,
|
||
"inputCapable": () => CompositorService.supportsInputConfig,
|
||
"isHyprland": () => CompositorService.isHyprland,
|
||
"isMango": () => CompositorService.isMango,
|
||
"isAqueous": () => CompositorService.isAqueous,
|
||
"nativeOverviewCapable": () => CompositorService.supportsNativeOverview,
|
||
"smartDockCapable": () => CompositorService.supportsSmartDock,
|
||
"workspaceFollowFocusCapable": () => CompositorService.supportsWorkspaceFollowFocus,
|
||
"isHyprlandOrNiri": () => CompositorService.isHyprland || CompositorService.isNiri,
|
||
"isNiriOrMango": () => CompositorService.isNiri || CompositorService.isMango,
|
||
"windowRulesCapable": () => CompositorService.supportsWindowRules,
|
||
"layoutCapable": () => CompositorService.supportsLayoutConfig,
|
||
"keybindsAvailable": () => KeybindsService.available,
|
||
"soundsAvailable": () => !MultimediaService.unavailable,
|
||
"cupsAvailable": () => CupsService.cupsAvailable,
|
||
"networkAvailable": () => NetworkService.networkAvailable,
|
||
"dmsConnected": () => DMSService.isConnected && DMSService.apiVersion >= 23,
|
||
"matugenAvailable": () => Theme.matugenAvailable,
|
||
"greeterAvailable": () => GreeterService.available,
|
||
"frameEnabled": () => SettingsData.frameEnabled,
|
||
"islandEnabled": () => root.islandBarFor("islandEnabled") !== null,
|
||
"islandFree": () => root.islandBarFor("islandFree") !== null,
|
||
"islandDocked": () => root.islandBarFor("islandDocked") !== null,
|
||
"dotEnabled": () => SettingsData.dotBarConfig?.enabled ?? false,
|
||
"cellularAvailable": () => NetworkService.cellularAvailable,
|
||
"dockEnabled": () => (SettingsData.dockConfigs ?? []).some(dock => dock.enabled)
|
||
})
|
||
|
||
readonly property var islandBarFilters: ({
|
||
"islandEnabled": cfg => true,
|
||
"islandFree": cfg => SettingsData.islandFreePlacement(cfg),
|
||
"islandDocked": cfg => !SettingsData.islandFreePlacement(cfg)
|
||
})
|
||
|
||
// The bar pages only show island rows for the selected bar, so a result has to pick one that fits its condition.
|
||
function islandBarFor(conditionKey, preferredId) {
|
||
const filter = islandBarFilters[conditionKey];
|
||
if (!filter)
|
||
return null;
|
||
const bars = SettingsData.enabledIslandBarConfigs.filter(filter);
|
||
return bars.find(cfg => cfg.id === preferredId) ?? bars[0] ?? null;
|
||
}
|
||
|
||
property var pluginSettingLabels: ({})
|
||
|
||
Component.onCompleted: indexFile.reload()
|
||
|
||
Instantiator {
|
||
model: (PluginService.availablePluginsList || []).filter(plugin => !!plugin.settingsPath)
|
||
|
||
FileView {
|
||
required property var modelData
|
||
|
||
path: "file://" + modelData.settingsPath
|
||
printErrors: false
|
||
onLoaded: root._indexPluginSettings(modelData.id, text())
|
||
}
|
||
}
|
||
|
||
property var barWidgetLabels: ({})
|
||
readonly property var presentBarWidgets: {
|
||
const present = {};
|
||
for (const config of SettingsData.barConfigs) {
|
||
for (const sectionId of ["left", "center", "right"]) {
|
||
for (const entry of config[sectionId + "Widgets"] ?? [])
|
||
present[typeof entry === "string" ? entry : entry.id] = true;
|
||
}
|
||
}
|
||
return present;
|
||
}
|
||
|
||
onPresentBarWidgetsChanged: _refreshTranslatedCache()
|
||
|
||
function _barWidgetSearchEntries() {
|
||
const entries = [];
|
||
for (const widget of BarWidgetCatalog.widgets) {
|
||
const present = presentBarWidgets[widget.id] === true;
|
||
entries.push({
|
||
section: "",
|
||
label: widget.text,
|
||
tabIndex: 22,
|
||
category: present ? "Bar widgets" : "Add widget",
|
||
keywords: [widget.id, "widget", "bar"],
|
||
icon: widget.icon,
|
||
description: widget.description,
|
||
conditionKey: "",
|
||
runtimeType: present ? "barWidget" : "barWidgetAdd",
|
||
runtimeId: widget.id
|
||
});
|
||
if (!present)
|
||
continue;
|
||
const labels = barWidgetLabels[BarWidgetCatalog.optionsFile(widget.id)] || [];
|
||
for (const label of labels) {
|
||
entries.push({
|
||
section: "",
|
||
label: label,
|
||
tabIndex: 22,
|
||
category: widget.text,
|
||
keywords: [widget.id, "widget", "bar"],
|
||
icon: widget.icon,
|
||
description: "",
|
||
conditionKey: "",
|
||
runtimeType: "barWidget",
|
||
runtimeId: widget.id
|
||
});
|
||
}
|
||
}
|
||
return entries;
|
||
}
|
||
|
||
function _indexPluginSettings(pluginId, source) {
|
||
const labels = [];
|
||
const pattern = /\blabel:\s*(?:I18n\.tr\()?"((?:[^"\\]|\\.)*)"/g;
|
||
let match;
|
||
while ((match = pattern.exec(source)) !== null) {
|
||
if (match[1] && labels.indexOf(match[1]) === -1)
|
||
labels.push(match[1]);
|
||
}
|
||
if (JSON.stringify(pluginSettingLabels[pluginId] ?? []) === JSON.stringify(labels))
|
||
return;
|
||
const next = Object.assign({}, pluginSettingLabels);
|
||
next[pluginId] = labels;
|
||
pluginSettingLabels = next;
|
||
_refreshTranslatedCache();
|
||
}
|
||
|
||
FileView {
|
||
id: indexFile
|
||
path: Qt.resolvedUrl("../translations/settings_search_index.json")
|
||
onLoaded: {
|
||
try {
|
||
const labels = {};
|
||
root.settingsIndex = JSON.parse(text()).filter(entry => {
|
||
if (entry.runtimeType !== "barWidgetOption")
|
||
return true;
|
||
labels[entry.optionFile] = (labels[entry.optionFile] ?? []).concat(entry.label);
|
||
return false;
|
||
});
|
||
root.barWidgetLabels = labels;
|
||
root.indexLoaded = true;
|
||
root._rebuildTranslationCache();
|
||
} catch (e) {
|
||
log.warn("Failed to parse index:", e);
|
||
root.settingsIndex = [];
|
||
root._translatedCache = [];
|
||
}
|
||
}
|
||
onLoadFailed: error => log.warn("Failed to load index:", error)
|
||
}
|
||
|
||
function registerCard(settingKey, item, flickable, collapsible) {
|
||
if (!settingKey)
|
||
return;
|
||
var cards = Object.assign({}, registeredCards);
|
||
cards[settingKey] = {
|
||
item: item,
|
||
flickable: flickable,
|
||
collapsible: collapsible ?? null
|
||
};
|
||
registeredCards = cards;
|
||
if (targetSection !== settingKey)
|
||
return;
|
||
_expandFor(cards[settingKey]);
|
||
scrollTimer.restart();
|
||
}
|
||
|
||
function unregisterCard(settingKey, item) {
|
||
if (!settingKey)
|
||
return;
|
||
if (item && registeredCards[settingKey] && registeredCards[settingKey].item !== item)
|
||
return;
|
||
var cards = Object.assign({}, registeredCards);
|
||
delete cards[settingKey];
|
||
registeredCards = cards;
|
||
}
|
||
|
||
function navigateToSection(section) {
|
||
targetSection = section;
|
||
_scrollPass = 0;
|
||
_stablePasses = 0;
|
||
_lastGeometry = "";
|
||
scrollTimer.interval = 50;
|
||
const entry = registeredCards[section];
|
||
if (!entry)
|
||
return;
|
||
_expandFor(entry);
|
||
scrollTimer.restart();
|
||
}
|
||
|
||
function _expandFor(entry) {
|
||
if (!entry)
|
||
return;
|
||
_expand(entry.collapsible);
|
||
_expand(entry.item);
|
||
}
|
||
|
||
function _expand(card) {
|
||
if (!card || card.collapsible !== true || card.expanded)
|
||
return;
|
||
card.expanded = true;
|
||
}
|
||
|
||
function scrollToTarget() {
|
||
if (!targetSection)
|
||
return;
|
||
const entry = registeredCards[targetSection];
|
||
if (!entry || !entry.item || !entry.flickable)
|
||
return;
|
||
const flickable = entry.flickable;
|
||
const item = entry.item;
|
||
const contentItem = flickable.contentItem;
|
||
|
||
if (!contentItem)
|
||
return;
|
||
const userScrolled = _scrollPass > 0 && Math.abs(flickable.contentY - _appliedY) > 1;
|
||
if (userScrolled) {
|
||
_finishScroll();
|
||
return;
|
||
}
|
||
const mapped = item.mapToItem(contentItem, 0, 0);
|
||
const maxY = Math.max(0, flickable.contentHeight - flickable.height);
|
||
const targetY = Math.min(maxY, Math.max(0, mapped.y - 16));
|
||
flickable.contentY = targetY;
|
||
_appliedY = flickable.contentY;
|
||
|
||
highlightSection = targetSection;
|
||
highlightTimer.restart();
|
||
const geometry = mapped.y + ":" + flickable.contentHeight;
|
||
_stablePasses = geometry === _lastGeometry ? _stablePasses + 1 : 0;
|
||
_lastGeometry = geometry;
|
||
_scrollPass++;
|
||
if (_stablePasses < stablePassesNeeded && _scrollPass < maxScrollPasses) {
|
||
scrollTimer.interval = settleInterval;
|
||
scrollTimer.restart();
|
||
return;
|
||
}
|
||
_finishScroll();
|
||
}
|
||
|
||
function _finishScroll() {
|
||
targetSection = "";
|
||
scrollTimer.interval = 50;
|
||
}
|
||
|
||
function clearHighlight() {
|
||
highlightSection = "";
|
||
}
|
||
|
||
Timer {
|
||
id: scrollTimer
|
||
interval: 50
|
||
onTriggered: root.scrollToTarget()
|
||
}
|
||
|
||
Timer {
|
||
id: highlightTimer
|
||
interval: 2500
|
||
onTriggered: root.highlightSection = ""
|
||
}
|
||
|
||
function checkCondition(item) {
|
||
if (!item.conditionKey)
|
||
return true;
|
||
const condFn = conditionMap[item.conditionKey];
|
||
if (!condFn)
|
||
return true;
|
||
return condFn();
|
||
}
|
||
|
||
function _itemLabel(item) {
|
||
switch (item.runtimeType) {
|
||
case "plugin":
|
||
return item.label;
|
||
case "pageLabel":
|
||
return SettingsTabs.page(SettingsTabs.pageForTabIndex(item.tabIndex))?.text ?? "";
|
||
default:
|
||
return I18n.tr(item.label);
|
||
}
|
||
}
|
||
|
||
function translateItem(item) {
|
||
const isRuntimePlugin = item.runtimeType === "plugin";
|
||
const label = _itemLabel(item);
|
||
const description = isRuntimePlugin ? (item.description || "") : I18n.tr(item.description || "");
|
||
const category = I18n.tr(item.category);
|
||
return {
|
||
section: item.section,
|
||
label: label,
|
||
tabIndex: item.tabIndex,
|
||
category: item.parentLabel ? I18n.tr(item.parentLabel) + " › " + category : category,
|
||
keywords: item.keywords || [],
|
||
icon: item.icon || "settings",
|
||
description: description,
|
||
conditionKey: item.conditionKey,
|
||
runtimeType: item.runtimeType || "",
|
||
runtimeId: item.runtimeId || "",
|
||
page: item.page || (isRuntimePlugin ? SettingsTabs.pluginPrefix + item.runtimeId : "")
|
||
};
|
||
}
|
||
|
||
function _rebuildTranslationCache() {
|
||
_cacheDirty = false;
|
||
var cache = [];
|
||
var items = settingsIndex.concat(_runtimeSearchEntries());
|
||
for (var i = 0; i < items.length; i++) {
|
||
var item = items[i];
|
||
var t = translateItem(item);
|
||
var sourceDescription = item.description || "";
|
||
var labelLower = _lowerVariants([item.label, t.label]);
|
||
var categoryLower = _lowerVariants([item.category, item.parentLabel, t.category]);
|
||
var descriptionLower = item.runtimeType === "plugin" ? [] : _lowerVariants([sourceDescription, t.description]);
|
||
cache.push({
|
||
section: t.section,
|
||
label: t.label,
|
||
tabIndex: t.tabIndex,
|
||
category: t.category,
|
||
keywords: t.keywords,
|
||
icon: t.icon,
|
||
description: t.description,
|
||
conditionKey: t.conditionKey,
|
||
runtimeType: t.runtimeType,
|
||
runtimeId: t.runtimeId,
|
||
page: t.page,
|
||
level: _entryLevel(t.section),
|
||
order: i,
|
||
labelSearch: labelLower,
|
||
categorySearch: categoryLower,
|
||
descriptionSearch: descriptionLower,
|
||
labelSquash: _squashVariants(labelLower),
|
||
categorySquash: _squashVariants(categoryLower),
|
||
descriptionSquash: _squashVariants(descriptionLower),
|
||
keywordsSquash: _squashVariants(t.keywords)
|
||
});
|
||
}
|
||
_translatedCache = cache;
|
||
}
|
||
|
||
function _runtimeSearchEntries() {
|
||
var entries = _barWidgetSearchEntries();
|
||
for (const entry of DashRegistry.entries) {
|
||
for (const spec of entry.options ?? []) {
|
||
entries.push({
|
||
section: "dashOptions:" + entry.id + ":" + spec.key,
|
||
label: spec.text,
|
||
tabIndex: 43,
|
||
category: entry.text,
|
||
parentLabel: "Dashboard",
|
||
keywords: [entry.id, entry.text, "dash", "options"],
|
||
icon: entry.icon,
|
||
description: spec.description ?? ""
|
||
});
|
||
}
|
||
}
|
||
var plugins = PluginService.availablePluginsList || [];
|
||
for (var i = 0; i < plugins.length; i++) {
|
||
var plugin = plugins[i];
|
||
entries.push({
|
||
section: "",
|
||
label: plugin.name || plugin.id,
|
||
tabIndex: 12,
|
||
category: "Plugins",
|
||
keywords: [plugin.id || ""],
|
||
icon: plugin.icon || "extension",
|
||
description: plugin.description || "",
|
||
conditionKey: "",
|
||
runtimeType: "plugin",
|
||
runtimeId: plugin.id || ""
|
||
});
|
||
var labels = pluginSettingLabels[plugin.id] || [];
|
||
for (var j = 0; j < labels.length; j++) {
|
||
entries.push({
|
||
section: "",
|
||
label: labels[j],
|
||
tabIndex: 12,
|
||
category: plugin.name || plugin.id,
|
||
keywords: [plugin.id || "", plugin.name || ""],
|
||
icon: plugin.icon || "extension",
|
||
description: "",
|
||
conditionKey: "",
|
||
runtimeType: "plugin",
|
||
runtimeId: plugin.id || ""
|
||
});
|
||
}
|
||
}
|
||
return entries;
|
||
}
|
||
|
||
function _lowerVariants(values) {
|
||
var out = [];
|
||
for (var i = 0; i < values.length; i++) {
|
||
var value = values[i];
|
||
if (!value)
|
||
continue;
|
||
var lower = String(value).toLowerCase();
|
||
if (out.indexOf(lower) === -1)
|
||
out.push(lower);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function _squash(value) {
|
||
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "");
|
||
}
|
||
|
||
function _squashVariants(values) {
|
||
var out = [];
|
||
for (var i = 0; i < values.length; i++) {
|
||
if (!values[i])
|
||
continue;
|
||
var squashed = _squash(values[i]);
|
||
if (squashed && out.indexOf(squashed) === -1)
|
||
out.push(squashed);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
readonly property var labelScores: ({
|
||
"exact": 10000,
|
||
"plural": 9500,
|
||
"prefix": 5000,
|
||
"wordStart": 3000,
|
||
"includes": 1000
|
||
})
|
||
readonly property var squashScores: ({
|
||
"exact": 9000,
|
||
"plural": 8500,
|
||
"prefix": 4500,
|
||
"wordStart": 900,
|
||
"includes": 900
|
||
})
|
||
// V4 regex has no \p{} classes, so word breaks are whitespace and punctuation ranges.
|
||
readonly property var wordBreak: /[\s!-\/:-@\[-`{-~\u2000-\u206f\u3000-\u303f]/
|
||
|
||
function _entryLevel(section) {
|
||
const id = String(section);
|
||
if (id.startsWith("_hub_"))
|
||
return 2;
|
||
if (id.startsWith("_tab_"))
|
||
return 1;
|
||
return 0;
|
||
}
|
||
|
||
function _singular(value) {
|
||
if (value.length <= 3 || !value.endsWith("s") || value.endsWith("ss"))
|
||
return value;
|
||
return value.slice(0, -1);
|
||
}
|
||
|
||
function _startsWord(field, query) {
|
||
for (var at = field.indexOf(query, 1); at > 0; at = field.indexOf(query, at + 1)) {
|
||
if (wordBreak.test(field[at - 1]))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function _labelScore(labels, query, scores) {
|
||
var score = 0;
|
||
for (var i = 0; i < labels.length; i++) {
|
||
var label = labels[i];
|
||
if (label === query)
|
||
return scores.exact;
|
||
if (_singular(label) === _singular(query))
|
||
score = Math.max(score, scores.plural);
|
||
else if (label.startsWith(query))
|
||
score = Math.max(score, scores.prefix);
|
||
else if (_startsWord(label, query))
|
||
score = Math.max(score, scores.wordStart);
|
||
else if (label.includes(query))
|
||
score = Math.max(score, scores.includes);
|
||
}
|
||
return score;
|
||
}
|
||
|
||
function _keywordScore(entry, query, querySquash) {
|
||
var score = 0;
|
||
for (const keyword of entry.keywords) {
|
||
if (keyword === query)
|
||
return 900;
|
||
if (keyword.startsWith(query))
|
||
score = Math.max(score, 800);
|
||
else if (keyword.includes(query))
|
||
score = Math.max(score, 400);
|
||
}
|
||
if (!querySquash)
|
||
return score;
|
||
for (const keyword of entry.keywordsSquash) {
|
||
if (keyword === querySquash)
|
||
return Math.max(score, 850);
|
||
if (keyword.startsWith(querySquash))
|
||
score = Math.max(score, 750);
|
||
}
|
||
return score;
|
||
}
|
||
|
||
function _fieldsContainWord(fields, word) {
|
||
for (var i = 0; i < fields.length; i++) {
|
||
if (fields[i].includes(word))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function _refreshTranslatedCache() {
|
||
if (!indexLoaded)
|
||
return;
|
||
_cacheDirty = true;
|
||
if (query)
|
||
results = _searchEntries(query, 15);
|
||
}
|
||
|
||
function _searchEntries(text, maxResults) {
|
||
var queryLower = (text || "").toLowerCase().trim();
|
||
if (!queryLower)
|
||
return [];
|
||
|
||
if (_cacheDirty)
|
||
_rebuildTranslationCache();
|
||
var querySquash = _squash(queryLower);
|
||
var queryWords = queryLower.split(/\s+/).filter(w => w.length > 0);
|
||
var scored = [];
|
||
var cache = _translatedCache;
|
||
var limit = maxResults > 0 ? maxResults : 15;
|
||
|
||
for (var i = 0; i < cache.length; i++) {
|
||
var entry = cache[i];
|
||
if (!checkCondition(entry))
|
||
continue;
|
||
|
||
var labelScore = _labelScore(entry.labelSearch, queryLower, labelScores);
|
||
if (querySquash)
|
||
labelScore = Math.max(labelScore, _labelScore(entry.labelSquash, querySquash, squashScores));
|
||
|
||
var score = Math.max(labelScore, _keywordScore(entry, queryLower, querySquash));
|
||
if (_fieldsContainWord(entry.categorySearch, queryLower) || (querySquash && _fieldsContainWord(entry.categorySquash, querySquash)))
|
||
score = Math.max(score, 500);
|
||
if (_fieldsContainWord(entry.descriptionSearch, queryLower) || (querySquash && _fieldsContainWord(entry.descriptionSquash, querySquash)))
|
||
score = Math.max(score, 250);
|
||
|
||
if (score === 0 && queryWords.length > 1) {
|
||
var allMatch = true;
|
||
for (var w = 0; w < queryWords.length; w++) {
|
||
var word = queryWords[w];
|
||
if (_fieldsContainWord(entry.labelSearch, word))
|
||
continue;
|
||
if (_fieldsContainWord(entry.descriptionSearch, word))
|
||
continue;
|
||
if (_fieldsContainWord(entry.categorySearch, word))
|
||
continue;
|
||
var inKeywords = false;
|
||
for (var k = 0; k < entry.keywords.length; k++) {
|
||
if (entry.keywords[k].includes(word)) {
|
||
inKeywords = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!inKeywords) {
|
||
allMatch = false;
|
||
break;
|
||
}
|
||
}
|
||
if (allMatch)
|
||
score = 300;
|
||
}
|
||
|
||
if (score > 0) {
|
||
scored.push({
|
||
item: entry,
|
||
score: score,
|
||
labelTier: labelScore >= labelScores.wordStart ? 2 : labelScore > 0 ? 1 : 0
|
||
});
|
||
}
|
||
}
|
||
|
||
scored.sort((a, b) => {
|
||
if (b.labelTier !== a.labelTier)
|
||
return b.labelTier - a.labelTier;
|
||
if (a.labelTier > 0 && b.item.level !== a.item.level)
|
||
return b.item.level - a.item.level;
|
||
if (b.score !== a.score)
|
||
return b.score - a.score;
|
||
if (b.item.level !== a.item.level)
|
||
return b.item.level - a.item.level;
|
||
const aRuntime = !!a.item.runtimeType;
|
||
const bRuntime = !!b.item.runtimeType;
|
||
if (aRuntime !== bRuntime)
|
||
return aRuntime ? 1 : -1;
|
||
if (a.item.label.length !== b.item.label.length)
|
||
return a.item.label.length - b.item.label.length;
|
||
return a.item.order - b.item.order;
|
||
});
|
||
return scored.slice(0, limit).map(s => s.item);
|
||
}
|
||
|
||
function searchForLauncher(text) {
|
||
return _searchEntries(text, 15);
|
||
}
|
||
|
||
function search(text) {
|
||
query = text;
|
||
if (!text) {
|
||
results = [];
|
||
return;
|
||
}
|
||
results = _searchEntries(text, 15);
|
||
}
|
||
|
||
function clear() {
|
||
query = "";
|
||
results = [];
|
||
}
|
||
}
|