pragma Singleton import qs.modules.common import qs.modules.common.models import qs.modules.common.functions import QtQuick import Quickshell import Quickshell.Io Singleton { id: root property string query: "" function ensurePrefix(prefix) { if ([Config.options.search.prefix.action, Config.options.search.prefix.app, Config.options.search.prefix.clipboard, Config.options.search.prefix.emojis, Config.options.search.prefix.math, Config.options.search.prefix.shellCommand, Config.options.search.prefix.webSearch,].some(i => root.query.startsWith(i))) { root.query = prefix + root.query.slice(1); } else { root.query = prefix + root.query; } } // https://specifications.freedesktop.org/menu/latest/category-registry.html property list mainRegisteredCategories: ["AudioVideo", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"] property list appCategories: DesktopEntries.applications.values.reduce((acc, entry) => { for (const category of entry.categories) { if (!acc.includes(category) && mainRegisteredCategories.includes(category)) { acc.push(category); } } return acc; }, []).sort() property var searchActions: [ { action: "accentcolor", execute: args => { Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--noswitch", "--color", ...(args != '' ? [`${args}`] : [])]); } }, { action: "dark", execute: () => { Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]); } }, { action: "konachanwallpaper", execute: () => { Quickshell.execDetached([Quickshell.shellPath("scripts/colors/random/random_konachan_wall.sh")]); } }, { action: "light", execute: () => { Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]); } }, { action: "superpaste", execute: args => { if (!/^(\d+)/.test(args.trim())) { // Invalid if doesn't start with numbers Quickshell.execDetached(["notify-send", Translation.tr("Superpaste"), Translation.tr("Usage: %1superpaste NUM_OF_ENTRIES[i]\nSupply i when you want images\nExamples:\n%1superpaste 4i for the last 4 images\n%1superpaste 7 for the last 7 entries").arg(Config.options.search.prefix.action), "-a", "Shell"]); return; } const syntaxMatch = /^(?:(\d+)(i)?)/.exec(args.trim()); const count = syntaxMatch[1] ? parseInt(syntaxMatch[1]) : 1; const isImage = !!syntaxMatch[2]; Cliphist.superpaste(count, isImage); } }, { action: "todo", execute: args => { Todo.addTask(args); } }, { action: "wallpaper", execute: () => { GlobalStates.wallpaperSelectorOpen = true; } }, { action: "wipeclipboard", execute: () => { Cliphist.wipe(); } }, ] property string mathResult: "" property bool clipboardWorkSafetyActive: { const enabled = Config.options.workSafety.enable.clipboard; const sensitiveNetwork = (StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords)); return enabled && sensitiveNetwork; } function containsUnsafeLink(entry) { if (entry == undefined) return false; const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords; return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords); } Timer { id: nonAppResultsTimer interval: Config.options.search.nonAppResultDelay onTriggered: { let expr = root.query; if (expr.startsWith(Config.options.search.prefix.math)) { expr = expr.slice(Config.options.search.prefix.math.length); } mathProc.calculateExpression(expr); } } Process { id: mathProc property list baseCommand: ["qalc", "-t"] function calculateExpression(expression) { mathProc.running = false; mathProc.command = baseCommand.concat(expression); mathProc.running = true; } stdout: SplitParser { onRead: data => { root.mathResult = data; } } } property list results: { // Search results are handled here ////////////////// Skip? ////////////////// if (root.query == "") return []; ///////////// Special cases /////////////// if (root.query.startsWith(Config.options.search.prefix.clipboard)) { // Clipboard const searchString = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.clipboard); return Cliphist.fuzzyQuery(searchString).map((entry, index, array) => { const mightBlurImage = Cliphist.entryIsImage(entry) && root.clipboardWorkSafetyActive; let shouldBlurImage = mightBlurImage; if (mightBlurImage) { shouldBlurImage = shouldBlurImage && (root.containsUnsafeLink(array[index - 1]) || root.containsUnsafeLink(array[index + 1])); } const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`; return resultComp.createObject(null, { rawValue: entry, name: StringUtils.cleanCliphistEntry(entry), verb: "", type: type, execute: () => { Cliphist.copy(entry); }, actions: [resultComp.createObject(null, { name: Translation.tr("Copy"), iconName: "content_copy", iconType: LauncherSearchResult.IconType.Material, execute: () => { Cliphist.copy(entry); } }), resultComp.createObject(null, { name: Translation.tr("Delete"), iconName: "delete", iconType: LauncherSearchResult.IconType.Material, execute: () => { Cliphist.deleteEntry(entry); } })], blurImage: shouldBlurImage }); }).filter(Boolean); } else if (root.query.startsWith(Config.options.search.prefix.emojis)) { // Clipboard const searchString = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.emojis); return Emojis.fuzzyQuery(searchString).map(entry => { const emoji = entry.match(/^\s*(\S+)/)?.[1] || ""; return resultComp.createObject(null, { rawValue: entry, name: entry.replace(/^\s*\S+\s+/, ""), iconName: emoji, iconType: LauncherSearchResult.IconType.Text, verb: Translation.tr("Copy"), type: Translation.tr("Emoji"), execute: () => { Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1]; } }); }).filter(Boolean); } ////////////////// Init /////////////////// nonAppResultsTimer.restart(); const mathResultObject = resultComp.createObject(null, { name: root.mathResult, verb: Translation.tr("Copy"), type: Translation.tr("Math result"), fontType: LauncherSearchResult.FontType.Monospace, iconName: 'calculate', iconType: LauncherSearchResult.IconType.Material, execute: () => { Quickshell.clipboardText = root.mathResult; } }); const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.query, Config.options.search.prefix.app)).map(entry => { return resultComp.createObject(null, { type: Translation.tr("App"), id: entry.id, name: entry.name, iconName: entry.icon, iconType: LauncherSearchResult.IconType.System, verb: Translation.tr("Open"), execute: () => { if (!entry.runInTerminal) entry.execute(); else { // Probably needs more proper escaping, but this will do for now Quickshell.execDetached(["bash", '-c', `${Config.options.apps.terminal} -e '${StringUtils.shellSingleQuoteEscape(entry.command.join(' '))}'`]); } }, comment: entry.comment, runInTerminal: entry.runInTerminal, genericName: entry.genericName, keywords: entry.keywords, actions: entry.actions.map(action => { return resultComp.createObject(null, { name: action.name, iconName: action.icon, iconType: LauncherSearchResult.IconType.System, execute: () => { if (!action.runInTerminal) action.execute(); else { Quickshell.execDetached(["bash", '-c', `${Config.options.apps.terminal} -e '${StringUtils.shellSingleQuoteEscape(action.command.join(' '))}'`]); } } }); }) }); }); const commandResultObject = resultComp.createObject(null, { name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.shellCommand).replace("file://", ""), verb: Translation.tr("Run"), type: Translation.tr("Command"), fontType: LauncherSearchResult.FontType.Monospace, iconName: 'terminal', iconType: LauncherSearchResult.IconType.Material, execute: () => { let cleanedCommand = root.query.replace("file://", ""); cleanedCommand = StringUtils.cleanPrefix(cleanedCommand, Config.options.search.prefix.shellCommand); if (cleanedCommand.startsWith(Config.options.search.prefix.shellCommand)) { cleanedCommand = cleanedCommand.slice(Config.options.search.prefix.shellCommand.length); } Quickshell.execDetached(["bash", "-c", root.query.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]); } }); const webSearchResultObject = resultComp.createObject(null, { name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch), verb: Translation.tr("Search"), type: Translation.tr("Web search"), iconName: 'travel_explore', iconType: LauncherSearchResult.IconType.Material, execute: () => { let query = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch); let url = Config.options.search.engineBaseUrl + query; for (let site of Config.options.search.excludedSites) { url += ` -site:${site}`; } Qt.openUrlExternally(url); } }); const launcherActionObjects = root.searchActions.map(action => { const actionString = `${Config.options.search.prefix.action}${action.action}`; if (actionString.startsWith(root.query) || root.query.startsWith(actionString)) { return resultComp.createObject(null, { name: root.query.startsWith(actionString) ? root.query : actionString, verb: Translation.tr("Run"), type: Translation.tr("Action"), iconName: 'settings_suggest', iconType: LauncherSearchResult.IconType.Material, execute: () => { action.execute(root.query.split(" ").slice(1).join(" ")); } }); } return null; }).filter(Boolean); //////// Prioritized by prefix ///////// let result = []; const startsWithNumber = /^\d/.test(root.query); const startsWithMathPrefix = root.query.startsWith(Config.options.search.prefix.math); const startsWithShellCommandPrefix = root.query.startsWith(Config.options.search.prefix.shellCommand); const startsWithWebSearchPrefix = root.query.startsWith(Config.options.search.prefix.webSearch); if (startsWithNumber || startsWithMathPrefix) { result.push(mathResultObject); } else if (startsWithShellCommandPrefix) { result.push(commandResultObject); } else if (startsWithWebSearchPrefix) { result.push(webSearchResultObject); } //////////////// Apps ////////////////// result = result.concat(appResultObjects); ////////// Launcher actions //////////// result = result.concat(launcherActionObjects); /// Math result, command, web search /// if (Config.options.search.prefix.showDefaultActionsWithoutPrefix) { if (!startsWithShellCommandPrefix) result.push(commandResultObject); if (!startsWithNumber && !startsWithMathPrefix) result.push(mathResultObject); if (!startsWithWebSearchPrefix) result.push(webSearchResultObject); } return result; } Component { id: resultComp LauncherSearchResult {} } }