1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

launcher: allow terminal apps

This commit is contained in:
bbedward
2026-01-02 21:56:56 -05:00
parent 8253ec4496
commit 77681fd387
2 changed files with 233 additions and 219 deletions

View File

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

View File

@@ -170,26 +170,35 @@ Singleton {
const userPrefix = SettingsData.launchPrefix?.trim() || ""; const userPrefix = SettingsData.launchPrefix?.trim() || "";
const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || ""; const defaultPrefix = Quickshell.env("DMS_DEFAULT_LAUNCH_PREFIX") || "";
const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix; const prefix = userPrefix.length > 0 ? userPrefix : defaultPrefix;
const workDir = desktopEntry.workingDirectory || Quickshell.env("HOME");
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
const shellCmd = prefix.length > 0 ? `${prefix} ${escapedCmd}` : escapedCmd;
if (desktopEntry.runInTerminal) {
const terminal = Quickshell.env("TERMINAL") || "xterm";
Quickshell.execDetached({
command: [terminal, "-e", "sh", "-c", shellCmd],
workingDirectory: workDir
});
return;
}
if (prefix.length > 0 && needsShellExecution(prefix)) { if (prefix.length > 0 && needsShellExecution(prefix)) {
const escapedCmd = cmd.map(arg => escapeShellArg(arg)).join(" ");
const shellCmd = `${prefix} ${escapedCmd}`;
Quickshell.execDetached({ Quickshell.execDetached({
command: ["sh", "-c", shellCmd], command: ["sh", "-c", shellCmd],
workingDirectory: desktopEntry.workingDirectory || Quickshell.env("HOME") workingDirectory: workDir
});
} else {
if (prefix.length > 0) {
const launchPrefix = prefix.split(" ");
cmd = launchPrefix.concat(cmd);
}
Quickshell.execDetached({
command: cmd,
workingDirectory: desktopEntry.workingDirectory || Quickshell.env("HOME")
}); });
return;
} }
if (prefix.length > 0) {
cmd = prefix.split(" ").concat(cmd);
}
Quickshell.execDetached({
command: cmd,
workingDirectory: workDir
});
} }
function launchDesktopAction(desktopEntry, action, useNvidia) { function launchDesktopAction(desktopEntry, action, useNvidia) {