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.
- {midiscripter-0.2 → midiscripter-0.3}/PKG-INFO +75 -36
- midiscripter-0.3/README.md +103 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/base/msg_base.py +0 -1
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/base/port_base.py +14 -10
- midiscripter-0.3/midiscripter/base/shared.py +140 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/cli/starters.py +3 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/file_event/file_event_msg.py +11 -5
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/file_event/file_event_port.py +0 -1
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/app.py +16 -9
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/button.py +3 -4
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/gui_msg.py +12 -7
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/gui_widget_base.py +12 -8
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/layout.py +2 -2
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/list.py +4 -5
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/mixins.py +7 -8
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/text.py +1 -3
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/log_widget.py +10 -12
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/main_window.py +13 -13
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/menu_bar.py +26 -50
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/ports_widget.py +74 -86
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/keyboard/keyboard_msg.py +10 -5
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/keyboard/keyboard_port.py +11 -8
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/logger/log.py +14 -14
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/metronome/metronome_port.py +7 -8
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/midi_msg.py +35 -21
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/midi_note_data.py +0 -1
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/midi_port.py +67 -39
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/midi_ports_update.py +1 -1
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/osc/osc_msg.py +10 -4
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/osc/osc_port.py +12 -9
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/osc/osc_query_maker.py +0 -1
- {midiscripter-0.2 → midiscripter-0.3}/pyproject.toml +4 -5
- midiscripter-0.2/README.md +0 -63
- midiscripter-0.2/midiscripter/base/shared.py +0 -62
- {midiscripter-0.2 → midiscripter-0.3}/LICENSE +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/base/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/cli/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/file_event/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/gui/gui_widgets/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/keyboard/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/logger/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/logger/console_sink.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/logger/html_sink.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/metronome/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/midi/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/osc/__init__.py +0 -0
- {midiscripter-0.2 → midiscripter-0.3}/midiscripter/resources/icon.ico +0 -0
- {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.
|
|
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
|
|
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
|
|
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 *
|
|
217
|
+
from midiscripter import *
|
|
210
218
|
|
|
211
|
-
midi_keyboard = MidiIn('
|
|
212
|
-
proxy_output = MidiOut('
|
|
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
|
|
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
|
-

|
|
235
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
278
|
+
1. Install [Python 3.11+](https://www.python.org/downloads/) including pip.
|
|
279
|
+
2. Run `pip install midiscripter`.
|
|
253
280
|
|
|
254
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+

|
|
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.
|
|
@@ -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
|
-
|
|
233
|
-
|
|
232
|
+
if not self._validate_msg_send(msg):
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
raise NotImplementedError
|
|
234
236
|
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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]
|
|
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(
|
|
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)
|
|
@@ -25,7 +25,9 @@ class ScripterGUI(QApplication):
|
|
|
25
25
|
def __init__(self):
|
|
26
26
|
super().__init__()
|
|
27
27
|
self.setOrganizationName('MIDI Scripter')
|
|
28
|
-
|
|
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(
|
|
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',
|
|
65
|
-
QSettings().setValue('restart closed to tray',
|
|
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:
|
|
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:
|
|
163
|
+
content: tuple[str, ...],
|
|
165
164
|
title: str | None = None,
|
|
166
165
|
color: str | tuple[int, int, int] | None = None,
|
|
167
166
|
*,
|