midiscripter 0.2__tar.gz → 0.3__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 (50) hide show
  1. {midiscripter-0.2 → midiscripter-0.3}/PKG-INFO +75 -36
  2. midiscripter-0.3/README.md +103 -0
  3. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/base/msg_base.py +0 -1
  4. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/base/port_base.py +14 -10
  5. midiscripter-0.3/midiscripter/base/shared.py +140 -0
  6. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/cli/starters.py +3 -0
  7. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/file_event/file_event_msg.py +11 -5
  8. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/file_event/file_event_port.py +0 -1
  9. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/app.py +16 -9
  10. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/button.py +3 -4
  11. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/gui_msg.py +12 -7
  12. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/gui_widget_base.py +12 -8
  13. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/layout.py +2 -2
  14. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/list.py +4 -5
  15. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/mixins.py +7 -8
  16. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/text.py +1 -3
  17. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/log_widget.py +10 -12
  18. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/main_window.py +13 -13
  19. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/menu_bar.py +26 -50
  20. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/ports_widget.py +74 -86
  21. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/keyboard/keyboard_msg.py +10 -5
  22. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/keyboard/keyboard_port.py +11 -8
  23. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/logger/log.py +14 -14
  24. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/metronome/metronome_port.py +7 -8
  25. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/midi_msg.py +35 -21
  26. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/midi_note_data.py +0 -1
  27. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/midi_port.py +67 -39
  28. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/midi_ports_update.py +1 -1
  29. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/osc/osc_msg.py +10 -4
  30. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/osc/osc_port.py +12 -9
  31. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/osc/osc_query_maker.py +0 -1
  32. {midiscripter-0.2 → midiscripter-0.3}/pyproject.toml +4 -5
  33. midiscripter-0.2/README.md +0 -63
  34. midiscripter-0.2/midiscripter/base/shared.py +0 -62
  35. {midiscripter-0.2 → midiscripter-0.3}/LICENSE +0 -0
  36. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/__init__.py +0 -0
  37. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/base/__init__.py +0 -0
  38. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/cli/__init__.py +0 -0
  39. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/file_event/__init__.py +0 -0
  40. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/__init__.py +0 -0
  41. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/__init__.py +0 -0
  42. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/keyboard/__init__.py +0 -0
  43. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/logger/__init__.py +0 -0
  44. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/logger/console_sink.py +0 -0
  45. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/logger/html_sink.py +0 -0
  46. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/metronome/__init__.py +0 -0
  47. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/__init__.py +0 -0
  48. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/osc/__init__.py +0 -0
  49. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/resources/icon.ico +0 -0
  50. {midiscripter-0.2 → midiscripter-0.3}/midiscripter/resources/icon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: midiscripter
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: Framework for scripting MIDI, keyboard and Open Sound Control input and output
5
5
  Project-URL: Documentation, https://maboroshy.github.io/midi-scripter
6
6
  Project-URL: Issues, https://github.com/Maboroshy/midi-scripter/issues
@@ -175,11 +175,8 @@ License-File: LICENSE
175
175
  Keywords: MIDI,OSC,automation,input,script
176
176
  Classifier: Development Status :: 4 - Beta
177
177
  Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
178
- Classifier: Operating System :: Microsoft :: Windows
179
- Classifier: Programming Language :: Python
180
178
  Classifier: Programming Language :: Python :: 3.11
181
179
  Classifier: Programming Language :: Python :: 3.12
182
- Classifier: Programming Language :: Python :: Implementation :: CPython
183
180
  Classifier: Topic :: Multimedia :: Sound/Audio
184
181
  Classifier: Topic :: Multimedia :: Sound/Audio :: MIDI
185
182
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -191,6 +188,8 @@ Requires-Dist: python-osc~=1.7
191
188
  Requires-Dist: python-rtmidi~=1.5
192
189
  Requires-Dist: pywin32; platform_system == 'Windows'
193
190
  Requires-Dist: watchdog~=4.0
191
+ Provides-Extra: dev
192
+ Requires-Dist: ruff; extra == 'dev'
194
193
  Provides-Extra: doc
195
194
  Requires-Dist: griffe-inherited-docstrings; extra == 'doc'
196
195
  Requires-Dist: mkdocs-material; extra == 'doc'
@@ -199,19 +198,28 @@ Description-Content-Type: text/markdown
199
198
 
200
199
  # MIDI Scripter
201
200
 
202
- MIDI Scripter is a framework for scripting MIDI, keyboard and Open Sound Control (OSC) input and output with a few lines of Python code.
201
+ MIDI Scripter is a framework for scripting MIDI, keyboard and Open Sound
202
+ Control (OSC) input and output with only a few lines of Python code.
203
203
 
204
- MIDI Scripter is intended for digital audio workstation (DAW) controls scripting but can also be used for general input conversion and automation. It fits where controller mappings are not enough but rewriting DAW controller integration is too much.
204
+ MIDI Scripter listens to selected input ports and feed incoming messages to
205
+ subscribed callables (functions, methods, etc.). These callables or any
206
+ other Python code can send modified or brand-new messages with
207
+ output ports. MIDI Scripter can act as a proxy to filter and transform and
208
+ convert the input.
209
+
210
+ MIDI Scripter included customizable GUI that provides messages logging,
211
+ "ready to paste" port and message declarations and one line of code widgets for
212
+ monitoring / controlling scripts.
205
213
 
206
214
  An octave transposer with GUI controls in 10 lines of code:
207
215
 
208
216
  ``` python
209
- from midiscripter import * # safe * import with no bloat
217
+ from midiscripter import *
210
218
 
211
- midi_keyboard = MidiIn('Port name') # GUI will provide you with port names
212
- proxy_output = MidiOut('loopMIDI Port') # using loopMIDI virtual port for output
219
+ midi_keyboard = MidiIn('MIDI Keyboard') # GUI will provide you with port names
220
+ proxy_output = MidiOut('To DAW') # using virtual port for output
213
221
 
214
- # GUI control in a single line, many widget available, custom widgets supported
222
+ # GUI control in a single line
215
223
  octave_selector = GuiButtonSelectorH(('-2', '-1', '0', '+1', '+2'), select='0')
216
224
 
217
225
  @midi_keyboard.subscribe # decorated function will receive port's messages
@@ -224,39 +232,70 @@ if __name__ == '__main__': # combine multiple scripts by importing them
224
232
  start_gui() # opens helpful customizable GUI
225
233
  ```
226
234
 
227
- ![Screenshot](https://github.com/Maboroshy/midi-scripter/blob/master/examples/octave_transposer/screenshot.png?raw=true)
235
+ ![Screenshot after some widget arrangement](https://github.com/Maboroshy/midi-scripter/blob/master/examples/octave_transposer/screenshot.png?raw=true)
236
+
237
+ [You can find more examples here.](https://github.com/Maboroshy/midi-scripter/tree/master/examples)
238
+
239
+ [Scripting guide and API documentation available.](https://maboroshy.github.io/midi-scripter)
240
+
241
+ Currently MIDI Scripter is at "beta" development stage. It's fully
242
+ functional but needs more user feedback. It works on Windows and Linx and
243
+ should work on macOS.
244
+
245
+ ## What it can do
246
+
247
+ For writing Python code:
228
248
 
229
- [Scripting guide and API documentation available](https://maboroshy.github.io/midi-scripter)
249
+ - Prepare MIDI, OSC and keyboard inputs and outputs with a single line,
250
+ without boilerplate code.
251
+ - Feed input messages to functions or any callables by "decorating" them.
252
+ - Work with common message objects instead of raw data different for each port
253
+ type.
254
+ - Send MIDI, OSC and keyboard output messages.
255
+ - Create GUI widgets with one line and arrange them with mouse on your script's
256
+ dashboard.
230
257
 
231
- Easy tasks for MIDI Scripter:
232
- 1. Filter, modify and route MIDI, OSC and keyboard messages in any way.
233
- 2. Map MIDI, OSC and keyboard to each other.
234
- 3. Map any Python code to input message.
235
- 4. Make extra "shift" keys to multiply MIDI controls.
236
- 5. Organize mappings into sets / scenes with GUI controls.
237
- 6. Make an extra overlay mappings on top of MIDI controller's default DAW integration by using proxy.
258
+ For live performance setups:
238
259
 
239
- Complex tasks for MIDI Scripter:
240
- 1. Create and map complex macros involving multiple hardware or virtual MIDI controllers.
241
- 2. Make custom sequencer or MIDI input generator.
242
- 3. Make music education ot trainer GUI application based on MIDI input.
260
+ - Make extra layers to multiply MIDI controls.
261
+ - Organize mappings into sets / scenes with GUI dashboard.
262
+ - Make an extra overlay mappings on top of MIDI controller's DAW integration by
263
+ using proxies.
264
+ - Create and map complex macros.
243
265
 
244
- Currently MIDI Scripter is at "beta" development stage. It's fully functional but needs some more testing. It targets only Windows for now but Linux and macOS support will follow.
266
+ For MIDI related apps:
267
+
268
+ - Make custom sequencers or MIDI output generators.
269
+ - Make basic music training GUI applications based on MIDI input.
270
+
271
+ For software control and automation:
272
+
273
+ - Use MIDI controllers or keyboard shortcuts to run any Python code.
274
+ - Use keyboard macros to control apps.
245
275
 
246
276
  ## Installation
247
- 1. [Python 3.11+](https://www.python.org/downloads/) with "Add python.exe to PATH" option.
248
- 2. [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html) for virtual MIDI output ports on Windows.
249
- 3. `pip install midiscripter` in console.
250
277
 
251
- ## Setup
252
- Run loopMIDI and add virtual ports you want to send MIDI messages. You can enable its autostart option.
278
+ 1. Install [Python 3.11+](https://www.python.org/downloads/) including pip.
279
+ 2. Run `pip install midiscripter`.
253
280
 
254
- Enable virtual MIDI output ports as MIDI inputs in DAW.
281
+ Extra steps for Windows:
282
+
283
+ 1. Enable `Add python .exe to PATH` option in Python installer.
284
+ 2. Install [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html)
285
+ and set required virtual MIDI ports inside it.
255
286
 
256
287
  ## Quick Start
257
- 1. Paste [script template](examples/script_template.py) to Python IDE or plain text editor. IDE is greatly recommended.
258
- 2. Run unchanged template script from IDE or by `python .\script_template.py` to open GUI for more info on available ports and their input.
259
- 3. Turn on the ports' checkboxes to enable them and watch the log for input messages.
260
- 4. Click on port names and messages to copy their declarations to the clipboard. Paste the declarations to your script.
261
- 5. Write the functions you need. Subscribe them to messages with `@input_port.subscribe` decorator. Use `log('messages')` for debugging. Use `output_port.send(msg)` to send modified or created messages from a function.
262
- 6. Restart the script from GUI.
288
+
289
+ 1. Paste [script template](examples/script_template.py) to Python IDE or plain
290
+ text editor. IDE is greatly recommended.
291
+ 2. Run unaltered template script from IDE or by `python` command to open GUI for
292
+ more info on available ports and their input.
293
+ 3. Turn on available ports' checkboxes to enable them, and watch the log for
294
+ input messages.
295
+ 4. Click on port names and messages to copy their declarations to the clipboard.
296
+ Paste the declarations to your script.
297
+ 5. Write the functions you need. Subscribe them to input messages with
298
+ `@input_port.subscribe` decorator. Use `log('messages')` for debugging.
299
+ Use `output_port.send(msg)` to send modified or created messages from a
300
+ function.
301
+ 6. Restart the script from GUI to check how in works.
@@ -0,0 +1,103 @@
1
+ # MIDI Scripter
2
+
3
+ MIDI Scripter is a framework for scripting MIDI, keyboard and Open Sound
4
+ Control (OSC) input and output with only a few lines of Python code.
5
+
6
+ MIDI Scripter listens to selected input ports and feed incoming messages to
7
+ subscribed callables (functions, methods, etc.). These callables or any
8
+ other Python code can send modified or brand-new messages with
9
+ output ports. MIDI Scripter can act as a proxy to filter and transform and
10
+ convert the input.
11
+
12
+ MIDI Scripter included customizable GUI that provides messages logging,
13
+ "ready to paste" port and message declarations and one line of code widgets for
14
+ monitoring / controlling scripts.
15
+
16
+ An octave transposer with GUI controls in 10 lines of code:
17
+
18
+ ``` python
19
+ from midiscripter import *
20
+
21
+ midi_keyboard = MidiIn('MIDI Keyboard') # GUI will provide you with port names
22
+ proxy_output = MidiOut('To DAW') # using virtual port for output
23
+
24
+ # GUI control in a single line
25
+ octave_selector = GuiButtonSelectorH(('-2', '-1', '0', '+1', '+2'), select='0')
26
+
27
+ @midi_keyboard.subscribe # decorated function will receive port's messages
28
+ def transpose(msg: MidiMsg) -> None:
29
+ if msg.type == MidiType.NOTE_ON or msg.type == MidiType.NOTE_OFF: # filter
30
+ msg.data1 += 12 * int(octave_selector.selected_item_text) # modify
31
+ proxy_output.send(msg) # route
32
+
33
+ if __name__ == '__main__': # combine multiple scripts by importing them
34
+ start_gui() # opens helpful customizable GUI
35
+ ```
36
+
37
+ ![Screenshot after some widget arrangement](https://github.com/Maboroshy/midi-scripter/blob/master/examples/octave_transposer/screenshot.png?raw=true)
38
+
39
+ [You can find more examples here.](https://github.com/Maboroshy/midi-scripter/tree/master/examples)
40
+
41
+ [Scripting guide and API documentation available.](https://maboroshy.github.io/midi-scripter)
42
+
43
+ Currently MIDI Scripter is at "beta" development stage. It's fully
44
+ functional but needs more user feedback. It works on Windows and Linx and
45
+ should work on macOS.
46
+
47
+ ## What it can do
48
+
49
+ For writing Python code:
50
+
51
+ - Prepare MIDI, OSC and keyboard inputs and outputs with a single line,
52
+ without boilerplate code.
53
+ - Feed input messages to functions or any callables by "decorating" them.
54
+ - Work with common message objects instead of raw data different for each port
55
+ type.
56
+ - Send MIDI, OSC and keyboard output messages.
57
+ - Create GUI widgets with one line and arrange them with mouse on your script's
58
+ dashboard.
59
+
60
+ For live performance setups:
61
+
62
+ - Make extra layers to multiply MIDI controls.
63
+ - Organize mappings into sets / scenes with GUI dashboard.
64
+ - Make an extra overlay mappings on top of MIDI controller's DAW integration by
65
+ using proxies.
66
+ - Create and map complex macros.
67
+
68
+ For MIDI related apps:
69
+
70
+ - Make custom sequencers or MIDI output generators.
71
+ - Make basic music training GUI applications based on MIDI input.
72
+
73
+ For software control and automation:
74
+
75
+ - Use MIDI controllers or keyboard shortcuts to run any Python code.
76
+ - Use keyboard macros to control apps.
77
+
78
+ ## Installation
79
+
80
+ 1. Install [Python 3.11+](https://www.python.org/downloads/) including pip.
81
+ 2. Run `pip install midiscripter`.
82
+
83
+ Extra steps for Windows:
84
+
85
+ 1. Enable `Add python .exe to PATH` option in Python installer.
86
+ 2. Install [loopMIDI](https://www.tobias-erichsen.de/software/loopmidi.html)
87
+ and set required virtual MIDI ports inside it.
88
+
89
+ ## Quick Start
90
+
91
+ 1. Paste [script template](examples/script_template.py) to Python IDE or plain
92
+ text editor. IDE is greatly recommended.
93
+ 2. Run unaltered template script from IDE or by `python` command to open GUI for
94
+ more info on available ports and their input.
95
+ 3. Turn on available ports' checkboxes to enable them, and watch the log for
96
+ input messages.
97
+ 4. Click on port names and messages to copy their declarations to the clipboard.
98
+ Paste the declarations to your script.
99
+ 5. Write the functions you need. Subscribe them to input messages with
100
+ `@input_port.subscribe` decorator. Use `log('messages')` for debugging.
101
+ Use `output_port.send(msg)` to send modified or created messages from a
102
+ function.
103
+ 6. Restart the script from GUI to check how in works.
@@ -1,5 +1,4 @@
1
1
  import enum
2
- from typing import Optional
3
2
  from collections.abc import Container
4
3
 
5
4
  import midiscripter.base.shared
@@ -2,8 +2,8 @@ import collections
2
2
  import contextlib
3
3
  import copy
4
4
  import traceback
5
- from typing import TYPE_CHECKING, TypeVar
6
5
  from collections.abc import Callable, Hashable
6
+ from typing import TYPE_CHECKING, TypeVar
7
7
 
8
8
  import midiscripter.base.shared
9
9
  from midiscripter.logger import log
@@ -13,13 +13,13 @@ if TYPE_CHECKING:
13
13
 
14
14
 
15
15
  @contextlib.contextmanager
16
- def _all_opened():
16
+ def _all_opened() -> None:
17
17
  for port in _PortRegistryMeta.instance_registry.values():
18
18
  port._open()
19
19
 
20
20
  for call in midiscripter.base.shared.run_after_ports_open_subscribed_calls:
21
21
 
22
- def __call_runner():
22
+ def __call_runner() -> None:
23
23
  try:
24
24
  log('Running {call}', call=call) # noqa: B023
25
25
  call() # noqa: B023
@@ -229,17 +229,21 @@ class Output(Port):
229
229
  Args:
230
230
  msg: Message to send.
231
231
  """
232
- with self._check_and_log_sent_message(msg):
233
- raise NotImplementedError
232
+ if not self._validate_msg_send(msg):
233
+ return
234
+
235
+ raise NotImplementedError
234
236
 
235
- @contextlib.contextmanager
236
- def _check_and_log_sent_message(self, msg: 'Msg'):
237
+ # noinspection PyUnreachableCode
238
+ self._log_msg_sent(msg)
239
+
240
+ def _validate_msg_send(self, msg: 'Msg') -> bool:
237
241
  if not self.is_enabled:
238
242
  log.red("Can't send message {msg}. {output} is disabled!", msg=msg, output=self)
239
- return
240
-
241
- yield
243
+ return False
244
+ return True
242
245
 
246
+ def _log_msg_sent(self, msg: 'Msg') -> None:
243
247
  if msg.source:
244
248
  log(
245
249
  '{output} sent message {msg} received {age_ms} ms ago',
@@ -0,0 +1,140 @@
1
+ import concurrent.futures
2
+ import os
3
+ import pathlib
4
+ import platform
5
+ import plistlib
6
+ import sys
7
+ import time
8
+ from collections.abc import Callable
9
+
10
+ import __main__
11
+
12
+ if platform.system() == 'Windows':
13
+ import win32api
14
+ import win32con
15
+ import win32process
16
+ import win32com.client
17
+
18
+
19
+ try:
20
+ script_path = __main__.__file__
21
+ except AttributeError: # in subprocess or IPython
22
+ script_path = None
23
+
24
+
25
+ thread_executor = concurrent.futures.ThreadPoolExecutor(100)
26
+
27
+
28
+ _precise_time_delta = time.time() - time.perf_counter()
29
+
30
+
31
+ def precise_epoch_time() -> float:
32
+ """current time in epoch format with nanosecond precision"""
33
+ return _precise_time_delta + time.perf_counter()
34
+
35
+
36
+ def restart_script() -> None:
37
+ """Exit and restart current script"""
38
+ os.execv(sys.executable, ['python', script_path])
39
+ exit(0)
40
+
41
+
42
+ def _raise_current_process_cpu_priority() -> None:
43
+ """Sets HIGH process priority in Windows for current python process"""
44
+ if platform.system() == 'Windows':
45
+ pid = win32api.GetCurrentProcessId()
46
+ handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid)
47
+ win32process.SetPriorityClass(handle, win32process.HIGH_PRIORITY_CLASS)
48
+
49
+
50
+ run_after_ports_open_subscribed_calls = []
51
+
52
+
53
+ # A Decorator
54
+ def run_after_ports_opened(function: Callable[[], None]) -> Callable:
55
+ """Decorator to subscribe a callable to run after all ports are opened at the script start
56
+
57
+ Args:
58
+ function: A callable with no arguments
59
+
60
+ Returns:
61
+ Subscribed callable.
62
+ """
63
+ if function not in run_after_ports_open_subscribed_calls:
64
+ run_after_ports_open_subscribed_calls.append(function)
65
+ return function
66
+
67
+
68
+ class AutostartManager:
69
+ def __init__(self):
70
+ self.__autostart_shortcut_prefix = 'MIDI Scripter - '
71
+ self.__script_path = pathlib.Path(script_path).resolve()
72
+ self.__shortcut_name = f'{self.__autostart_shortcut_prefix}{self.__script_path.stem}'
73
+
74
+ if platform.system() == 'Windows':
75
+ self.__autostart_dir_path = (
76
+ pathlib.Path.home()
77
+ / 'AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup'
78
+ )
79
+ __shortcut_extension = 'lnk'
80
+ elif platform.system() == 'Linux' or platform.system() == 'Darwin':
81
+ self.__autostart_dir_path = pathlib.Path.home() / '.config/autostart'
82
+ __shortcut_extension = 'desktop'
83
+ elif platform.system() == 'Darwin':
84
+ self.__autostart_dir_path = pathlib.Path.home() / 'Library/LaunchAgents'
85
+ __shortcut_extension = 'plist'
86
+
87
+ self.__autostart_shortcut_path = (
88
+ self.__autostart_dir_path / f'{self.__shortcut_name}.{__shortcut_extension}'
89
+ )
90
+
91
+ def _check_if_enabled(self) -> bool:
92
+ return self.__autostart_shortcut_path.is_file()
93
+
94
+ def __other_scripts_shortcuts(self) -> list[pathlib.Path]:
95
+ return [
96
+ path
97
+ for path in self.__autostart_dir_path.iterdir()
98
+ if (
99
+ path.name.startswith(self.__autostart_shortcut_prefix)
100
+ and path.is_file()
101
+ and path != self.__autostart_shortcut_path.resolve()
102
+ )
103
+ ]
104
+
105
+ def _check_if_other_scripts_present(self) -> bool:
106
+ return bool(self.__other_scripts_shortcuts())
107
+
108
+ def _enable(self) -> None:
109
+ if platform.system() == 'Windows':
110
+ shell = win32com.client.Dispatch('WScript.Shell')
111
+ shortcut = shell.CreateShortCut(str(self.__autostart_shortcut_path.resolve()))
112
+ shortcut.Targetpath = str(pathlib.Path(sys.executable).parent / 'pythonw.exe')
113
+ shortcut.WorkingDirectory = str(self.__script_path.parent.resolve())
114
+ shortcut.Arguments = f'"{self.__script_path}" "--tray"'
115
+ shortcut.save()
116
+
117
+ elif platform.system() == 'Linux':
118
+ desktop_file_content = (
119
+ '[Desktop Entry]\n'
120
+ f'Name={self.__shortcut_name}\n'
121
+ f'Exec=python "{self.__script_path}" --tray\n'
122
+ 'Type=Application\n'
123
+ 'Terminal=false\n'
124
+ )
125
+ self.__autostart_shortcut_path.write_text(desktop_file_content)
126
+
127
+ elif platform.system() == 'Darwin': # Not tested, help needed
128
+ plist_file_content = {
129
+ 'Label': self.__shortcut_name,
130
+ 'RunAtLoad': True,
131
+ 'ProgramArguments': [sys.executable, str(self.__script_path), '--tray'],
132
+ }
133
+ self.__autostart_shortcut_path.write_bytes(plistlib.dumps(plist_file_content))
134
+
135
+ def _disable(self) -> None:
136
+ self.__autostart_shortcut_path.unlink(missing_ok=True)
137
+
138
+ def _disable_others(self) -> None:
139
+ for path in self.__other_scripts_shortcuts():
140
+ path.unlink()
@@ -32,6 +32,9 @@ def start_silent() -> NoReturn:
32
32
 
33
33
  def _run_cli_loop() -> NoReturn:
34
34
  """Opens the ports and loops until broken by user."""
35
+ if not midiscripter.base.shared.script_path:
36
+ raise RuntimeError('Starter can only be called from a script')
37
+
35
38
  midiscripter.base.shared._raise_current_process_cpu_priority()
36
39
  with midiscripter.base.port_base._all_opened():
37
40
  while True:
@@ -1,9 +1,10 @@
1
1
  import pathlib
2
- from typing import TYPE_CHECKING, Optional, Union
2
+ from typing import TYPE_CHECKING
3
3
 
4
4
  import midiscripter.base.msg_base
5
5
 
6
6
  if TYPE_CHECKING:
7
+ from collections.abc import Container
7
8
  from midiscripter.file_event.file_event_port import FileEventIn
8
9
 
9
10
 
@@ -26,24 +27,29 @@ class FileEventMsg(midiscripter.base.msg_base.Msg):
26
27
  path: pathlib.Path
27
28
  """File path of event"""
28
29
 
29
- source: Optional['FileEventIn']
30
+ source: 'None | FileEventIn'
30
31
 
31
32
  def __init__(
32
33
  self,
33
34
  type: FileEventType | str,
34
35
  path: pathlib.Path,
35
36
  *,
36
- source: Optional['FileEventIn'] = None,
37
+ source: 'None | FileEventIn' = None,
37
38
  ):
38
39
  """
39
40
  Args:
40
41
  type: File event type
41
42
  path: File path
42
- source (FileEventIn): The [`FileEventIn`][midiscripter.FileEventIn] instance that generated the message
43
+ source (FileEventIn): The [`FileEventIn`][midiscripter.FileEventIn]
44
+ instance that generated the message
43
45
  """
44
46
  super().__init__(type, source)
45
47
  self.type = type
46
48
  self.path = path
47
49
 
48
- def matches(self, type=None, path=None):
50
+ def matches(
51
+ self,
52
+ type: 'None | Container[FileEventType] | FileEventType | str' = None,
53
+ path: 'None | Container[pathlib.Path] | pathlib.Path' = None,
54
+ ) -> bool:
49
55
  return super().matches(type, path)
@@ -1,5 +1,4 @@
1
1
  import pathlib
2
- from typing import Union
3
2
 
4
3
  import watchdog.events
5
4
  import watchdog.observers
@@ -25,7 +25,9 @@ class ScripterGUI(QApplication):
25
25
  def __init__(self):
26
26
  super().__init__()
27
27
  self.setOrganizationName('MIDI Scripter')
28
- self.setApplicationName(pathlib.Path(midiscripter.base.shared.script_path).name)
28
+ if midiscripter.base.shared.script_path:
29
+ self.setApplicationName(pathlib.Path(midiscripter.base.shared.script_path).name)
30
+
29
31
  icon_path = pathlib.Path(midiscripter.__file__).parent / 'resources' / 'icon.ico'
30
32
  self.setWindowIcon(QIcon(str(icon_path)))
31
33
 
@@ -33,7 +35,7 @@ class ScripterGUI(QApplication):
33
35
  self.request_restart.connect(self, self.restart)
34
36
  self.aboutToQuit.connect(self.__cleanup)
35
37
 
36
- def prepare_main_window(self, minimized_to_tray: bool = False):
38
+ def prepare_main_window(self, minimized_to_tray: bool = False) -> None:
37
39
  self.main_window = midiscripter.gui.main_window.MainWindow(self.widgets_to_add)
38
40
 
39
41
  if not minimized_to_tray:
@@ -41,7 +43,9 @@ class ScripterGUI(QApplication):
41
43
  else:
42
44
  self.main_window.close()
43
45
 
44
- def restart_at_file_change(self, msg: 'midiscripter.file_event.file_change_msg.FileEventMsg'):
46
+ def restart_at_file_change(
47
+ self, msg: 'midiscripter.file_event.file_change_msg.FileEventMsg'
48
+ ) -> None:
45
49
  if msg.type not in (
46
50
  midiscripter.file_event.file_event_msg.FileEventType.CREATED,
47
51
  midiscripter.file_event.file_event_msg.FileEventType.MODIFIED,
@@ -57,15 +61,15 @@ class ScripterGUI(QApplication):
57
61
 
58
62
  self.request_restart.emit()
59
63
 
60
- def restart(self):
64
+ def restart(self) -> NoReturn:
61
65
  if pathlib.Path(midiscripter.base.shared.script_path).is_file():
62
66
  # These settings saved only for restart
63
67
  self.processEvents() # update win status to get correct status
64
- QSettings().setValue('restart win minimized', int(self.main_window.isMinimized()))
65
- QSettings().setValue('restart closed to tray', int(not self.main_window.isVisible()))
68
+ QSettings().setValue('restart win minimized', self.main_window.isMinimized())
69
+ QSettings().setValue('restart closed to tray', not self.main_window.isVisible())
66
70
  self.exit(1467)
67
71
 
68
- def __cleanup(self):
72
+ def __cleanup(self) -> None:
69
73
  self.main_window.close()
70
74
  self.main_window.tray.hide()
71
75
 
@@ -74,13 +78,13 @@ class ScripterGUI(QApplication):
74
78
  app_instance = ScripterGUI()
75
79
 
76
80
 
77
- def add_qwidget(qwidget: QWidget):
81
+ def add_qwidget(qwidget: QWidget) -> None:
78
82
  """Add custom pyside6 QWidget to the GUI"""
79
83
  if qwidget not in midiscripter.gui.app.ScripterGUI.widgets_to_add:
80
84
  midiscripter.gui.app.ScripterGUI.widgets_to_add.append(qwidget)
81
85
 
82
86
 
83
- def remove_qwidget(qwidget: QWidget):
87
+ def remove_qwidget(qwidget: QWidget) -> None:
84
88
  """Remove custom pyside6 QWidget to the GUI"""
85
89
  try:
86
90
  midiscripter.gui.app.ScripterGUI.widgets_to_add.remove(qwidget)
@@ -89,6 +93,9 @@ def remove_qwidget(qwidget: QWidget):
89
93
 
90
94
 
91
95
  def start_gui() -> NoReturn:
96
+ if not midiscripter.base.shared.script_path:
97
+ raise RuntimeError('Starter can only be called from a script')
98
+
92
99
  """Starts the script and runs GUI. Logging goes to GUI Log widget."""
93
100
  midiscripter.base.shared._raise_current_process_cpu_priority()
94
101
 
@@ -1,4 +1,3 @@
1
- from typing import Optional, Union
2
1
  from collections.abc import Sequence
3
2
 
4
3
  from PySide6.QtWidgets import *
@@ -113,7 +112,7 @@ class ButtonGroupWidgetHorizontal(WrappedQWidgetMixin, QWidget):
113
112
 
114
113
  self.qt_button_group.idReleased.connect(self.selection_changed_signal)
115
114
 
116
- def set_selection(self, selection: int | str):
115
+ def set_selection(self, selection: int | str) -> None:
117
116
  try:
118
117
  self.wrapped_qt_buttons_map[selection].click()
119
118
  except KeyError:
@@ -134,7 +133,7 @@ class GuiButtonSelectorH(GuiWidget):
134
133
 
135
134
  def __init__(
136
135
  self,
137
- content: Sequence[str],
136
+ content: tuple[str, ...],
138
137
  title: str | None = None,
139
138
  color: str | tuple[int, int, int] | None = None,
140
139
  *,
@@ -161,7 +160,7 @@ class GuiButtonSelectorV(GuiWidget):
161
160
 
162
161
  def __init__(
163
162
  self,
164
- content: Sequence[str],
163
+ content: tuple[str, ...],
165
164
  title: str | None = None,
166
165
  color: str | tuple[int, int, int] | None = None,
167
166
  *,