diff --git a/dank-qml-common b/dank-qml-common index 334c32a81..2d206c20d 160000 --- a/dank-qml-common +++ b/dank-qml-common @@ -1 +1 @@ -Subproject commit 334c32a814b5129d47ca757c29db3071df5620ea +Subproject commit 2d206c20d102fb77edf35a41b098f9cc086d1acf diff --git a/flake.lock b/flake.lock index a9604bf9c..c33dad0dd 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "dank-qml-common": { "flake": false, "locked": { - "lastModified": 1790713744, - "narHash": "sha256-hDpFEPmzXG/7xHnoRFwQMIEjYDJ59dwjUKsqLR9GGvg=", + "lastModified": 1790715373, + "narHash": "sha256-hiykGTIlWoRLntC4BWTZ/HIfTQ1aABmAvDyu9t3Sb+A=", "owner": "AvengeMedia", "repo": "dank-qml-common", - "rev": "334c32a814b5129d47ca757c29db3071df5620ea", + "rev": "2d206c20d102fb77edf35a41b098f9cc086d1acf", "type": "github" }, "original": { diff --git a/quickshell/Modules/Settings/PluginBrowser.qml b/quickshell/Modules/Settings/PluginBrowser.qml index ef5e7934f..ec7f8fd04 100644 --- a/quickshell/Modules/Settings/PluginBrowser.qml +++ b/quickshell/Modules/Settings/PluginBrowser.qml @@ -846,7 +846,9 @@ RegistryBrowserWindow { } ] - overlay: Rectangle { + overlayActive: detailPluginId !== "" + + overlay: Item { id: detailPane property var plugin: ({}) @@ -859,23 +861,7 @@ RegistryBrowserWindow { plugin = livePlugin; } - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: 0 - anchors.bottom: parent.bottom - z: 10 - color: Theme.floatingWindowSurface - opacity: root.detailPluginId !== "" ? 1 : 0 - visible: opacity > 0 - - Behavior on opacity { - NumberAnimation { - duration: Theme.expressiveDurations.expressiveEffects - easing.type: Easing.BezierSpline - easing.bezierCurve: Theme.expressiveCurves.expressiveEffects - } - } + anchors.fill: parent Connections { target: root diff --git a/quickshell/Modules/Settings/RegistryBrowserWindow.qml b/quickshell/Modules/Settings/RegistryBrowserWindow.qml index aedc50ff6..7b02e65d8 100644 --- a/quickshell/Modules/Settings/RegistryBrowserWindow.qml +++ b/quickshell/Modules/Settings/RegistryBrowserWindow.qml @@ -19,7 +19,8 @@ DankFloatingWindow { property alias aboveSearch: aboveSearchSlot.data property alias belowSearch: belowSearchSlot.data property alias listContent: listSlot.data - property alias overlay: browserContent.data + property alias overlay: overlaySlot.data + property bool overlayActive: false readonly property alias installConfirm: urlInstallConfirm readonly property alias listArea: listSlot @@ -189,58 +190,80 @@ DankFloatingWindow { anchors.topMargin: Theme.spacingM anchors.bottomMargin: Theme.spacingL - Column { - id: aboveSearchSlot - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: Theme.spacingS - } + Item { + id: browserBody + anchors.fill: parent + opacity: 1 - overlaySlot.opacity + visible: opacity > 0 - DankSearchField { - id: browserSearchField - anchors.left: parent.left - anchors.right: parent.right - anchors.top: aboveSearchSlot.bottom - anchors.topMargin: aboveSearchSlot.height > 0 ? Theme.spacingM : 0 - height: Theme.fieldHeightLarge - textColor: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - placeholderText: root.searchPlaceholder - text: root.searchQuery - focus: true - ignoreLeftRightKeys: true - keyForwardTargets: [browserKeyHandler] - onTextEdited: { - root.searchQuery = text; - root.applySearch(); + Column { + id: aboveSearchSlot + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Theme.spacingS + } + + DankSearchField { + id: browserSearchField + anchors.left: parent.left + anchors.right: parent.right + anchors.top: aboveSearchSlot.bottom + anchors.topMargin: aboveSearchSlot.height > 0 ? Theme.spacingM : 0 + height: Theme.fieldHeightLarge + textColor: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + placeholderText: root.searchPlaceholder + text: root.searchQuery + focus: true + ignoreLeftRightKeys: true + keyForwardTargets: [browserKeyHandler] + onTextEdited: { + root.searchQuery = text; + root.applySearch(); + } + } + + Column { + id: belowSearchSlot + anchors.left: parent.left + anchors.right: parent.right + anchors.top: browserSearchField.bottom + anchors.topMargin: height > 0 ? Theme.spacingM : 0 + spacing: Theme.spacingS + } + + Item { + id: listSlot + anchors.left: parent.left + anchors.right: parent.right + anchors.top: belowSearchSlot.bottom + anchors.topMargin: Theme.spacingM + anchors.bottom: parent.bottom + + Item { + anchors.fill: parent + visible: root.isLoading + + DankSpinner { + anchors.centerIn: parent + running: root.isLoading + } + } } } - Column { - id: belowSearchSlot - anchors.left: parent.left - anchors.right: parent.right - anchors.top: browserSearchField.bottom - anchors.topMargin: height > 0 ? Theme.spacingM : 0 - spacing: Theme.spacingS - } - Item { - id: listSlot - anchors.left: parent.left - anchors.right: parent.right - anchors.top: belowSearchSlot.bottom - anchors.topMargin: Theme.spacingM - anchors.bottom: parent.bottom + id: overlaySlot + anchors.fill: parent + opacity: root.overlayActive ? 1 : 0 + visible: opacity > 0 - Item { - anchors.fill: parent - visible: root.isLoading - - DankSpinner { - anchors.centerIn: parent - running: root.isLoading + Behavior on opacity { + NumberAnimation { + duration: Theme.expressiveDurations.expressiveEffects + easing.type: Easing.BezierSpline + easing.bezierCurve: Theme.expressiveCurves.expressiveEffects } } } diff --git a/quickshell/PLUGINS/README.md b/quickshell/PLUGINS/README.md index 9a73cc0f7..fa940583d 100644 --- a/quickshell/PLUGINS/README.md +++ b/quickshell/PLUGINS/README.md @@ -722,7 +722,7 @@ QtObject { } ``` -A synchronous variant is supported too - declare `check()` with no argument and return the result directly. When the check fails the enable toggle reverts and the error is shown as a toast (the `details` are expandable, and any `http(s)` URL in them becomes a clickable link). Plugins without a `startupCheck` are unaffected. See `ExampleStartupCheck` for a complete plugin; the last error per plugin is available at `PluginService.pluginLoadErrors[pluginId]`. +A synchronous variant is supported too - declare `check()` with no argument and return the result directly. When the check fails the enable toggle reverts and the error is shown as a toast (the `details` are expandable, and any `http(s)` URL in them becomes a clickable link). A check that fails to load, has no `check` function, or throws blocks activation the same way, with its error as the message. The check also runs again on reload. Plugins without a `startupCheck` are unaffected. See `ExampleStartupCheck` for a complete plugin; the last error per plugin is available at `PluginService.pluginLoadErrors[pluginId]`. ## Plugin Global Variables diff --git a/quickshell/Services/PluginService.qml b/quickshell/Services/PluginService.qml index 82018a8be..462a504fa 100644 --- a/quickshell/Services/PluginService.qml +++ b/quickshell/Services/PluginService.qml @@ -893,10 +893,8 @@ Singleton { function _makeStartupCheckObject(pluginId, plugin) { const comp = Qt.createComponent(pluginComponentUrl(pluginId, plugin.startupCheckPath), Component.PreferSynchronous); - if (comp.status === Component.Error) { - log.error("startupCheck component error", pluginId, comp.errorString()); - return null; - } + if (comp.status === Component.Error) + throw new Error(comp.errorString()); return comp.createObject(root); } @@ -915,8 +913,12 @@ Singleton { return ok; } - const probe = _makeStartupCheckObject(pluginId, plugin); + let probe = null; + let settled = false; const finish = result => { + if (settled) + return; + settled = true; if (probe) probe.destroy(); const err = _normalizeStartupError(result); @@ -937,28 +939,20 @@ Singleton { onResult(ok); }; - const check = probe ? probe.check : null; - if (typeof check !== "function") { - finish(null); - return true; - } - if (check.length >= 1) { - try { - check(finish); - } catch (e) { - log.warn("startupCheck threw for", pluginId, e.message); - finish(null); - } - return true; - } - let r = null; try { - r = check(); + probe = _makeStartupCheckObject(pluginId, plugin); + const check = probe?.check; + if (typeof check !== "function") + throw new Error("startupCheck has no check function"); + if (check.length >= 1) + check(finish); + else + finish(check()); } catch (e) { - log.warn("startupCheck threw for", pluginId, e.message); - r = null; + const message = String(e?.message || e); + log.warn("startupCheck failed for", pluginId, message); + finish(message); } - finish(r); return true; } @@ -973,7 +967,7 @@ Singleton { const plugin = availablePlugins[pluginId]; if (plugin) _loadPluginTranslations(pluginId, plugin.pluginDirectory); - return loadPlugin(pluginId); + return runStartupGate(pluginId); } function ensureLauncherInstance(pluginId) { diff --git a/quickshell/tests/plugin-reload.test.mjs b/quickshell/tests/plugin-reload.test.mjs index cb36f0bde..17af83ca3 100644 --- a/quickshell/tests/plugin-reload.test.mjs +++ b/quickshell/tests/plugin-reload.test.mjs @@ -176,6 +176,33 @@ test("updated startup checks still prevent activation on failure", () => { context.forceRescanPlugin("fixture"); pending.shift()(); assert.equal(context.isPluginLoaded("fixture"), true); + files.set("file:///plugins/fixture/Check.qml", { + check: () => "dependency removed", + destroy() {} + }); + context.reloadPlugin("fixture"); + assert.equal(context.isPluginLoaded("fixture"), false); + assert.equal(failures.at(-1), "dependency removed"); +}); + +test("startup checks that cannot run prevent activation", () => { + const { context, files, pending, manifest, failures } = service(); + const createComponent = context.Qt.createComponent; + context.Qt.createComponent = url => url.startsWith("file:///plugins/fixture/Broken.qml") ? { status: 3, errorString: () => "module missing" } : createComponent(url); + const cases = [ + ["./Broken.qml", null, "module missing"], + ["./Check.qml", { destroy() {} }, "startupCheck has no check function"], + ["./Check.qml", { check() { throw new Error("sync throw"); }, destroy() {} }, "sync throw"], + ["./Check.qml", { check(done) { throw new Error("async throw"); }, destroy() {} }, "async throw"] + ]; + for (const [path, probe, error] of cases) { + manifest.startupCheck = path; + files.set("file:///plugins/fixture/Check.qml", probe); + context.forceRescanPlugin("fixture"); + pending.shift()(); + assert.equal(context.isPluginLoaded("fixture"), false, error); + assert.equal(failures.at(-1), error); + } }); test("composite updates refresh every surface without creating daemons synchronously", () => {