diff --git a/.github/workflows/test_validate_plugins.py b/.github/workflows/test_validate_plugins.py index 6887fc3..5400fd2 100644 --- a/.github/workflows/test_validate_plugins.py +++ b/.github/workflows/test_validate_plugins.py @@ -66,6 +66,27 @@ class AllowedTagsTests(unittest.TestCase): self.assertTrue(any("tags[2] must be a non-empty string" in error for error in errors)) +class DescriptionTests(unittest.TestCase): + def validate_description(self, description: object) -> list[str]: + validator = validate_plugins.Validator(Path("/repo")) + validator.validate_description(Path("/repo/example/plugin.toml"), description) + return validator.errors + + def test_accepts_description_at_limit(self) -> None: + self.assertEqual( + self.validate_description("x" * validate_plugins.DESCRIPTION_MAX_CHARS), + [], + ) + + def test_rejects_description_over_limit(self) -> None: + errors = self.validate_description( + "x" * (validate_plugins.DESCRIPTION_MAX_CHARS + 1) + ) + self.assertEqual(len(errors), 1) + self.assertIn("is 121 characters", errors[0]) + self.assertIn("at or below 120", errors[0]) + + class PluginConfigAccessorTests(unittest.TestCase): def test_accepts_universal_accessor(self) -> None: self.assertEqual( diff --git a/.github/workflows/validate-plugins.py b/.github/workflows/validate-plugins.py index a5046db..c1cf05c 100644 --- a/.github/workflows/validate-plugins.py +++ b/.github/workflows/validate-plugins.py @@ -14,6 +14,7 @@ from typing import Any DEFAULT_ROOT = Path(__file__).resolve().parents[2] SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$") LAUNCHER_PREFIX_RE = re.compile(r"^[a-z]+$") +DESCRIPTION_MAX_CHARS = 120 ALLOWED_TAGS = { "ai", "animation", @@ -475,6 +476,18 @@ class Validator: f"tags[{index}] '{tag}' is not an allowed tag", ) + def validate_description(self, manifest_path: Path, value: Any) -> None: + if not is_non_empty_string(value): + return + + length = len(value) + if length > DESCRIPTION_MAX_CHARS: + self.add_error( + manifest_path, + f"root field 'description' is {length} characters; " + f"keep catalog descriptions at or below {DESCRIPTION_MAX_CHARS}", + ) + def validate_root_fields(self, manifest_path: Path, manifest: dict[str, Any]) -> None: unknown = sorted(set(manifest) - ROOT_FIELDS) for field in unknown: @@ -502,6 +515,9 @@ class Validator: if "tags" in manifest: self.validate_tags(manifest_path, manifest["tags"]) + if "description" in manifest: + self.validate_description(manifest_path, manifest["description"]) + if "deprecated" in manifest and not isinstance(manifest["deprecated"], bool): self.add_error(manifest_path, "root field 'deprecated' must be a bool") diff --git a/README.md b/README.md index 5be7ad9..28dd7ff 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ Open a PR against `main`. CI validates your manifest, entry scripts, required fi - `version` is semver and gets bumped on every change to the plugin. - `min_noctalia` is the Noctalia version you actually tested against. Users on older builds are then told the plugin needs an upgrade instead of getting a broken install. +- `description` is concise catalog copy, limited to 120 characters. Put feature details in the plugin's README. - `license` is set in `plugin.toml`. You keep the copyright on your plugin; if it is not MIT, put a `LICENSE` file in your plugin directory. There is no repo-wide license covering contributed plugins. - Screenshots or a short video for anything with a visual surface. diff --git a/sharednd/plugin.toml b/sharednd/plugin.toml index d57ddd1..9d8f09d 100644 --- a/sharednd/plugin.toml +++ b/sharednd/plugin.toml @@ -13,14 +13,14 @@ id = "whyoolw/sharednd" name = "ShareDND" -version = "1.1.0" +version = "1.1.1" min_noctalia = "5.0.0" author = "whyoolw" license = "MIT" dependencies = ["niri"] tags = ["privacy", "niri"] icon = "screen-share" -description = "Enables notification Do Not Disturb while the screen is shared (niri screencast) and turns notifications back on when sharing stops. Requires niri." +description = "Automatically enables notification Do Not Disturb while a niri screencast is active." [[setting]] key = "only_active"