Skip to content

Commit 798903c

Browse files
authored
Merge pull request #4 from strohganoff/feature/global-actions
Feature/global actions
2 parents 7caf6a1 + d1291b1 commit 798903c

14 files changed

+449
-173
lines changed

README.md

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@ This guide will help you set up your first Stream Deck plugin using the library.
4141
- Stream Deck software installed
4242
- A valid `manifest.json` file for your plugin
4343

44-
### Creating an Action
44+
### Creating Actions
4545

46-
An **Action** represents a specific functionality in your plugin. You can create multiple actions, each with its own set of event handlers.
46+
The SDK provides two types of actions: `Action` and `GlobalAction`. Each represents functionality with different scopes in your plugin, determining how events are handled.
47+
48+
#### Regular Actions
49+
50+
An `Action` handles events that are specifically associated with it based on event metadata. When the Stream Deck sends an event, the action's handlers only run if the event metadata indicates it was triggered by or is intended for that specific action instance.
4751

4852
```python
4953
from streamdeck import Action
@@ -52,18 +56,33 @@ from streamdeck import Action
5256
my_action = Action(uuid="com.example.myplugin.myaction")
5357
```
5458

59+
#### Global Actions
60+
61+
A `GlobalAction` runs its event handlers for all events of a given type, regardless of which action the events were originally intended for. Unlike regular Actions which only process events specifically targeted at their UUID, GlobalActions handle events meant for any action in the plugin, making them useful for implementing plugin-wide behaviors or monitoring.
62+
63+
```python
64+
from streamdeck import GlobalAction
65+
66+
# Create a global action
67+
my_global_action = GlobalAction()
68+
```
69+
70+
Choose `GlobalAction` when you want to handle events at the plugin-scope (i.e. globally) without filtering by action, and `Action` when you need to process events specific to particular actions.
71+
72+
Note that an action with its UUID still needs to be defined in the manifest.json. Global Actions are an abstract component unique to this library — the global behavior is not how the Stream Deck software itself handles registering actions and publishing events.
73+
5574
### Registering Event Handlers
5675

5776
Use the `.on()` method to register event handlers for specific events.
5877

5978
```python
6079
@my_action.on("keyDown")
61-
def handle_key_down(event):
62-
print("Key Down event received:", event)
80+
def handle_key_down(event_data):
81+
print("Key Down event received:", event_data)
6382

6483
@my_action.on("willAppear")
65-
def handle_will_appear(event):
66-
print("Will Appear event received:", event)
84+
def handle_will_appear(event_data):
85+
print("Will Appear event received:", event_data)
6786
```
6887

6988
!!!INFO Handlers for action-specific events are dispatched only if the event is triggered by the associated action, ensuring isolation and predictability. For other types of events that are not associated with a specific action, handlers are dispatched without such restrictions.
@@ -159,22 +178,34 @@ Below is an example of the pyproject.toml configuration and how to run the plugi
159178

160179
## Simple Example
161180

162-
Below is a complete example that creates a plugin with a single action. The action handles the `keyDown` event and simply prints a statement that the event occurred.
181+
Below is a complete example that creates a plugin with a single action. The action handles the `keyDown` and `applicationDidLaunch` event and simply prints a statement that an event occurred.
163182

164183
```python
165184
# main.py
166185
import logging
167-
from streamdeck import Action, PluginManager, events
186+
from streamdeck import Action, GlobalAction, PluginManager, events
168187
169188
logger = logging.getLogger("myaction")
170189
171190
# Define your action
172191
my_action = Action(uuid="com.example.myplugin.myaction")
173192
174-
# Register event handlers
193+
# Define your global action
194+
my_global_action = GlobalAction()
195+
196+
# Register event handlers for regular action
197+
@my_action.on("applicationDidLaunch")
198+
def handle_application_did_launch(event_data: events.ApplicationDidLaunch):
199+
logger.debug("Application Did Launch event recieved:", event_data)
200+
175201
@my_action.on("keyDown")
176-
def handle_key_down(event):
177-
logger.debug("Key Down event received:", event)
202+
def handle_key_down(event_data: events.KeyDown):
203+
logger.debug("Key Down event received:", event_data)
204+
205+
# Register event handlers for global action
206+
@my_global_action.on("keyDown")
207+
def handle_global_key_down(event_data: events.KeyDown):
208+
logger.debug("Global Key Down event received:", event_data)
178209
```
179210

180211
```toml

pyproject.toml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,11 @@
4747
"Programming Language :: Python :: 3.11",
4848
"Programming Language :: Python :: 3.12",
4949
"Development Status :: 4 - Beta",
50-
"Environment :: Desktop",
50+
"Environment :: MacOS X",
51+
"Intended Audience :: Developers",
5152
"License :: OSI Approved :: MIT License",
52-
"Topic :: CLI",
53-
"Topic :: Stream Deck",
54-
"Topic :: Stream Deck :: Plugin",
55-
"Topic :: Stream Deck :: Plugin :: SDK",
56-
"Topic :: Stream Deck :: SDK",
53+
"Topic :: Software Development :: Libraries :: Application Frameworks",
54+
"Topic :: Software Development :: Libraries :: Python Modules",
5755
"Typing :: Typed",
5856
]
5957
requires-python = ">=3.9"

streamdeck/__main__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import tomli as toml
1212

13-
from streamdeck.actions import Action
13+
from streamdeck.actions import ActionBase
1414
from streamdeck.cli.errors import (
1515
DirectoryNotFoundError,
1616
NotAFileError,
@@ -146,7 +146,7 @@ def read_streamdeck_config_from_pyproject(plugin_dir: Path) -> StreamDeckConfigD
146146

147147
class ActionLoader:
148148
@classmethod
149-
def load_actions(cls: type[Self], plugin_dir: Path, files: list[str]) -> Generator[Action, None, None]:
149+
def load_actions(cls: type[Self], plugin_dir: Path, files: list[str]) -> Generator[ActionBase, None, None]:
150150
# Ensure the parent directory of the plugin modules is in `sys.path`,
151151
# so that import statements in the plugin module will work as expected.
152152
if str(plugin_dir) not in sys.path:
@@ -191,12 +191,12 @@ def _load_module_from_file(filepath: Path) -> ModuleType:
191191
return module
192192

193193
@staticmethod
194-
def _get_actions_from_loaded_module(module: ModuleType) -> Generator[Action, None, None]:
194+
def _get_actions_from_loaded_module(module: ModuleType) -> Generator[ActionBase, None, None]:
195195
# Iterate over all attributes in the module to find Action subclasses
196196
for attribute_name in dir(module):
197197
attribute = getattr(module, attribute_name)
198-
# Check if the attribute is an instance of the Action class
199-
if isinstance(attribute, Action):
198+
# Check if the attribute is an instance of the Action class or GlobalAction class.
199+
if issubclass(type(attribute), ActionBase):
200200
yield attribute
201201

202202

streamdeck/actions.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from abc import ABC
34
from collections import defaultdict
45
from functools import cached_property
56
from logging import getLogger
@@ -39,24 +40,17 @@
3940
}
4041

4142

42-
class Action:
43-
"""Represents an action that can be performed, with event handlers for specific event types."""
43+
class ActionBase(ABC):
44+
"""Base class for all actions."""
4445

45-
def __init__(self, uuid: str):
46+
def __init__(self):
4647
"""Initialize an Action instance.
4748
4849
Args:
4950
uuid (str): The unique identifier for the action.
5051
"""
51-
self.uuid = uuid
52-
5352
self._events: dict[EventNameStr, set[EventHandlerFunc]] = defaultdict(set)
5453

55-
@cached_property
56-
def name(self) -> str:
57-
"""The name of the action, derived from the last part of the UUID."""
58-
return self.uuid.split(".")[-1]
59-
6054
def on(self, event_name: EventNameStr, /) -> Callable[[EventHandlerFunc], EventHandlerFunc]:
6155
"""Register an event handler for a specific event.
6256
@@ -96,17 +90,42 @@ def get_event_handlers(self, event_name: EventNameStr, /) -> Generator[EventHand
9690
msg = f"Provided event name for pulling handlers from action does not exist: {event_name}"
9791
raise KeyError(msg)
9892

93+
if event_name not in self._events:
94+
return
95+
9996
yield from self._events[event_name]
10097

10198

99+
class GlobalAction(ActionBase):
100+
"""Represents an action that is performed at the plugin level, meaning it isn't associated with a specific device or action."""
101+
102+
103+
class Action(ActionBase):
104+
"""Represents an action that can be performed for a specific action, with event handlers for specific event types."""
105+
106+
def __init__(self, uuid: str):
107+
"""Initialize an Action instance.
108+
109+
Args:
110+
uuid (str): The unique identifier for the action.
111+
"""
112+
super().__init__()
113+
self.uuid = uuid
114+
115+
@cached_property
116+
def name(self) -> str:
117+
"""The name of the action, derived from the last part of the UUID."""
118+
return self.uuid.split(".")[-1]
119+
120+
102121
class ActionRegistry:
103122
"""Manages the registration and retrieval of actions and their event handlers."""
104123

105124
def __init__(self) -> None:
106125
"""Initialize an ActionRegistry instance."""
107-
self._plugin_actions: list[Action] = []
126+
self._plugin_actions: list[ActionBase] = []
108127

109-
def register(self, action: Action) -> None:
128+
def register(self, action: ActionBase) -> None:
110129
"""Register an action with the registry.
111130
112131
Args:
@@ -126,9 +145,10 @@ def get_action_handlers(self, event_name: EventNameStr, event_action_uuid: str |
126145
EventHandlerFunc: The event handler functions for the specified event.
127146
"""
128147
for action in self._plugin_actions:
129-
# If the event is action-specific, only get handlers for that action, as we don't want to trigger
148+
# If the event is action-specific (i.e is not a GlobalAction and has a UUID attribute),
149+
# only get handlers for that action, as we don't want to trigger
130150
# and pass this event to handlers for other actions.
131-
if event_action_uuid is not None and event_action_uuid != action.uuid:
151+
if event_action_uuid is not None and (hasattr(action, "uuid") and action.uuid != event_action_uuid):
132152
continue
133153

134154
yield from action.get_event_handlers(event_name)

streamdeck/manager.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from streamdeck.actions import ActionRegistry
88
from streamdeck.command_sender import StreamDeckCommandSender
9-
from streamdeck.models.events import event_adapter
9+
from streamdeck.models.events import ContextualEventMixin, event_adapter
1010
from streamdeck.utils.logging import configure_streamdeck_logger
1111
from streamdeck.websocket import WebSocketClient
1212

@@ -61,8 +61,8 @@ def register_action(self, action: Action) -> None:
6161
Args:
6262
action (Action): The action to register.
6363
"""
64-
# First, configure a logger for the action, giving it the last part of its uuid as name.
65-
action_component_name = action.uuid.split(".")[-1]
64+
# First, configure a logger for the action, giving it the last part of its uuid as name (if it has one).
65+
action_component_name = action.uuid.split(".")[-1] if hasattr(action, "uuid") else "global"
6666
configure_streamdeck_logger(name=action_component_name, plugin_uuid=self.uuid)
6767

6868
self._registry.register(action)
@@ -83,7 +83,7 @@ def run(self) -> None:
8383
logger.debug("Event received: %s", data.event)
8484

8585
# If the event is action-specific, we'll pass the action's uuid to the handler to ensure only the correct action is triggered.
86-
event_action_uuid: str | None = cast(str, data.action) if data.is_action_specific() else None
86+
event_action_uuid = data.action if isinstance(data, ContextualEventMixin) else None
8787

8888
for handler in self._registry.get_action_handlers(event_name=data.event, event_action_uuid=event_action_uuid):
8989
# TODO: from contextual event occurences, save metadata to the action's properties.

0 commit comments

Comments
 (0)