pymidi-controller 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymidi_controller-0.1.0/pymidi_controller.egg-info → pymidi_controller-0.3.0}/PKG-INFO +85 -6
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/README.md +83 -4
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/cli.py +31 -10
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/data/systemd/pymidi-controller.service +3 -1
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/config_manager.py +11 -1
- pymidi_controller-0.3.0/pymidi_controller/utils/function_manager.py +74 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0/pymidi_controller.egg-info}/PKG-INFO +85 -6
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/SOURCES.txt +1 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pyproject.toml +2 -2
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/LICENSE +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/MANIFEST.in +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/__init__.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/__init__.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/elgato.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/elgato_discovery.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/hue.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/hue_discovery.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/config.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/core.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/data/default_config.yaml +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/__init__.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/color_cycle.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/midi_utils.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/service_manager.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/state_manager.py +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/dependency_links.txt +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/entry_points.txt +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/requires.txt +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/top_level.txt +0 -0
- {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymidi-controller
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Daemon that maps MIDI events to system actions (Elgato, Hue,
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Daemon that maps MIDI events to system actions (Elgato, Hue, or custom functions).
|
|
5
5
|
Author-email: Alex Sandilands <sandilands.alex@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/AlexSandilands/pymidi-controller
|
|
@@ -43,6 +43,7 @@ A lightweight Python CLI and background MIDI listener for controlling Philips Hu
|
|
|
43
43
|
- [Hue Commands](#hue-commands)
|
|
44
44
|
- [Elgato Commands](#elgato-commands)
|
|
45
45
|
- [MIDI Commands](#midi-commands)
|
|
46
|
+
- [Custom Function Commands](#custom-function-commands)
|
|
46
47
|
- [🎹 MIDI Binding Guide](#-midi-binding-guide)
|
|
47
48
|
- [⚙️ Service (Optional)](#️-service-optional)
|
|
48
49
|
|
|
@@ -149,10 +150,10 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
149
150
|
|
|
150
151
|
### Global
|
|
151
152
|
|
|
152
|
-
| Command
|
|
153
|
-
|
|
|
154
|
-
| `pymidi init`
|
|
155
|
-
| `pymidi run` | Start the MIDI listener (`--mode blocking`) |
|
|
153
|
+
| Command | Description |
|
|
154
|
+
| ---------------------------------------------- | ------------------------------------------- |
|
|
155
|
+
| `pymidi init` | Create default config file and function dir |
|
|
156
|
+
| `pymidi run [--mode interactive \| blocking]` | Start the MIDI listener (`--mode blocking`) |
|
|
156
157
|
|
|
157
158
|
### Service
|
|
158
159
|
|
|
@@ -194,6 +195,13 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
194
195
|
| -------------------- | ------------------------------------------ |
|
|
195
196
|
| `pymidi midi listen` | Print incoming MIDI event keys for mapping |
|
|
196
197
|
|
|
198
|
+
### Custom Function Commands
|
|
199
|
+
|
|
200
|
+
| Command | Description |
|
|
201
|
+
| ---------------------------------------------- | ----------------------------------------------------- |
|
|
202
|
+
| `pymidi function list` | List all available custom functions |
|
|
203
|
+
| `pymidi function run <name> [args...]` | Run a custom function by name with optional arguments |
|
|
204
|
+
|
|
197
205
|
---
|
|
198
206
|
|
|
199
207
|
## 🎹 MIDI Binding Guide
|
|
@@ -219,6 +227,77 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
219
227
|
|
|
220
228
|
---
|
|
221
229
|
|
|
230
|
+
## 📜 Custom User Functions
|
|
231
|
+
|
|
232
|
+
You can define your own Python functions to run on MIDI events. These live in:
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
~/.config/pymidi-controller/functions/
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This directory is created by `pymidi init` and permissioned as `0700` (only accessible to your user).
|
|
239
|
+
|
|
240
|
+
### Supported Formats
|
|
241
|
+
|
|
242
|
+
#### Option 1: Single `.py` file
|
|
243
|
+
|
|
244
|
+
**Structure:**
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
functions/
|
|
248
|
+
└── my_function.py
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Content:**
|
|
252
|
+
|
|
253
|
+
Note, the function must have a main() function, and this will be the entry point.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
def main(*args):
|
|
257
|
+
print("Hello from my_function", args)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Option 2: Package folder
|
|
261
|
+
|
|
262
|
+
You can also use a subfolder with either an `__init__.py` or a `main.py`:
|
|
263
|
+
These must have a main() function.
|
|
264
|
+
|
|
265
|
+
**Structure A:**
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
functions/
|
|
269
|
+
└── my_package/
|
|
270
|
+
└── __init__.py
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Structure B:**
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
functions/
|
|
277
|
+
└── my_package/
|
|
278
|
+
└── main.py
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Content:**
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
def main(*args):
|
|
285
|
+
print("Hello from package function", args)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Then bind it in your config:
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
bindings:
|
|
292
|
+
control_change:0:10:127:
|
|
293
|
+
- custom
|
|
294
|
+
- my_function
|
|
295
|
+
- optional_arg1
|
|
296
|
+
- optional_arg2
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
222
301
|
## ⚙️ Service (Optional)
|
|
223
302
|
|
|
224
303
|
To run automatically at login:
|
|
@@ -19,6 +19,7 @@ A lightweight Python CLI and background MIDI listener for controlling Philips Hu
|
|
|
19
19
|
- [Hue Commands](#hue-commands)
|
|
20
20
|
- [Elgato Commands](#elgato-commands)
|
|
21
21
|
- [MIDI Commands](#midi-commands)
|
|
22
|
+
- [Custom Function Commands](#custom-function-commands)
|
|
22
23
|
- [🎹 MIDI Binding Guide](#-midi-binding-guide)
|
|
23
24
|
- [⚙️ Service (Optional)](#️-service-optional)
|
|
24
25
|
|
|
@@ -125,10 +126,10 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
125
126
|
|
|
126
127
|
### Global
|
|
127
128
|
|
|
128
|
-
| Command
|
|
129
|
-
|
|
|
130
|
-
| `pymidi init`
|
|
131
|
-
| `pymidi run` | Start the MIDI listener (`--mode blocking`) |
|
|
129
|
+
| Command | Description |
|
|
130
|
+
| ---------------------------------------------- | ------------------------------------------- |
|
|
131
|
+
| `pymidi init` | Create default config file and function dir |
|
|
132
|
+
| `pymidi run [--mode interactive \| blocking]` | Start the MIDI listener (`--mode blocking`) |
|
|
132
133
|
|
|
133
134
|
### Service
|
|
134
135
|
|
|
@@ -170,6 +171,13 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
170
171
|
| -------------------- | ------------------------------------------ |
|
|
171
172
|
| `pymidi midi listen` | Print incoming MIDI event keys for mapping |
|
|
172
173
|
|
|
174
|
+
### Custom Function Commands
|
|
175
|
+
|
|
176
|
+
| Command | Description |
|
|
177
|
+
| ---------------------------------------------- | ----------------------------------------------------- |
|
|
178
|
+
| `pymidi function list` | List all available custom functions |
|
|
179
|
+
| `pymidi function run <name> [args...]` | Run a custom function by name with optional arguments |
|
|
180
|
+
|
|
173
181
|
---
|
|
174
182
|
|
|
175
183
|
## 🎹 MIDI Binding Guide
|
|
@@ -195,6 +203,77 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
195
203
|
|
|
196
204
|
---
|
|
197
205
|
|
|
206
|
+
## 📜 Custom User Functions
|
|
207
|
+
|
|
208
|
+
You can define your own Python functions to run on MIDI events. These live in:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
~/.config/pymidi-controller/functions/
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This directory is created by `pymidi init` and permissioned as `0700` (only accessible to your user).
|
|
215
|
+
|
|
216
|
+
### Supported Formats
|
|
217
|
+
|
|
218
|
+
#### Option 1: Single `.py` file
|
|
219
|
+
|
|
220
|
+
**Structure:**
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
functions/
|
|
224
|
+
└── my_function.py
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Content:**
|
|
228
|
+
|
|
229
|
+
Note, the function must have a main() function, and this will be the entry point.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
def main(*args):
|
|
233
|
+
print("Hello from my_function", args)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### Option 2: Package folder
|
|
237
|
+
|
|
238
|
+
You can also use a subfolder with either an `__init__.py` or a `main.py`:
|
|
239
|
+
These must have a main() function.
|
|
240
|
+
|
|
241
|
+
**Structure A:**
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
functions/
|
|
245
|
+
└── my_package/
|
|
246
|
+
└── __init__.py
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Structure B:**
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
functions/
|
|
253
|
+
└── my_package/
|
|
254
|
+
└── main.py
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Content:**
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
def main(*args):
|
|
261
|
+
print("Hello from package function", args)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Then bind it in your config:
|
|
265
|
+
|
|
266
|
+
```yaml
|
|
267
|
+
bindings:
|
|
268
|
+
control_change:0:10:127:
|
|
269
|
+
- custom
|
|
270
|
+
- my_function
|
|
271
|
+
- optional_arg1
|
|
272
|
+
- optional_arg2
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
198
277
|
## ⚙️ Service (Optional)
|
|
199
278
|
|
|
200
279
|
To run automatically at login:
|
|
@@ -32,21 +32,32 @@ def main():
|
|
|
32
32
|
# Service Commands
|
|
33
33
|
# ----------------------------------------------------------------
|
|
34
34
|
svc = subparsers.add_parser("service", help="Install or manage the systemd service")
|
|
35
|
+
scope_grp = svc.add_mutually_exclusive_group(required=True)
|
|
36
|
+
scope_grp.add_argument("--user", action="store_true", help="Operate on the current user's service (~/.config/systemd/user)")
|
|
37
|
+
scope_grp.add_argument("--system", action="store_true", help="Operate on the system service (/etc/systemd/system)")
|
|
35
38
|
svc_sub = svc.add_subparsers(dest="svc_cmd", required=True)
|
|
39
|
+
|
|
36
40
|
# install
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
inst_grp.add_argument("--user", action="store_true", help="Install to user's systemd")
|
|
40
|
-
inst_grp.add_argument("--system", action="store_true", help="Install system-wide (requires sudo)")
|
|
41
|
+
svc_sub.add_parser("install", help="Install and enable the service unit")
|
|
42
|
+
|
|
41
43
|
# uninstall
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
uninst_grp.add_argument("--user", action="store_true", help="Remove from user's systemd")
|
|
45
|
-
uninst_grp.add_argument("--system", action="store_true", help="Remove from system-wide systemd")
|
|
44
|
+
svc_sub.add_parser("uninstall", help="Disable and remove the service unit")
|
|
45
|
+
|
|
46
46
|
# stop, enable, log
|
|
47
|
-
svc_sub.add_parser("stop",
|
|
47
|
+
svc_sub.add_parser("stop", help="Stop the service")
|
|
48
48
|
svc_sub.add_parser("enable", help="Enable the service")
|
|
49
|
-
svc_sub.add_parser("log",
|
|
49
|
+
svc_sub.add_parser("log", help="Show service logs")
|
|
50
|
+
|
|
51
|
+
# ----------------------------------------------------------------
|
|
52
|
+
# Custom Function Commands
|
|
53
|
+
# ----------------------------------------------------------------
|
|
54
|
+
func = subparsers.add_parser("function", help="Custom function management")
|
|
55
|
+
func_sub = func.add_subparsers(dest="func_cmd", required=True)
|
|
56
|
+
func_sub.add_parser("list", help="List available custom functions")
|
|
57
|
+
# Run a custom function
|
|
58
|
+
run_func = func_sub.add_parser("run", help="Run a custom function")
|
|
59
|
+
run_func.add_argument("name", help="Name of the custom function to run")
|
|
60
|
+
run_func.add_argument("args", nargs=argparse.REMAINDER, help="Arguments to pass to the function")
|
|
50
61
|
|
|
51
62
|
# ----------------------------------------------------------------
|
|
52
63
|
# Hue Commands
|
|
@@ -102,6 +113,16 @@ def main():
|
|
|
102
113
|
core_run(mode=args.mode)
|
|
103
114
|
sys.exit(0)
|
|
104
115
|
|
|
116
|
+
if args.command == "function":
|
|
117
|
+
if args.func_cmd == "list":
|
|
118
|
+
from pymidi_controller.utils.function_manager import list_functions
|
|
119
|
+
list_functions()
|
|
120
|
+
elif args.func_cmd == "run":
|
|
121
|
+
from pymidi_controller.utils.function_manager import load_user_function
|
|
122
|
+
func = load_user_function(args.name)
|
|
123
|
+
func(*args.args)
|
|
124
|
+
sys.exit(0)
|
|
125
|
+
|
|
105
126
|
if args.command == "service":
|
|
106
127
|
scope = ServiceScope.USER if getattr(args, "user", False) else ServiceScope.SYSTEM
|
|
107
128
|
service_dispatch(args.svc_cmd, scope)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
[Unit]
|
|
2
2
|
Description=🎹 MIDI Controller Background Service
|
|
3
|
-
After=
|
|
3
|
+
After=graphical-session.target sound.target
|
|
4
|
+
Wants=graphical-session.target sound.target
|
|
4
5
|
|
|
5
6
|
[Service]
|
|
6
7
|
Type=simple
|
|
7
8
|
Environment=PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin
|
|
9
|
+
ExecStartPre=/bin/sleep 5
|
|
8
10
|
ExecStart=/usr/bin/env pymidi-controller run --mode blocking
|
|
9
11
|
Restart=always
|
|
10
12
|
RestartSec=5
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/config_manager.py
RENAMED
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
XDG = Path.home() / ".config" if "XDG_CONFIG_HOME" not in __import__("os").environ else Path(__import__("os").environ["XDG_CONFIG_HOME"])
|
|
8
8
|
CFG_DIR = XDG / "pymidi-controller"
|
|
9
9
|
CFG_FILE = CFG_DIR / "config.yaml"
|
|
10
|
+
FUNC_DIR = CFG_DIR / "functions"
|
|
10
11
|
|
|
11
12
|
def ensure_config_dir():
|
|
12
13
|
CFG_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -30,10 +31,19 @@ def init_config():
|
|
|
30
31
|
ensure_config_dir()
|
|
31
32
|
data = resources.files("pymidi_controller").joinpath("data/default_config.yaml")
|
|
32
33
|
|
|
34
|
+
# Ensure the config file exists
|
|
33
35
|
if not CFG_FILE.exists():
|
|
34
36
|
CFG_FILE.write_text(data.read_text())
|
|
35
37
|
os.chmod(CFG_FILE, 0o600)
|
|
36
38
|
print(f"Created {CFG_FILE}, please discover your API keys & set midi mappings.")
|
|
37
39
|
else:
|
|
38
40
|
print(f"{CFG_FILE} already exists, skipping.")
|
|
39
|
-
|
|
41
|
+
|
|
42
|
+
# Ensure the functions directory exists
|
|
43
|
+
if not FUNC_DIR.exists():
|
|
44
|
+
FUNC_DIR.mkdir(parents=True)
|
|
45
|
+
os.chmod(FUNC_DIR, 0o700)
|
|
46
|
+
print(f"Created {FUNC_DIR}, please add your custom functions here.")
|
|
47
|
+
else:
|
|
48
|
+
print(f"{FUNC_DIR} already exists, skipping.")
|
|
49
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import importlib.util
|
|
3
|
+
from types import ModuleType
|
|
4
|
+
from pymidi_controller.utils.config_manager import FUNC_DIR
|
|
5
|
+
|
|
6
|
+
CACHE: dict[str, tuple[ModuleType, float]] = {} # name → (module, mtime)
|
|
7
|
+
|
|
8
|
+
def _ensure_path():
|
|
9
|
+
"""Ensure FUNC_DIR is on sys.path (for relative imports inside user modules)."""
|
|
10
|
+
if FUNC_DIR.exists() and str(FUNC_DIR) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(FUNC_DIR))
|
|
12
|
+
|
|
13
|
+
def _load_module_from_path(name: str, path: str) -> ModuleType:
|
|
14
|
+
"""Safely load a module from a specific file path."""
|
|
15
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
16
|
+
if not spec or not spec.loader:
|
|
17
|
+
raise ImportError(f"Cannot load module '{name}' from {path}")
|
|
18
|
+
module = importlib.util.module_from_spec(spec)
|
|
19
|
+
try:
|
|
20
|
+
spec.loader.exec_module(module)
|
|
21
|
+
except Exception as e:
|
|
22
|
+
raise RuntimeError(f"Error while executing module '{name}': {e}")
|
|
23
|
+
return module
|
|
24
|
+
|
|
25
|
+
def load_user_function(name: str):
|
|
26
|
+
"""Loads and returns the `main()` function from a user-defined module."""
|
|
27
|
+
_ensure_path()
|
|
28
|
+
|
|
29
|
+
# Locate the file on disk
|
|
30
|
+
file_py = FUNC_DIR / f"{name}.py"
|
|
31
|
+
file_pkg = FUNC_DIR / name / "__init__.py"
|
|
32
|
+
file_alt = FUNC_DIR / name / "main.py"
|
|
33
|
+
|
|
34
|
+
filepath = None
|
|
35
|
+
for candidate in (file_py, file_pkg, file_alt):
|
|
36
|
+
if candidate.exists():
|
|
37
|
+
filepath = candidate
|
|
38
|
+
break
|
|
39
|
+
|
|
40
|
+
if filepath is None:
|
|
41
|
+
raise FileNotFoundError(f"No module '{name}' found in {FUNC_DIR}")
|
|
42
|
+
|
|
43
|
+
mtime = filepath.stat().st_mtime
|
|
44
|
+
|
|
45
|
+
# Load (or reload) the module from file
|
|
46
|
+
if name in CACHE:
|
|
47
|
+
_, cached_mtime = CACHE[name]
|
|
48
|
+
if mtime > cached_mtime:
|
|
49
|
+
module = _load_module_from_path(name, str(filepath)) # reload if changed
|
|
50
|
+
CACHE[name] = (module, mtime)
|
|
51
|
+
else:
|
|
52
|
+
module = CACHE[name][0] # reuse cached module
|
|
53
|
+
else:
|
|
54
|
+
module = _load_module_from_path(name, str(filepath))
|
|
55
|
+
CACHE[name] = (module, mtime)
|
|
56
|
+
|
|
57
|
+
if not hasattr(module, "main") or not callable(module.main):
|
|
58
|
+
raise RuntimeError(f"User module '{name}' must define a callable main() function")
|
|
59
|
+
|
|
60
|
+
return module.main
|
|
61
|
+
|
|
62
|
+
def list_functions():
|
|
63
|
+
"""Prints available function modules in FUNC_DIR."""
|
|
64
|
+
_ensure_path()
|
|
65
|
+
if not FUNC_DIR.exists():
|
|
66
|
+
print(f"Function directory not found: {FUNC_DIR}")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
print("Available functions:")
|
|
70
|
+
for path in FUNC_DIR.iterdir():
|
|
71
|
+
if path.is_dir() and ((path / "__init__.py").exists() or (path / "main.py").exists()):
|
|
72
|
+
print(f" - {path.name}")
|
|
73
|
+
elif path.suffix == ".py":
|
|
74
|
+
print(f" - {path.stem}")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymidi-controller
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Daemon that maps MIDI events to system actions (Elgato, Hue,
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Daemon that maps MIDI events to system actions (Elgato, Hue, or custom functions).
|
|
5
5
|
Author-email: Alex Sandilands <sandilands.alex@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/AlexSandilands/pymidi-controller
|
|
@@ -43,6 +43,7 @@ A lightweight Python CLI and background MIDI listener for controlling Philips Hu
|
|
|
43
43
|
- [Hue Commands](#hue-commands)
|
|
44
44
|
- [Elgato Commands](#elgato-commands)
|
|
45
45
|
- [MIDI Commands](#midi-commands)
|
|
46
|
+
- [Custom Function Commands](#custom-function-commands)
|
|
46
47
|
- [🎹 MIDI Binding Guide](#-midi-binding-guide)
|
|
47
48
|
- [⚙️ Service (Optional)](#️-service-optional)
|
|
48
49
|
|
|
@@ -149,10 +150,10 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
149
150
|
|
|
150
151
|
### Global
|
|
151
152
|
|
|
152
|
-
| Command
|
|
153
|
-
|
|
|
154
|
-
| `pymidi init`
|
|
155
|
-
| `pymidi run` | Start the MIDI listener (`--mode blocking`) |
|
|
153
|
+
| Command | Description |
|
|
154
|
+
| ---------------------------------------------- | ------------------------------------------- |
|
|
155
|
+
| `pymidi init` | Create default config file and function dir |
|
|
156
|
+
| `pymidi run [--mode interactive \| blocking]` | Start the MIDI listener (`--mode blocking`) |
|
|
156
157
|
|
|
157
158
|
### Service
|
|
158
159
|
|
|
@@ -194,6 +195,13 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
194
195
|
| -------------------- | ------------------------------------------ |
|
|
195
196
|
| `pymidi midi listen` | Print incoming MIDI event keys for mapping |
|
|
196
197
|
|
|
198
|
+
### Custom Function Commands
|
|
199
|
+
|
|
200
|
+
| Command | Description |
|
|
201
|
+
| ---------------------------------------------- | ----------------------------------------------------- |
|
|
202
|
+
| `pymidi function list` | List all available custom functions |
|
|
203
|
+
| `pymidi function run <name> [args...]` | Run a custom function by name with optional arguments |
|
|
204
|
+
|
|
197
205
|
---
|
|
198
206
|
|
|
199
207
|
## 🎹 MIDI Binding Guide
|
|
@@ -219,6 +227,77 @@ Use `pymidi <group> --help` for details on each context.
|
|
|
219
227
|
|
|
220
228
|
---
|
|
221
229
|
|
|
230
|
+
## 📜 Custom User Functions
|
|
231
|
+
|
|
232
|
+
You can define your own Python functions to run on MIDI events. These live in:
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
~/.config/pymidi-controller/functions/
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This directory is created by `pymidi init` and permissioned as `0700` (only accessible to your user).
|
|
239
|
+
|
|
240
|
+
### Supported Formats
|
|
241
|
+
|
|
242
|
+
#### Option 1: Single `.py` file
|
|
243
|
+
|
|
244
|
+
**Structure:**
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
functions/
|
|
248
|
+
└── my_function.py
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Content:**
|
|
252
|
+
|
|
253
|
+
Note, the function must have a main() function, and this will be the entry point.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
def main(*args):
|
|
257
|
+
print("Hello from my_function", args)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### Option 2: Package folder
|
|
261
|
+
|
|
262
|
+
You can also use a subfolder with either an `__init__.py` or a `main.py`:
|
|
263
|
+
These must have a main() function.
|
|
264
|
+
|
|
265
|
+
**Structure A:**
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
functions/
|
|
269
|
+
└── my_package/
|
|
270
|
+
└── __init__.py
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Structure B:**
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
functions/
|
|
277
|
+
└── my_package/
|
|
278
|
+
└── main.py
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Content:**
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
def main(*args):
|
|
285
|
+
print("Hello from package function", args)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Then bind it in your config:
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
bindings:
|
|
292
|
+
control_change:0:10:127:
|
|
293
|
+
- custom
|
|
294
|
+
- my_function
|
|
295
|
+
- optional_arg1
|
|
296
|
+
- optional_arg2
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
222
301
|
## ⚙️ Service (Optional)
|
|
223
302
|
|
|
224
303
|
To run automatically at login:
|
|
@@ -22,6 +22,7 @@ pymidi_controller/data/systemd/pymidi-controller.service
|
|
|
22
22
|
pymidi_controller/utils/__init__.py
|
|
23
23
|
pymidi_controller/utils/color_cycle.py
|
|
24
24
|
pymidi_controller/utils/config_manager.py
|
|
25
|
+
pymidi_controller/utils/function_manager.py
|
|
25
26
|
pymidi_controller/utils/midi_utils.py
|
|
26
27
|
pymidi_controller/utils/service_manager.py
|
|
27
28
|
pymidi_controller/utils/state_manager.py
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pymidi-controller"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Daemon that maps MIDI events to system actions (Elgato, Hue,
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Daemon that maps MIDI events to system actions (Elgato, Hue, or custom functions)."
|
|
5
5
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
6
|
license = "MIT"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/elgato_discovery.py
RENAMED
|
File without changes
|
|
File without changes
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/hue_discovery.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/data/default_config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/service_manager.py
RENAMED
|
File without changes
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/state_manager.py
RENAMED
|
File without changes
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|