1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00
Files
DankMaterialShell/quickshell/Services/AppSearchService.qml
2025-11-17 20:53:55 -05:00

479 lines
15 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
Singleton {
id: root
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
readonly property int maxResults: 10
readonly property int frecencySampleSize: 10
readonly property var timeBuckets: [{
"maxDays": 4,
"weight": 100
}, {
"maxDays": 14,
"weight": 70
}, {
"maxDays": 31,
"weight": 50
}, {
"maxDays": 90,
"weight": 30
}, {
"maxDays": 99999,
"weight": 10
}]
function tokenize(text) {
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(w => w.length > 0)
}
function wordBoundaryMatch(text, query) {
const textWords = tokenize(text)
const queryWords = tokenize(query)
if (queryWords.length === 0)
return false
if (queryWords.length > textWords.length)
return false
for (var i = 0; i <= textWords.length - queryWords.length; i++) {
let allMatch = true
for (var j = 0; j < queryWords.length; j++) {
if (!textWords[i + j].startsWith(queryWords[j])) {
allMatch = false
break
}
}
if (allMatch)
return true
}
return false
}
function levenshteinDistance(s1, s2) {
const len1 = s1.length
const len2 = s2.length
const matrix = []
for (var i = 0; i <= len1; i++) {
matrix[i] = [i]
}
for (var j = 0; j <= len2; j++) {
matrix[0][j] = j
}
for (var i = 1; i <= len1; i++) {
for (var j = 1; j <= len2; j++) {
const 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)
}
}
return matrix[len1][len2]
}
function fuzzyMatchScore(text, query) {
const queryLower = query.toLowerCase()
const maxDistance = query.length <= 2 ? 0 : query.length === 3 ? 1 : query.length <= 6 ? 2 : 3
let bestScore = 0
const distance = levenshteinDistance(text.toLowerCase(), queryLower)
if (distance <= maxDistance) {
const maxLen = Math.max(text.length, query.length)
bestScore = 1 - (distance / maxLen)
}
const words = tokenize(text)
for (const word of words) {
const wordDistance = levenshteinDistance(word, queryLower)
if (wordDistance <= maxDistance) {
const maxLen = Math.max(word.length, query.length)
const score = 1 - (wordDistance / maxLen)
bestScore = Math.max(bestScore, score)
}
}
return bestScore
}
function calculateFrecency(app) {
const usageRanking = AppUsageHistoryData.appUsageRanking || {}
const appId = app.id || (app.execString || app.exec || "")
const idVariants = [appId, appId.replace(".desktop", ""), app.id, app.id ? app.id.replace(".desktop", "") : null].filter(id => id)
let usageData = null
for (const variant of idVariants) {
if (usageRanking[variant]) {
usageData = usageRanking[variant]
break
}
}
if (!usageData || !usageData.usageCount) {
return {
"frecency": 0,
"daysSinceUsed": 999999
}
}
const usageCount = usageData.usageCount || 0
const lastUsed = usageData.lastUsed || 0
const now = Date.now()
const daysSinceUsed = (now - lastUsed) / (1000 * 60 * 60 * 24)
let timeBucketWeight = 10
for (const bucket of timeBuckets) {
if (daysSinceUsed <= bucket.maxDays) {
timeBucketWeight = bucket.weight
break
}
}
const contextBonus = 100
const sampleSize = Math.min(usageCount, frecencySampleSize)
const frecency = (timeBucketWeight * contextBonus * sampleSize) / 100
return {
"frecency": frecency,
"daysSinceUsed": daysSinceUsed
}
}
function searchApplications(query) {
if (!query || query.length === 0) {
return applications
}
if (applications.length === 0)
return []
const queryLower = query.toLowerCase().trim()
const scoredApps = []
const results = []
for (const app of applications) {
const name = (app.name || "").toLowerCase()
const genericName = (app.genericName || "").toLowerCase()
const comment = (app.comment || "").toLowerCase()
const keywords = app.keywords ? app.keywords.map(k => k.toLowerCase()) : []
let textScore = 0
let matchType = "none"
if (name === queryLower) {
textScore = 10000
matchType = "exact"
} else if (name.startsWith(queryLower)) {
textScore = 5000
matchType = "prefix"
} else if (wordBoundaryMatch(name, queryLower)) {
textScore = 1000
matchType = "word_boundary"
} else if (name.includes(queryLower)) {
textScore = 500
matchType = "substring"
} else if (genericName && genericName.startsWith(queryLower)) {
textScore = 800
matchType = "generic_prefix"
} else if (genericName && genericName.includes(queryLower)) {
textScore = 400
matchType = "generic"
}
if (matchType === "none" && keywords.length > 0) {
for (const keyword of keywords) {
if (keyword.startsWith(queryLower)) {
textScore = 300
matchType = "keyword_prefix"
break
} else if (keyword.includes(queryLower)) {
textScore = 150
matchType = "keyword"
break
}
}
}
if (matchType === "none" && comment && comment.includes(queryLower)) {
textScore = 50
matchType = "comment"
}
if (matchType === "none") {
const fuzzyScore = fuzzyMatchScore(name, queryLower)
if (fuzzyScore > 0) {
textScore = fuzzyScore * 100
matchType = "fuzzy"
}
}
if (matchType !== "none") {
const frecencyData = calculateFrecency(app)
results.push({
"app": app,
"textScore": textScore,
"frecency": frecencyData.frecency,
"daysSinceUsed": frecencyData.daysSinceUsed,
"matchType": matchType
})
}
}
for (const result of results) {
const frecencyBonus = result.frecency > 0 ? Math.min(result.frecency / 10, 2000) : 0
const recencyBonus = result.daysSinceUsed < 1 ? 1500 : result.daysSinceUsed < 7 ? 1000 : result.daysSinceUsed < 30 ? 500 : 0
const finalScore = result.textScore + frecencyBonus + recencyBonus
scoredApps.push({
"app": result.app,
"score": finalScore
})
}
scoredApps.sort((a, b) => b.score - a.score)
return scoredApps.slice(0, maxResults).map(item => item.app)
}
function getCategoriesForApp(app) {
if (!app?.categories)
return []
const categoryMap = {
"AudioVideo": I18n.tr("Media"),
"Audio": I18n.tr("Media"),
"Video": I18n.tr("Media"),
"Development": I18n.tr("Development"),
"TextEditor": I18n.tr("Development"),
"IDE": I18n.tr("Development"),
"Education": I18n.tr("Education"),
"Game": I18n.tr("Games"),
"Graphics": I18n.tr("Graphics"),
"Photography": I18n.tr("Graphics"),
"Network": I18n.tr("Internet"),
"WebBrowser": I18n.tr("Internet"),
"Email": I18n.tr("Internet"),
"Office": I18n.tr("Office"),
"WordProcessor": I18n.tr("Office"),
"Spreadsheet": I18n.tr("Office"),
"Presentation": I18n.tr("Office"),
"Science": I18n.tr("Science"),
"Settings": I18n.tr("Settings"),
"System": I18n.tr("System"),
"Utility": I18n.tr("Utilities"),
"Accessories": I18n.tr("Utilities"),
"FileManager": I18n.tr("Utilities"),
"TerminalEmulator": I18n.tr("Utilities")
}
const mappedCategories = new Set()
for (const cat of app.categories) {
if (categoryMap[cat])
mappedCategories.add(categoryMap[cat])
}
return Array.from(mappedCategories)
}
property var categoryIcons: ({
"All": "apps",
"Media": "music_video",
"Development": "code",
"Games": "sports_esports",
"Graphics": "photo_library",
"Internet": "web",
"Office": "content_paste",
"Settings": "settings",
"System": "host",
"Utilities": "build"
})
function getCategoryIcon(category) {
// Check if it's a plugin category
const pluginIcon = getPluginCategoryIcon(category)
if (pluginIcon) {
return pluginIcon
}
return categoryIcons[category] || "folder"
}
function getAllCategories() {
const categories = new Set([I18n.tr("All")])
for (const app of applications) {
const appCategories = getCategoriesForApp(app)
appCategories.forEach(cat => categories.add(cat))
}
// Add plugin categories
const pluginCategories = getPluginCategories()
pluginCategories.forEach(cat => categories.add(cat))
const result = Array.from(categories).sort()
return result
}
function getAppsInCategory(category) {
if (category === I18n.tr("All")) {
return applications
}
// Check if it's a plugin category
const pluginItems = getPluginItems(category, "")
if (pluginItems.length > 0) {
return pluginItems
}
return applications.filter(app => {
const appCategories = getCategoriesForApp(app)
return appCategories.includes(category)
})
}
// Plugin launcher support functions
function getPluginCategories() {
if (typeof PluginService === "undefined") {
return []
}
const categories = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
const categoryName = plugin.name || pluginId
categories.push(categoryName)
}
return categories
}
function getPluginCategoryIcon(category) {
if (typeof PluginService === "undefined")
return null
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
if ((plugin.name || pluginId) === category) {
return plugin.icon || "extension"
}
}
return null
}
function getAllPluginItems() {
if (typeof PluginService === "undefined") {
return []
}
let allItems = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const categoryName = launchers[pluginId].name || pluginId
const items = getPluginItems(categoryName, "")
allItems = allItems.concat(items)
}
return allItems
}
function getPluginItems(category, query) {
if (typeof PluginService === "undefined")
return []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
if ((plugin.name || pluginId) === category) {
return getPluginItemsForPlugin(pluginId, query)
}
}
return []
}
function getPluginItemsForPlugin(pluginId, query) {
if (typeof PluginService === "undefined") {
return []
}
const component = PluginService.pluginLauncherComponents[pluginId]
if (!component)
return []
try {
const instance = component.createObject(root, {
"pluginService": PluginService
})
if (instance && typeof instance.getItems === "function") {
const items = instance.getItems(query || "")
instance.destroy()
return items || []
}
if (instance) {
instance.destroy()
}
} catch (e) {
console.warn("AppSearchService: Error getting items from plugin", pluginId, ":", e)
}
return []
}
function executePluginItem(item, pluginId) {
if (typeof PluginService === "undefined")
return false
const component = PluginService.pluginLauncherComponents[pluginId]
if (!component)
return false
try {
const instance = component.createObject(root, {
"pluginService": PluginService
})
if (instance && typeof instance.executeItem === "function") {
instance.executeItem(item)
instance.destroy()
return true
}
if (instance) {
instance.destroy()
}
} catch (e) {
console.warn("AppSearchService: Error executing item from plugin", pluginId, ":", e)
}
return false
}
function searchPluginItems(query) {
if (typeof PluginService === "undefined")
return []
let allItems = []
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const items = getPluginItemsForPlugin(pluginId, query)
allItems = allItems.concat(items)
}
return allItems
}
}