midiscripter 0.2__py3-none-any.whl
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/__init__.py +9 -0
- midiscripter/base/__init__.py +0 -0
- midiscripter/base/msg_base.py +95 -0
- midiscripter/base/port_base.py +251 -0
- midiscripter/base/shared.py +62 -0
- midiscripter/cli/__init__.py +1 -0
- midiscripter/cli/starters.py +41 -0
- midiscripter/file_event/__init__.py +2 -0
- midiscripter/file_event/file_event_msg.py +49 -0
- midiscripter/file_event/file_event_port.py +76 -0
- midiscripter/gui/__init__.py +11 -0
- midiscripter/gui/app.py +111 -0
- midiscripter/gui/gui_widgets/__init__.py +0 -0
- midiscripter/gui/gui_widgets/button.py +177 -0
- midiscripter/gui/gui_widgets/gui_msg.py +55 -0
- midiscripter/gui/gui_widgets/gui_widget_base.py +169 -0
- midiscripter/gui/gui_widgets/layout.py +63 -0
- midiscripter/gui/gui_widgets/list.py +67 -0
- midiscripter/gui/gui_widgets/mixins.py +114 -0
- midiscripter/gui/gui_widgets/text.py +52 -0
- midiscripter/gui/log_widget.py +156 -0
- midiscripter/gui/main_window.py +125 -0
- midiscripter/gui/menu_bar.py +206 -0
- midiscripter/gui/ports_widget.py +309 -0
- midiscripter/keyboard/__init__.py +4 -0
- midiscripter/keyboard/keyboard_msg.py +117 -0
- midiscripter/keyboard/keyboard_port.py +103 -0
- midiscripter/logger/__init__.py +3 -0
- midiscripter/logger/console_sink.py +55 -0
- midiscripter/logger/html_sink.py +61 -0
- midiscripter/logger/log.py +128 -0
- midiscripter/metronome/__init__.py +1 -0
- midiscripter/metronome/metronome_port.py +72 -0
- midiscripter/midi/__init__.py +4 -0
- midiscripter/midi/midi_msg.py +182 -0
- midiscripter/midi/midi_note_data.py +104 -0
- midiscripter/midi/midi_port.py +183 -0
- midiscripter/midi/midi_ports_update.py +56 -0
- midiscripter/osc/__init__.py +3 -0
- midiscripter/osc/osc_msg.py +42 -0
- midiscripter/osc/osc_port.py +95 -0
- midiscripter/osc/osc_query_maker.py +53 -0
- midiscripter/resources/icon.ico +0 -0
- midiscripter/resources/icon.svg +19 -0
- midiscripter-0.2.dist-info/METADATA +262 -0
- midiscripter-0.2.dist-info/RECORD +49 -0
- midiscripter-0.2.dist-info/WHEEL +4 -0
- midiscripter-0.2.dist-info/entry_points.txt +2 -0
- midiscripter-0.2.dist-info/licenses/LICENSE +165 -0
midiscripter/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from collections.abc import Container
|
|
4
|
+
|
|
5
|
+
import midiscripter.base.shared
|
|
6
|
+
from midiscripter.base.port_base import Input
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AttrEnum(enum.StrEnum):
|
|
10
|
+
def __repr__(self):
|
|
11
|
+
return f'{self.__class__.__name__}({self.value.__repr__()})'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Msg:
|
|
15
|
+
"""The data object generated by input port that is sent as an argument to it's registered calls
|
|
16
|
+
and can be sent with an output port."""
|
|
17
|
+
|
|
18
|
+
type: str
|
|
19
|
+
"""Message type description for filtering and representation"""
|
|
20
|
+
|
|
21
|
+
ctime: float
|
|
22
|
+
"""Message creation time in epoch format"""
|
|
23
|
+
|
|
24
|
+
source: Input
|
|
25
|
+
"""Input port instance that generated the message."""
|
|
26
|
+
|
|
27
|
+
__match_args__: tuple[str] = ('type',)
|
|
28
|
+
|
|
29
|
+
def __init__(self, type: str, source: Input | None = None):
|
|
30
|
+
"""
|
|
31
|
+
Args:
|
|
32
|
+
source: Input port instance that generated the message
|
|
33
|
+
"""
|
|
34
|
+
self.type = type
|
|
35
|
+
self.source = source
|
|
36
|
+
self.ctime = midiscripter.base.shared.precise_epoch_time()
|
|
37
|
+
|
|
38
|
+
self.ctime: float # workaround for mkdocstrings issue #607
|
|
39
|
+
"""Message creation time in epoch format"""
|
|
40
|
+
|
|
41
|
+
def __repr__(self):
|
|
42
|
+
return f'{self.__class__.__name__}({", ".join(str(value) for value in self.__as_tuple())})'
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
return ' | '.join(str(value) for value in self.__as_tuple() if value is not None)
|
|
46
|
+
|
|
47
|
+
def __eq__(self, other_msg: 'Msg'):
|
|
48
|
+
return type(self) is type(other_msg) and self.__as_tuple() == other_msg.__as_tuple()
|
|
49
|
+
|
|
50
|
+
def matches(self, *args_conditions, **kwargs_conditions) -> bool:
|
|
51
|
+
"""Checks if message's attributes match all provided attribute conditions:
|
|
52
|
+
1. If condition is `None` or omitted it matches anything.
|
|
53
|
+
2. If condition equals attribute it matches the attribute.
|
|
54
|
+
3. If condition is a container and contains the message attribute it matches the attribute.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if all attribute match, False if any are not
|
|
58
|
+
"""
|
|
59
|
+
attr_conditions = dict(zip(self.__match_args__, args_conditions, strict=False))
|
|
60
|
+
attr_conditions.update(kwargs_conditions)
|
|
61
|
+
|
|
62
|
+
for parameter, condition in attr_conditions.items():
|
|
63
|
+
if condition is None:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
attr = getattr(self, parameter)
|
|
68
|
+
except AttributeError:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if attr == condition:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
isinstance(condition, Container)
|
|
76
|
+
and not isinstance(condition, str)
|
|
77
|
+
and attr in condition
|
|
78
|
+
):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
return False
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def _age_ms(self) -> float:
|
|
86
|
+
"""Time passed since message creation in milliseconds."""
|
|
87
|
+
return round((midiscripter.base.shared.precise_epoch_time() - self.ctime) * 1000, 3)
|
|
88
|
+
|
|
89
|
+
def __as_tuple(self) -> tuple:
|
|
90
|
+
"""Converts message to a tuple based on message's __match_args__ class attribute
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Tuple with values of attributes specified in __match_args__ class attribute.
|
|
94
|
+
"""
|
|
95
|
+
return tuple(getattr(self, attr_name) for attr_name in self.__match_args__)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import contextlib
|
|
3
|
+
import copy
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
6
|
+
from collections.abc import Callable, Hashable
|
|
7
|
+
|
|
8
|
+
import midiscripter.base.shared
|
|
9
|
+
from midiscripter.logger import log
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from midiscripter.base.msg_base import Msg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@contextlib.contextmanager
|
|
16
|
+
def _all_opened():
|
|
17
|
+
for port in _PortRegistryMeta.instance_registry.values():
|
|
18
|
+
port._open()
|
|
19
|
+
|
|
20
|
+
for call in midiscripter.base.shared.run_after_ports_open_subscribed_calls:
|
|
21
|
+
|
|
22
|
+
def __call_runner():
|
|
23
|
+
try:
|
|
24
|
+
log('Running {call}', call=call) # noqa: B023
|
|
25
|
+
call() # noqa: B023
|
|
26
|
+
except Exception as exc:
|
|
27
|
+
log.red(''.join(traceback.format_exception(exc)))
|
|
28
|
+
|
|
29
|
+
midiscripter.base.shared.thread_executor.submit(__call_runner)
|
|
30
|
+
|
|
31
|
+
yield
|
|
32
|
+
|
|
33
|
+
for port in _PortRegistryMeta.instance_registry.values():
|
|
34
|
+
port._close()
|
|
35
|
+
|
|
36
|
+
log._flush()
|
|
37
|
+
log._sink = None
|
|
38
|
+
midiscripter.base.shared.thread_executor.shutdown(wait=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _PortRegistryMeta(type):
|
|
42
|
+
"""Metaclass that enforces one uid - one port instance (singleton) rule"""
|
|
43
|
+
|
|
44
|
+
__singleton_instance_type = TypeVar('__singleton_instance_type', bound='Port')
|
|
45
|
+
"""Type for correct IDE recognition and code completion for returned port subclasses"""
|
|
46
|
+
|
|
47
|
+
instance_registry: dict[tuple[str, Hashable], __singleton_instance_type] = {}
|
|
48
|
+
"""Declared ports register as port class name and port uid to port instance map"""
|
|
49
|
+
|
|
50
|
+
def __call__(
|
|
51
|
+
cls: type[__singleton_instance_type], *args, **kwargs
|
|
52
|
+
) -> __singleton_instance_type:
|
|
53
|
+
"""
|
|
54
|
+
Args:
|
|
55
|
+
*args: if the class' `_force_uid` attribute is `None` (default)
|
|
56
|
+
the metaclass uses the first argument as uid
|
|
57
|
+
**kwargs: -
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The singleton class instance that has requested uid
|
|
61
|
+
"""
|
|
62
|
+
uid = cls._force_uid or args[0]
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
return cls.instance_registry[(cls.__name__, uid)]
|
|
66
|
+
except KeyError:
|
|
67
|
+
instance = super().__call__(*args, **kwargs)
|
|
68
|
+
cls.instance_registry[(cls.__name__, uid)] = instance
|
|
69
|
+
return instance
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Port(metaclass=_PortRegistryMeta):
|
|
73
|
+
"""Port base class
|
|
74
|
+
|
|
75
|
+
Notes:
|
|
76
|
+
Port declaration with `uid` of an already existing port
|
|
77
|
+
will return the existing port (singleton)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
is_enabled: bool
|
|
81
|
+
"""`True` if port is listening messages / ready to send messages"""
|
|
82
|
+
|
|
83
|
+
_force_uid = None
|
|
84
|
+
"""UID override for classes that have can have only one instance per whole class,
|
|
85
|
+
like keyboard port class which can have only one instance for the system.
|
|
86
|
+
Object for these classe are declared without arguments.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, uid: Hashable):
|
|
90
|
+
"""
|
|
91
|
+
Args:
|
|
92
|
+
uid: Port's unique ID that will always lead to the same port instance
|
|
93
|
+
"""
|
|
94
|
+
self._uid = uid
|
|
95
|
+
self.is_enabled = False
|
|
96
|
+
|
|
97
|
+
self.is_enabled: bool # workaround for mkdocstrings issue #607
|
|
98
|
+
"""`True` if port is listening messages / ready to send messages"""
|
|
99
|
+
|
|
100
|
+
def __repr__(self):
|
|
101
|
+
if self._force_uid:
|
|
102
|
+
return f'{self.__class__.__name__}()'
|
|
103
|
+
else:
|
|
104
|
+
return f'{self.__class__.__name__}({self._uid.__repr__()})'
|
|
105
|
+
|
|
106
|
+
def __str__(self):
|
|
107
|
+
return str(self._uid)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def _is_available(self) -> bool:
|
|
111
|
+
"""Port is available and can be opened."""
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def _open(self) -> None:
|
|
115
|
+
"""Prepares and activates the port
|
|
116
|
+
|
|
117
|
+
Notes:
|
|
118
|
+
Supposed to be overridden in subclasses
|
|
119
|
+
Should have a check against second opening
|
|
120
|
+
Must set `is_enabled` parameter to `True`
|
|
121
|
+
"""
|
|
122
|
+
self.is_enabled = True
|
|
123
|
+
|
|
124
|
+
def _close(self) -> None:
|
|
125
|
+
"""Deactivates the port.
|
|
126
|
+
|
|
127
|
+
Notes:
|
|
128
|
+
Supposed to be overridden in subclasses
|
|
129
|
+
Must set `is_enabled` parameter to `False`
|
|
130
|
+
"""
|
|
131
|
+
self.is_enabled = False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Input(Port):
|
|
135
|
+
"""Input port base class"""
|
|
136
|
+
|
|
137
|
+
is_enabled: bool
|
|
138
|
+
"""`True` if port is listening and generating messages."""
|
|
139
|
+
|
|
140
|
+
subscribed_calls: list[Callable]
|
|
141
|
+
"""Callables that will be called with incoming messages. Can be modified."""
|
|
142
|
+
|
|
143
|
+
_call_statistics: dict[Callable, collections.deque[float]] = {}
|
|
144
|
+
"""Statistics for each call's execution time in milliseconds for the last 20 runs."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, uid: Hashable):
|
|
147
|
+
super().__init__(uid)
|
|
148
|
+
self.subscribed_calls: list[Callable] = []
|
|
149
|
+
|
|
150
|
+
self.subscribed_calls: list[Callable] # workaround for mkdocstrings issue #607
|
|
151
|
+
"""Callables that will be called with incoming messages. Can be modified."""
|
|
152
|
+
|
|
153
|
+
# A Decorator
|
|
154
|
+
def subscribe(self, function: Callable[['Msg'], None]) -> Callable:
|
|
155
|
+
"""Decorator to subscribe a callable to the input's messages
|
|
156
|
+
|
|
157
|
+
??? Example
|
|
158
|
+
``` python
|
|
159
|
+
@input_instance.subscribe
|
|
160
|
+
def function(msg: Msg) -> None:
|
|
161
|
+
pass
|
|
162
|
+
```
|
|
163
|
+
``` python
|
|
164
|
+
input_instance.subscribe(object.method)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
function: A callable that takes the input port's message as the only argument.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Subscribed callable.
|
|
172
|
+
"""
|
|
173
|
+
if function not in self.subscribed_calls:
|
|
174
|
+
self._call_statistics[function] = collections.deque(maxlen=20)
|
|
175
|
+
self.subscribed_calls.append(function)
|
|
176
|
+
log('{input} subscribed {call}', input=self, call=function)
|
|
177
|
+
return function
|
|
178
|
+
|
|
179
|
+
def _send_input_msg_to_calls(self, msg: 'Msg') -> None:
|
|
180
|
+
"""Sends the message received by the input port to subscribed calls.
|
|
181
|
+
|
|
182
|
+
Notes:
|
|
183
|
+
Not supposed to be overridden in subclasses.
|
|
184
|
+
Supposed to be called from the listener thread started
|
|
185
|
+
by the subclass implementation of `open` method.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
msg: A message received by the input port to send to it's registered calls.
|
|
189
|
+
"""
|
|
190
|
+
log('{input} got message {msg}', input=self, msg=msg)
|
|
191
|
+
|
|
192
|
+
if self.is_enabled:
|
|
193
|
+
# pre-copying messages to reduce jitter a little
|
|
194
|
+
msg_copies = [copy.copy(msg) for _ in range(len(self.subscribed_calls))]
|
|
195
|
+
midiscripter.base.shared.thread_executor.map(
|
|
196
|
+
self.__call_worker, self.subscribed_calls, msg_copies
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def __call_worker(self, function: Callable, msg: 'Msg') -> None:
|
|
200
|
+
"""Function called in thread for each subscribed call and each received message.
|
|
201
|
+
|
|
202
|
+
Notes:
|
|
203
|
+
Not supposed to be overridden in subclasses.
|
|
204
|
+
Supposed to be called from `_send_input_msg_to_calls` method.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
function: Subscribed callable.
|
|
208
|
+
msg: Received message to use as callable only argument.
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
function(msg)
|
|
212
|
+
self._call_statistics[function].append(msg._age_ms)
|
|
213
|
+
except TypeError:
|
|
214
|
+
function()
|
|
215
|
+
self._call_statistics[function].append(msg._age_ms)
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
log.red(''.join(traceback.format_exception(exc)))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class Output(Port):
|
|
221
|
+
"""Output port base class"""
|
|
222
|
+
|
|
223
|
+
is_enabled: bool
|
|
224
|
+
"""`True` if port is ready to send messages"""
|
|
225
|
+
|
|
226
|
+
def send(self, msg: 'Msg') -> None:
|
|
227
|
+
"""Send message using the output port.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
msg: Message to send.
|
|
231
|
+
"""
|
|
232
|
+
with self._check_and_log_sent_message(msg):
|
|
233
|
+
raise NotImplementedError
|
|
234
|
+
|
|
235
|
+
@contextlib.contextmanager
|
|
236
|
+
def _check_and_log_sent_message(self, msg: 'Msg'):
|
|
237
|
+
if not self.is_enabled:
|
|
238
|
+
log.red("Can't send message {msg}. {output} is disabled!", msg=msg, output=self)
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
yield
|
|
242
|
+
|
|
243
|
+
if msg.source:
|
|
244
|
+
log(
|
|
245
|
+
'{output} sent message {msg} received {age_ms} ms ago',
|
|
246
|
+
output=self,
|
|
247
|
+
msg=msg,
|
|
248
|
+
age_ms=msg._age_ms,
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
log('{output} sent message {msg}', output=self, msg=msg)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
import __main__
|
|
9
|
+
|
|
10
|
+
if platform.system() == 'Windows':
|
|
11
|
+
import win32api
|
|
12
|
+
import win32con
|
|
13
|
+
import win32process
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
script_path = __main__.__file__
|
|
18
|
+
except AttributeError: # in subprocess
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
thread_executor = concurrent.futures.ThreadPoolExecutor(100)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_precise_time_delta = time.time() - time.perf_counter()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def precise_epoch_time():
|
|
29
|
+
"""current time in epoch format with nanosecond precision"""
|
|
30
|
+
return _precise_time_delta + time.perf_counter()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def restart_script():
|
|
34
|
+
"""Exit and restart current script"""
|
|
35
|
+
os.execv(sys.executable, ['python', script_path])
|
|
36
|
+
exit(0)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _raise_current_process_cpu_priority() -> None:
|
|
40
|
+
"""Sets HIGH process priority in Windows for current python process"""
|
|
41
|
+
if platform.system() == 'Windows':
|
|
42
|
+
pid = win32api.GetCurrentProcessId()
|
|
43
|
+
handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, True, pid)
|
|
44
|
+
win32process.SetPriorityClass(handle, win32process.HIGH_PRIORITY_CLASS)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
run_after_ports_open_subscribed_calls = []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# A Decorator
|
|
51
|
+
def run_after_ports_opened(function: Callable[[], None]) -> Callable:
|
|
52
|
+
"""Decorator to subscribe a callable to run after all ports are opened at the script start
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
function: A callable with no arguments
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Subscribed callable.
|
|
59
|
+
"""
|
|
60
|
+
if function not in run_after_ports_open_subscribed_calls:
|
|
61
|
+
run_after_ports_open_subscribed_calls.append(function)
|
|
62
|
+
return function
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from midiscripter.cli.starters import start_cli_debug, start_silent
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import NoReturn
|
|
3
|
+
|
|
4
|
+
import midiscripter.base.port_base
|
|
5
|
+
import midiscripter.base.shared
|
|
6
|
+
import midiscripter.logger.console_sink
|
|
7
|
+
import midiscripter.logger.log
|
|
8
|
+
import midiscripter.midi
|
|
9
|
+
from midiscripter.logger import log
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def start_cli_debug() -> NoReturn:
|
|
13
|
+
"""Starts the script with log output to console.
|
|
14
|
+
Console prints increase latency and jitter. Use for debugging only.
|
|
15
|
+
"""
|
|
16
|
+
log._sink = midiscripter.logger.console_sink.ConsoleSink()
|
|
17
|
+
log('')
|
|
18
|
+
log('Available MIDI inputs:')
|
|
19
|
+
[log('{input}', input=port_name) for port_name in midiscripter.midi.MidiIn._available_names]
|
|
20
|
+
log('')
|
|
21
|
+
log('Available MIDI outputs:')
|
|
22
|
+
[log('{output}', output=port_name) for port_name in midiscripter.midi.MidiOut._available_names]
|
|
23
|
+
log('')
|
|
24
|
+
_run_cli_loop()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def start_silent() -> NoReturn:
|
|
28
|
+
"""Starts the script without logging. The fastest way to run the script."""
|
|
29
|
+
log.is_enabled = False
|
|
30
|
+
_run_cli_loop()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run_cli_loop() -> NoReturn:
|
|
34
|
+
"""Opens the ports and loops until broken by user."""
|
|
35
|
+
midiscripter.base.shared._raise_current_process_cpu_priority()
|
|
36
|
+
with midiscripter.base.port_base._all_opened():
|
|
37
|
+
while True:
|
|
38
|
+
try:
|
|
39
|
+
time.sleep(1)
|
|
40
|
+
except (KeyboardInterrupt, SystemExit):
|
|
41
|
+
break
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
3
|
+
|
|
4
|
+
import midiscripter.base.msg_base
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from midiscripter.file_event.file_event_port import FileEventIn
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileEventType(midiscripter.base.msg_base.AttrEnum):
|
|
11
|
+
# Names are hardcoded, equal watchdog's event types
|
|
12
|
+
MOVED = 'MOVED'
|
|
13
|
+
DELETED = 'DELETED'
|
|
14
|
+
CREATED = 'CREATED'
|
|
15
|
+
MODIFIED = 'MODIFIED'
|
|
16
|
+
CLOSED = 'CLOSED'
|
|
17
|
+
OPENED = 'OPENED'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileEventMsg(midiscripter.base.msg_base.Msg):
|
|
21
|
+
___match_args__ = ('type', 'path')
|
|
22
|
+
|
|
23
|
+
type: FileEventType
|
|
24
|
+
"""File event type"""
|
|
25
|
+
|
|
26
|
+
path: pathlib.Path
|
|
27
|
+
"""File path of event"""
|
|
28
|
+
|
|
29
|
+
source: Optional['FileEventIn']
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
type: FileEventType | str,
|
|
34
|
+
path: pathlib.Path,
|
|
35
|
+
*,
|
|
36
|
+
source: Optional['FileEventIn'] = None,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Args:
|
|
40
|
+
type: File event type
|
|
41
|
+
path: File path
|
|
42
|
+
source (FileEventIn): The [`FileEventIn`][midiscripter.FileEventIn] instance that generated the message
|
|
43
|
+
"""
|
|
44
|
+
super().__init__(type, source)
|
|
45
|
+
self.type = type
|
|
46
|
+
self.path = path
|
|
47
|
+
|
|
48
|
+
def matches(self, type=None, path=None):
|
|
49
|
+
return super().matches(type, path)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import watchdog.events
|
|
5
|
+
import watchdog.observers
|
|
6
|
+
|
|
7
|
+
import midiscripter.base.port_base
|
|
8
|
+
import midiscripter.file_event
|
|
9
|
+
import midiscripter.logger
|
|
10
|
+
|
|
11
|
+
shared_observer = watchdog.observers.Observer()
|
|
12
|
+
shared_observer.daemon = True
|
|
13
|
+
shared_observer.start()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileEventIn(midiscripter.base.port_base.Input, watchdog.events.FileSystemEventHandler):
|
|
17
|
+
"""File system events input port. Watches file/directory modifications.
|
|
18
|
+
Produces [`FileEventMsg`][midiscripter.FileEventMsg] objects.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, path: str | pathlib.Path, recursive: bool = False):
|
|
22
|
+
"""
|
|
23
|
+
Args:
|
|
24
|
+
path: File/directory path to watch
|
|
25
|
+
recursive: `True` to watch directory path recursively
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(path, str):
|
|
28
|
+
path = pathlib.Path(path)
|
|
29
|
+
|
|
30
|
+
midiscripter.base.port_base.Input.__init__(self, path)
|
|
31
|
+
watchdog.events.FileSystemEventHandler.__init__(self)
|
|
32
|
+
|
|
33
|
+
self.__path = path
|
|
34
|
+
|
|
35
|
+
if self.__path.is_dir():
|
|
36
|
+
self.__path_to_watch = self.__path
|
|
37
|
+
self.__watch_dir_changes = True
|
|
38
|
+
else:
|
|
39
|
+
self.__path_to_watch = self.__path.parent # some editors recreate a file on save
|
|
40
|
+
self.__watch_dir_changes = False
|
|
41
|
+
|
|
42
|
+
self.__recursive = recursive
|
|
43
|
+
self.__watch = None
|
|
44
|
+
|
|
45
|
+
def __repr__(self):
|
|
46
|
+
return f"{self.__class__.__name__}('{str(self._uid)}')"
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
return f"'{self.__path.relative_to(self.__path.parent.parent)}' watcher"
|
|
50
|
+
|
|
51
|
+
def _open(self) -> None:
|
|
52
|
+
self.__watch = shared_observer.schedule(self, str(self.__path_to_watch), self.__recursive)
|
|
53
|
+
if not shared_observer.is_alive():
|
|
54
|
+
shared_observer.start()
|
|
55
|
+
self.is_enabled = True
|
|
56
|
+
midiscripter.logger.log('Opened {input}', input=self)
|
|
57
|
+
|
|
58
|
+
def _close(self) -> None:
|
|
59
|
+
if self.__watch:
|
|
60
|
+
shared_observer.unschedule(self.__watch)
|
|
61
|
+
self.is_enabled = False
|
|
62
|
+
midiscripter.logger.log('Stopped {input}', input=self)
|
|
63
|
+
|
|
64
|
+
def on_any_event(self, event: watchdog.events.FileSystemEvent) -> None:
|
|
65
|
+
# Override of `watchdog.events.FileSystemEventHandler` method.
|
|
66
|
+
# Runs on each file system change in `_path`.
|
|
67
|
+
if not self.is_enabled:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
event_path = pathlib.Path(event.src_path)
|
|
71
|
+
|
|
72
|
+
if self.__watch_dir_changes or event_path == self.__path:
|
|
73
|
+
msg = midiscripter.file_event.file_event_msg.FileEventMsg(
|
|
74
|
+
event.event_type.upper(), self.__path, source=self
|
|
75
|
+
)
|
|
76
|
+
self._send_input_msg_to_calls(msg)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from midiscripter.gui.app import add_qwidget, remove_qwidget, start_gui
|
|
2
|
+
from midiscripter.gui.gui_widgets.button import (
|
|
3
|
+
GuiButton,
|
|
4
|
+
GuiButtonSelectorH,
|
|
5
|
+
GuiButtonSelectorV,
|
|
6
|
+
GuiToggleButton,
|
|
7
|
+
)
|
|
8
|
+
from midiscripter.gui.gui_widgets.gui_msg import GuiEventMsg, GuiEventType
|
|
9
|
+
from midiscripter.gui.gui_widgets.layout import GuiWidgetLayout
|
|
10
|
+
from midiscripter.gui.gui_widgets.list import GuiListSelector
|
|
11
|
+
from midiscripter.gui.gui_widgets.text import GuiText
|