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.
Files changed (30) hide show
  1. {pymidi_controller-0.1.0/pymidi_controller.egg-info → pymidi_controller-0.3.0}/PKG-INFO +85 -6
  2. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/README.md +83 -4
  3. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/cli.py +31 -10
  4. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/data/systemd/pymidi-controller.service +3 -1
  5. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/config_manager.py +11 -1
  6. pymidi_controller-0.3.0/pymidi_controller/utils/function_manager.py +74 -0
  7. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0/pymidi_controller.egg-info}/PKG-INFO +85 -6
  8. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/SOURCES.txt +1 -0
  9. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pyproject.toml +2 -2
  10. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/LICENSE +0 -0
  11. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/MANIFEST.in +0 -0
  12. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/__init__.py +0 -0
  13. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/__init__.py +0 -0
  14. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/elgato.py +0 -0
  15. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/elgato_discovery.py +0 -0
  16. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/hue.py +0 -0
  17. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/actions/hue_discovery.py +0 -0
  18. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/config.py +0 -0
  19. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/core.py +0 -0
  20. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/data/default_config.yaml +0 -0
  21. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/__init__.py +0 -0
  22. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/color_cycle.py +0 -0
  23. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/midi_utils.py +0 -0
  24. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/service_manager.py +0 -0
  25. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller/utils/state_manager.py +0 -0
  26. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/dependency_links.txt +0 -0
  27. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/entry_points.txt +0 -0
  28. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/requires.txt +0 -0
  29. {pymidi_controller-0.1.0 → pymidi_controller-0.3.0}/pymidi_controller.egg-info/top_level.txt +0 -0
  30. {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.1.0
4
- Summary: Daemon that maps MIDI events to system actions (Elgato, Hue, etc.)
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 | Description |
153
- | ------------- | ------------------------------------------- |
154
- | `pymidi init` | Create default config file |
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 | Description |
129
- | ------------- | ------------------------------------------- |
130
- | `pymidi init` | Create default config file |
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
- inst = svc_sub.add_parser("install", help="Install and enable the service unit")
38
- inst_grp = inst.add_mutually_exclusive_group(required=True)
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
- uninst = svc_sub.add_parser("uninstall", help="Disable and remove the service unit")
43
- uninst_grp = uninst.add_mutually_exclusive_group(required=True)
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", help="Stop the service")
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", help="Show service logs")
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=network.target
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
@@ -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
- return
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.1.0
4
- Summary: Daemon that maps MIDI events to system actions (Elgato, Hue, etc.)
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 | Description |
153
- | ------------- | ------------------------------------------- |
154
- | `pymidi init` | Create default config file |
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.1.0"
4
- description = "Daemon that maps MIDI events to system actions (Elgato, Hue, etc.)"
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"]