diff --git a/ActionDialog.qml b/ActionDialog.qml index 3721801..67745ec 100644 --- a/ActionDialog.qml +++ b/ActionDialog.qml @@ -31,110 +31,101 @@ FocusScope { property color marketplaceRed: Color.urgent readonly property var actions: PaletteViewModel.actionOptions(plugin, readOnly) - readonly property var selectedAction: selectedChoice >= 0 - && selectedChoice < actions.length ? actions[selectedChoice] : null + readonly property var selectedAction: selectedChoice >= 0 && selectedChoice < actions.length ? actions[selectedChoice] : null readonly property bool helpDelayRunning: helpDelay.running - readonly property string selectedOperation: String( - selectedAction && selectedAction.operation || "cancel") - readonly property bool terminalAllowed: !readOnly && plugin - && plugin.installable === true - && String(plugin.repository || "").length > 0 - && String(plugin.source || "") !== "submission" + readonly property string selectedOperation: String(selectedAction && selectedAction.operation || "cancel") + readonly property bool terminalAllowed: !readOnly && plugin && (plugin.installable === true || plugin.installed === true) && String(plugin.source || "") !== "submission" readonly property bool terminalInstall: terminalAllowed && installInTerminal - readonly property string reviewedCommit: String(plugin - && (plugin.commit || plugin.listingValidatedCommit) || "") - readonly property bool marketplaceListed: plugin - && plugin.marketplaceListed === true - readonly property bool listedUserPlugin: marketplaceListed - && plugin.builtIn !== true - readonly property bool metricsAvailable: marketplaceListed - && plugin.metricsAvailable === true - readonly property string verificationStatus: String(plugin - && plugin.verificationStatus || "") - readonly property string activityState: CatalogModel.activityState( - plugin, Date.now()) - readonly property bool starsAvailable: root.listedUserPlugin && plugin - && plugin.stars !== null && plugin.stars !== undefined - readonly property string previewImageUrl: String(plugin - && plugin.previewImageUrl || "") - readonly property string previewThumbnailUrl: String(plugin - && plugin.previewThumbnailUrl || previewImageUrl) - readonly property int previewWidth: Number(plugin - && plugin.previewWidth || 0) - readonly property int previewHeight: Number(plugin - && plugin.previewHeight || 0) - readonly property bool hasPreview: readOnly - && previewImageUrl.length > 0 && previewThumbnailUrl.length > 0 - readonly property bool previewReady: hasPreview - && previewCardSource.length > 0 && previewDetailSource.length > 0 - readonly property int preferredReadOnlyHeight: - Style.spacing.panelPadding * 2 + contentColumn.implicitHeight - + Style.space(7) + Style.space(38) + readonly property string reviewedCommit: String(plugin && (plugin.commit || plugin.listingValidatedCommit) || "") + readonly property bool marketplaceListed: plugin && plugin.marketplaceListed === true + readonly property bool listedUserPlugin: marketplaceListed && plugin.builtIn !== true + readonly property bool metricsAvailable: marketplaceListed && plugin.metricsAvailable === true + readonly property string verificationStatus: String(plugin && plugin.verificationStatus || "") + readonly property string activityState: CatalogModel.activityState(plugin, Date.now()) + readonly property bool starsAvailable: root.listedUserPlugin && plugin && plugin.stars !== null && plugin.stars !== undefined + readonly property string previewImageUrl: String(plugin && plugin.previewImageUrl || "") + readonly property string previewThumbnailUrl: String(plugin && plugin.previewThumbnailUrl || previewImageUrl) + readonly property int previewWidth: Number(plugin && plugin.previewWidth || 0) + readonly property int previewHeight: Number(plugin && plugin.previewHeight || 0) + readonly property bool hasPreview: previewImageUrl.length > 0 || previewThumbnailUrl.length > 0 || previewCardSource.length > 0 || (plugin && plugin.installed === true) + readonly property bool previewReady: hasPreview && (previewCardSource.length > 0 || previewDetailSource.length > 0 || previewThumbnail.status === Image.Ready) + readonly property int preferredReadOnlyHeight: Style.spacing.panelPadding * 2 + contentColumn.implicitHeight + Style.space(7) + Style.space(38) readonly property var badgeItems: { var values = [] - if (activityState === "updated") values.push({ - label: "UPDATED", color: marketplaceYellow, tooltip: "Version updated " - + "within the last 12 hours" - }) - else if (activityState === "new") values.push({ - label: "NEW", color: marketplaceGreen, tooltip: "Listed within the " - + "last 12 hours" - }) - if (listedUserPlugin) values.push({ - label: verificationStatus === "verified" ? "VERIFIED" : "UNVERIFIED", - color: verificationStatus === "verified" - ? marketplaceGreen : marketplaceOrange, - tooltip: verificationHelp - }) + if (activityState === "updated") + values.push({ + label: "UPDATED", + color: marketplaceYellow, + tooltip: "Version updated " + "within the last 12 hours" + }) + else if (activityState === "new") + values.push({ + label: "NEW", + color: marketplaceGreen, + tooltip: "Listed within the " + "last 12 hours" + }) + if (listedUserPlugin) + values.push({ + label: verificationStatus === "verified" ? "VERIFIED" : "UNVERIFIED", + color: verificationStatus === "verified" ? marketplaceGreen : marketplaceOrange, + tooltip: verificationHelp + }) return values } readonly property var metricItems: { var values = [] - if (starsAvailable) values.push({ - label: "stars", icon: "\uf005", value: CatalogModel.formatCount( - plugin.stars), color: marketplaceYellow, - tooltip: "GitHub repository stars" - }) + if (starsAvailable) + values.push({ + label: "stars", + icon: "\uf005", + value: CatalogModel.formatCount(plugin.stars), + color: marketplaceYellow, + tooltip: "GitHub repository stars" + }) if (metricsAvailable && plugin) { - values.push({ label: "views", icon: "\uf441", + values.push({ + label: "views", + icon: "\uf441", value: CatalogModel.formatCount(plugin.views), - color: marketplaceOrange, tooltip: "Marketplace detail views" }) - values.push({ label: "copies", icon: "\uf0c5", + color: marketplaceOrange, + tooltip: "Marketplace detail views" + }) + values.push({ + label: "copies", + icon: "\uf0c5", value: CatalogModel.formatCount(plugin.copies), - color: marketplaceOrange, tooltip: "Successful command copies" }) - values.push({ label: "hearts", icon: "\uf004", + color: marketplaceOrange, + tooltip: "Successful command copies" + }) + values.push({ + label: "hearts", + icon: "\uf004", value: CatalogModel.formatCount(plugin.hearts), - color: marketplaceRed, tooltip: "Anonymous marketplace hearts" }) + color: marketplaceRed, + tooltip: "Anonymous marketplace hearts" + }) } return values } - readonly property string verificationHelp: verificationStatus === "verified" - ? "Verified means automated or maintainer checks were associated with " - + "the listed commit. It is not a security audit." - : "Unverified means there is no current verification. It does not mean " - + "the plugin is malicious." + readonly property string verificationHelp: verificationStatus === "verified" ? "Verified means automated or maintainer checks were associated with " + "the listed commit. It is not a security audit." : "Unverified means there is no current verification. It does not mean " + "the plugin is malicious." readonly property string operationText: { var id = String(plugin && plugin.id || "") - if (selectedOperation === "add") return (terminalInstall - ? "omarchy plugin add --enable" - : "omarchy plugin add --enable --yes") - + " && omarchy restart shell" + if (selectedOperation === "add") + return (terminalInstall ? "omarchy plugin add --enable" : "omarchy plugin add --enable --yes") + " && omarchy restart shell" if (selectedOperation === "remove") - return "omarchy plugin remove " + id + " --yes" + return (terminalInstall ? "omarchy plugin remove " + id : "omarchy plugin remove " + id + " --yes") + " (cleans widgets, caches, and state files)" if (selectedOperation === "update") - return "omarchy plugin update " + id - + " --yes && omarchy restart shell" + return (terminalInstall ? "omarchy plugin update " + id : "omarchy plugin update " + id + " --yes") + " && omarchy restart shell" if (selectedOperation === "enable") return "omarchy plugin enable " + id if (selectedOperation === "disable") return "omarchy plugin disable " + id return "No system change" } - readonly property bool selectedMutates: ["add", "remove", "update", - "enable", "disable"].indexOf(selectedOperation) >= 0 + readonly property bool selectedMutates: ["add", "remove", "update", "enable", "disable"].indexOf(selectedOperation) >= 0 signal actionRequested(string operation) - signal canceled() + signal canceled signal previewRequested(string url, string name, int width, int height) signal terminalInstallToggled(bool enabled) @@ -153,26 +144,30 @@ FocusScope { } function selectChoice(index, immediateHelp) { - if (index < 0 || index >= actions.length) return + if (index < 0 || index >= actions.length) + return selectedChoice = index helpDelay.stop() helpText = "" var action = actions[index] if (action.available === false && String(action.reason || "")) { - if (immediateHelp === true) helpText = String(action.reason) - else helpDelay.restart() + if (immediateHelp === true) + helpText = String(action.reason) + else + helpDelay.restart() } } function moveChoice(offset) { - if (actions.length === 0) return - selectChoice((selectedChoice + offset + actions.length) - % actions.length, false) + if (actions.length === 0) + return + selectChoice((selectedChoice + offset + actions.length) % actions.length, false) } function choose() { var action = selectedAction - if (!action) return + if (!action) + return if (readOnly) { canceled() return @@ -185,11 +180,13 @@ FocusScope { canceled() return } - if (!busy) actionRequested(String(action.operation)) + if (!busy) + actionRequested(String(action.operation)) } function handleKey(event) { - if (!opened) return false + if (!opened) + return false if (event.key === Qt.Key_Left || event.key === Qt.Key_Backtab) { moveChoice(-1) return true @@ -202,8 +199,7 @@ FocusScope { terminalInstallToggled(!installInTerminal) return true } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter - || event.key === Qt.Key_Space) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) { choose() return true } @@ -211,10 +207,9 @@ FocusScope { } function requestPreview() { - if (!previewReady) return - previewRequested(previewDetailSource, - String(plugin && plugin.name || "Plugin preview"), - previewWidth, previewHeight) + if (!previewReady) + return + previewRequested(previewDetailSource, String(plugin && plugin.name || "Plugin preview"), previewWidth, previewHeight) } visible: opened @@ -255,225 +250,155 @@ FocusScope { width: contentFlick.width spacing: Style.space(7) - Text { - width: parent.width - text: root.readOnly ? "PLUGIN INFORMATION" : "PLUGIN ACTIONS" - textFormat: Text.PlainText - color: root.marketplaceOrange - font.family: root.fontFamily - font.pixelSize: Style.font.body - font.bold: true - font.letterSpacing: Style.space(1) - elide: Text.ElideRight - } - - Text { - width: parent.width - text: String(root.plugin && root.plugin.name || "") - textFormat: Text.PlainText - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.heading + Style.space(2) - font.bold: true - elide: Text.ElideRight - } - - Text { - width: parent.width - text: String(root.plugin && root.plugin.id || "") - textFormat: Text.PlainText - color: root.marketplaceOrange - opacity: 0.88 - font.family: root.fontFamily - font.pixelSize: Style.font.body - elide: Text.ElideRight - } - - Text { - visible: String(root.plugin && root.plugin.description || "").length > 0 - width: parent.width - text: String(root.plugin && root.plugin.description || "") - textFormat: Text.PlainText - color: root.foreground - opacity: 0.90 - font.family: root.fontFamily - font.pixelSize: Style.font.subtitle - lineHeight: 1.25 - wrapMode: Text.Wrap - maximumLineCount: root.readOnly ? 100 : 2 - elide: root.readOnly ? Text.ElideNone : Text.ElideRight - } - - Rectangle { - visible: root.hasPreview - width: parent.width - height: visible ? Style.space(250) : 0 - radius: Style.cornerRadius - color: root.background - border.width: Math.max(1, Style.space(1)) - border.color: Util.alpha(root.marketplaceOrange, 0.48) - clip: true - - Image { - id: previewThumbnail - anchors.fill: parent - anchors.margins: Math.max(1, Style.space(1)) - source: root.opened && root.previewReady - ? root.previewCardSource : "" - asynchronous: true - cache: true - fillMode: Image.PreserveAspectFit - mipmap: true + Text { + width: parent.width + text: root.readOnly ? "PLUGIN INFORMATION" : "PLUGIN ACTIONS" + textFormat: Text.PlainText + color: root.marketplaceOrange + font.family: root.fontFamily + font.pixelSize: Style.font.body + font.bold: true + font.letterSpacing: Style.space(1) + elide: Text.ElideRight } Text { - anchors.centerIn: parent - visible: root.hasPreview && (root.previewLoading - || previewThumbnail.status === Image.Null - || previewThumbnail.status === Image.Loading - || previewThumbnail.status === Image.Error - ) - text: root.previewFailed || (!root.previewLoading - && previewThumbnail.status === Image.Error) - ? "Preview could not be loaded" : "Loading preview..." + width: parent.width + text: String(root.plugin && root.plugin.name || "") textFormat: Text.PlainText color: root.foreground - opacity: 0.72 + font.family: root.fontFamily + font.pixelSize: Style.font.heading + Style.space(2) + font.bold: true + elide: Text.ElideRight + } + + Text { + width: parent.width + text: String(root.plugin && root.plugin.id || "") + textFormat: Text.PlainText + color: root.marketplaceOrange + opacity: 0.88 font.family: root.fontFamily font.pixelSize: Style.font.body + elide: Text.ElideRight } - MouseArea { - id: previewClickArea - anchors.centerIn: previewThumbnail - width: previewThumbnail.paintedWidth - height: previewThumbnail.paintedHeight - enabled: root.previewReady - && previewThumbnail.status === Image.Ready - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: root.requestPreview() + Text { + visible: String(root.plugin && root.plugin.description || "").length > 0 + width: parent.width + text: String(root.plugin && root.plugin.description || "") + textFormat: Text.PlainText + color: root.foreground + opacity: 0.90 + font.family: root.fontFamily + font.pixelSize: Style.font.subtitle + lineHeight: 1.25 + wrapMode: Text.Wrap + maximumLineCount: root.readOnly ? 100 : 2 + elide: root.readOnly ? Text.ElideNone : Text.ElideRight } Rectangle { - visible: previewClickArea.enabled - anchors.right: previewClickArea.right - anchors.bottom: previewClickArea.bottom - anchors.margins: Style.spacing.sm - width: previewHint.implicitWidth + Style.spacing.md - height: Style.space(28) - radius: Style.space(4) - color: Util.alpha(root.background, 0.90) + visible: root.hasPreview + width: parent.width + height: visible ? Style.space(250) : 0 + radius: Style.cornerRadius + color: root.background border.width: Math.max(1, Style.space(1)) - border.color: Util.alpha(root.marketplaceOrange, 0.62) - - Text { - id: previewHint - anchors.centerIn: parent - text: "\uf065 Enlarge" - textFormat: Text.PlainText - color: root.marketplaceOrange - font.family: Style.font.family - font.pixelSize: Style.font.body - font.bold: true - } - } - } - - Rectangle { - width: parent.width - height: Math.max(1, Style.space(1)) - color: Util.alpha(root.foreground, 0.16) - } - - Row { - width: parent.width - height: Style.space(38) - spacing: Style.spacing.md - - Column { - width: (parent.width - parent.spacing) / 2 - spacing: Style.space(2) - - Text { - text: "AUTHOR" - textFormat: Text.PlainText - color: root.marketplaceOrange - font.family: root.fontFamily - font.pixelSize: Style.font.caption - font.bold: true + border.color: Util.alpha(root.marketplaceOrange, 0.48) + clip: true + + Image { + id: previewThumbnail + anchors.fill: parent + anchors.margins: Math.max(1, Style.space(1)) + source: { + if (!root.opened) + return "" + if (root.previewCardSource) + return root.previewCardSource + if (root.previewDetailSource) + return root.previewDetailSource + if (root.previewThumbnailUrl) + return root.previewThumbnailUrl + if (root.previewImageUrl) + return root.previewImageUrl + if (root.plugin && root.plugin.id) { + var cfg = Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config") + return "file://" + cfg + "/omarchy/plugins/" + root.plugin.id + "/preview.png" + } + return "" + } + asynchronous: true + cache: true + fillMode: Image.PreserveAspectFit + mipmap: true } Text { - width: parent.width - text: String(root.plugin && root.plugin.author || "Unknown") + anchors.centerIn: parent + visible: root.hasPreview && (root.previewLoading || previewThumbnail.status === Image.Null || previewThumbnail.status === Image.Loading || previewThumbnail.status === Image.Error) + text: root.previewFailed || (!root.previewLoading && previewThumbnail.status === Image.Error) ? "Preview could not be loaded" : "Loading preview..." textFormat: Text.PlainText color: root.foreground + opacity: 0.72 font.family: root.fontFamily - font.pixelSize: Style.font.subtitle - elide: Text.ElideRight + font.pixelSize: Style.font.body } - } - Column { - width: (parent.width - parent.spacing) / 2 - spacing: Style.space(2) - - Text { - text: "VERSION" - textFormat: Text.PlainText - color: root.marketplaceOrange - font.family: root.fontFamily - font.pixelSize: Style.font.caption - font.bold: true + MouseArea { + id: previewClickArea + anchors.centerIn: previewThumbnail + width: previewThumbnail.paintedWidth + height: previewThumbnail.paintedHeight + enabled: root.previewReady && previewThumbnail.status === Image.Ready + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: root.requestPreview() } - Text { - width: parent.width - text: String(root.plugin && root.plugin.version || "Unknown") - textFormat: Text.PlainText - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.subtitle - elide: Text.ElideRight + Rectangle { + visible: previewClickArea.enabled + anchors.right: previewClickArea.right + anchors.bottom: previewClickArea.bottom + anchors.margins: Style.spacing.sm + width: previewHint.implicitWidth + Style.spacing.md + height: Style.space(28) + radius: Style.space(4) + color: Util.alpha(root.background, 0.90) + border.width: Math.max(1, Style.space(1)) + border.color: Util.alpha(root.marketplaceOrange, 0.62) + + Text { + id: previewHint + anchors.centerIn: parent + text: "\uf065 Enlarge" + textFormat: Text.PlainText + color: root.marketplaceOrange + font.family: Style.font.family + font.pixelSize: Style.font.body + font.bold: true + } } } - } - Text { - width: parent.width - text: "SOURCE " + String(root.plugin - && root.plugin.sourceLabel || "Unknown") - + (String(root.plugin && root.plugin.warning || "") - ? " " + String(root.plugin.warning) : "") - textFormat: Text.PlainText - color: String(root.plugin && root.plugin.warning || "") - ? root.marketplaceOrange : root.foreground - opacity: 0.88 - font.family: root.fontFamily - font.pixelSize: Style.font.subtitle - elide: Text.ElideRight - } + Rectangle { + width: parent.width + height: Math.max(1, Style.space(1)) + color: Util.alpha(root.foreground, 0.16) + } - Rectangle { - width: parent.width - height: Style.space(root.reviewedCommit.length > 0 ? 62 : 38) - radius: Style.cornerRadius - color: Util.alpha(root.foreground, 0.055) - border.width: Math.max(1, Style.space(1)) - border.color: Util.alpha(root.foreground, 0.12) - - Column { - anchors.fill: parent - anchors.margins: Style.spacing.sm - spacing: Style.space(4) + Row { + width: parent.width + height: Style.space(38) + spacing: Style.spacing.md - Row { - width: parent.width - height: Style.space(18) + Column { + width: (parent.width - parent.spacing) / 2 + spacing: Style.space(2) Text { - width: Style.space(92) - text: "REPOSITORY" + text: "AUTHOR" textFormat: Text.PlainText color: root.marketplaceOrange font.family: root.fontFamily @@ -482,25 +407,22 @@ FocusScope { } Text { - width: parent.width - Style.space(92) - text: String(root.plugin - && root.plugin.repository || "Not supplied") + width: parent.width + text: String(root.plugin && root.plugin.author || "Unknown") textFormat: Text.PlainText color: root.foreground font.family: root.fontFamily - font.pixelSize: Style.font.body - elide: Text.ElideMiddle + font.pixelSize: Style.font.subtitle + elide: Text.ElideRight } } - Row { - visible: root.reviewedCommit.length > 0 - width: parent.width - height: visible ? Style.space(18) : 0 + Column { + width: (parent.width - parent.spacing) / 2 + spacing: Style.space(2) Text { - width: Style.space(92) - text: "REVIEWED" + text: "VERSION" textFormat: Text.PlainText color: root.marketplaceOrange font.family: root.fontFamily @@ -509,273 +431,333 @@ FocusScope { } Text { - width: parent.width - Style.space(92) - text: root.reviewedCommit + width: parent.width + text: String(root.plugin && root.plugin.version || "Unknown") textFormat: Text.PlainText color: root.foreground - opacity: 0.78 font.family: root.fontFamily - font.pixelSize: Style.font.body - elide: Text.ElideMiddle + font.pixelSize: Style.font.subtitle + elide: Text.ElideRight } } } - } - - Flow { - id: badgeFlow - visible: root.badgeItems.length > 0 - width: parent.width - height: visible ? Style.space(24) : 0 - spacing: Style.space(6) - - Repeater { - model: root.badgeItems - - delegate: Rectangle { - id: statusBadge - required property var modelData - width: badgeText.implicitWidth + Style.spacing.md - height: Style.space(24) - radius: Style.space(3) - color: Util.alpha(modelData.color, 0.10) - border.width: Math.max(1, Style.space(1)) - border.color: Util.alpha(modelData.color, 0.72) - Text { - id: badgeText - anchors.centerIn: parent - text: statusBadge.modelData.label - textFormat: Text.PlainText - color: statusBadge.modelData.color - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - font.bold: true - font.letterSpacing: Style.space(1) - } - - MouseArea { - id: badgeHover - anchors.fill: parent - hoverEnabled: true - } - - PanelToolTip { - visible: badgeHover.containsMouse - text: String(statusBadge.modelData.tooltip || "") - fontFamily: root.fontFamily - } - } + Text { + width: parent.width + text: "SOURCE " + String(root.plugin && root.plugin.sourceLabel || "Unknown") + (String(root.plugin && root.plugin.warning || "") ? " " + String(root.plugin.warning) : "") + textFormat: Text.PlainText + color: String(root.plugin && root.plugin.warning || "") ? root.marketplaceOrange : root.foreground + opacity: 0.88 + font.family: root.fontFamily + font.pixelSize: Style.font.subtitle + elide: Text.ElideRight } - } - - Flow { - id: metricFlow - readonly property int columnCount: width < Style.space(560) ? 2 : 4 - readonly property real chipWidth: (width - spacing - * (columnCount - 1)) / columnCount - visible: root.metricItems.length > 0 - width: parent.width - height: visible ? Math.ceil(root.metricItems.length / columnCount) - * Style.space(42) + Math.max(0, - Math.ceil(root.metricItems.length / columnCount) - 1) * spacing : 0 - spacing: Style.space(7) - Repeater { - model: root.metricItems + Rectangle { + width: parent.width + height: Style.space(root.reviewedCommit.length > 0 ? 62 : 38) + radius: Style.cornerRadius + color: Util.alpha(root.foreground, 0.055) + border.width: Math.max(1, Style.space(1)) + border.color: Util.alpha(root.foreground, 0.12) - delegate: Rectangle { - id: metricChip - required property var modelData - width: metricFlow.chipWidth - height: Style.space(42) - radius: Style.cornerRadius - color: Util.alpha(modelData.color, 0.075) - border.width: Math.max(1, Style.space(1)) - border.color: Util.alpha(modelData.color, 0.46) + Column { + anchors.fill: parent + anchors.margins: Style.spacing.sm + spacing: Style.space(4) Row { - id: metricContent - anchors.centerIn: parent - height: Math.max(metricIcon.implicitHeight, - metricLabel.implicitHeight) - spacing: Style.space(7) + width: parent.width + height: Style.space(18) Text { - id: metricIcon - height: metricContent.height - text: metricChip.modelData.icon + width: Style.space(92) + text: "REPOSITORY" textFormat: Text.PlainText - color: metricChip.modelData.color - font.family: Style.font.family - font.pixelSize: Style.font.iconLarge - verticalAlignment: Text.AlignVCenter + color: root.marketplaceOrange + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true } Text { - id: metricLabel - height: metricContent.height - text: metricChip.modelData.value + " " - + metricChip.modelData.label + width: parent.width - Style.space(92) + text: String(root.plugin && root.plugin.repository || "Not supplied") textFormat: Text.PlainText color: root.foreground font.family: root.fontFamily - font.pixelSize: Style.font.subtitle - font.bold: true - verticalAlignment: Text.AlignVCenter + font.pixelSize: Style.font.body + elide: Text.ElideMiddle } } - MouseArea { - id: metricHover - anchors.fill: parent - hoverEnabled: true - } + Row { + visible: root.reviewedCommit.length > 0 + width: parent.width + height: visible ? Style.space(18) : 0 - PanelToolTip { - visible: metricHover.containsMouse - text: String(metricChip.modelData.tooltip || "") - fontFamily: root.fontFamily + Text { + width: Style.space(92) + text: "REVIEWED" + textFormat: Text.PlainText + color: root.marketplaceOrange + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Text { + width: parent.width - Style.space(92) + text: root.reviewedCommit + textFormat: Text.PlainText + color: root.foreground + opacity: 0.78 + font.family: root.fontFamily + font.pixelSize: Style.font.body + elide: Text.ElideMiddle + } } } } - } - Text { - visible: root.marketplaceListed && !root.metricsAvailable - width: parent.width - text: "Interaction totals are not cached yet" - textFormat: Text.PlainText - color: root.marketplaceOrange - opacity: 0.82 - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - } + Flow { + id: badgeFlow + visible: root.badgeItems.length > 0 + width: parent.width + height: visible ? Style.space(24) : 0 + spacing: Style.space(6) + + Repeater { + model: root.badgeItems + + delegate: Rectangle { + id: statusBadge + required property var modelData + width: badgeText.implicitWidth + Style.spacing.md + height: Style.space(24) + radius: Style.space(3) + color: Util.alpha(modelData.color, 0.10) + border.width: Math.max(1, Style.space(1)) + border.color: Util.alpha(modelData.color, 0.72) - Text { - visible: !root.marketplaceListed - width: parent.width - text: "Not listed on Omarchy Plugins" - textFormat: Text.PlainText - color: root.marketplaceOrange - opacity: 0.82 - font.family: root.fontFamily - font.pixelSize: Style.font.subtitle - } + Text { + id: badgeText + anchors.centerIn: parent + text: statusBadge.modelData.label + textFormat: Text.PlainText + color: statusBadge.modelData.color + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: true + font.letterSpacing: Style.space(1) + } - Flow { - visible: root.marketplaceListed && root.plugin - && Array.isArray(root.plugin.tags) && root.plugin.tags.length > 0 - width: parent.width - spacing: Style.space(4) + MouseArea { + id: badgeHover + anchors.fill: parent + hoverEnabled: true + } - Repeater { - model: root.plugin && Array.isArray(root.plugin.tags) - ? root.plugin.tags : [] + PanelToolTip { + visible: badgeHover.containsMouse + text: String(statusBadge.modelData.tooltip || "") + fontFamily: root.fontFamily + } + } + } + } - delegate: Rectangle { - required property string modelData - width: tagText.implicitWidth + Style.spacing.sm - height: Style.space(24) - radius: Style.space(4) - color: Util.alpha(root.marketplaceOrange, 0.065) - border.width: Math.max(1, Style.space(1)) - border.color: Util.alpha(root.marketplaceOrange, 0.34) + Flow { + id: metricFlow + readonly property int columnCount: width < Style.space(560) ? 2 : 4 + readonly property real chipWidth: (width - spacing * (columnCount - 1)) / columnCount + visible: root.metricItems.length > 0 + width: parent.width + height: visible ? Math.ceil(root.metricItems.length / columnCount) * Style.space(42) + Math.max(0, Math.ceil(root.metricItems.length / columnCount) - 1) * spacing : 0 + spacing: Style.space(7) + + Repeater { + model: root.metricItems + + delegate: Rectangle { + id: metricChip + required property var modelData + width: metricFlow.chipWidth + height: Style.space(42) + radius: Style.cornerRadius + color: Util.alpha(modelData.color, 0.075) + border.width: Math.max(1, Style.space(1)) + border.color: Util.alpha(modelData.color, 0.46) + + Row { + id: metricContent + anchors.centerIn: parent + height: Math.max(metricIcon.implicitHeight, metricLabel.implicitHeight) + spacing: Style.space(7) + + Text { + id: metricIcon + height: metricContent.height + text: metricChip.modelData.icon + textFormat: Text.PlainText + color: metricChip.modelData.color + font.family: Style.font.family + font.pixelSize: Style.font.iconLarge + verticalAlignment: Text.AlignVCenter + } + + Text { + id: metricLabel + height: metricContent.height + text: metricChip.modelData.value + " " + metricChip.modelData.label + textFormat: Text.PlainText + color: root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.subtitle + font.bold: true + verticalAlignment: Text.AlignVCenter + } + } - Text { - id: tagText - anchors.centerIn: parent - text: modelData - textFormat: Text.PlainText - color: root.marketplaceOrange - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall + MouseArea { + id: metricHover + anchors.fill: parent + hoverEnabled: true + } + + PanelToolTip { + visible: metricHover.containsMouse + text: String(metricChip.modelData.tooltip || "") + fontFamily: root.fontFamily + } } } } - } - Item { - visible: root.terminalAllowed - width: parent.width - height: visible ? Style.space(28) : 0 + Text { + visible: root.marketplaceListed && !root.metricsAvailable + width: parent.width + text: "Interaction totals are not cached yet" + textFormat: Text.PlainText + color: root.marketplaceOrange + opacity: 0.82 + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } Text { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: "Run Add in Omarchy terminal (T)" + visible: !root.marketplaceListed + width: parent.width + text: "Not listed on Omarchy Plugins" textFormat: Text.PlainText - color: root.installInTerminal ? root.foreground : Color.muted + color: root.marketplaceOrange + opacity: 0.82 font.family: root.fontFamily - font.pixelSize: Style.font.body + font.pixelSize: Style.font.subtitle } - ToggleSwitch { - id: terminalToggle - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - trackHeight: Style.space(18) - cursorPad: Style.space(3) - checked: root.installInTerminal - foreground: root.foreground - onToggled: root.terminalInstallToggled(!checked) - - PanelToolTip { - visible: terminalToggle.containsMouse - text: root.installInTerminal - ? "Use the faster background installer" - : "Stream output and use native interactive prompts" - fontFamily: root.fontFamily + Flow { + visible: root.marketplaceListed && root.plugin && Array.isArray(root.plugin.tags) && root.plugin.tags.length > 0 + width: parent.width + spacing: Style.space(4) + + Repeater { + model: root.plugin && Array.isArray(root.plugin.tags) ? root.plugin.tags : [] + + delegate: Rectangle { + required property string modelData + width: tagText.implicitWidth + Style.spacing.sm + height: Style.space(24) + radius: Style.space(4) + color: Util.alpha(root.marketplaceOrange, 0.065) + border.width: Math.max(1, Style.space(1)) + border.color: Util.alpha(root.marketplaceOrange, 0.34) + + Text { + id: tagText + anchors.centerIn: parent + text: modelData + textFormat: Text.PlainText + color: root.marketplaceOrange + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + } } } - } - Rectangle { - visible: root.selectedMutates - width: parent.width - height: visible ? Style.space(root.selectedOperation === "add" - ? 48 : 34) : 0 - radius: Style.cornerRadius - color: Util.alpha(root.selectedOperation === "add" - ? root.warningColor : root.foreground, 0.10) + Item { + visible: root.terminalAllowed + width: parent.width + height: visible ? Style.space(28) : 0 - Text { - anchors.fill: parent - anchors.margins: Style.spacing.sm - text: root.selectedOperation === "add" - ? "Plugins run unsandboxed. Marketplace checks are not a " - + "security audit.\n" + root.operationText - : root.operationText - textFormat: Text.PlainText - color: root.selectedOperation === "add" - ? root.warningColor : root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.body - wrapMode: Text.Wrap - elide: Text.ElideRight + Text { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: root.selectedOperation === "remove" ? "Run Uninstall in Omarchy terminal (T)" : (root.selectedOperation === "update" ? "Run Update in Omarchy terminal (T)" : "Run Add in Omarchy terminal (T)") + textFormat: Text.PlainText + color: root.installInTerminal ? root.foreground : Color.muted + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + + ToggleSwitch { + id: terminalToggle + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + trackHeight: Style.space(18) + cursorPad: Style.space(3) + checked: root.installInTerminal + foreground: root.foreground + onToggled: root.terminalInstallToggled(!checked) + + PanelToolTip { + visible: terminalToggle.containsMouse + text: root.installInTerminal ? "Use the faster background installer" : "Stream output and use native interactive prompts" + fontFamily: root.fontFamily + } + } } - } - Rectangle { - visible: root.helpText.length > 0 - width: parent.width - height: visible ? Style.space(42) : 0 - radius: Style.cornerRadius - color: Util.alpha(root.warningColor, 0.12) + Rectangle { + visible: root.selectedMutates + width: parent.width + height: visible ? Style.space(root.selectedOperation === "add" ? 48 : 34) : 0 + radius: Style.cornerRadius + color: Util.alpha(root.selectedOperation === "add" ? root.warningColor : root.foreground, 0.10) - Text { - anchors.fill: parent - anchors.margins: Style.spacing.sm - text: root.helpText - textFormat: Text.PlainText - color: root.warningColor - font.family: root.fontFamily - font.pixelSize: Style.font.body - wrapMode: Text.Wrap - elide: Text.ElideRight + Text { + anchors.fill: parent + anchors.margins: Style.spacing.sm + text: root.selectedOperation === "add" ? "Plugins run unsandboxed. Marketplace checks are not a " + "security audit.\n" + root.operationText : root.operationText + textFormat: Text.PlainText + color: root.selectedOperation === "add" ? root.warningColor : root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.body + wrapMode: Text.Wrap + elide: Text.ElideRight + } } - } + Rectangle { + visible: root.helpText.length > 0 + width: parent.width + height: visible ? Style.space(42) : 0 + radius: Style.cornerRadius + color: Util.alpha(root.warningColor, 0.12) + + Text { + anchors.fill: parent + anchors.margins: Style.spacing.sm + text: root.helpText + textFormat: Text.PlainText + color: root.warningColor + font.family: root.fontFamily + font.pixelSize: Style.font.body + wrapMode: Text.Wrap + elide: Text.ElideRight + } + } } } @@ -797,34 +779,22 @@ FocusScope { id: actionButton required property var modelData required property int index - width: (actionRow.width - actionRow.spacing - * Math.max(0, root.actions.length - 1)) - / Math.max(1, root.actions.length) + width: (actionRow.width - actionRow.spacing * Math.max(0, root.actions.length - 1)) / Math.max(1, root.actions.length) height: parent.height radius: Style.cornerRadius - color: root.selectedChoice === index - ? root.selectedBackground : "transparent" - border.width: root.readOnly && root.selectedChoice === index - ? Math.max(1, Style.space(1)) : 0 + color: root.selectedChoice === index ? root.selectedBackground : "transparent" + border.width: root.readOnly && root.selectedChoice === index ? Math.max(1, Style.space(1)) : 0 border.color: root.marketplaceYellow - opacity: modelData.available === false - && root.selectedChoice !== index ? 0.42 : 1 + opacity: modelData.available === false && root.selectedChoice !== index ? 0.42 : 1 Text { anchors.centerIn: parent - text: root.busy && root.selectedChoice === actionButton.index - && actionButton.modelData.operation !== "cancel" - && actionButton.modelData.operation !== "close" - ? "Working..." : actionButton.modelData.label - color: root.selectedChoice === actionButton.index - ? root.selectedText - : (actionButton.modelData.dangerous === true - ? root.warningColor : root.foreground) + text: root.busy && root.selectedChoice === actionButton.index && actionButton.modelData.operation !== "cancel" && actionButton.modelData.operation !== "close" ? "Working..." : actionButton.modelData.label + color: root.selectedChoice === actionButton.index ? root.selectedText : (actionButton.modelData.dangerous === true ? root.warningColor : root.foreground) font.family: root.fontFamily font.pixelSize: Style.font.title font.bold: root.readOnly } - } } } diff --git a/CatalogModel.js b/CatalogModel.js index ea077db..0304fe7 100644 --- a/CatalogModel.js +++ b/CatalogModel.js @@ -105,10 +105,15 @@ function activityState(record, nowValue) { function marketplaceAssetUrl(value, variant) { var path = cleanText(value) + if (!path) return "" + if (path.indexOf("http://") === 0 || path.indexOf("https://") === 0 || path.indexOf("file://") === 0) + return path var suffix = variant === "card" ? "card" : "detail" var pattern = new RegExp("^assets/img/plugins/[A-Za-z0-9._-]+-" + suffix + "\\.webp$") - return pattern.test(path) ? "https://omarchyplugins.com/" + path : "" + if (pattern.test(path)) return "https://omarchyplugins.com/" + path + if (path.indexOf("assets/img/plugins/") === 0) return "https://omarchyplugins.com/" + path + return "" } function normalizeRecord(value) { @@ -136,11 +141,16 @@ function normalizeRecord(value) { record.addedAt = cleanText(record.addedAt) record.listedAt = cleanText(record.listedAt) record.versionUpdatedAt = cleanText(record.versionUpdatedAt) - record.previewImageUrl = record.marketplaceListed - ? marketplaceAssetUrl(record.previewImage, "detail") : "" - record.previewThumbnailUrl = record.marketplaceListed - ? marketplaceAssetUrl(record.previewThumbnail, "card") - || record.previewImageUrl : "" + record.previewImageUrl = marketplaceAssetUrl(record.previewImage, "detail") + record.previewThumbnailUrl = marketplaceAssetUrl(record.previewThumbnail, "card") + || record.previewImageUrl + if (!record.previewImageUrl && !record.previewThumbnailUrl && record.id) { + var configBase = (typeof Quickshell !== "undefined" && Quickshell && Quickshell.env) + ? (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + : "/home/harsh/.config" + record.previewThumbnailUrl = "file://" + configBase + "/omarchy/plugins/" + record.id + "/preview.png" + record.previewImageUrl = record.previewThumbnailUrl + } record.previewWidth = count(record.previewWidth) record.previewHeight = count(record.previewHeight) record.previewThumbnailWidth = count(record.previewThumbnailWidth) diff --git a/PaletteResultRow.qml b/PaletteResultRow.qml index ff73c84..cf243df 100644 --- a/PaletteResultRow.qml +++ b/PaletteResultRow.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import qs.Commons Rectangle { @@ -19,17 +20,23 @@ Rectangle { required property bool separatorBefore required property bool dangerous + property string previewThumbnailUrl: "" + property string previewImageUrl: "" + property bool installed: false + property bool builtIn: false + property bool selected: false property bool settingsMenuOpen: false property bool pointerInteractive: true - property int rowHeight: Style.space(60) + property int rowHeight: Style.space(62) property color foreground: Color.menu.text property color selectedBackground: Color.menu.selectedBackground property color selectedText: Color.menu.selectedText property color urgent: Color.urgent + property color accent: Color.accent - signal hovered() - signal activated() + signal hovered + signal activated signal repositoryRequested(string url) height: rowHeight @@ -48,15 +55,104 @@ Rectangle { MouseArea { anchors.fill: parent hoverEnabled: true - cursorShape: root.pointerInteractive - ? Qt.PointingHandCursor : Qt.ArrowCursor - onEntered: if (root.pointerInteractive) root.hovered() - onClicked: if (root.pointerInteractive) root.activated() + cursorShape: root.pointerInteractive ? Qt.PointingHandCursor : Qt.ArrowCursor + onEntered: if (root.pointerInteractive) + root.hovered() + onClicked: if (root.pointerInteractive) + root.activated() } - Column { + // Left Section: Index Number + Text { + id: indexLabel + visible: !root.settingsMenuOpen anchors.left: parent.left - anchors.leftMargin: Style.spacing.md + anchors.leftMargin: Style.spacing.sm + anchors.verticalCenter: parent.verticalCenter + width: Style.space(26) + text: String(root.index + 1) + textFormat: Text.PlainText + color: root.selected ? root.selectedText : Qt.darker(root.foreground, 1.45) + font.family: Style.font.menuFamily + font.pixelSize: Style.font.body + font.bold: true + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideNone + } + + // Thumbnail Container + Rectangle { + id: thumbBox + visible: !root.settingsMenuOpen + anchors.left: indexLabel.right + anchors.leftMargin: Style.space(4) + anchors.verticalCenter: parent.verticalCenter + width: Style.space(48) + height: Style.space(40) + radius: Style.space(6) + color: Util.alpha(root.foreground, 0.08) + border.width: 1 + border.color: root.selected ? Util.alpha(root.selectedText, 0.35) : Util.alpha(root.foreground, 0.14) + clip: true + + Image { + id: thumbImage + anchors.fill: parent + anchors.margins: 1 + asynchronous: true + cache: true + fillMode: Image.PreserveAspectCrop + mipmap: true + source: { + if (root.previewThumbnailUrl) + return root.previewThumbnailUrl + if (root.previewImageUrl) + return root.previewImageUrl + if (root.pluginId) { + var cfg = Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config") + return "file://" + cfg + "/omarchy/plugins/" + root.pluginId + "/preview.png" + } + return "" + } + } + + Text { + anchors.centerIn: parent + visible: thumbImage.status !== Image.Ready + text: { + var id = root.pluginId.toLowerCase() + if (id.indexOf("ram") >= 0 || id.indexOf("memory") >= 0) + return "󰍛" + if (id.indexOf("monitor") >= 0 || id.indexOf("display") >= 0) + return "󰍹" + if (id.indexOf("audio") >= 0 || id.indexOf("volume") >= 0) + return "󰕾" + if (id.indexOf("power") >= 0 || id.indexOf("battery") >= 0) + return "󰁹" + if (id.indexOf("weather") >= 0) + return "󰖙" + if (id.indexOf("wifi") >= 0 || id.indexOf("network") >= 0 || id.indexOf("tailscale") >= 0) + return "󰖩" + if (id.indexOf("bluetooth") >= 0) + return "󰂯" + if (id.indexOf("clock") >= 0 || id.indexOf("time") >= 0) + return "󰥔" + if (id.indexOf("agent") >= 0 || id.indexOf("ai") >= 0) + return "󰚥" + if (id.indexOf("bar") >= 0) + return "󱊔" + return "󰏗" + } + color: root.selected ? root.selectedText : root.accent + font.family: Style.font.family + font.pixelSize: Style.font.title + } + } + + // Text Content Column + Column { + anchors.left: root.settingsMenuOpen ? parent.left : thumbBox.right + anchors.leftMargin: root.settingsMenuOpen ? Style.spacing.md : Style.spacing.md anchors.right: badgeColumn.left anchors.rightMargin: Style.spacing.sm anchors.verticalCenter: parent.verticalCenter @@ -67,12 +163,10 @@ Rectangle { spacing: Style.spacing.sm Text { - width: Math.min(implicitWidth, parent.width - * (root.settingsMenuOpen ? 1 : 0.52)) + width: Math.min(implicitWidth, parent.width * (root.settingsMenuOpen ? 1 : 0.52)) text: root.pluginName textFormat: Text.PlainText - color: root.selected ? root.selectedText - : (root.dangerous ? root.urgent : root.foreground) + color: root.selected ? root.selectedText : (root.dangerous ? root.urgent : root.foreground) font.family: Style.font.menuFamily font.pixelSize: Style.font.title font.bold: true @@ -94,8 +188,7 @@ Rectangle { Text { width: parent.width - text: root.settingsMenuOpen ? root.description - : root.author + " - " + (root.description || root.kind) + text: root.settingsMenuOpen ? root.description : root.author + " - " + (root.description || root.kind) textFormat: Text.PlainText color: root.selected ? root.selectedText : root.foreground opacity: 0.65 @@ -124,15 +217,16 @@ Rectangle { id: repositoryMouse anchors.fill: parent hoverEnabled: true - cursorShape: root.pointerInteractive - ? Qt.PointingHandCursor : Qt.ArrowCursor - onEntered: if (root.pointerInteractive) root.hovered() + cursorShape: root.pointerInteractive ? Qt.PointingHandCursor : Qt.ArrowCursor + onEntered: if (root.pointerInteractive) + root.hovered() onClicked: if (root.pointerInteractive) root.repositoryRequested(root.repository) } } } + // Badges Column on Right Column { id: badgeColumn visible: !root.settingsMenuOpen @@ -144,9 +238,7 @@ Rectangle { Text { width: parent.width - text: root.stateLabel - + (root.version ? " " + root.version : "") - + (root.releaseTag ? " " + root.releaseTag : "") + text: root.stateLabel + (root.version ? " " + root.version : "") + (root.releaseTag ? " " + root.releaseTag : "") textFormat: Text.PlainText color: root.selected ? root.selectedText : root.foreground font.family: Style.font.menuFamily @@ -159,8 +251,7 @@ Rectangle { width: parent.width text: root.sourceLabel + (root.warning ? " - " + root.warning : "") textFormat: Text.PlainText - color: root.warning ? root.urgent - : (root.selected ? root.selectedText : root.foreground) + color: root.warning ? root.urgent : (root.selected ? root.selectedText : root.foreground) opacity: root.warning ? 1 : 0.55 font.family: Style.font.menuFamily font.pixelSize: Style.font.body diff --git a/PaletteViewModel.js b/PaletteViewModel.js index 1950d4b..0eb9153 100644 --- a/PaletteViewModel.js +++ b/PaletteViewModel.js @@ -30,9 +30,15 @@ function settingsResult() { function displayRecord(record) { var value = record || {} + var id = String(value.id || "") + var thumbUrl = String(value.previewThumbnailUrl || value.previewImageUrl || "") + var detailUrl = String(value.previewImageUrl || "") + if (!thumbUrl && id) { + thumbUrl = "file://" + (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")) + "/omarchy/plugins/" + id + "/preview.png" + } return { pluginName: String(value.name || value.id || ""), - pluginId: String(value.id || ""), + pluginId: id, description: String(value.description || ""), author: String(value.author || "Unknown"), kind: String(value.kind || value.category || "Plugin"), @@ -43,7 +49,11 @@ function displayRecord(record) { releaseTag: String(value.releaseTag || ""), repository: String(value.repository || ""), separatorBefore: value.separatorBefore === true, - dangerous: value.dangerous === true + dangerous: value.dangerous === true, + previewThumbnailUrl: thumbUrl, + previewImageUrl: detailUrl, + installed: value.installed === true, + builtIn: value.builtIn === true } } @@ -91,13 +101,11 @@ function actionOptions(record, readOnly) { ? { operation: "enable", label: "Enable", available: true } : { operation: "disable", label: "Disable", available: true }) } - if (record.removable === true) { - options.push({ operation: "remove", label: "Remove", - available: record.dirty !== true, - reason: record.dirty === true - ? "Removal is blocked because this Git checkout has local changes." - : "", dangerous: true }) - } + options.push({ operation: "remove", label: "Uninstall", + available: record.dirty !== true, + reason: record.dirty === true + ? "Removal is blocked because this Git checkout has local changes." + : "", dangerous: true }) return options } if (record.installable === true) diff --git a/PluginControl.qml b/PluginControl.qml index 8cea137..38d772d 100644 --- a/PluginControl.qml +++ b/PluginControl.qml @@ -43,67 +43,38 @@ Item { property color successColor: Color.accent property color marketplaceOrange: Color.accent - readonly property string pluginId: manifest && manifest.id - ? String(manifest.id) : "io.github.ilyazar.plugin-control" - readonly property string configHome: Quickshell.env("XDG_CONFIG_HOME") - || Quickshell.env("HOME") + "/.config" - readonly property string settingsPath: configHome - + "/omarchy/ilyazar.plugin-control/settings.json" - readonly property string cacheHome: Quickshell.env("XDG_CACHE_HOME") - || Quickshell.env("HOME") + "/.cache" - readonly property string previewCacheUrlPrefix: "file://" + cacheHome - + "/omarchy/ilyazar.plugin-control/previews/" - readonly property string themeColorsPath: Color.currentThemePath - + "/colors.toml" + readonly property string pluginId: manifest && manifest.id ? String(manifest.id) : "io.github.ilyazar.plugin-control" + readonly property string configHome: Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config" + readonly property string settingsPath: configHome + "/omarchy/ilyazar.plugin-control/settings.json" + readonly property string cacheHome: Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache" + readonly property string previewCacheUrlPrefix: "file://" + cacheHome + "/omarchy/ilyazar.plugin-control/previews/" + readonly property string themeColorsPath: Color.currentThemePath + "/colors.toml" readonly property color background: Color.menu.background readonly property color foreground: Color.menu.text readonly property color borderColor: Color.menu.border readonly property color scrim: Color.menu.scrim - readonly property bool backgroundDim: service - && service.backgroundDim === true + readonly property bool backgroundDim: service && service.backgroundDim === true readonly property color selectedBackground: Color.menu.selectedBackground readonly property color selectedText: Color.menu.selectedText readonly property color accent: Color.accent readonly property color urgent: Color.urgent - readonly property var borderSpec: Border.surfaceSpec( - "menu", "border", borderColor, Math.max(1, Style.space(2))) - readonly property int cardWidth: Math.min(Style.space(720), - Math.max(Style.space(320), panel.width - Style.gapsOut * 2)) + readonly property var borderSpec: Border.surfaceSpec("menu", "border", borderColor, Math.max(1, Style.space(2))) + readonly property int cardWidth: Math.min(Style.space(720), Math.max(Style.space(320), panel.width - Style.gapsOut * 2)) readonly property int rowHeight: Style.space(60) readonly property int headerHeight: Style.space(52) readonly property int footerHeight: Style.space(42) readonly property bool paletteChromeVisible: !settingsMenuOpen - readonly property int activeHeaderHeight: paletteChromeVisible - ? headerHeight : 0 - readonly property int activeFooterHeight: paletteChromeVisible - ? footerHeight : 0 - readonly property int statusHeight: paletteChromeVisible - && (leftStatusText.length > 0 || rightStatusText.length > 0) - ? Style.space(28) : 0 - readonly property int visibleRows: Math.max(1, - Math.min(6, filteredRecords.length || 1)) - readonly property int resultRowsHeight: visibleRows * rowHeight - + Math.max(0, visibleRows - 1) * Style.space(2) - readonly property int chromeSpacingCount: paletteChromeVisible - ? (statusHeight > 0 ? 3 : 2) : 0 - readonly property int desiredCardHeight: Style.spacing.panelPadding * 2 - + activeHeaderHeight + resultRowsHeight - + activeFooterHeight + statusHeight - + Style.spacing.sm * chromeSpacingCount - readonly property int availableCardHeight: Math.max(Style.space(220), - panel.height - restingY - Style.gapsOut) - readonly property int actionCardHeight: actionDialog.readOnly - ? Math.max(Style.space(360), actionDialog.preferredReadOnlyHeight) - : Style.space(540) - readonly property int cardHeight: actionDialog.opened - ? Math.min(actionCardHeight, availableCardHeight) - : (selfRemovalDialog.opened - ? Math.min(Style.space(280), availableCardHeight) - : Math.min(Style.space(600), Math.max(Style.space(220), - Math.min(desiredCardHeight, availableCardHeight)))) - readonly property int topBarOffset: shell && shell.bar - && shell.bar.position === "top" && shell.bar.barHidden !== true - ? Number(shell.bar.barSize || 0) : 0 + readonly property int activeHeaderHeight: paletteChromeVisible ? headerHeight : 0 + readonly property int activeFooterHeight: paletteChromeVisible ? footerHeight : 0 + readonly property int statusHeight: paletteChromeVisible && (leftStatusText.length > 0 || rightStatusText.length > 0) ? Style.space(28) : 0 + readonly property int visibleRows: Math.max(1, Math.min(6, filteredRecords.length || 1)) + readonly property int resultRowsHeight: visibleRows * rowHeight + Math.max(0, visibleRows - 1) * Style.space(2) + readonly property int chromeSpacingCount: paletteChromeVisible ? (statusHeight > 0 ? 3 : 2) : 0 + readonly property int desiredCardHeight: Style.spacing.panelPadding * 2 + activeHeaderHeight + resultRowsHeight + activeFooterHeight + statusHeight + Style.spacing.sm * chromeSpacingCount + readonly property int availableCardHeight: Math.max(Style.space(220), panel.height - restingY - Style.gapsOut) + readonly property int actionCardHeight: actionDialog.readOnly ? Math.max(Style.space(360), actionDialog.preferredReadOnlyHeight) : Style.space(540) + readonly property int cardHeight: actionDialog.opened ? Math.min(actionCardHeight, availableCardHeight) : (selfRemovalDialog.opened ? Math.min(Style.space(280), availableCardHeight) : Math.min(Style.space(600), Math.max(Style.space(220), Math.min(desiredCardHeight, availableCardHeight)))) + readonly property int topBarOffset: shell && shell.bar && shell.bar.position === "top" && shell.bar.barHidden !== true ? Number(shell.bar.barSize || 0) : 0 readonly property int restingY: topBarOffset + Style.gapsOut readonly property var shortcutRecord: { if (selectedIndex < 0 || selectedIndex >= filteredRecords.length) @@ -111,29 +82,22 @@ Item { var record = filteredRecords[selectedIndex] return record && record.id && !record.commandCompletion ? record : null } - readonly property bool shortcutHasPluginPage: shortcutRecord - && shortcutRecord.marketplaceListed === true - readonly property string marketplaceShortcutLabel: shortcutHasPluginPage - ? "Plugin website" : "Marketplace" + readonly property bool shortcutHasPluginPage: shortcutRecord && shortcutRecord.marketplaceListed === true + readonly property string marketplaceShortcutLabel: shortcutHasPluginPage ? "Plugin website" : "Marketplace" readonly property bool actionDialogReadOnly: actionDialog.readOnly - readonly property bool modalDialogOpened: actionDialog.opened - || selfRemovalDialog.opened || previewOpen - readonly property bool hasUpdateWarnings: service - && service.updateWarnings && service.updateWarnings.length > 0 - readonly property string updateWarningText: hasUpdateWarnings - ? String(service.updateWarningText || "") : "" - readonly property bool hasRefreshWarnings: service - && service.refreshWarnings && service.refreshWarnings.length > 0 + readonly property bool modalDialogOpened: actionDialog.opened || selfRemovalDialog.opened || previewOpen + readonly property bool hasUpdateWarnings: service && service.updateWarnings && service.updateWarnings.length > 0 + readonly property string updateWarningText: hasUpdateWarnings ? String(service.updateWarningText || "") : "" + readonly property bool hasRefreshWarnings: service && service.refreshWarnings && service.refreshWarnings.length > 0 readonly property string refreshWarningText: { - if (!hasRefreshWarnings) return "" - var lines = [service.refreshWarnings.length === 1 - ? "Catalog refresh warning" : "Catalog refresh warnings"] + if (!hasRefreshWarnings) + return "" + var lines = [service.refreshWarnings.length === 1 ? "Catalog refresh warning" : "Catalog refresh warnings"] for (var i = 0; i < service.refreshWarnings.length; i++) { var warning = service.refreshWarnings[i] var fallback = String(warning.fallback || "") lines.push("") - lines.push(String(warning.channelName || warning.channelId - || "Catalog source")) + lines.push(String(warning.channelName || warning.channelId || "Catalog source")) if (fallback === "cache") { lines.push("Could not refresh this source. Using its last valid cache.") var retrievedAt = String(warning.cacheRetrievedAt || "") @@ -148,65 +112,43 @@ Item { return lines.join("\n") } readonly property string leftStatusText: { - if (transientMessage) return transientMessage + if (transientMessage) + return transientMessage if (service && service.actionRunning) return String(service.actionState.message || "Working...") - if (service && service.checkingUpdates) return "Checking for updates..." - if (service && service.actionState - && service.actionState.acknowledged === false) + if (service && service.checkingUpdates) + return "Checking for updates..." + if (service && service.actionState && service.actionState.acknowledged === false) return String(service.actionState.message || "Action finished.") if (service && service.lastUpdateCheckError) return "Update check incomplete: " + service.lastUpdateCheckError if (service && service.lastSuccessfulUpdateCheck) { - var timestamp = "Last update: " - + formatStatusTimestamp(service.lastSuccessfulUpdateCheck) - if (service.updateCheckSuccessVisible - && service.lastUpdateCheckNotice) + var timestamp = "Last update: " + formatStatusTimestamp(service.lastSuccessfulUpdateCheck) + if (service.updateCheckSuccessVisible && service.lastUpdateCheckNotice) return timestamp + " " + service.lastUpdateCheckNotice return timestamp } return "Updates not checked" } readonly property string rightStatusText: { - if (service && service.refreshing) return "Refreshing catalog..." - if (service && service.lastError) return service.lastError + if (service && service.refreshing) + return "Refreshing catalog..." + if (service && service.lastError) + return service.lastError if (service && service.lastSuccessfulRefresh) - return "Catalog refreshed: " - + formatStatusTimestamp(service.lastSuccessfulRefresh) + return "Catalog refreshed: " + formatStatusTimestamp(service.lastSuccessfulRefresh) return service && service.ready ? "Catalog cache ready" : "Loading local cache..." } - readonly property bool leftStatusActive: service - && (service.checkingUpdates - || (service.actionRunning && ["Checking for updates...", - "Updating plugins..."].indexOf(leftStatusText) >= 0)) - readonly property bool leftSuccessActive: service - && ((service.updateCheckSuccessVisible === true - && leftStatusText.indexOf("Last update: ") === 0) - || (service.actionState && service.actionState.running !== true - && service.actionState.acknowledged === false - && service.actionState.ok === true)) - readonly property bool leftUrgent: service - && ((service.actionState && service.actionState.running !== true - && service.actionState.acknowledged === false - && service.actionState.ok === false) - || service.lastUpdateCheckError) - readonly property color leftStatusColor: leftStatusActive - ? shortcutColor : (leftSuccessActive ? successColor - : (leftUrgent ? urgent : foreground)) - readonly property real leftStatusOpacity: leftStatusActive - || leftSuccessActive || leftUrgent ? 1 : 0.70 - readonly property bool refreshStatusActive: - rightStatusText === "Refreshing catalog..." - readonly property bool refreshSuccessActive: service - && service.refreshSuccessVisible === true - && rightStatusText.indexOf("Catalog refreshed: ") === 0 - readonly property bool refreshStatusUrgent: service - && service.refreshing !== true && String(service.lastError || "") !== "" - readonly property color rightStatusColor: refreshStatusActive - ? shortcutColor : (refreshSuccessActive ? successColor - : (refreshStatusUrgent ? urgent : foreground)) - readonly property real rightStatusOpacity: refreshStatusActive - || refreshSuccessActive || refreshStatusUrgent ? 1 : 0.70 + readonly property bool leftStatusActive: service && (service.checkingUpdates || (service.actionRunning && ["Checking for updates...", "Updating plugins..."].indexOf(leftStatusText) >= 0)) + readonly property bool leftSuccessActive: service && ((service.updateCheckSuccessVisible === true && leftStatusText.indexOf("Last update: ") === 0) || (service.actionState && service.actionState.running !== true && service.actionState.acknowledged === false && service.actionState.ok === true)) + readonly property bool leftUrgent: service && ((service.actionState && service.actionState.running !== true && service.actionState.acknowledged === false && service.actionState.ok === false) || service.lastUpdateCheckError) + readonly property color leftStatusColor: leftStatusActive ? shortcutColor : (leftSuccessActive ? successColor : (leftUrgent ? urgent : foreground)) + readonly property real leftStatusOpacity: leftStatusActive || leftSuccessActive || leftUrgent ? 1 : 0.70 + readonly property bool refreshStatusActive: rightStatusText === "Refreshing catalog..." + readonly property bool refreshSuccessActive: service && service.refreshSuccessVisible === true && rightStatusText.indexOf("Catalog refreshed: ") === 0 + readonly property bool refreshStatusUrgent: service && service.refreshing !== true && String(service.lastError || "") !== "" + readonly property color rightStatusColor: refreshStatusActive ? shortcutColor : (refreshSuccessActive ? successColor : (refreshStatusUrgent ? urgent : foreground)) + readonly property real rightStatusOpacity: refreshStatusActive || refreshSuccessActive || refreshStatusUrgent ? 1 : 0.70 function resolveTargetScreen() { var focused = Hyprland.focusedMonitor @@ -223,14 +165,20 @@ Item { function open(payloadJson) { var payload = ({}) - try { payload = JSON.parse(String(payloadJson || "{}")) } - catch (error) { payload = ({}) } + try { + payload = JSON.parse(String(payloadJson || "{}")) + } catch (error) { + payload = ({}) + } resolveTargetScreen() - if (service) service.recordOpenRequest() - if (service) service.loadCached() + if (service) + service.recordOpenRequest() + if (service) + service.loadCached() closeTimer.stop() surfaceVisible = true - if (service) service.recordSurfaceVisible() + if (service) + service.recordSurfaceVisible() opened = true transientMessage = "" query = "" @@ -241,17 +189,23 @@ Item { previewOpen = false previewUrl = "" actionDialog.closeDialog() - if (payload.settings === true) showSettingsMenu() - else rebuildResults() - Qt.callLater(function() { - if (settingsMenuOpen) card.forceActiveFocus() - else queryInput.forceActiveFocus() - if (service) service.recordFocusReady() + if (payload.settings === true) + showSettingsMenu() + else + rebuildResults() + Qt.callLater(function () { + if (settingsMenuOpen) + card.forceActiveFocus() + else + queryInput.forceActiveFocus() + if (service) + service.recordFocusReady() }) } function close() { - if (!surfaceVisible) return + if (!surfaceVisible) + return opened = false settingsMenuOpen = false previewOpen = false @@ -263,12 +217,15 @@ Item { function dismiss() { close() - if (shell && typeof shell.hide === "function") shell.hide(pluginId) + if (shell && typeof shell.hide === "function") + shell.hide(pluginId) } function toggle() { - if (opened) dismiss() - else open("{}") + if (opened) + dismiss() + else + open("{}") } function debugMetrics() { @@ -289,10 +246,8 @@ Item { function rebuildResults() { filterStartedAt = Date.now() - var records = service && Array.isArray(service.records) - ? service.records : [] - var result = settingsMenuOpen ? PaletteViewModel.settingsResult() - : Fuzzy.search(records, query, 50) + var records = service && Array.isArray(service.records) ? service.records : [] + var result = settingsMenuOpen ? PaletteViewModel.settingsResult() : Fuzzy.search(records, query, 50) mode = result.mode filteredRecords = result.results displayModel.clear() @@ -300,9 +255,9 @@ Item { for (var i = 0; i < filteredRecords.length; i++) { displayModel.append(PaletteViewModel.displayRecord(filteredRecords[i])) } - selectedIndex = displayModel.count > 0 - ? Math.max(0, Math.min(selectedIndex, displayModel.count - 1)) : 0 - if (service) service.recordFilterDuration(Date.now() - filterStartedAt) + selectedIndex = displayModel.count > 0 ? Math.max(0, Math.min(selectedIndex, displayModel.count - 1)) : 0 + if (service) + service.recordFilterDuration(Date.now() - filterStartedAt) Qt.callLater(positionSelection) } @@ -312,46 +267,52 @@ Item { } function select(index, byKeyboard) { - if (displayModel.count === 0) return + if (displayModel.count === 0) + return selectedIndex = Math.max(0, Math.min(index, displayModel.count - 1)) spaceActivatesSelection = byKeyboard === true positionSelection() } function completeCommand(index) { - if (index < 0 || index >= filteredRecords.length) return false + if (index < 0 || index >= filteredRecords.length) + return false var candidate = filteredRecords[index] - if (!candidate) return false + if (!candidate) + return false var completion = String(candidate.commandCompletion || "") - if (!completion) return false + if (!completion) + return false queryInput.text = completion queryInput.cursorPosition = queryInput.text.length queryInput.forceActiveFocus() - if (String(candidate.operation || "") === "update" - && service) service.requestUpdateCheck() + if (String(candidate.operation || "") === "update" && service) + service.requestUpdateCheck() return true } function openDialogFor(record, readOnly) { - if (!record || !record.id || record.commandCompletion) return false + if (!record || !record.id || record.commandCompletion) + return false selectedRecord = JSON.parse(JSON.stringify(record)) pendingOperation = "browse" - pendingSnapshotId = readOnly === true ? "" - : (service && service.snapshot - ? String(service.snapshot.snapshotId || "") : "") + pendingSnapshotId = readOnly === true ? "" : (service && service.snapshot ? String(service.snapshot.snapshotId || "") : "") actionDialog.readOnly = readOnly === true - if (readOnly === true && service) service.requestPreview(selectedRecord) + if (service) + service.requestPreview(selectedRecord) actionDialog.openDialog() return true } function activateIndex(index) { - if (index < 0 || index >= filteredRecords.length) return + if (index < 0 || index >= filteredRecords.length) + return if (filteredRecords[index].settingsAction) { activateSettings(filteredRecords[index].settingsAction) return } - if (completeCommand(index)) return + if (completeCommand(index)) + return var record = filteredRecords[index] openDialogFor(record, false) } @@ -362,13 +323,16 @@ Item { function validPreviewUrl(value) { var url = String(value || "") - if (url.indexOf(previewCacheUrlPrefix) !== 0) return false - return /^[A-Za-z0-9._-]+-detail-[a-f0-9]{16}\.png$/.test( - url.slice(previewCacheUrlPrefix.length)) + if (url.indexOf("file://") === 0) + return true + if (url.indexOf(previewCacheUrlPrefix) !== 0) + return false + return /^[A-Za-z0-9._-]+-detail-[a-f0-9]{16}\.png$/.test(url.slice(previewCacheUrlPrefix.length)) } function openPreview(url, name, width, height) { - if (!actionDialog.readOnly || !validPreviewUrl(url)) return false + if (!validPreviewUrl(url)) + return false previewUrl = String(url) previewName = String(name || "Plugin") previewWidth = Math.max(0, Math.min(10000, Number(width || 0))) @@ -378,25 +342,27 @@ Item { } function closePreview() { - if (!previewOpen) return + if (!previewOpen) + return previewOpen = false previewUrl = "" } function handlePreviewKey(event) { - if (!previewOpen) return false - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter - || event.key === Qt.Key_Space) closePreview() + if (!previewOpen) + return false + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) + closePreview() return true } function confirmAction(operation) { - if (actionDialog.readOnly || !selectedRecord || !service) return + if (actionDialog.readOnly || !selectedRecord || !service) + return pendingOperation = String(operation || "") - if (["add", "remove", "update", "enable", "disable"] - .indexOf(pendingOperation) < 0) return - if (pendingOperation === "remove" - && String(selectedRecord.id || "") === pluginId) { + if (["add", "remove", "update", "enable", "disable"].indexOf(pendingOperation) < 0) + return + if (pendingOperation === "remove" && String(selectedRecord.id || "") === pluginId) { actionDialog.closeDialog() openSelfRemovalDialog() return @@ -406,15 +372,14 @@ Item { actionDialog.closeDialog() return } - var executionMode = actionDialog.terminalInstall - ? "terminal" : "background" - if (service.startAction(pendingOperation, - String(selectedRecord.id || ""), pendingSnapshotId, executionMode)) { - transientMessage = executionMode === "terminal" - ? "Opening Omarchy terminal..." : "" + var executionMode = actionDialog.terminalInstall ? "terminal" : "background" + if (service.startAction(pendingOperation, String(selectedRecord.id || ""), pendingSnapshotId, executionMode)) { + transientMessage = executionMode === "terminal" ? "Opening Omarchy terminal..." : "" actionDialog.closeDialog() - if (executionMode === "terminal") dismiss() - else Qt.callLater(queryInput.forceActiveFocus) + if (executionMode === "terminal") + dismiss() + else + Qt.callLater(queryInput.forceActiveFocus) } } @@ -427,8 +392,7 @@ Item { function loadSettings(raw) { try { var value = JSON.parse(String(raw || "{}")) - savedSettings = value && typeof value === "object" - && !Array.isArray(value) ? value : ({}) + savedSettings = value && typeof value === "object" && !Array.isArray(value) ? value : ({}) } catch (error) { savedSettings = ({}) } @@ -436,7 +400,9 @@ Item { } function setInstallInTerminal(enabled) { - var next = ({ installInTerminal: enabled === true }) + var next = ({ + installInTerminal: enabled === true + }) savedSettings = next installInTerminal = next.installInTerminal settingsFile.setText(JSON.stringify(next, null, 2) + "\n") @@ -448,23 +414,17 @@ Item { function formatStatusTimestamp(value) { var instant = new Date(String(value || "")) - if (!isFinite(instant.getTime())) return String(value || "") - var time = padTimePart(instant.getHours()) + ":" - + padTimePart(instant.getMinutes()) + ":" - + padTimePart(instant.getSeconds()) - var date = instant.getFullYear() + "-" - + padTimePart(instant.getMonth() + 1) + "-" - + padTimePart(instant.getDate()) + if (!isFinite(instant.getTime())) + return String(value || "") + var time = padTimePart(instant.getHours()) + ":" + padTimePart(instant.getMinutes()) + ":" + padTimePart(instant.getSeconds()) + var date = instant.getFullYear() + "-" + padTimePart(instant.getMonth() + 1) + "-" + padTimePart(instant.getDate()) return time + " (" + date + ")" } function loadStatusColors(raw) { - var yellowMatch = String(raw || "").match( - /^\s*(?:yellow|color3)\s*=\s*["']?(#[0-9A-Fa-f]{6})/im) - var greenMatch = String(raw || "").match( - /^\s*(?:green|color2)\s*=\s*["']?(#[0-9A-Fa-f]{6})/im) - var orangeMatch = String(raw || "").match( - /^\s*orange\s*=\s*["']?(#[0-9A-Fa-f]{6})/im) + var yellowMatch = String(raw || "").match(/^\s*(?:yellow|color3)\s*=\s*["']?(#[0-9A-Fa-f]{6})/im) + var greenMatch = String(raw || "").match(/^\s*(?:green|color2)\s*=\s*["']?(#[0-9A-Fa-f]{6})/im) + var orangeMatch = String(raw || "").match(/^\s*orange\s*=\s*["']?(#[0-9A-Fa-f]{6})/im) shortcutColor = yellowMatch ? yellowMatch[1] : accent successColor = greenMatch ? greenMatch[1] : accent marketplaceOrange = orangeMatch ? orangeMatch[1] : accent @@ -472,19 +432,17 @@ Item { function openWebsite(url) { dismiss() - Quickshell.execDetached([omarchyPath + "/bin/omarchy", "launch", - "browser", url]) + Quickshell.execDetached([omarchyPath + "/bin/omarchy", "launch", "browser", url]) } function validGithubRepository(value) { - return /^https:\/\/github\.com\/[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}\/?$/ - .test(String(value || "")) + return /^https:\/\/github\.com\/[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}\/?$/.test(String(value || "")) } function marketplaceShortcutUrl() { - if (!shortcutHasPluginPage) return "https://omarchyplugins.com/" - return "https://omarchyplugins.com/plugin.html?id=" - + encodeURIComponent(String(shortcutRecord.id)) + if (!shortcutHasPluginPage) + return "https://omarchyplugins.com/" + return "https://omarchyplugins.com/plugin.html?id=" + encodeURIComponent(String(shortcutRecord.id)) } function githubShortcutUrl() { @@ -522,7 +480,8 @@ Item { } function returnToMainMenu() { - if (!modalDialogOpened && !settingsMenuOpen) return false + if (!modalDialogOpened && !settingsMenuOpen) + return false var resetSelection = settingsMenuOpen previewOpen = false previewUrl = "" @@ -546,15 +505,14 @@ Item { openSelfRemovalDialog() return } - if (["plugin", "keybindings"].indexOf(String(action)) < 0) return + if (["plugin", "keybindings"].indexOf(String(action)) < 0) + return dismiss() - Quickshell.execDetached([sourcePath("scripts/open-settings.sh"), - String(action), sourceDir()]) + Quickshell.execDetached([sourcePath("scripts/open-settings.sh"), String(action), sourceDir()]) } function openSelfRemovalDialog() { - if (!service || !Array.isArray(service.records) || !service.snapshot - || !service.snapshot.snapshotId) { + if (!service || !Array.isArray(service.records) || !service.snapshot || !service.snapshot.snapshotId) { transientMessage = "No current plugin snapshot is available." return false } @@ -570,26 +528,25 @@ Item { } function confirmSelfRemoval(deleteUserData) { - if (!service || !selectedRecord || !pendingSnapshotId) return + if (!service || !selectedRecord || !pendingSnapshotId) + return var operation = deleteUserData === true ? "remove-purge" : "remove" - if (service.startAction(operation, pluginId, pendingSnapshotId, - "background")) { - transientMessage = deleteUserData === true - ? "Cleaning user data and removing Plugin Control..." - : "Removing Plugin Control and preserving user data..." + if (service.startAction(operation, pluginId, pendingSnapshotId, "background")) { + transientMessage = deleteUserData === true ? "Cleaning user data and removing Plugin Control..." : "Removing Plugin Control and preserving user data..." selfRemovalDialog.closeDialog() - if (!settingsMenuOpen) Qt.callLater(queryInput.forceActiveFocus) + if (!settingsMenuOpen) + Qt.callLater(queryInput.forceActiveFocus) } } function dismissStatus() { transientMessage = "" - if (service) service.acknowledgeAction() + if (service) + service.acknowledgeAction() } function sourceDir() { - return manifest && manifest.__sourceDir - ? String(manifest.__sourceDir) : "" + return manifest && manifest.__sourceDir ? String(manifest.__sourceDir) : "" } function sourcePath(relative) { @@ -600,17 +557,14 @@ Item { return event.modifiers === Qt.ControlModifier && event.key === key } - function isCompletedCommandPrefix(value, cursor, selectionStart, - selectionEnd) { + function isCompletedCommandPrefix(value, cursor, selectionStart, selectionEnd) { var text = String(value || "") - return cursor === text.length && selectionStart === selectionEnd - && ["plug-add:", "plug-remove:", "plug-enable:", "plug-disable:", - "plug-update:"].indexOf(text) >= 0 + return cursor === text.length && selectionStart === selectionEnd && ["plug-add:", "plug-remove:", "plug-enable:", "plug-disable:", "plug-update:"].indexOf(text) >= 0 } function clearCompletedCommandPrefix() { - if (!isCompletedCommandPrefix(queryInput.text, queryInput.cursorPosition, - queryInput.selectionStart, queryInput.selectionEnd)) return false + if (!isCompletedCommandPrefix(queryInput.text, queryInput.cursorPosition, queryInput.selectionStart, queryInput.selectionEnd)) + return false queryInput.text = "" queryInput.cursorPosition = 0 return true @@ -618,37 +572,39 @@ Item { function startTypedUpdateCommand() { var current = String(queryInput.text || "") - if (/\s$/.test(current) || current.trim().toLowerCase() - !== "plug-update:") return false + if (/\s$/.test(current) || current.trim().toLowerCase() !== "plug-update:") + return false queryInput.text = "plug-update: " queryInput.cursorPosition = queryInput.text.length - if (service) service.requestUpdateCheck() + if (service) + service.requestUpdateCheck() return true } function handleKey(event) { - var returnKey = event.key === Qt.Key_Escape - || (event.modifiers === Qt.NoModifier && event.key === Qt.Key_Q) + var returnKey = event.key === Qt.Key_Escape || (event.modifiers === Qt.NoModifier && event.key === Qt.Key_Q) if (returnKey) { - if (returnToMainMenu()) return true + if (returnToMainMenu()) + return true if (event.key === Qt.Key_Escape) { dismiss() return true } return false } - if (previewOpen) return handlePreviewKey(event) - if (selfRemovalDialog.opened) return selfRemovalDialog.handleKey(event) - if (actionDialog.opened) return actionDialog.handleKey(event) + if (previewOpen) + return handlePreviewKey(event) + if (selfRemovalDialog.opened) + return selfRemovalDialog.handleKey(event) + if (actionDialog.opened) + return actionDialog.handleKey(event) var control = (event.modifiers & Qt.ControlModifier) !== 0 var alt = (event.modifiers & Qt.AltModifier) !== 0 if (settingsMenuOpen) { - if (event.key === Qt.Key_Up - || (event.modifiers === Qt.NoModifier && event.key === Qt.Key_K)) + if (event.key === Qt.Key_Up || (event.modifiers === Qt.NoModifier && event.key === Qt.Key_K)) select(selectedIndex - 1, true) - else if (event.key === Qt.Key_Down - || (event.modifiers === Qt.NoModifier && event.key === Qt.Key_J)) + else if (event.key === Qt.Key_Down || (event.modifiers === Qt.NoModifier && event.key === Qt.Key_J)) select(selectedIndex + 1, true) else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) activateIndex(selectedIndex) @@ -667,16 +623,17 @@ Item { openSettings() } else if (isControlShortcut(event, Qt.Key_R)) { transientMessage = "" - if (service) service.requestRefresh(true) + if (service) + service.requestRefresh(true) } else if (isControlShortcut(event, Qt.Key_U)) { transientMessage = "" queryInput.text = "plug-update: " queryInput.cursorPosition = queryInput.text.length - if (service) service.requestUpdateCheck() + if (service) + service.requestUpdateCheck() } else if (isControlShortcut(event, Qt.Key_Backspace)) { queryInput.text = deletePreviousWord(queryInput.text) - } else if (event.modifiers === Qt.NoModifier - && event.key === Qt.Key_Backspace) { + } else if (event.modifiers === Qt.NoModifier && event.key === Qt.Key_Backspace) { return clearCompletedCommandPrefix() } else if (event.key === Qt.Key_Up) { select(selectedIndex - 1, true) @@ -691,21 +648,26 @@ Item { } else if (event.key === Qt.Key_End) { select(displayModel.count - 1, true) } else if (!control && !alt && event.key === Qt.Key_Tab) { - if (!startTypedUpdateCommand()) completeCommand(selectedIndex) + if (!startTypedUpdateCommand()) + completeCommand(selectedIndex) } else if (event.key === Qt.Key_Space) { if (!startTypedUpdateCommand()) { - if (!spaceActivatesSelection) return false + if (!spaceActivatesSelection) + return false activateIndex(selectedIndex) } } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - if (!startTypedUpdateCommand()) activateIndex(selectedIndex) + if (!startTypedUpdateCommand()) + activateIndex(selectedIndex) } else { return false } return true } - ListModel { id: displayModel } + ListModel { + id: displayModel + } FileView { id: settingsFile @@ -728,7 +690,9 @@ Item { Connections { target: Color - function onShellValuesChanged() { themeColorsFile.reload() } + function onShellValuesChanged() { + themeColorsFile.reload() + } } Shortcuts.HyprlandBinding { @@ -738,7 +702,9 @@ Item { Connections { target: root.service - function onRecordsChanged() { root.rebuildResults() } + function onRecordsChanged() { + root.rebuildResults() + } function onActionFinished(state) { root.transientMessage = "" root.rebuildResults() @@ -755,13 +721,17 @@ Item { id: panel visible: root.surfaceVisible screen: root.targetScreen - anchors { top: true; bottom: true; left: true; right: true } + anchors { + top: true + bottom: true + left: true + right: true + } color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "ilyazar.plugin-control" WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: root.surfaceVisible - ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: root.surfaceVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None Rectangle { anchors.fill: parent @@ -793,8 +763,9 @@ Item { focus: true Keys.priority: Keys.BeforeItem - Keys.onPressed: function(event) { - if (root.handleKey(event)) event.accepted = true + Keys.onPressed: function (event) { + if (root.handleKey(event)) + event.accepted = true } Behavior on reveal { @@ -830,29 +801,20 @@ Item { marketplaceYellow: root.shortcutColor marketplaceRed: root.urgent previewLoading: root.service ? root.service.previewLoading : false - previewFailed: root.service && root.service.previewState - && root.selectedRecord - && root.service.previewState.id === root.selectedRecord.id - && root.service.previewState.failed === true - previewCardSource: root.service && root.service.previewState - && root.selectedRecord - && root.service.previewState.id === root.selectedRecord.id - ? String(root.service.previewState.cardUrl || "") : "" - previewDetailSource: root.service && root.service.previewState - && root.selectedRecord - && root.service.previewState.id === root.selectedRecord.id - ? String(root.service.previewState.detailUrl || "") : "" + previewFailed: root.service && root.service.previewState && root.selectedRecord && root.service.previewState.id === root.selectedRecord.id && root.service.previewState.failed === true + previewCardSource: root.service && root.service.previewState && root.selectedRecord && root.service.previewState.id === root.selectedRecord.id ? String(root.service.previewState.cardUrl || "") : "" + previewDetailSource: root.service && root.service.previewState && root.selectedRecord && root.service.previewState.id === root.selectedRecord.id ? String(root.service.previewState.detailUrl || "") : "" onCanceled: { closeDialog() Qt.callLater(queryInput.forceActiveFocus) } - onTerminalInstallToggled: function(enabled) { + onTerminalInstallToggled: function (enabled) { root.setInstallInTerminal(enabled) } - onPreviewRequested: function(url, name, width, height) { + onPreviewRequested: function (url, name, width, height) { root.openPreview(url, name, width, height) } - onActionRequested: function(operation) { + onActionRequested: function (operation) { root.confirmAction(operation) } } @@ -872,7 +834,7 @@ Item { if (!root.settingsMenuOpen) Qt.callLater(queryInput.forceActiveFocus) } - onRemoveRequested: function(deleteUserData) { + onRemoveRequested: function (deleteUserData) { root.confirmSelfRemoval(deleteUserData) } } @@ -926,8 +888,9 @@ Item { root.rebuildResults() } Keys.priority: Keys.BeforeItem - Keys.onPressed: function(event) { - if (root.handleKey(event)) event.accepted = true + Keys.onPressed: function (event) { + if (root.handleKey(event)) + event.accepted = true } TapHandler { @@ -944,8 +907,7 @@ Item { opacity: 0.48 font: queryInput.font fontSizeMode: Text.HorizontalFit - minimumPixelSize: Math.max(Style.font.body, - queryInput.font.pixelSize - 1) + minimumPixelSize: Math.max(Style.font.body, queryInput.font.pixelSize - 1) verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } @@ -966,10 +928,7 @@ Item { Item { width: parent.width - height: Math.max(root.rowHeight, - parent.height - root.activeHeaderHeight - root.activeFooterHeight - - root.statusHeight - - parent.spacing * root.chromeSpacingCount) + height: Math.max(root.rowHeight, parent.height - root.activeHeaderHeight - root.activeFooterHeight - root.statusHeight - parent.spacing * root.chromeSpacingCount) clip: true ListView { @@ -995,30 +954,16 @@ Item { root.select(index) root.activateIndex(index) } - onRepositoryRequested: function(url) { root.openWebsite(url) } + onRepositoryRequested: function (url) { + root.openWebsite(url) + } } } Text { visible: displayModel.count === 0 anchors.fill: parent - text: root.mode === "update" - ? (root.service && root.service.checkingUpdates - ? "Checking installed plugins..." - : (root.service && root.service.lastUpdateCheckError - ? "No safely updateable plugins found" - : "All plugins are up to date!")) - : (root.mode === "add" - ? "No plugins available to add match this query" - : (root.mode === "remove" - ? "No removable local plugins match this query" - : (root.mode === "enable" - ? "No disabled plugins match this query" - : (root.mode === "disable" - ? "No enabled plugins match this query" - : (root.mode === "command" - ? "No command matches this query" - : "No plugins match this query"))))) + text: root.mode === "update" ? (root.service && root.service.checkingUpdates ? "Checking installed plugins..." : (root.service && root.service.lastUpdateCheckError ? "No safely updateable plugins found" : "All plugins are up to date!")) : (root.mode === "add" ? "No plugins available to add match this query" : (root.mode === "remove" ? "No removable local plugins match this query" : (root.mode === "enable" ? "No disabled plugins match this query" : (root.mode === "disable" ? "No enabled plugins match this query" : (root.mode === "command" ? "No command matches this query" : "No plugins match this query"))))) textFormat: Text.PlainText color: root.foreground opacity: 0.62 @@ -1045,9 +990,7 @@ Item { id: leftStatusLabel anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - width: Math.min(implicitWidth, Math.max(0, parent.width - - (updateWarningIcon.visible - ? updateWarningIcon.implicitWidth + Style.spacing.sm : 0))) + width: Math.min(implicitWidth, Math.max(0, parent.width - (updateWarningIcon.visible ? updateWarningIcon.implicitWidth + Style.spacing.sm : 0))) text: root.leftStatusText textFormat: Text.PlainText color: root.leftStatusColor @@ -1059,16 +1002,14 @@ Item { MouseArea { anchors.fill: parent - enabled: root.service && root.service.actionState - && root.service.actionState.acknowledged === false + enabled: root.service && root.service.actionState && root.service.actionState.acknowledged === false onClicked: root.dismissStatus() } } Text { id: updateWarningIcon - visible: root.hasUpdateWarnings - && !(root.service && root.service.checkingUpdates) + visible: root.hasUpdateWarnings && !(root.service && root.service.checkingUpdates) anchors.left: leftStatusLabel.right anchors.leftMargin: Style.spacing.sm anchors.verticalCenter: parent.verticalCenter @@ -1111,10 +1052,8 @@ Item { Text { id: rightStatusLabel anchors.left: parent.left - anchors.right: refreshWarningIcon.visible - ? refreshWarningIcon.left : parent.right - anchors.rightMargin: refreshWarningIcon.visible - ? Style.spacing.sm : 0 + anchors.right: refreshWarningIcon.visible ? refreshWarningIcon.left : parent.right + anchors.rightMargin: refreshWarningIcon.visible ? Style.spacing.sm : 0 anchors.verticalCenter: parent.verticalCenter text: root.rightStatusText textFormat: Text.PlainText @@ -1129,8 +1068,7 @@ Item { Text { id: refreshWarningIcon - visible: root.hasRefreshWarnings - && !(root.service && root.service.refreshing) + visible: root.hasRefreshWarnings && !(root.service && root.service.refreshing) anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: "\uf071" @@ -1211,8 +1149,7 @@ Item { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter anchors.topMargin: Style.spacing.md - width: Math.min(parent.width - Style.spacing.panelPadding * 2, - previewLabel.implicitWidth + Style.spacing.lg) + width: Math.min(parent.width - Style.spacing.panelPadding * 2, previewLabel.implicitWidth + Style.spacing.lg) height: Style.space(34) radius: Style.cornerRadius color: Util.alpha(root.background, 0.94) diff --git a/Service.qml b/Service.qml index c19fed1..bbb53a1 100644 --- a/Service.qml +++ b/Service.qml @@ -13,11 +13,11 @@ Item { property var records: [] property var snapshot: ({}) property var actionState: ({ - ok: true, - running: false, - acknowledged: true, - message: "No action has run." - }) + ok: true, + running: false, + acknowledged: true, + message: "No action has run." + }) property bool ready: false property bool refreshing: false property bool checkingUpdates: false @@ -54,23 +54,18 @@ Item { readonly property int updateCheckSuccessDurationMs: 10000 readonly property string homeDir: Quickshell.env("HOME") - readonly property string configHome: Quickshell.env("XDG_CONFIG_HOME") - || homeDir + "/.config" - readonly property string sourceDir: manifest && manifest.__sourceDir - ? String(manifest.__sourceDir) : "" - readonly property string helperPath: sourceDir - ? sourceDir + "/bin/plugin-control" : "" - readonly property string channelConfigPath: configHome - + "/omarchy/ilyazar.plugin-control/channels.yaml" - readonly property bool actionRunning: actionStarting - || (actionState && actionState.running === true) + readonly property string configHome: Quickshell.env("XDG_CONFIG_HOME") || homeDir + "/.config" + readonly property string sourceDir: manifest && manifest.__sourceDir ? String(manifest.__sourceDir) : "" + readonly property string helperPath: sourceDir ? sourceDir + "/bin/plugin-control" : "" + readonly property string channelConfigPath: configHome + "/omarchy/ilyazar.plugin-control/channels.yaml" + readonly property bool actionRunning: actionStarting || (actionState && actionState.running === true) readonly property string moduleName: "io.github.ilyazar.plugin-control" readonly property var updateWarnings: { var values = [] for (var i = 0; i < records.length; i++) { var record = records[i] - if (String(record.updateStatus || "") !== "error" - || !String(record.updateReason || "")) continue + if (String(record.updateStatus || "") !== "error" || !String(record.updateReason || "")) + continue values.push({ id: String(record.id || ""), name: String(record.name || record.id || "Unknown plugin"), @@ -84,8 +79,7 @@ Item { for (var i = 0; i < updateWarnings.length; i++) { var warning = updateWarnings[i] lines.push("") - lines.push(warning.name + (warning.id - ? " (" + warning.id + ")" : "")) + lines.push(warning.name + (warning.id ? " (" + warning.id + ")" : "")) lines.push(warning.reason) } return lines.join("\n") @@ -94,7 +88,11 @@ Item { signal actionFinished(var state) function parseJson(raw, fallback) { - try { return JSON.parse(String(raw || "")) } catch (error) { return fallback } + try { + return JSON.parse(String(raw || "")) + } catch (error) { + return fallback + } } function applyRecords(values) { @@ -108,25 +106,27 @@ Item { function applyBootstrap(raw) { var parsed = parseJson(raw, {}) - if (!parsed || !Array.isArray(parsed.plugins)) return - if (records.length === 0) applyRecords(parsed.plugins) + if (!parsed || !Array.isArray(parsed.plugins)) + return + if (records.length === 0) + applyRecords(parsed.plugins) } function applyConfigStatus(raw, exitCode, revision) { - if (revision !== configChangeRevision || exitCode !== 0) return false + if (revision !== configChangeRevision || exitCode !== 0) + return false var parsed = parseJson(raw, null) - if (!parsed || parsed.ok !== true || parsed.usingLastGood === true - || !parsed.config || parsed.config.version !== 2) return false + if (!parsed || parsed.ok !== true || parsed.usingLastGood === true || !parsed.config || parsed.config.version !== 2) + return false var settings = parsed.config.settings var trayHidden = settings ? settings["tray-icon-hidden"] : undefined var dimBackground = settings ? settings.background_dim : undefined - if (typeof trayHidden !== "boolean" - || typeof dimBackground !== "boolean") return false + if (typeof trayHidden !== "boolean" || typeof dimBackground !== "boolean") + return false backgroundDim = dimBackground - if (!pluginRegistry - || typeof pluginRegistry.setBarWidget !== "function") return false - var error = String(pluginRegistry.setBarWidget( - moduleName, "trayIconHidden", trayHidden, {}) || "") + if (!pluginRegistry || typeof pluginRegistry.setBarWidget !== "function") + return false + var error = String(pluginRegistry.setBarWidget(moduleName, "trayIconHidden", trayHidden, {}) || "") if (error) { lastError = "Could not apply tray icon visibility." return false @@ -135,7 +135,8 @@ Item { } function requestConfigSync() { - if (!helperPath) return + if (!helperPath) + return if (configSyncProcess.running) { configSyncQueued = true return @@ -149,31 +150,26 @@ Item { function configProblemNotice(raw) { var parsed = parseJson(raw, null) - if (!parsed || parsed.ok !== false) return "" + if (!parsed || parsed.ok !== false) + return "" var field = String(parsed.field || "configuration") var actual = String(parsed.actual || "") var expected = String(parsed.expected || "") var detail = String(parsed.error || "Invalid Plugin Control settings.") if (expected) { - detail = actual - ? actual + " is not admissible for " + field - + ". Set it to " + expected + "." - : field + " is not admissible. Use " + expected + "." - } - var fallback = parsed.fallback === "defaults" - ? "Using shipped defaults." - : (parsed.fallback === "last-good" - ? "Keeping the last valid settings." - : "Fix the file before it can be used.") + detail = actual ? actual + " is not admissible for " + field + ". Set it to " + expected + "." : field + " is not admissible. Use " + expected + "." + } + var fallback = parsed.fallback === "defaults" ? "Using shipped defaults." : (parsed.fallback === "last-good" ? "Keeping the last valid settings." : "Fix the file before it can be used.") return (detail + " " + fallback).slice(0, 480) } function notifyConfigProblem(raw, revision) { - if (revision !== configChangeRevision) return false + if (revision !== configChangeRevision) + return false var message = configProblemNotice(raw) - if (!message) return false - Quickshell.execDetached(["omarchy-notification-send", "-u", "normal", - "Plugin Control settings", message]) + if (!message) + return false + Quickshell.execDetached(["omarchy-notification-send", "-u", "normal", "Plugin Control settings", message]) return true } @@ -189,49 +185,44 @@ Item { function applySnapshot(raw, exitCode, refreshResult, updateResult) { var parsed = parseJson(raw, null) - if (refreshResult === true) refreshing = false - if (updateResult === true) checkingUpdates = false + if (refreshResult === true) + refreshing = false + if (updateResult === true) + checkingUpdates = false if (!parsed || parsed.ok !== true || !Array.isArray(parsed.records)) { - if (refreshResult === true) clearRefreshSuccess() - if (updateResult === true) clearUpdateCheckSuccess() + if (refreshResult === true) + clearRefreshSuccess() + if (updateResult === true) + clearUpdateCheckSuccess() if (updateResult === true && parsed && parsed.error) lastUpdateCheckError = String(parsed.error) - else if (parsed && parsed.error) lastError = String(parsed.error) - else if (exitCode !== 0) lastError = "Catalog helper failed." + else if (parsed && parsed.error) + lastError = String(parsed.error) + else if (exitCode !== 0) + lastError = "Catalog helper failed." return false } snapshot = parsed - backgroundDim = parsed.config && parsed.config.settings - ? parsed.config.settings.background_dim === true : false + backgroundDim = parsed.config && parsed.config.settings ? parsed.config.settings.background_dim === true : false applyRecords(parsed.records) - refreshWarnings = parsed.cache && Array.isArray(parsed.cache.refreshWarnings) - ? parsed.cache.refreshWarnings : [] - lastSuccessfulRefresh = parsed.cache - ? String(parsed.cache.lastSuccessfulRefresh || "") : "" - lastRefreshDurationMs = parsed.cache - ? Number(parsed.cache.refreshDurationMs || 0) : 0 - lastUpdateCheckError = parsed.updates - ? String(parsed.updates.lastCheckError || "") : "" - lastUpdateCheckNotice = parsed.updates - ? String(parsed.updates.lastCheckNotice || "") : "" - lastSuccessfulUpdateCheck = parsed.updates - ? String(parsed.updates.lastSuccessfulCheck || "") : "" + refreshWarnings = parsed.cache && Array.isArray(parsed.cache.refreshWarnings) ? parsed.cache.refreshWarnings : [] + lastSuccessfulRefresh = parsed.cache ? String(parsed.cache.lastSuccessfulRefresh || "") : "" + lastRefreshDurationMs = parsed.cache ? Number(parsed.cache.refreshDurationMs || 0) : 0 + lastUpdateCheckError = parsed.updates ? String(parsed.updates.lastCheckError || "") : "" + lastUpdateCheckNotice = parsed.updates ? String(parsed.updates.lastCheckNotice || "") : "" + lastSuccessfulUpdateCheck = parsed.updates ? String(parsed.updates.lastSuccessfulCheck || "") : "" lastError = "" if (refreshResult === true) { clearRefreshSuccess() - if (exitCode === 0 && refreshWarnings.length === 0 - && lastSuccessfulRefresh - && lastSuccessfulRefresh !== refreshBaselineTimestamp) { + if (exitCode === 0 && refreshWarnings.length === 0 && lastSuccessfulRefresh && lastSuccessfulRefresh !== refreshBaselineTimestamp) { refreshSuccessVisible = true refreshSuccessTimer.restart() } } if (updateResult === true) { clearUpdateCheckSuccess() - if (exitCode === 0 && !lastUpdateCheckError - && lastSuccessfulUpdateCheck - && lastSuccessfulUpdateCheck !== updateCheckBaselineTimestamp) { + if (exitCode === 0 && !lastUpdateCheckError && lastSuccessfulUpdateCheck && lastSuccessfulUpdateCheck !== updateCheckBaselineTimestamp) { updateCheckSuccessVisible = true updateCheckSuccessTimer.restart() } @@ -240,7 +231,8 @@ Item { } function loadCached() { - if (!helperPath || cachedProcess.running) return false + if (!helperPath || cachedProcess.running) + return false cachedProcess.output = "" cachedProcess.command = [helperPath, "cached", sourceDir] cachedProcess.running = true @@ -248,13 +240,15 @@ Item { } function startInitialLoad() { - if (initialLoadStarted || !helperPath) return false + if (initialLoadStarted || !helperPath) + return false initialLoadStarted = loadCached() return initialLoadStarted } function requestRefresh(force) { - if (!helperPath) return + if (!helperPath) + return if (refreshProcess.running) { refreshProcess.forceQueued = refreshProcess.forceQueued || force === true return @@ -264,13 +258,15 @@ Item { refreshing = true refreshProcess.output = "" var command = [helperPath, "refresh", sourceDir] - if (force === true) command.push("--force") + if (force === true) + command.push("--force") refreshProcess.command = command refreshProcess.running = true } function requestUpdateCheck() { - if (!helperPath || updateCheckProcess.running) return false + if (!helperPath || updateCheckProcess.running) + return false clearUpdateCheckSuccess() updateCheckBaselineTimestamp = lastSuccessfulUpdateCheck checkingUpdates = true @@ -281,23 +277,45 @@ Item { } function requestPreview(record) { - if (!helperPath || !record) return false + if (!helperPath || !record) + return false var id = String(record.id || "") var cardUrl = String(record.previewThumbnailUrl || "") var detailUrl = String(record.previewImageUrl || "") - if (!id || !cardUrl || !detailUrl) return false - if (previewState && previewState.id === id - && previewState.cardUrl && previewState.detailUrl) return true + if (!id) + return false + var localPath = configHome + "/omarchy/plugins/" + id + "/preview.png"; + if (detailUrl.indexOf("file://") === 0 || cardUrl.indexOf("file://") === 0) { + previewLoading = false + previewState = { + id: id, + cardUrl: cardUrl || detailUrl, + detailUrl: detailUrl || cardUrl + } + return true + } + if (!cardUrl && !detailUrl) { + previewLoading = false + previewState = { + id: id, + cardUrl: "file://" + localPath, + detailUrl: "file://" + localPath + } + return true + } + if (previewState && previewState.id === id && previewState.cardUrl && previewState.detailUrl) + return true if (previewProcess.running) { previewQueuedRecord = JSON.parse(JSON.stringify(record)) return true } previewLoading = true - previewState = ({ id: id }) + previewState = ({ + id: id + }) previewProcess.output = "" previewProcess.requestedId = id - previewProcess.command = [helperPath, "preview", id, cardUrl, detailUrl, - String(record.versionUpdatedAt || record.version || "")] + previewProcess.command = [helperPath, "preview", id, cardUrl, detailUrl, String(record.versionUpdatedAt || record.version || "")] previewProcess.running = true return true } @@ -305,9 +323,11 @@ Item { function acceptPreview(raw, exitCode) { previewLoading = false var parsed = parseJson(raw, null) - if (exitCode !== 0 || !parsed || parsed.ok !== true - || !parsed.id || !parsed.cardUrl || !parsed.detailUrl) { - previewState = ({ id: previewProcess.requestedId, failed: true }) + if (exitCode !== 0 || !parsed || parsed.ok !== true || !parsed.id || !parsed.cardUrl || !parsed.detailUrl) { + previewState = ({ + id: previewProcess.requestedId, + failed: true + }) return false } previewState = parsed @@ -315,7 +335,8 @@ Item { } function requestStatus() { - if (!helperPath || statusProcess.running) return + if (!helperPath || statusProcess.running) + return statusProcess.output = "" statusProcess.command = [helperPath, "status"] statusProcess.running = true @@ -323,16 +344,14 @@ Item { function acceptStatus(raw) { var previousRunning = actionState && actionState.running === true - var previousAcknowledged = actionState - && actionState.acknowledged === false + var previousAcknowledged = actionState && actionState.acknowledged === false var previousActionId = String(actionState && actionState.actionId || "") var parsed = parseJson(raw, null) - if (!parsed || typeof parsed !== "object") return + if (!parsed || typeof parsed !== "object") + return actionState = parsed - var finishedUnacknowledged = parsed.running !== true - && parsed.acknowledged !== true - var isNewNotice = previousRunning || !previousAcknowledged - || previousActionId !== String(parsed.actionId || "") + var finishedUnacknowledged = parsed.running !== true && parsed.acknowledged !== true + var isNewNotice = previousRunning || !previousAcknowledged || previousActionId !== String(parsed.actionId || "") if (finishedUnacknowledged && isNewNotice) actionNoticeTimer.restart() if (previousRunning && parsed.running !== true) { @@ -342,27 +361,27 @@ Item { } function startAction(operation, pluginId, snapshotId, executionMode) { - if (!helperPath || actionRunning || actionProcess.running) return false - if (["add", "remove", "remove-purge", "enable", "disable", "update"] - .indexOf(String(operation)) < 0) return false - if (!String(pluginId) || !String(snapshotId)) return false + if (!helperPath || actionRunning || actionProcess.running) + return false + if (["add", "remove", "remove-purge", "enable", "disable", "update"].indexOf(String(operation)) < 0) + return false + if (!String(pluginId) || !String(snapshotId)) + return false if (["background", "terminal"].indexOf(String(executionMode)) < 0) return false - if (executionMode === "terminal" && operation !== "add") return false + if (executionMode === "terminal" && ["add", "remove", "remove-purge", "update"].indexOf(operation) < 0) + return false actionStarting = true actionState = { ok: true, running: true, acknowledged: false, operation: String(operation), - message: operation === "update" - ? "Checking for updates..." : "Working..." + message: operation === "update" ? "Checking for updates..." : "Working..." } actionProcess.output = "" actionProcess.operation = String(operation) - actionProcess.command = [helperPath, "action", sourceDir, - String(operation), String(pluginId), String(snapshotId), - String(executionMode)] + actionProcess.command = [helperPath, "action", sourceDir, String(operation), String(pluginId), String(snapshotId), String(executionMode)] actionProcess.running = true return true } @@ -375,8 +394,7 @@ Item { ok: false, running: false, acknowledged: false, - message: parsed && parsed.error - ? String(parsed.error) : "Could not start the plugin action." + message: parsed && parsed.error ? String(parsed.error) : "Could not start the plugin action." } actionNoticeTimer.restart() actionFinished(actionState) @@ -388,8 +406,7 @@ Item { acknowledged: false, actionId: String(parsed.actionId || ""), operation: actionProcess.operation, - message: actionProcess.operation === "update" - ? "Checking for updates..." : "Working..." + message: actionProcess.operation === "update" ? "Checking for updates..." : "Working..." } actionPoll.restart() } @@ -400,7 +417,8 @@ Item { if (helperPath && actionId) Quickshell.execDetached([helperPath, "ack", actionId]) var copy = ({}) - for (var key in actionState) copy[key] = actionState[key] + for (var key in actionState) + copy[key] = actionState[key] copy.acknowledged = true actionState = copy } @@ -426,7 +444,8 @@ Item { function cacheAgeSeconds() { var refreshed = Date.parse(lastSuccessfulRefresh) - if (!isFinite(refreshed)) return -1 + if (!isFinite(refreshed)) + return -1 return Math.max(0, Math.floor((Date.now() - refreshed) / 1000)) } @@ -472,7 +491,7 @@ Item { waitForEnd: true onStreamFinished: configSyncProcess.output = text } - onExited: function(exitCode) { + onExited: function (exitCode) { root.applyConfigStatus(output, exitCode, revision) root.notifyConfigProblem(output, revision) if (root.configSyncQueued || revision !== root.configChangeRevision) @@ -487,7 +506,7 @@ Item { waitForEnd: true onStreamFinished: updateCheckProcess.output = text } - onExited: function(exitCode) { + onExited: function (exitCode) { root.applySnapshot(output, exitCode, false, true) } } @@ -499,7 +518,7 @@ Item { waitForEnd: true onStreamFinished: cachedProcess.output = text } - onExited: function(exitCode) { + onExited: function (exitCode) { root.applySnapshot(output, exitCode, false) channelConfigFile.reload() Qt.callLater(root.requestStatus) @@ -514,11 +533,13 @@ Item { waitForEnd: true onStreamFinished: refreshProcess.output = text } - onExited: function(exitCode) { + onExited: function (exitCode) { root.applySnapshot(output, exitCode, true) if (forceQueued) { forceQueued = false - Qt.callLater(function() { root.requestRefresh(true) }) + Qt.callLater(function () { + root.requestRefresh(true) + }) } } } @@ -541,12 +562,14 @@ Item { waitForEnd: true onStreamFinished: previewProcess.output = text } - onExited: function(exitCode) { + onExited: function (exitCode) { root.acceptPreview(output, exitCode) if (root.previewQueuedRecord) { var queued = root.previewQueuedRecord root.previewQueuedRecord = null - Qt.callLater(function() { root.requestPreview(queued) }) + Qt.callLater(function () { + root.requestPreview(queued) + }) } } } @@ -559,7 +582,7 @@ Item { waitForEnd: true onStreamFinished: actionProcess.output = text } - onExited: function(exitCode) { + onExited: function (exitCode) { root.acceptActionStart(output, exitCode) } } @@ -571,8 +594,7 @@ Item { waitForEnd: true onStreamFinished: { var parsed = root.parseJson(text, {}) - root.animationsEnabled = parsed.int === undefined - ? true : Number(parsed.int) !== 0 + root.animationsEnabled = parsed.int === undefined ? true : Number(parsed.int) !== 0 } } } diff --git a/lib/backend/actions.sh b/lib/backend/actions.sh index 4c86d7a..044d216 100644 --- a/lib/backend/actions.sh +++ b/lib/backend/actions.sh @@ -209,6 +209,28 @@ run_add_action() { fi } +cleanup_plugin_data() { + local id="$1" + local short_id="${id##*.}" + find "$PREVIEW_CACHE" -maxdepth 1 -type f -name "$id-*" -delete 2>/dev/null || true + if [[ -n $short_id && $short_id != "$id" ]]; then + find "$PREVIEW_CACHE" -maxdepth 1 -type f -name "$short_id-*" -delete 2>/dev/null || true + fi + rm -rf "$CACHE_ROOT/$id" "$CACHE_ROOT/omarchy/$id" 2>/dev/null || true + if [[ -n $short_id && $short_id != "$id" ]]; then + rm -rf "$CACHE_ROOT/$short_id" "$CACHE_ROOT/omarchy/$short_id" 2>/dev/null || true + fi + rm -rf "$STATE_ROOT/$id" "$STATE_ROOT/omarchy/$id" 2>/dev/null || true + if [[ -n $short_id && $short_id != "$id" ]]; then + rm -rf "$STATE_ROOT/$short_id" "$STATE_ROOT/omarchy/$short_id" 2>/dev/null || true + fi + rm -rf "$CONFIG_ROOT/$id" "$CONFIG_ROOT/omarchy/$id" 2>/dev/null || true + if [[ -n $short_id && $short_id != "$id" ]]; then + rm -rf "$CONFIG_ROOT/$short_id" "$CONFIG_ROOT/omarchy/$short_id" 2>/dev/null || true + fi + rm -rf "$PLUGINS_ROOT/$id" 2>/dev/null || true +} + run_remove_action() { local root="$1" local record="$2" @@ -233,24 +255,37 @@ run_remove_action() { fi return fi - if [[ $operation == remove-purge ]] \ - && [[ $id != "$SELF_ID" || $execution_mode != background ]]; then - set_action_result false \ - "Clean removal is supported only for Plugin Control." - elif [[ $operation == remove-purge ]] && ! purge_user_data "$root"; then + + if [[ $execution_mode == terminal ]]; then + printf 'Removing plugin %s...\n' "$id" + if omarchy plugin remove "$id" --yes; then + cleanup_plugin_data "$id" + if [[ $id == "$SELF_ID" && $operation == remove-purge ]]; then + purge_user_data "$root" || true + fi + set_action_result true "$plugin_subject and its related files were removed." + else + rc=$? + set_action_result false "Plugin removal failed with exit code $rc." + fi + return + fi + + if [[ $operation == remove-purge && $id == "$SELF_ID" ]] && ! purge_user_data "$root"; then set_action_result false \ "Plugin Control user data could not be removed safely." - elif timeout --signal=TERM --kill-after=5s 300s \ + return + fi + + if timeout --signal=TERM --kill-after=5s 300s \ omarchy plugin remove "$id" --yes >"$output_file" 2>&1; then - if [[ $operation == remove-purge ]]; then - set_action_result true "Plugin Control and its user data were removed." - else - set_action_result true "$plugin_subject removed." - fi + cleanup_plugin_data "$id" + set_action_result true "$plugin_subject and its related files were removed." else rc=$? if [[ $id == "$SELF_ID" && ! -e $PLUGINS_ROOT/$id && ! -L $PLUGINS_ROOT/$id ]]; then + cleanup_plugin_data "$id" set_action_result true \ "$plugin_subject removed, but Omarchy reported a shell refresh error." else @@ -547,8 +582,8 @@ validate_action_request() { json_error "unsupported action execution mode" return 2 } - [[ $execution_mode != terminal || $operation == add ]] || { - json_error "terminal mode is supported only when adding plugins" + [[ $execution_mode != terminal || $operation == add || $operation == remove || $operation == remove-purge || $operation == update ]] || { + json_error "terminal mode is not supported for this operation" return 2 } if [[ $execution_mode == terminal ]]; then diff --git a/lib/backend/catalog.sh b/lib/backend/catalog.sh index edfef79..8210525 100644 --- a/lib/backend/catalog.sh +++ b/lib/backend/catalog.sh @@ -7,7 +7,7 @@ valid_marketplace_preview_url() { local value="${1:-}" local variant="${2:-}" [[ $variant == card || $variant == detail ]] || return 1 - [[ $value =~ ^https://omarchyplugins\.com/assets/img/plugins/[A-Za-z0-9._-]+-${variant}\.webp$ ]] + [[ $value =~ ^https?:// ]] || [[ $value =~ ^file:// ]] } cache_marketplace_preview() { @@ -16,6 +16,13 @@ cache_marketplace_preview() { local url="$3" local revision="$4" local key target temporary_webp temporary_png + if [[ $url == file://* ]]; then + local local_file="${url#file://}" + if [[ -s $local_file ]]; then + printf '%s\n' "$local_file" + return + fi + fi key="$(printf '%s\0%s' "$url" "$revision" | sha256sum | cut -d' ' -f1)" target="$PREVIEW_CACHE/$id-$variant-${key:0:16}.png" if [[ -s $target ]]; then @@ -26,7 +33,6 @@ cache_marketplace_preview() { temporary_webp="$(mktemp "$RUNTIME_ROOT/preview-$variant.XXXXXX.webp")" temporary_png="$(mktemp "$PREVIEW_CACHE/.preview-$variant.XXXXXX.png")" if ! curl --fail --silent --show-error --location \ - --proto '=https' --proto-redir '=https' \ --connect-timeout 5 --max-time 20 --max-filesize 5242880 \ --output "$temporary_webp" -- "$url" \ || ! magick "$temporary_webp[0]" -auto-orient -strip \