diff --git a/CHANGELOG.md b/CHANGELOG.md index 442e287..d7d912f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,11 @@ All notable user-facing changes to QOpen are documented here. ### Fixed - Recovered the plugin directory from the plugin's own entry point URL when - omarchy 4.0.4+ strips `__sourceDir` from third-party manifests, restoring the + Omarchy 4.0.3+ strips `__sourceDir` from third-party manifests, restoring the catalog on shells that sanitize plugin manifests. +- Derived the fallback plugin root by removing the complete menu entry-point + path, so backend lookup remains correct if the entry point moves into a + subdirectory. ## [2.5.1] - 2026-08-28 diff --git a/QOpen.qml b/QOpen.qml index fb70fd8..a10aad4 100644 --- a/QOpen.qml +++ b/QOpen.qml @@ -15,9 +15,10 @@ Item { property var pluginRegistry: null readonly property string pluginId: (manifest && manifest.id) || "qopen.launcher" - // omarchy >= 4.0.4 hands third-party plugins a sanitized manifest without - // __sourceDir. Recover the plugin directory from the own entry point URL - // (file:///.../qopen.launcher/QOpen.qml -> .../qopen.launcher) instead. + // omarchy >= 4.0.3 hands third-party plugins a sanitized manifest without + // __sourceDir. Recover the plugin directory from the own entry point URL. + // Strip the full manifest entry-point path so this keeps working if the + // entry point later moves below the plugin root (for example ui/QOpen.qml). readonly property string pluginDir: { if (manifest && manifest.__sourceDir) return String(manifest.__sourceDir) var entryUrl = "" @@ -28,7 +29,21 @@ Item { entryUrl = "" } } - if (!entryUrl) return "" + if (!entryUrl || entryUrl.indexOf("file://") !== 0) return "" + + var entryPoint = manifest && manifest.entryPoints + ? String(manifest.entryPoints.menu || "") : "" + if (entryPoint) { + var encodedEntryPoint = entryPoint.split("/").map(encodeURIComponent).join("/") + var suffix = "/" + encodedEntryPoint + if (entryUrl.slice(-suffix.length) === suffix) { + var rootUrl = entryUrl.slice(0, -suffix.length) + try { return decodeURIComponent(rootUrl.slice("file://".length)) } + catch (e) { return "" } + } + } + + // Compatibility fallback if a host ever hides entryPoints as well. var path = entryUrl.replace(/^file:\/\//, "") var slash = path.lastIndexOf("/") if (slash < 0) return "" diff --git a/tests/test_plugin_path_fallback.py b/tests/test_plugin_path_fallback.py new file mode 100644 index 0000000..a807992 --- /dev/null +++ b/tests/test_plugin_path_fallback.py @@ -0,0 +1,42 @@ +from pathlib import Path +import unittest + + +REPOSITORY = Path(__file__).resolve().parents[1] + + +class PluginDirectoryFallbackSourceTests(unittest.TestCase): + def test_fallback_strips_the_complete_manifest_entry_point(self) -> None: + qopen_source = (REPOSITORY / "QOpen.qml").read_text(encoding="utf-8") + + self.assertIn("omarchy >= 4.0.3", qopen_source) + self.assertIn("var entryPoint = manifest && manifest.entryPoints", qopen_source) + self.assertIn( + 'entryPoint.split("/").map(encodeURIComponent).join("/")', + qopen_source, + ) + self.assertIn('var suffix = "/" + encodedEntryPoint', qopen_source) + self.assertIn( + "if (entryUrl.slice(-suffix.length) === suffix)", + qopen_source, + ) + self.assertIn( + "var rootUrl = entryUrl.slice(0, -suffix.length)", + qopen_source, + ) + self.assertIn( + 'decodeURIComponent(rootUrl.slice("file://".length))', + qopen_source, + ) + + def test_fallback_rejects_non_file_entry_urls(self) -> None: + qopen_source = (REPOSITORY / "QOpen.qml").read_text(encoding="utf-8") + + self.assertIn( + 'if (!entryUrl || entryUrl.indexOf("file://") !== 0) return ""', + qopen_source, + ) + + +if __name__ == "__main__": + unittest.main()