launcher search service: use proper type instead of json clumps

This commit is contained in:
end-4
2025-12-03 22:07:06 +01:00
parent 05aae36e82
commit 4055ad48fa
5 changed files with 161 additions and 82 deletions
@@ -590,8 +590,9 @@ Singleton {
// false will make (some) stuff also be like that for accuracy. // false will make (some) stuff also be like that for accuracy.
// Example: the right-click menu of the Start button // Example: the right-click menu of the Start button
property JsonObject tweaks: JsonObject { property JsonObject tweaks: JsonObject {
property bool smootherMenuAnimations: true
property bool switchHandlePositionFix: true property bool switchHandlePositionFix: true
property bool smootherMenuAnimations: true
property bool smootherSearchBar: true
} }
property JsonObject bar: JsonObject { property JsonObject bar: JsonObject {
property bool bottom: true property bool bottom: true
@@ -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<string> keywords: []
}
@@ -2,6 +2,7 @@
import qs import qs
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.models
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import QtQuick import QtQuick
@@ -12,20 +13,27 @@ import Quickshell.Hyprland
RippleButton { RippleButton {
id: root id: root
property var entry property LauncherSearchResult entry
property string query property string query
property bool entryShown: entry?.shown ?? true property bool entryShown: entry?.shown ?? true
property string itemType: entry?.type ?? Translation.tr("App") property string itemType: entry?.type ?? Translation.tr("App")
property string itemName: entry?.name ?? "" 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 var itemExecute: entry?.execute
property string fontType: entry?.fontType ?? "main" property var fontType: switch(entry?.fontType) {
property string itemClickActionName: entry?.clickActionName ?? "Open" case LauncherSearchResult.FontType.Monospace:
property string bigText: entry?.bigText ?? "" return "monospace"
property string materialSymbol: entry?.materialSymbol ?? "" case LauncherSearchResult.FontType.Normal:
property string cliphistRawString: entry?.cliphistRawString ?? "" 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 bool blurImage: entry?.blurImage ?? false
property string blurImageText: entry?.blurImageText ?? "Image hidden"
visible: root.entryShown visible: root.entryShown
property int horizontalMargin: 10 property int horizontalMargin: 10
@@ -97,7 +105,7 @@ RippleButton {
} }
Keys.onPressed: (event) => { Keys.onPressed: (event) => {
if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) { 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) { if (deleteAction) {
deleteAction.execute() deleteAction.execute()
@@ -126,16 +134,24 @@ RippleButton {
Loader { Loader {
id: iconLoader id: iconLoader
active: true active: true
sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent : sourceComponent: switch(root.iconType) {
root.bigText ? bigTextComponent : case LauncherSearchResult.IconType.Material:
root.itemIcon !== "" ? iconImageComponent : return materialSymbolComponent
null case LauncherSearchResult.IconType.Text:
return bigTextComponent
case LauncherSearchResult.IconType.System:
return iconImageComponent
case LauncherSearchResult.IconType.None:
return null
default:
return null
}
} }
Component { Component {
id: iconImageComponent id: iconImageComponent
IconImage { IconImage {
source: Quickshell.iconPath(root.itemIcon, "image-missing") source: Quickshell.iconPath(root.iconName, "image-missing")
width: 35 width: 35
height: 35 height: 35
} }
@@ -217,7 +233,6 @@ RippleButton {
maxWidth: contentColumn.width maxWidth: contentColumn.width
maxHeight: 140 maxHeight: 140
blur: root.blurImage blur: root.blurImage
blurText: root.blurImageText
} }
} }
} }
@@ -243,8 +258,8 @@ RippleButton {
delegate: RippleButton { delegate: RippleButton {
id: actionButton id: actionButton
required property var modelData required property var modelData
property string iconName: modelData.icon ?? "" property var iconType: modelData.iconType
property string materialIconName: modelData.materialIcon ?? "" property string iconName: modelData.iconName ?? ""
implicitHeight: 34 implicitHeight: 34
implicitWidth: 34 implicitWidth: 34
@@ -256,16 +271,16 @@ RippleButton {
anchors.centerIn: parent anchors.centerIn: parent
Loader { Loader {
anchors.centerIn: parent anchors.centerIn: parent
active: !(actionButton.iconName !== "") || actionButton.materialIconName active: actionButton.iconType === LauncherSearchResult.IconType.Material || actionButton.iconName === ""
sourceComponent: MaterialSymbol { sourceComponent: MaterialSymbol {
text: actionButton.materialIconName || "video_settings" text: actionButton.iconName || "video_settings"
font.pixelSize: Appearance.font.pixelSize.hugeass font.pixelSize: Appearance.font.pixelSize.hugeass
color: Appearance.m3colors.m3onSurface color: Appearance.m3colors.m3onSurface
} }
} }
Loader { Loader {
anchors.centerIn: parent anchors.centerIn: parent
active: actionButton.materialIconName.length == 0 && actionButton.iconName && actionButton.iconName !== "" active: actionButton.iconType === LauncherSearchResult.IconType.System && actionButton.iconName !== ""
sourceComponent: IconImage { sourceComponent: IconImage {
source: Quickshell.iconPath(actionButton.iconName) source: Quickshell.iconPath(actionButton.iconName)
implicitSize: 20 implicitSize: 20
@@ -42,12 +42,6 @@ Item { // Wrapper
LauncherSearch.query = text; 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 => { Keys.onPressed: event => {
// Prevent Esc and Backspace from registering // Prevent Esc and Backspace from registering
if (event.key === Qt.Key_Escape) if (event.key === Qt.Key_Escape)
@@ -4,7 +4,6 @@ import qs.modules.common
import qs.modules.common.models import qs.modules.common.models
import qs.modules.common.functions import qs.modules.common.functions
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
@@ -12,6 +11,15 @@ Singleton {
id: root id: root
property string query: "" 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: [ property var searchActions: [
{ {
action: "accentcolor", action: "accentcolor",
@@ -74,12 +82,13 @@ Singleton {
property string mathResult: "" property string mathResult: ""
property bool clipboardWorkSafetyActive: { property bool clipboardWorkSafetyActive: {
const enabled = Config.options.workSafety.enable.clipboard; 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; return enabled && sensitiveNetwork;
} }
function containsUnsafeLink(entry) { function containsUnsafeLink(entry) {
if (entry == undefined) return false; if (entry == undefined)
return false;
const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords; const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords;
return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords); return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords);
} }
@@ -128,95 +137,121 @@ Singleton {
shouldBlurImage = shouldBlurImage && (root.containsUnsafeLink(array[index - 1]) || root.containsUnsafeLink(array[index + 1])); shouldBlurImage = shouldBlurImage && (root.containsUnsafeLink(array[index - 1]) || root.containsUnsafeLink(array[index + 1]));
} }
const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`; const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`;
return { return resultComp.createObject(null, {
key: type, rawValue: entry,
cliphistRawString: entry,
name: StringUtils.cleanCliphistEntry(entry), name: StringUtils.cleanCliphistEntry(entry),
clickActionName: "", verb: "",
type: type, type: type,
execute: () => { execute: () => {
Cliphist.copy(entry); Cliphist.copy(entry);
}, },
actions: [ actions: [resultComp.createObject(null, {
{ name: Translation.tr("Copy"),
name: "Copy", iconName: "content_copy",
materialIcon: "content_copy", iconType: LauncherSearchResult.IconType.Material,
execute: () => { execute: () => {
Cliphist.copy(entry); Cliphist.copy(entry);
} }
}, }), resultComp.createObject(null, {
{ name: Translation.tr("Delete"),
name: "Delete", iconName: "delete",
materialIcon: "delete", iconType: LauncherSearchResult.IconType.Material,
execute: () => { execute: () => {
Cliphist.deleteEntry(entry); Cliphist.deleteEntry(entry);
} }
} })],
], blurImage: shouldBlurImage
blurImage: shouldBlurImage, });
blurImageText: Translation.tr("Work safety")
};
}).filter(Boolean); }).filter(Boolean);
} else if (root.query.startsWith(Config.options.search.prefix.emojis)) { } else if (root.query.startsWith(Config.options.search.prefix.emojis)) {
// Clipboard // Clipboard
const searchString = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.emojis); const searchString = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.emojis);
return Emojis.fuzzyQuery(searchString).map(entry => { return Emojis.fuzzyQuery(searchString).map(entry => {
const emoji = entry.match(/^\s*(\S+)/)?.[1] || ""; const emoji = entry.match(/^\s*(\S+)/)?.[1] || "";
return { return resultComp.createObject(null, {
key: emoji, rawValue: entry,
cliphistRawString: entry,
bigText: emoji,
name: entry.replace(/^\s*\S+\s+/, ""), name: entry.replace(/^\s*\S+\s+/, ""),
clickActionName: "", iconName: emoji,
type: "Emoji", iconType: LauncherSearchResult.IconType.Text,
verb: Translation.tr("Copy"),
type: Translation.tr("Emoji"),
execute: () => { execute: () => {
Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1]; Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1];
} }
}; });
}).filter(Boolean); }).filter(Boolean);
} }
////////////////// Init /////////////////// ////////////////// Init ///////////////////
nonAppResultsTimer.restart(); nonAppResultsTimer.restart();
const mathResultObject = { const mathResultObject = resultComp.createObject(null, {
key: `Math result: ${root.mathResult}`,
name: root.mathResult, name: root.mathResult,
clickActionName: Translation.tr("Copy"), verb: Translation.tr("Copy"),
type: Translation.tr("Math result"), type: Translation.tr("Math result"),
fontType: "monospace", fontType: LauncherSearchResult.FontType.Monospace,
materialSymbol: 'calculate', iconName: 'calculate',
iconType: LauncherSearchResult.IconType.Material,
execute: () => { execute: () => {
Quickshell.clipboardText = root.mathResult; 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 = { const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.query, Config.options.search.prefix.app)).map(entry => {
key: `cmd ${root.query}`, 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://", ""), 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"), type: Translation.tr("Run command"),
fontType: "monospace", fontType: LauncherSearchResult.FontType.Monospace,
materialSymbol: 'terminal', iconName: 'terminal',
iconType: LauncherSearchResult.IconType.Material,
execute: () => { execute: () => {
let cleanedCommand = root.query.replace("file://", ""); let cleanedCommand = root.query.replace("file://", "");
cleanedCommand = StringUtils.cleanPrefix(cleanedCommand, Config.options.search.prefix.shellCommand); cleanedCommand = StringUtils.cleanPrefix(cleanedCommand, Config.options.search.prefix.shellCommand);
if (cleanedCommand.startsWith(Config.options.search.prefix.shellCommand)) { if (cleanedCommand.startsWith(Config.options.search.prefix.shellCommand)) {
cleanedCommand = cleanedCommand.slice(Config.options.search.prefix.shellCommand.length); 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 = { const webSearchResultObject = resultComp.createObject(null, {
key: `website ${root.query}`,
name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch), name: StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch),
clickActionName: Translation.tr("Search"), verb: Translation.tr("Search"),
type: Translation.tr("Search the web"), type: Translation.tr("Search the web"),
materialSymbol: 'travel_explore', iconName: 'travel_explore',
iconType: LauncherSearchResult.IconType.Material,
execute: () => { execute: () => {
let query = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch); let query = StringUtils.cleanPrefix(root.query, Config.options.search.prefix.webSearch);
let url = Config.options.search.engineBaseUrl + query; let url = Config.options.search.engineBaseUrl + query;
@@ -225,20 +260,20 @@ Singleton {
} }
Qt.openUrlExternally(url); Qt.openUrlExternally(url);
} }
}; });
const launcherActionObjects = root.searchActions.map(action => { const launcherActionObjects = root.searchActions.map(action => {
const actionString = `${Config.options.search.prefix.action}${action.action}`; const actionString = `${Config.options.search.prefix.action}${action.action}`;
if (actionString.startsWith(root.query) || root.query.startsWith(actionString)) { if (actionString.startsWith(root.query) || root.query.startsWith(actionString)) {
return { return resultComp.createObject(null, {
key: `Action ${actionString}`,
name: root.query.startsWith(actionString) ? root.query : actionString, name: root.query.startsWith(actionString) ? root.query : actionString,
clickActionName: Translation.tr("Run"), verb: Translation.tr("Run"),
type: Translation.tr("Action"), type: Translation.tr("Action"),
materialSymbol: 'settings_suggest', iconName: 'settings_suggest',
iconType: LauncherSearchResult.IconType.Material,
execute: () => { execute: () => {
action.execute(root.query.split(" ").slice(1).join(" ")); action.execute(root.query.split(" ").slice(1).join(" "));
} }
}; });
} }
return null; return null;
}).filter(Boolean); }).filter(Boolean);
@@ -275,4 +310,9 @@ Singleton {
return result; return result;
} }
Component {
id: resultComp
LauncherSearchResult {}
}
} }