diff --git a/dots/.config/quickshell/ii/modules/ii/overview/SearchBar.qml b/dots/.config/quickshell/ii/modules/ii/overview/SearchBar.qml index 5cd7b9ac5..6a5de6a7e 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/SearchBar.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/SearchBar.qml @@ -81,7 +81,7 @@ RowLayout { } } - onTextChanged: root.searchingText = text + onTextChanged: LauncherSearch.query = text onAccepted: { if (appResults.count > 0) { diff --git a/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml b/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml index 03ebfce1b..c0ca28d62 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml @@ -14,81 +14,11 @@ import Quickshell.Io Item { // Wrapper id: root readonly property string xdgConfigHome: Directories.config - property string searchingText: "" + property string searchingText: LauncherSearch.query property bool showResults: searchingText != "" implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2 implicitHeight: searchBar.implicitHeight + searchBar.verticalPadding * 2 + Appearance.sizes.elevationMargin * 2 - 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; - } - - 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(); - } - }, - ] - function focusFirstItem() { appResults.currentIndex = 0; } @@ -103,13 +33,13 @@ Item { // Wrapper function cancelSearch() { searchBar.searchInput.selectAll(); - root.searchingText = ""; + LauncherSearch.query = ""; searchBar.animateWidth = true; } function setSearchingText(text) { searchBar.searchInput.text = text; - root.searchingText = text; + LauncherSearch.query = text; } function containsUnsafeLink(entry) { @@ -118,34 +48,6 @@ Item { // Wrapper return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords); } - Timer { - id: nonAppResultsTimer - interval: Config.options.search.nonAppResultDelay - onTriggered: { - let expr = root.searchingText; - if (expr.startsWith(Config.options.search.prefix.math)) { - expr = expr.slice(Config.options.search.prefix.math.length); - } - mathProcess.calculateExpression(expr); - } - } - - Process { - id: mathProcess - property list baseCommand: ["qalc", "-t"] - function calculateExpression(expression) { - mathProcess.running = false; - mathProcess.command = baseCommand.concat(expression); - mathProcess.running = true; - } - stdout: SplitParser { - onRead: data => { - root.mathResult = data; - root.focusFirstItem(); - } - } - } - Keys.onPressed: event => { // Prevent Esc and Backspace from registering if (event.key === Qt.Key_Escape) @@ -285,167 +187,9 @@ Item { // Wrapper model: ScriptModel { id: model objectProp: "key" - values: { - // Search results are handled here - ////////////////// Skip? ////////////////// - if (root.searchingText == "") - return []; - - ///////////// Special cases /////////////// - if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) { - // Clipboard - const searchString = StringUtils.cleanPrefix(root.searchingText, 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 && (containsUnsafeLink(array[index - 1]) || containsUnsafeLink(array[index + 1])); - } - const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}` - return { - key: type, - cliphistRawString: entry, - name: StringUtils.cleanCliphistEntry(entry), - clickActionName: "", - type: type, - execute: () => { - Cliphist.copy(entry) - }, - actions: [ - { - name: "Copy", - materialIcon: "content_copy", - execute: () => { - Cliphist.copy(entry); - } - }, - { - name: "Delete", - materialIcon: "delete", - execute: () => { - Cliphist.deleteEntry(entry); - } - } - ], - blurImage: shouldBlurImage, - blurImageText: Translation.tr("Work safety") - }; - }).filter(Boolean); - } - else if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) { - // Clipboard - const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.emojis); - return Emojis.fuzzyQuery(searchString).map(entry => { - const emoji = entry.match(/^\s*(\S+)/)?.[1] || "" - return { - key: emoji, - cliphistRawString: entry, - bigText: emoji, - name: entry.replace(/^\s*\S+\s+/, ""), - clickActionName: "", - type: "Emoji", - execute: () => { - Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1]; - } - }; - }).filter(Boolean); - } - - ////////////////// Init /////////////////// - nonAppResultsTimer.restart(); - const mathResultObject = { - key: `Math result: ${root.mathResult}`, - name: root.mathResult, - clickActionName: Translation.tr("Copy"), - type: Translation.tr("Math result"), - fontType: "monospace", - materialSymbol: 'calculate', - execute: () => { - Quickshell.clipboardText = root.mathResult; - } - }; - const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.app)).map(entry => { - entry.clickActionName = Translation.tr("Launch"); - entry.type = Translation.tr("App"); - entry.key = entry.execute - return entry; - }) - const commandResultObject = { - key: `cmd ${root.searchingText}`, - name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.shellCommand).replace("file://", ""), - clickActionName: Translation.tr("Run"), - type: Translation.tr("Run command"), - fontType: "monospace", - materialSymbol: 'terminal', - execute: () => { - let cleanedCommand = root.searchingText.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", searchingText.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]); - } - }; - const webSearchResultObject = { - key: `website ${root.searchingText}`, - name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch), - clickActionName: Translation.tr("Search"), - type: Translation.tr("Search the web"), - materialSymbol: 'travel_explore', - execute: () => { - let query = StringUtils.cleanPrefix(root.searchingText, 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.searchingText) || root.searchingText.startsWith(actionString)) { - return { - key: `Action ${actionString}`, - name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString, - clickActionName: Translation.tr("Run"), - type: Translation.tr("Action"), - materialSymbol: 'settings_suggest', - execute: () => { - action.execute(root.searchingText.split(" ").slice(1).join(" ")); - } - }; - } - return null; - }).filter(Boolean); - - //////// Prioritized by prefix ///////// - let result = []; - const startsWithNumber = /^\d/.test(root.searchingText); - const startsWithMathPrefix = root.searchingText.startsWith(Config.options.search.prefix.math); - const startsWithShellCommandPrefix = root.searchingText.startsWith(Config.options.search.prefix.shellCommand); - const startsWithWebSearchPrefix = root.searchingText.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; + values: LauncherSearch.results + onValuesChanged: { + root.focusFirstItem(); } } diff --git a/dots/.config/quickshell/ii/services/LauncherSearch.qml b/dots/.config/quickshell/ii/services/LauncherSearch.qml new file mode 100644 index 000000000..bf23c1c48 --- /dev/null +++ b/dots/.config/quickshell/ii/services/LauncherSearch.qml @@ -0,0 +1,278 @@ +pragma Singleton + +import qs.modules.common +import qs.modules.common.models +import qs.modules.common.functions +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property string query: "" + 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 { + key: type, + cliphistRawString: entry, + name: StringUtils.cleanCliphistEntry(entry), + clickActionName: "", + type: type, + execute: () => { + Cliphist.copy(entry); + }, + actions: [ + { + name: "Copy", + materialIcon: "content_copy", + execute: () => { + Cliphist.copy(entry); + } + }, + { + name: "Delete", + materialIcon: "delete", + execute: () => { + Cliphist.deleteEntry(entry); + } + } + ], + blurImage: shouldBlurImage, + blurImageText: Translation.tr("Work safety") + }; + }).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 { + key: emoji, + cliphistRawString: entry, + bigText: emoji, + name: entry.replace(/^\s*\S+\s+/, ""), + clickActionName: "", + type: "Emoji", + execute: () => { + Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1]; + } + }; + }).filter(Boolean); + } + + ////////////////// Init /////////////////// + nonAppResultsTimer.restart(); + const mathResultObject = { + key: `Math result: ${root.mathResult}`, + name: root.mathResult, + clickActionName: Translation.tr("Copy"), + type: Translation.tr("Math result"), + fontType: "monospace", + materialSymbol: 'calculate', + execute: () => { + Quickshell.clipboardText = root.mathResult; + } + }; + const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.query, Config.options.search.prefix.app)).map(entry => { + entry.clickActionName = Translation.tr("Launch"); + entry.type = Translation.tr("App"); + entry.key = entry.execute; + return entry; + }); + const commandResultObject = { + key: `cmd ${root.query}`, + name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.shellCommand).replace("file://", ""), + clickActionName: Translation.tr("Run"), + type: Translation.tr("Run command"), + fontType: "monospace", + materialSymbol: 'terminal', + 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", searchingText.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]); + } + }; + const webSearchResultObject = { + key: `website ${root.query}`, + name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch), + clickActionName: Translation.tr("Search"), + type: Translation.tr("Search the web"), + materialSymbol: 'travel_explore', + 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 { + key: `Action ${actionString}`, + name: root.query.startsWith(actionString) ? root.query : actionString, + clickActionName: Translation.tr("Run"), + type: Translation.tr("Action"), + materialSymbol: 'settings_suggest', + 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; + } +}