diff --git a/core/cmd/dms/commands_keybinds.go b/core/cmd/dms/commands_keybinds.go index c7611c4f..f442f622 100644 --- a/core/cmd/dms/commands_keybinds.go +++ b/core/cmd/dms/commands_keybinds.go @@ -57,6 +57,7 @@ var keybindsRemoveCmd = &cobra.Command{ } func init() { + keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON") keybindsShowCmd.Flags().String("path", "", "Override config path for the provider") keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay") keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked") @@ -110,12 +111,21 @@ func initializeProviders() { } } -func runKeybindsList(_ *cobra.Command, _ []string) { +func runKeybindsList(cmd *cobra.Command, _ []string) { providerList := keybinds.GetDefaultRegistry().List() + asJSON, _ := cmd.Flags().GetBool("json") + + if asJSON { + output, _ := json.Marshal(providerList) + fmt.Fprintln(os.Stdout, string(output)) + return + } + if len(providerList) == 0 { fmt.Fprintln(os.Stdout, "No providers available") return } + fmt.Fprintln(os.Stdout, "Available providers:") for _, name := range providerList { fmt.Fprintf(os.Stdout, " - %s\n", name) diff --git a/quickshell/Modals/Spotlight/SpotlightContent.qml b/quickshell/Modals/Spotlight/SpotlightContent.qml index c5ac4bba..890306a0 100644 --- a/quickshell/Modals/Spotlight/SpotlightContent.qml +++ b/quickshell/Modals/Spotlight/SpotlightContent.qml @@ -151,7 +151,7 @@ Item { fileSearchController.openSelected(); } event.accepted = true; - } else if (event.key === Qt.Key_Menu || event.key == Qt.Key_F10) { + } else if (event.key === Qt.Key_Menu || event.key == Qt.Key_F10 || (event.key === Qt.Key_Right && searchMode === "apps" && appLauncher.viewMode === "list")) { if (searchMode === "apps" && appLauncher.model.count > 0) { const selectedApp = appLauncher.model.get(appLauncher.selectedIndex); const menu = usePopupContextMenu ? popupContextMenu : layerContextMenuLoader.item; diff --git a/quickshell/Modals/Spotlight/SpotlightContextMenuContent.qml b/quickshell/Modals/Spotlight/SpotlightContextMenuContent.qml index 2ec5a5f6..7f5ac57c 100644 --- a/quickshell/Modals/Spotlight/SpotlightContextMenuContent.qml +++ b/quickshell/Modals/Spotlight/SpotlightContextMenuContent.qml @@ -11,12 +11,88 @@ Item { property int selectedMenuIndex: 0 property bool keyboardNavigation: false - signal hideRequested() + signal hideRequested readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null + readonly property var actualItem: (currentApp && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null + + function getPluginContextMenuActions() { + if (!currentApp || !currentApp.isPlugin || !actualItem) + return []; + + const pluginId = appLauncher.getPluginIdForItem(actualItem); + if (!pluginId) { + console.log("[ContextMenu] No pluginId found for item:", JSON.stringify(actualItem.categories)); + return []; + } + + const instance = PluginService.pluginInstances[pluginId]; + if (!instance) { + console.log("[ContextMenu] No instance for pluginId:", pluginId); + return []; + } + if (typeof instance.getContextMenuActions !== "function") { + console.log("[ContextMenu] Instance has no getContextMenuActions:", pluginId); + return []; + } + + const actions = instance.getContextMenuActions(actualItem); + if (!Array.isArray(actions)) + return []; + + return actions; + } + + function executePluginAction(actionData) { + if (!currentApp || !actualItem) + return; + + const pluginId = appLauncher.getPluginIdForItem(actualItem); + if (!pluginId) + return; + + const instance = PluginService.pluginInstances[pluginId]; + if (!instance) + return; + + if (typeof actionData === "function") { + actionData(); + } else if (typeof instance.executeContextMenuAction === "function") { + instance.executeContextMenuAction(actualItem, actionData); + } + + if (appLauncher) + appLauncher.updateFilteredModel(); + + hideRequested(); + } + readonly property var menuItems: { const items = []; + + if (currentApp && currentApp.isPlugin) { + const pluginActions = getPluginContextMenuActions(); + for (let i = 0; i < pluginActions.length; i++) { + const act = pluginActions[i]; + items.push({ + type: "item", + icon: act.icon || "", + text: act.text || act.name || "", + action: () => executePluginAction(act.action) + }); + } + if (items.length === 0) { + items.push({ + type: "item", + icon: "content_copy", + text: I18n.tr("Copy"), + action: launchCurrentApp + }); + } + return items; + } + const appId = desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : ""; const isPinned = SessionData.isPinnedApp(appId); @@ -172,18 +248,25 @@ Item { focus: keyboardNavigation Keys.onPressed: event => { - if (event.key === Qt.Key_Down) { + switch (event.key) { + case Qt.Key_Down: selectNext(); event.accepted = true; - } else if (event.key === Qt.Key_Up) { + break; + case Qt.Key_Up: selectPrevious(); event.accepted = true; - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + break; + case Qt.Key_Return: + case Qt.Key_Enter: activateSelected(); event.accepted = true; - } else if (event.key === Qt.Key_Escape) { + break; + case Qt.Key_Escape: + case Qt.Key_Left: hideRequested(); event.accepted = true; + break; } } diff --git a/quickshell/Modules/AppDrawer/AppLauncher.qml b/quickshell/Modules/AppDrawer/AppLauncher.qml index e5e9015f..4b3db9c5 100644 --- a/quickshell/Modules/AppDrawer/AppLauncher.qml +++ b/quickshell/Modules/AppDrawer/AppLauncher.qml @@ -205,7 +205,8 @@ Item { "isPlugin": isPluginItem, "isCore": app.isCore === true, "isBuiltInLauncher": app.isBuiltInLauncher === true, - "appIndex": uniqueApps.length - 1 + "appIndex": uniqueApps.length - 1, + "pinned": app._pinned === true }); } }); @@ -350,7 +351,11 @@ Item { function checkPluginTriggers(query) { if (!query) - return { triggered: false, pluginCategory: "", query: "" }; + return { + triggered: false, + pluginCategory: "", + query: "" + }; const builtInTriggers = AppSearchService.getBuiltInLauncherTriggers(); for (const trigger in builtInTriggers) { @@ -371,7 +376,11 @@ Item { } if (typeof PluginService === "undefined") - return { triggered: false, pluginCategory: "", query: "" }; + return { + triggered: false, + pluginCategory: "", + query: "" + }; const triggers = PluginService.getAllPluginTriggers(); for (const trigger in triggers) { @@ -391,7 +400,11 @@ Item { }; } - return { triggered: false, pluginCategory: "", query: "" }; + return { + triggered: false, + pluginCategory: "", + query: "" + }; } function getPluginIdForItem(item) { diff --git a/quickshell/Widgets/AppLauncherGridDelegate.qml b/quickshell/Widgets/AppLauncherGridDelegate.qml index d4269fd1..f18bd94b 100644 --- a/quickshell/Widgets/AppLauncherGridDelegate.qml +++ b/quickshell/Widgets/AppLauncherGridDelegate.qml @@ -39,22 +39,39 @@ Rectangle { anchors.centerIn: parent spacing: Theme.spacingS - AppIconRenderer { - property int computedIconSize: Math.min(root.maxIconSize, Math.max(root.minIconSize, root.cellWidth * root.iconSizeRatio)) - - width: computedIconSize - height: computedIconSize + Item { + width: iconRenderer.computedIconSize + height: iconRenderer.computedIconSize anchors.horizontalCenter: parent.horizontalCenter - iconValue: (model.icon && model.icon !== "") ? model.icon : "" - iconSize: computedIconSize - fallbackText: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" - materialIconSizeAdjustment: root.iconMaterialSizeAdjustment - unicodeIconScale: root.iconUnicodeScale - fallbackTextScale: Math.min(28, computedIconSize * 0.5) / computedIconSize - iconMargins: 0 - fallbackLeftMargin: root.iconFallbackLeftMargin - fallbackRightMargin: root.iconFallbackRightMargin - fallbackBottomMargin: root.iconFallbackBottomMargin + + AppIconRenderer { + id: iconRenderer + property int computedIconSize: Math.min(root.maxIconSize, Math.max(root.minIconSize, root.cellWidth * root.iconSizeRatio)) + + width: computedIconSize + height: computedIconSize + iconValue: (model.icon && model.icon !== "") ? model.icon : "" + iconSize: computedIconSize + fallbackText: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" + materialIconSizeAdjustment: root.iconMaterialSizeAdjustment + unicodeIconScale: root.iconUnicodeScale + fallbackTextScale: Math.min(28, computedIconSize * 0.5) / computedIconSize + iconMargins: 0 + fallbackLeftMargin: root.iconFallbackLeftMargin + fallbackRightMargin: root.iconFallbackRightMargin + fallbackBottomMargin: root.iconFallbackBottomMargin + } + + DankIcon { + visible: model.pinned === true + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: -4 + anchors.bottomMargin: -4 + name: "push_pin" + size: 14 + color: Theme.primary + } } StyledText { @@ -95,13 +112,11 @@ Rectangle { } } onPressAndHold: mouse => { - if (!root.isPlugin) { - const globalPos = mapToItem(null, mouse.x, mouse.y); - root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y); - } + const globalPos = mapToItem(null, mouse.x, mouse.y); + root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y); } onPressed: mouse => { - if (mouse.button === Qt.RightButton && !root.isPlugin) { + if (mouse.button === Qt.RightButton) { const globalPos = mapToItem(null, mouse.x, mouse.y); root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y); mouse.accepted = true; diff --git a/quickshell/Widgets/AppLauncherListDelegate.qml b/quickshell/Widgets/AppLauncherListDelegate.qml index f9531211..281ad60d 100644 --- a/quickshell/Widgets/AppLauncherListDelegate.qml +++ b/quickshell/Widgets/AppLauncherListDelegate.qml @@ -62,16 +62,31 @@ Rectangle { width: (model.icon !== undefined && model.icon !== "") ? (parent.width - root.iconSize - Theme.spacingL) : parent.width spacing: Theme.spacingXS - StyledText { + Row { width: parent.width - text: model.name || "" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - wrapMode: Text.NoWrap - maximumLineCount: 1 + spacing: Theme.spacingXS + + StyledText { + width: parent.width - (pinIcon.visible ? pinIcon.width + Theme.spacingXS : 0) + text: model.name || "" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + wrapMode: Text.NoWrap + maximumLineCount: 1 + anchors.verticalCenter: parent.verticalCenter + } + + DankIcon { + id: pinIcon + visible: model.pinned === true + name: "push_pin" + size: Theme.fontSizeMedium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } } StyledText { @@ -111,13 +126,11 @@ Rectangle { } } onPressAndHold: mouse => { - if (!root.isPlugin) { - const globalPos = mapToItem(null, mouse.x, mouse.y); - root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y); - } + const globalPos = mapToItem(null, mouse.x, mouse.y); + root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y); } onPressed: mouse => { - if (mouse.button === Qt.RightButton && !root.isPlugin) { + if (mouse.button === Qt.RightButton) { const globalPos = mapToItem(null, mouse.x, mouse.y); root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y); mouse.accepted = true;