From 4055ad48fa776608521cf52dddaf78e5feb8e3e8 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Wed, 3 Dec 2025 22:07:06 +0100 Subject: [PATCH] launcher search service: use proper type instead of json clumps --- .../quickshell/ii/modules/common/Config.qml | 3 +- .../common/models/LauncherSearchResult.qml | 29 ++++ .../ii/modules/ii/overview/SearchItem.qml | 55 ++++--- .../ii/modules/ii/overview/SearchWidget.qml | 6 - .../quickshell/ii/services/LauncherSearch.qml | 150 +++++++++++------- 5 files changed, 161 insertions(+), 82 deletions(-) create mode 100644 dots/.config/quickshell/ii/modules/common/models/LauncherSearchResult.qml diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index d26f04d65..b0b767a44 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -590,8 +590,9 @@ Singleton { // false will make (some) stuff also be like that for accuracy. // Example: the right-click menu of the Start button property JsonObject tweaks: JsonObject { - property bool smootherMenuAnimations: true property bool switchHandlePositionFix: true + property bool smootherMenuAnimations: true + property bool smootherSearchBar: true } property JsonObject bar: JsonObject { property bool bottom: true diff --git a/dots/.config/quickshell/ii/modules/common/models/LauncherSearchResult.qml b/dots/.config/quickshell/ii/modules/common/models/LauncherSearchResult.qml new file mode 100644 index 000000000..15beef12e --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/models/LauncherSearchResult.qml @@ -0,0 +1,29 @@ +import QtQuick +import Quickshell + +QtObject { + enum IconType { Material, Text, System, None } + enum FontType { Normal, Monospace } + + // General stuff + property string type: "" + property var fontType: LauncherSearchResult.FontType.Normal + property string name: "" + property string rawValue: "" + property string iconName: "" + property var iconType: LauncherSearchResult.IconType.None + property string verb: "" + property bool blurImage: false + property var execute: () => { + print("Not implemented"); + } + property var actions: [] + + // Stuff needed for DesktopEntry objects + property bool shown: true + property string comment: "" + property bool runInTerminal: false + property string genericName: "" + property list keywords: [] + +} diff --git a/dots/.config/quickshell/ii/modules/ii/overview/SearchItem.qml b/dots/.config/quickshell/ii/modules/ii/overview/SearchItem.qml index 8f72db56a..a0f9b248d 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/SearchItem.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/SearchItem.qml @@ -2,6 +2,7 @@ import qs import qs.services import qs.modules.common +import qs.modules.common.models import qs.modules.common.widgets import qs.modules.common.functions import QtQuick @@ -12,20 +13,27 @@ import Quickshell.Hyprland RippleButton { id: root - property var entry + property LauncherSearchResult entry property string query property bool entryShown: entry?.shown ?? true property string itemType: entry?.type ?? Translation.tr("App") property string itemName: entry?.name ?? "" - property string itemIcon: entry?.icon ?? "" + property var iconType: entry?.iconType + property string iconName: entry?.iconName ?? "" property var itemExecute: entry?.execute - property string fontType: entry?.fontType ?? "main" - property string itemClickActionName: entry?.clickActionName ?? "Open" - property string bigText: entry?.bigText ?? "" - property string materialSymbol: entry?.materialSymbol ?? "" - property string cliphistRawString: entry?.cliphistRawString ?? "" + property var fontType: switch(entry?.fontType) { + case LauncherSearchResult.FontType.Monospace: + return "monospace" + case LauncherSearchResult.FontType.Normal: + return "main" + default: + return "main" + } + property string itemClickActionName: entry?.verb ?? "Open" + property string bigText: entry?.iconType === LauncherSearchResult.IconType.Text ? entry?.iconName ?? "" : "" + property string materialSymbol: entry.iconType === LauncherSearchResult.IconType.Material ? entry?.iconName ?? "" : "" + property string cliphistRawString: entry?.rawValue ?? "" property bool blurImage: entry?.blurImage ?? false - property string blurImageText: entry?.blurImageText ?? "Image hidden" visible: root.entryShown property int horizontalMargin: 10 @@ -97,7 +105,7 @@ RippleButton { } Keys.onPressed: (event) => { if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) { - const deleteAction = root.entry.actions.find(action => action.name == "Delete"); + const deleteAction = root.entry.actions.find(action => action.name == Translation.tr("Delete")); if (deleteAction) { deleteAction.execute() @@ -126,16 +134,24 @@ RippleButton { Loader { id: iconLoader active: true - sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent : - root.bigText ? bigTextComponent : - root.itemIcon !== "" ? iconImageComponent : - null + sourceComponent: switch(root.iconType) { + case LauncherSearchResult.IconType.Material: + return materialSymbolComponent + case LauncherSearchResult.IconType.Text: + return bigTextComponent + case LauncherSearchResult.IconType.System: + return iconImageComponent + case LauncherSearchResult.IconType.None: + return null + default: + return null + } } Component { id: iconImageComponent IconImage { - source: Quickshell.iconPath(root.itemIcon, "image-missing") + source: Quickshell.iconPath(root.iconName, "image-missing") width: 35 height: 35 } @@ -217,7 +233,6 @@ RippleButton { maxWidth: contentColumn.width maxHeight: 140 blur: root.blurImage - blurText: root.blurImageText } } } @@ -243,8 +258,8 @@ RippleButton { delegate: RippleButton { id: actionButton required property var modelData - property string iconName: modelData.icon ?? "" - property string materialIconName: modelData.materialIcon ?? "" + property var iconType: modelData.iconType + property string iconName: modelData.iconName ?? "" implicitHeight: 34 implicitWidth: 34 @@ -256,16 +271,16 @@ RippleButton { anchors.centerIn: parent Loader { anchors.centerIn: parent - active: !(actionButton.iconName !== "") || actionButton.materialIconName + active: actionButton.iconType === LauncherSearchResult.IconType.Material || actionButton.iconName === "" sourceComponent: MaterialSymbol { - text: actionButton.materialIconName || "video_settings" + text: actionButton.iconName || "video_settings" font.pixelSize: Appearance.font.pixelSize.hugeass color: Appearance.m3colors.m3onSurface } } Loader { anchors.centerIn: parent - active: actionButton.materialIconName.length == 0 && actionButton.iconName && actionButton.iconName !== "" + active: actionButton.iconType === LauncherSearchResult.IconType.System && actionButton.iconName !== "" sourceComponent: IconImage { source: Quickshell.iconPath(actionButton.iconName) implicitSize: 20 diff --git a/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml b/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml index c0ca28d62..6b982c4d2 100644 --- a/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml +++ b/dots/.config/quickshell/ii/modules/ii/overview/SearchWidget.qml @@ -42,12 +42,6 @@ Item { // Wrapper LauncherSearch.query = text; } - function containsUnsafeLink(entry) { - if (entry == undefined) return false; - const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords; - return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords); - } - Keys.onPressed: event => { // Prevent Esc and Backspace from registering if (event.key === Qt.Key_Escape) diff --git a/dots/.config/quickshell/ii/services/LauncherSearch.qml b/dots/.config/quickshell/ii/services/LauncherSearch.qml index bf23c1c48..89d9e0056 100644 --- a/dots/.config/quickshell/ii/services/LauncherSearch.qml +++ b/dots/.config/quickshell/ii/services/LauncherSearch.qml @@ -4,7 +4,6 @@ import qs.modules.common import qs.modules.common.models import qs.modules.common.functions import QtQuick -import QtQuick.Controls import Quickshell import Quickshell.Io @@ -12,6 +11,15 @@ 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; + } + } + property var searchActions: [ { action: "accentcolor", @@ -74,12 +82,13 @@ Singleton { 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)) + const sensitiveNetwork = (StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords)); return enabled && sensitiveNetwork; } function containsUnsafeLink(entry) { - if (entry == undefined) return false; + if (entry == undefined) + return false; const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords; return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords); } @@ -128,95 +137,121 @@ Singleton { shouldBlurImage = shouldBlurImage && (root.containsUnsafeLink(array[index - 1]) || root.containsUnsafeLink(array[index + 1])); } const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`; - return { - key: type, - cliphistRawString: entry, + return resultComp.createObject(null, { + rawValue: entry, name: StringUtils.cleanCliphistEntry(entry), - clickActionName: "", + verb: "", type: type, execute: () => { Cliphist.copy(entry); }, - actions: [ - { - name: "Copy", - materialIcon: "content_copy", + actions: [resultComp.createObject(null, { + name: Translation.tr("Copy"), + iconName: "content_copy", + iconType: LauncherSearchResult.IconType.Material, execute: () => { Cliphist.copy(entry); } - }, - { - name: "Delete", - materialIcon: "delete", + }), resultComp.createObject(null, { + name: Translation.tr("Delete"), + iconName: "delete", + iconType: LauncherSearchResult.IconType.Material, execute: () => { Cliphist.deleteEntry(entry); } - } - ], - blurImage: shouldBlurImage, - blurImageText: Translation.tr("Work safety") - }; + })], + 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 { - key: emoji, - cliphistRawString: entry, - bigText: emoji, + return resultComp.createObject(null, { + rawValue: entry, name: entry.replace(/^\s*\S+\s+/, ""), - clickActionName: "", - type: "Emoji", + 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 = { - key: `Math result: ${root.mathResult}`, + const mathResultObject = resultComp.createObject(null, { name: root.mathResult, - clickActionName: Translation.tr("Copy"), + verb: Translation.tr("Copy"), type: Translation.tr("Math result"), - fontType: "monospace", - materialSymbol: 'calculate', + 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 => { - entry.clickActionName = Translation.tr("Launch"); - entry.type = Translation.tr("App"); - entry.key = entry.execute; - return entry; }); - const commandResultObject = { - key: `cmd ${root.query}`, + const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.query, Config.options.search.prefix.app)).map(entry => { + return resultComp.createObject(null, { + type: Translation.tr("App"), + name: entry.name, + iconName: entry.icon, + iconType: LauncherSearchResult.IconType.System, + verb: Translation.tr("Launch"), + 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://", ""), - clickActionName: Translation.tr("Run"), + verb: Translation.tr("Run"), type: Translation.tr("Run command"), - fontType: "monospace", - materialSymbol: 'terminal', + 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", searchingText.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]); + Quickshell.execDetached(["bash", "-c", root.query.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]); } - }; - const webSearchResultObject = { - key: `website ${root.query}`, + }); + const webSearchResultObject = resultComp.createObject(null, { name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch), - clickActionName: Translation.tr("Search"), + verb: Translation.tr("Search"), type: Translation.tr("Search the web"), - materialSymbol: 'travel_explore', + 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; @@ -225,20 +260,20 @@ Singleton { } 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}`, + return resultComp.createObject(null, { name: root.query.startsWith(actionString) ? root.query : actionString, - clickActionName: Translation.tr("Run"), + verb: Translation.tr("Run"), type: Translation.tr("Action"), - materialSymbol: 'settings_suggest', + iconName: 'settings_suggest', + iconType: LauncherSearchResult.IconType.Material, execute: () => { action.execute(root.query.split(" ").slice(1).join(" ")); } - }; + }); } return null; }).filter(Boolean); @@ -275,4 +310,9 @@ Singleton { return result; } + + Component { + id: resultComp + LauncherSearchResult {} + } }