feat: validate widget gesture actions in plugin manifests

This commit is contained in:
Lemmy
2026-07-25 09:42:23 -04:00
parent ef9c40030a
commit 360462f5d3
2 changed files with 102 additions and 1 deletions
@@ -307,5 +307,52 @@ Configure the update interval in plugin settings.
self.assertTrue(any("## Settings" in error for error in self.validate_readme(without_settings)))
class WidgetActionsTests(unittest.TestCase):
def validate_actions(self, entry: dict, plugin_api: object = 14) -> list[str]:
validator = validate_plugins.Validator(Path("/repo"))
validator.validate_widget_fields(
Path("/repo/example/plugin.toml"),
"widget[0]",
entry,
plugin_api,
)
return validator.errors
def test_accepts_every_gesture(self) -> None:
actions = {gesture: "volume-mute" for gesture in validate_plugins.WIDGET_GESTURES}
self.assertEqual(self.validate_actions({"actions": actions}), [])
def test_accepts_exec_and_none(self) -> None:
self.assertEqual(
self.validate_actions({"actions": {"middle": "exec playerctl pause", "right": "none"}}),
[],
)
def test_entry_without_actions_is_fine(self) -> None:
self.assertEqual(self.validate_actions({"id": "bar"}), [])
def test_rejects_unknown_gesture(self) -> None:
self.assertNotEqual(self.validate_actions({"actions": {"ctrl+left": "volume-mute"}}), [])
def test_rejects_non_table(self) -> None:
self.assertNotEqual(self.validate_actions({"actions": "volume-mute"}), [])
def test_rejects_non_string_action(self) -> None:
self.assertNotEqual(self.validate_actions({"actions": {"middle": 42}}), [])
def test_rejects_empty_action(self) -> None:
self.assertNotEqual(self.validate_actions({"actions": {"middle": ""}}), [])
def test_rejects_bare_exec(self) -> None:
self.assertNotEqual(self.validate_actions({"actions": {"middle": "exec"}}), [])
def test_requires_plugin_api_14(self) -> None:
errors = self.validate_actions({"actions": {"middle": "volume-mute"}}, plugin_api=13)
self.assertTrue(any("plugin_api >= 14" in error for error in errors))
def test_widget_entry_accepts_actions_field(self) -> None:
self.assertIn("actions", validate_plugins.ENTRY_FIELDS["widget"])
if __name__ == "__main__":
unittest.main()
+55 -1
View File
@@ -112,6 +112,17 @@ SETTING_OWNER_TYPES = ("widget", "panel", "desktop_widget", "launcher_provider")
SETTING_TYPES = {"string", "string_list", "bool", "glyph", "select", "folder", "file", "int", "color"}
PANEL_PLACEMENTS = {"attached", "floating"}
PANEL_KEYBOARD_FOCUS = {"on_demand", "exclusive", "none"}
WIDGET_GESTURES = {
"left",
"right",
"middle",
"back",
"forward",
"scroll_up",
"scroll_down",
"scroll_left",
"scroll_right",
}
PANEL_POSITIONS = {
"auto",
"center",
@@ -132,7 +143,7 @@ ROOT_FIELDS = set(ROOT_STRING_FIELDS) | set(ROOT_ARRAY_FIELDS) | set(ENTRY_TYPES
}
BASE_ENTRY_FIELDS = {"id", "entry"}
ENTRY_FIELDS = {
"widget": BASE_ENTRY_FIELDS | {"setting"},
"widget": BASE_ENTRY_FIELDS | {"setting", "actions"},
"panel": BASE_ENTRY_FIELDS
| {
"setting",
@@ -778,6 +789,46 @@ class Validator:
manifest_path, context, "capture_keys must be an array of key chord strings"
)
def validate_widget_fields(
self,
manifest_path: Path,
context: str,
entry: dict[str, Any],
plugin_api: Any,
) -> None:
if "actions" not in entry:
return
if not is_int(plugin_api) or plugin_api < 14:
self.add_context_error(manifest_path, context, "actions requires plugin_api >= 14")
return
actions = entry["actions"]
if not isinstance(actions, dict):
self.add_context_error(
manifest_path, context, "actions must be a table of gesture bindings"
)
return
for gesture, action in actions.items():
if gesture not in WIDGET_GESTURES:
valid = ", ".join(sorted(WIDGET_GESTURES))
self.add_context_error(
manifest_path, context, f"actions.{gesture} is not a gesture (expected one of: {valid})"
)
continue
if not is_non_empty_string(action):
self.add_context_error(
manifest_path, context, f"actions.{gesture} must be a non-empty string"
)
continue
# The shell resolves the verb against its live IPC registry, which is not available
# here, so only the grammar is checked: "<verb> [args]", "exec <command>", or "none".
if action.split()[0] == "exec" and len(action.split(maxsplit=1)) < 2:
self.add_context_error(
manifest_path, context, f"actions.{gesture}: exec needs a command line"
)
def validate_entries(
self,
manifest_path: Path,
@@ -825,6 +876,9 @@ class Validator:
if entry_type == "panel":
self.validate_panel_fields(manifest_path, context, entry, manifest.get("plugin_api"))
if entry_type == "widget":
self.validate_widget_fields(manifest_path, context, entry, manifest.get("plugin_api"))
if entry_type in SETTING_OWNER_TYPES and "setting" in entry:
self.validate_settings(
manifest_path,