make-selection 1.0.1__tar.gz → 1.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.
- {make_selection-1.0.1 → make_selection-1.0.3}/PKG-INFO +1 -1
- {make_selection-1.0.1 → make_selection-1.0.3}/pyproject.toml +1 -1
- make_selection-1.0.3/src/make_selection/make_selection.py +242 -0
- {make_selection-1.0.1 → make_selection-1.0.3}/src/make_selection.egg-info/PKG-INFO +1 -1
- make_selection-1.0.1/src/make_selection/make_selection.py +0 -167
- {make_selection-1.0.1 → make_selection-1.0.3}/LICENSE +0 -0
- {make_selection-1.0.1 → make_selection-1.0.3}/README.md +0 -0
- {make_selection-1.0.1 → make_selection-1.0.3}/setup.cfg +0 -0
- {make_selection-1.0.1 → make_selection-1.0.3}/src/make_selection/__init__.py +0 -0
- {make_selection-1.0.1 → make_selection-1.0.3}/src/make_selection.egg-info/SOURCES.txt +0 -0
- {make_selection-1.0.1 → make_selection-1.0.3}/src/make_selection.egg-info/dependency_links.txt +0 -0
- {make_selection-1.0.1 → make_selection-1.0.3}/src/make_selection.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for interactive command line menu. Simply accepts a list of str-able objects
|
|
3
|
+
and allows user to select using arrow keys.
|
|
4
|
+
|
|
5
|
+
Ansi escape codes are used as described here: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
|
|
6
|
+
"""
|
|
7
|
+
# TODO: work on all platforms
|
|
8
|
+
# TODO: multi_select: Maintain original index for re-insertion.
|
|
9
|
+
# TODO: multi_select: TAB switches to delete mode.
|
|
10
|
+
import sys
|
|
11
|
+
if sys.platform != "win32":
|
|
12
|
+
raise NotImplementedError("This module is only available on Windows.")
|
|
13
|
+
import msvcrt
|
|
14
|
+
import ctypes
|
|
15
|
+
from typing import Any
|
|
16
|
+
from copy import copy
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
stdout = -11
|
|
20
|
+
enable_ansi_codes = 7
|
|
21
|
+
kernel32 = ctypes.windll.kernel32
|
|
22
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(stdout), enable_ansi_codes)
|
|
23
|
+
|
|
24
|
+
ANSI_MOVE_CURSOR = "\x1b[{up}F\r\x1b[{right}C"
|
|
25
|
+
ANSI_HIGHLIGHT = "\x1b[30;47m"
|
|
26
|
+
ANSI_HIGHLIGHT_SEARCH_STRING = "\x1b[95;47m"
|
|
27
|
+
ANSI_YELLOW = "\x1b[93m"
|
|
28
|
+
ANSI_BLUE = "\x1b[94m"
|
|
29
|
+
ANSI_MAGENTA = "\x1b[95m"
|
|
30
|
+
ANSI_GREEN = "\x1b[92m"
|
|
31
|
+
ANSI_RESET = "\x1b[0m"
|
|
32
|
+
|
|
33
|
+
SPECIAL_KEY = 224
|
|
34
|
+
UP_ARROW = 72
|
|
35
|
+
DOWN_ARROW = 80
|
|
36
|
+
ENTER_KEY = 13
|
|
37
|
+
CTL_C = 3
|
|
38
|
+
BACKSPACE = 8
|
|
39
|
+
SPACEBAR = 32
|
|
40
|
+
CTL_RIGHT = 116
|
|
41
|
+
|
|
42
|
+
class Mode(Enum):
|
|
43
|
+
NORMAL = 0
|
|
44
|
+
MULTI_SELECT = 1
|
|
45
|
+
MULTI_DELETE = 2
|
|
46
|
+
|
|
47
|
+
class Option:
|
|
48
|
+
def __init__(self, obj: Any, sub_string_start: int=0) -> None:
|
|
49
|
+
self.value = obj
|
|
50
|
+
self.sub_string_start = sub_string_start
|
|
51
|
+
|
|
52
|
+
class Menu:
|
|
53
|
+
def __init__(self, options: list, label: str, window_size: int=10, multi_select: bool=False) -> None:
|
|
54
|
+
assert options
|
|
55
|
+
assert label
|
|
56
|
+
assert 1 <= window_size and window_size <= 25
|
|
57
|
+
window_size = min((len(options)), window_size)
|
|
58
|
+
|
|
59
|
+
self.options_original = [Option(op) for op in options]
|
|
60
|
+
self.options_current = copy(self.options_original)
|
|
61
|
+
self.options_selected = []
|
|
62
|
+
self.search_string = ""
|
|
63
|
+
self.label = label
|
|
64
|
+
self.selected_index = 0
|
|
65
|
+
self.window_top = 0
|
|
66
|
+
self.window_size_original = window_size
|
|
67
|
+
self.window_size_current = window_size
|
|
68
|
+
self.help_string_multi_select = "Enter: Select, Ctl+C: Cancel, Ctl\u2192: Done"
|
|
69
|
+
self.help_string_normal = "Enter: Select, Ctl+C: Cancel"
|
|
70
|
+
if multi_select:
|
|
71
|
+
self.mode = Mode.MULTI_SELECT
|
|
72
|
+
self.help_string = self.help_string_multi_select
|
|
73
|
+
else:
|
|
74
|
+
self.mode = Mode.NORMAL
|
|
75
|
+
self.help_string = self.help_string_normal
|
|
76
|
+
|
|
77
|
+
def show(self):
|
|
78
|
+
self.printMenu()
|
|
79
|
+
while True:
|
|
80
|
+
something_changed = False
|
|
81
|
+
char = self.getChar()
|
|
82
|
+
if char == SPECIAL_KEY:
|
|
83
|
+
char = self.getChar()
|
|
84
|
+
if char == CTL_RIGHT:
|
|
85
|
+
if self.mode == Mode.MULTI_SELECT:
|
|
86
|
+
self.printSelected()
|
|
87
|
+
return self.multiSelectGetValues(self.options_selected)
|
|
88
|
+
elif 1 < len(self.options_current):
|
|
89
|
+
# NOTE: Update selected index
|
|
90
|
+
if char == UP_ARROW:
|
|
91
|
+
self.selected_index = (self.selected_index - 1) % len(self.options_current)
|
|
92
|
+
something_changed = True
|
|
93
|
+
elif char == DOWN_ARROW:
|
|
94
|
+
self.selected_index = (self.selected_index + 1) % len(self.options_current)
|
|
95
|
+
something_changed = True
|
|
96
|
+
|
|
97
|
+
# NOTE: Update window
|
|
98
|
+
if something_changed:
|
|
99
|
+
bottom = self.window_top + self.window_size_current
|
|
100
|
+
if self.selected_index < self.window_top:
|
|
101
|
+
self.window_top = self.selected_index
|
|
102
|
+
elif bottom <= self.selected_index:
|
|
103
|
+
self.window_top = self.selected_index - self.window_size_current + 1
|
|
104
|
+
elif self.isSearchableChar(char):
|
|
105
|
+
char = chr(char)
|
|
106
|
+
self.search_string = f"{self.search_string}{char}".lstrip()
|
|
107
|
+
if self.search_string:
|
|
108
|
+
print(char, end="", flush=True)
|
|
109
|
+
self.search(self.options_current)
|
|
110
|
+
something_changed = True
|
|
111
|
+
elif char == BACKSPACE and 0 < len(self.search_string):
|
|
112
|
+
self.search_string = self.search_string[:-1]
|
|
113
|
+
# NOTE: Move left once ([1D), print space, move left again
|
|
114
|
+
print("\x1b[1D \x1b[1D", end="", flush=True)
|
|
115
|
+
self.search(self.options_original)
|
|
116
|
+
something_changed = True
|
|
117
|
+
elif char == ENTER_KEY and self.options_current:
|
|
118
|
+
if self.mode == Mode.MULTI_SELECT:
|
|
119
|
+
self.multiSelectAdd()
|
|
120
|
+
something_changed = True
|
|
121
|
+
elif self.mode == Mode.NORMAL:
|
|
122
|
+
self.printSelected()
|
|
123
|
+
return self.options_current[self.selected_index].value
|
|
124
|
+
elif char == CTL_C:
|
|
125
|
+
self.clearMenu()
|
|
126
|
+
print("cancelled")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
if something_changed:
|
|
130
|
+
self.clearMenu()
|
|
131
|
+
self.printMenu()
|
|
132
|
+
|
|
133
|
+
def search(self, search_list: list) -> None:
|
|
134
|
+
found_options = []
|
|
135
|
+
for o in search_list:
|
|
136
|
+
found_i = str(o.value).lower().find(self.search_string.lower())
|
|
137
|
+
if found_i != -1:
|
|
138
|
+
o.sub_string_start = found_i
|
|
139
|
+
found_options.append(o)
|
|
140
|
+
self.options_current = found_options
|
|
141
|
+
self.resetWindow()
|
|
142
|
+
|
|
143
|
+
def multiSelectAdd(self) -> None:
|
|
144
|
+
selected_option = self.options_current[self.selected_index]
|
|
145
|
+
self.options_current.remove(selected_option)
|
|
146
|
+
self.options_original.remove(selected_option)
|
|
147
|
+
self.options_selected.append(selected_option)
|
|
148
|
+
self.resetWindow()
|
|
149
|
+
|
|
150
|
+
def multiSelectGetValues(self, options: list[Option]) -> list[Any]:
|
|
151
|
+
return [op.value for op in options]
|
|
152
|
+
|
|
153
|
+
def getChar(self) -> int:
|
|
154
|
+
return ord(msvcrt.getch())
|
|
155
|
+
|
|
156
|
+
def isSearchableChar(self, char):
|
|
157
|
+
return (32 <= char and char <= 126)
|
|
158
|
+
|
|
159
|
+
def resetWindow(self):
|
|
160
|
+
self.window_top = 0
|
|
161
|
+
self.selected_index = 0
|
|
162
|
+
self.window_size_current = min((len(self.options_current), self.window_size_original))
|
|
163
|
+
|
|
164
|
+
def clearMenu(self):
|
|
165
|
+
"""
|
|
166
|
+
Clears from beginning of current line to end of screen (not end of line).
|
|
167
|
+
This assumes the cursor is on the top line (label), which is currently always true.
|
|
168
|
+
|
|
169
|
+
[0G move cursor to column 0.
|
|
170
|
+
[J clears to end of screen (not end of line).
|
|
171
|
+
"""
|
|
172
|
+
print("\x1b[0G\x1b[J", end="", flush=True)
|
|
173
|
+
|
|
174
|
+
def printMenu(self):
|
|
175
|
+
header = f"{ANSI_BLUE}{self.label}>{ANSI_RESET}{self.search_string}"
|
|
176
|
+
header_num_lines = 1
|
|
177
|
+
if self.mode == Mode.MULTI_SELECT:
|
|
178
|
+
header += f"\n{ANSI_GREEN}{len(self.options_selected)} items in list!{ANSI_RESET}"
|
|
179
|
+
header_num_lines = 2
|
|
180
|
+
print(header)
|
|
181
|
+
|
|
182
|
+
bottom = self.window_top + self.window_size_current
|
|
183
|
+
if not self.options_current:
|
|
184
|
+
print(f"{ANSI_BLUE}no matches found{ANSI_RESET}")
|
|
185
|
+
# NOTE: window size is 0 here after empty search, but we are printing 1 line
|
|
186
|
+
# so we need to set it so the footer prints correctly
|
|
187
|
+
self.window_size_current = 1
|
|
188
|
+
else:
|
|
189
|
+
for i in range(self.window_top, bottom):
|
|
190
|
+
option_to_print = str(self.options_current[i].value)
|
|
191
|
+
highlight_beg = self.options_current[i].sub_string_start
|
|
192
|
+
highlight_end = highlight_beg + len(self.search_string)
|
|
193
|
+
opt_beg = option_to_print[0 : highlight_beg]
|
|
194
|
+
opt_mid = option_to_print[highlight_beg : highlight_end]
|
|
195
|
+
opt_end = option_to_print[highlight_end :]
|
|
196
|
+
if i == self.selected_index:
|
|
197
|
+
option_to_print = f"{ANSI_HIGHLIGHT}{opt_beg}{ANSI_HIGHLIGHT_SEARCH_STRING}{opt_mid}{ANSI_HIGHLIGHT}{opt_end}{ANSI_RESET}"
|
|
198
|
+
else:
|
|
199
|
+
option_to_print = f"{opt_beg}{ANSI_MAGENTA}{opt_mid}{ANSI_RESET}{opt_end}"
|
|
200
|
+
print(option_to_print)
|
|
201
|
+
|
|
202
|
+
footer = f"{ANSI_YELLOW}{self.help_string}{ANSI_RESET}\n"
|
|
203
|
+
footer_num_lines = 1
|
|
204
|
+
print(f"{footer}{ANSI_MOVE_CURSOR.format(up=self.window_size_current + header_num_lines + footer_num_lines, right=len(self.label) + len(self.search_string) + 1)}", end="", flush=True)
|
|
205
|
+
|
|
206
|
+
def printSelected(self):
|
|
207
|
+
self.clearMenu()
|
|
208
|
+
if self.mode == Mode.MULTI_SELECT:
|
|
209
|
+
if len(self.options_selected) == 1:
|
|
210
|
+
print(f"{self.label}> ({len(self.options_selected)} item) {self.multiSelectGetValues(self.options_selected)}")
|
|
211
|
+
else:
|
|
212
|
+
print(f"{self.label}> ({len(self.options_selected)} items) [{self.options_selected[0].value}, ...]")
|
|
213
|
+
else:
|
|
214
|
+
print(f"{self.label}> {self.options_current[self.selected_index].value}")
|
|
215
|
+
|
|
216
|
+
def makeSelection(options: list[Any], label: str, window_size: int=None, multi_select: bool=False) -> Any:
|
|
217
|
+
"""
|
|
218
|
+
Entry point for menu selection.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
options
|
|
223
|
+
List of str-able objects.
|
|
224
|
+
label
|
|
225
|
+
Label to describe the items being selected.
|
|
226
|
+
window_size
|
|
227
|
+
Max number of items to show at once.
|
|
228
|
+
multi_select
|
|
229
|
+
Select list of items.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
Selected value or list of selected values.
|
|
234
|
+
"""
|
|
235
|
+
if window_size:
|
|
236
|
+
return Menu(options, label, window_size=window_size, multi_select=multi_select).show()
|
|
237
|
+
else:
|
|
238
|
+
return Menu(options, label, multi_select=multi_select).show()
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
print(f"Returns: '{makeSelection(['interactive', 'cli', 'menu'], 'make_selection')}'")
|
|
242
|
+
print(f"Returns: '{makeSelection(['interactive', 'cli', 'menu'], 'make_selection', multi_select=True)}'")
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Module for interactive command line menu. Simply accepts a list of str-able objects
|
|
3
|
-
and allows user to select using arrow keys.
|
|
4
|
-
"""
|
|
5
|
-
import sys
|
|
6
|
-
if sys.platform != "win32":
|
|
7
|
-
raise NotImplementedError("This module is only available on Windows.")
|
|
8
|
-
from typing import Any
|
|
9
|
-
import msvcrt
|
|
10
|
-
import ctypes
|
|
11
|
-
|
|
12
|
-
stdout = -11
|
|
13
|
-
enable_ansi_codes = 7
|
|
14
|
-
kernel32 = ctypes.windll.kernel32
|
|
15
|
-
kernel32.SetConsoleMode(kernel32.GetStdHandle(stdout), enable_ansi_codes)
|
|
16
|
-
|
|
17
|
-
ANSI_MOVE_CURSOR = "\x1b[{up}F\r\x1b[{right}C"
|
|
18
|
-
ANSI_HIGHLIGHT = "\x1b[30;47m"
|
|
19
|
-
ANSI_YELLOW = "\x1b[93m"
|
|
20
|
-
ANSI_BLUE = "\x1b[94m"
|
|
21
|
-
ANSI_RESET = "\x1b[0m"
|
|
22
|
-
SPECIAL_KEY = 224
|
|
23
|
-
UP_ARROW = 72
|
|
24
|
-
DOWN_ARROW = 80
|
|
25
|
-
ENTER_KEY = 13
|
|
26
|
-
CTL_C = 3
|
|
27
|
-
BACKSPACE = 8
|
|
28
|
-
SPACEBAR = 32
|
|
29
|
-
|
|
30
|
-
class Menu:
|
|
31
|
-
def __init__(self, options: list, label: str, window_size: int=10) -> None:
|
|
32
|
-
assert options
|
|
33
|
-
assert label
|
|
34
|
-
assert 1 <= window_size
|
|
35
|
-
window_size = min((len(options)), window_size)
|
|
36
|
-
|
|
37
|
-
self.options_original = options
|
|
38
|
-
self.options_current = options
|
|
39
|
-
self.search_indices = []
|
|
40
|
-
self.search_string = ""
|
|
41
|
-
self.label = label
|
|
42
|
-
self.selected_index = 0
|
|
43
|
-
self.window_top = 0
|
|
44
|
-
self.window_size_original = window_size
|
|
45
|
-
self.window_size_current = window_size
|
|
46
|
-
self.help_string = "Enter: Select, Ctl+C: Cancel"
|
|
47
|
-
|
|
48
|
-
def show(self):
|
|
49
|
-
print(f"{ANSI_BLUE}{self.label}>{ANSI_RESET}")
|
|
50
|
-
self.printMenu()
|
|
51
|
-
while True:
|
|
52
|
-
something_changed = False
|
|
53
|
-
char = self.getChar()
|
|
54
|
-
if char == SPECIAL_KEY:
|
|
55
|
-
char = self.getChar()
|
|
56
|
-
if 1 < len(self.options_current):
|
|
57
|
-
# Update selected index
|
|
58
|
-
if char == UP_ARROW:
|
|
59
|
-
self.selected_index = (self.selected_index - 1) % len(self.options_current)
|
|
60
|
-
something_changed = True
|
|
61
|
-
elif char == DOWN_ARROW:
|
|
62
|
-
self.selected_index = (self.selected_index + 1) % len(self.options_current)
|
|
63
|
-
something_changed = True
|
|
64
|
-
|
|
65
|
-
# Update window
|
|
66
|
-
if something_changed:
|
|
67
|
-
bottom = self.window_top + self.window_size_current
|
|
68
|
-
if self.selected_index < self.window_top:
|
|
69
|
-
self.window_top = self.selected_index
|
|
70
|
-
elif bottom <= self.selected_index:
|
|
71
|
-
self.window_top = self.selected_index - self.window_size_current + 1
|
|
72
|
-
elif self.isSearchableChar(char):
|
|
73
|
-
char = chr(char)
|
|
74
|
-
self.search_string = f"{self.search_string}{char}".lstrip()
|
|
75
|
-
if self.search_string:
|
|
76
|
-
print(char, end="", flush=True)
|
|
77
|
-
self.search(self.options_current)
|
|
78
|
-
something_changed = True
|
|
79
|
-
elif char == BACKSPACE and 0 < len(self.search_string):
|
|
80
|
-
self.search_string = self.search_string[:-1]
|
|
81
|
-
# Move left, print space, move left again
|
|
82
|
-
print("\x1b[1D \x1b[1D", end="", flush=True)
|
|
83
|
-
self.search(self.options_original)
|
|
84
|
-
something_changed = True
|
|
85
|
-
elif char == ENTER_KEY and self.options_current:
|
|
86
|
-
self.printSelected()
|
|
87
|
-
return self.options_current[self.selected_index]
|
|
88
|
-
elif char == CTL_C:
|
|
89
|
-
self.clearMenu(clear_label=True)
|
|
90
|
-
print("cancelled")
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
if something_changed:
|
|
94
|
-
self.clearMenu()
|
|
95
|
-
self.printMenu()
|
|
96
|
-
|
|
97
|
-
def search(self, search_list: list) -> None:
|
|
98
|
-
found_indices = []
|
|
99
|
-
found_options = []
|
|
100
|
-
for o in search_list:
|
|
101
|
-
found_i = str(o).lower().find(self.search_string.lower())
|
|
102
|
-
if found_i != -1:
|
|
103
|
-
found_indices.append(found_i)
|
|
104
|
-
found_options.append(o)
|
|
105
|
-
self.options_current = found_options
|
|
106
|
-
self.search_indices = found_indices
|
|
107
|
-
|
|
108
|
-
# Reset window after modifying options
|
|
109
|
-
self.window_top = 0
|
|
110
|
-
self.selected_index = 0
|
|
111
|
-
self.window_size_current = min((len(self.options_current), self.window_size_original))
|
|
112
|
-
|
|
113
|
-
def getChar(self) -> int:
|
|
114
|
-
return ord(msvcrt.getch())
|
|
115
|
-
|
|
116
|
-
def isSearchableChar(self, char):
|
|
117
|
-
return (32 <= char and char <= 126)
|
|
118
|
-
|
|
119
|
-
def clearMenu(self, clear_label=False):
|
|
120
|
-
if clear_label:
|
|
121
|
-
print("\x1b[0G\x1b[J", end="", flush=True)
|
|
122
|
-
else:
|
|
123
|
-
print("\x1b[J")
|
|
124
|
-
|
|
125
|
-
def printMenu(self):
|
|
126
|
-
bottom = self.window_top + self.window_size_current
|
|
127
|
-
if not self.options_current:
|
|
128
|
-
print(f"{ANSI_BLUE}no matches found{ANSI_RESET}")
|
|
129
|
-
# NOTE: window size is 0 here after empty search, but we are printing 1 line
|
|
130
|
-
# so we need to set it so the footer prints correctly
|
|
131
|
-
self.window_size_current = 1
|
|
132
|
-
else:
|
|
133
|
-
for i in range(self.window_top, bottom):
|
|
134
|
-
if i == self.selected_index:
|
|
135
|
-
print(f"{ANSI_HIGHLIGHT}{self.options_current[i]}{ANSI_RESET}")
|
|
136
|
-
else:
|
|
137
|
-
print(self.options_current[i])
|
|
138
|
-
print(f"{ANSI_YELLOW}{self.help_string}{ANSI_RESET}\n{ANSI_MOVE_CURSOR.format(up=self.window_size_current + 2, right=len(self.label) + len(self.search_string) + 1)}", end="", flush=True)
|
|
139
|
-
|
|
140
|
-
def printSelected(self):
|
|
141
|
-
self.clearMenu(clear_label=True)
|
|
142
|
-
print(f"{self.label}> {self.options_current[self.selected_index]}")
|
|
143
|
-
|
|
144
|
-
def makeSelection(options: list[Any], label: str, window_size: int=None) -> Any:
|
|
145
|
-
"""
|
|
146
|
-
Entry point for menu selection.
|
|
147
|
-
|
|
148
|
-
Parameters
|
|
149
|
-
----------
|
|
150
|
-
options
|
|
151
|
-
List of str-able objects.
|
|
152
|
-
label
|
|
153
|
-
Label to describe the items being selected.
|
|
154
|
-
window_size
|
|
155
|
-
Max number of items to show at once.
|
|
156
|
-
|
|
157
|
-
Returns
|
|
158
|
-
-------
|
|
159
|
-
Selected value.
|
|
160
|
-
"""
|
|
161
|
-
if window_size:
|
|
162
|
-
return Menu(options, label, window_size).show()
|
|
163
|
-
else:
|
|
164
|
-
return Menu(options, label).show()
|
|
165
|
-
|
|
166
|
-
if __name__ == "__main__":
|
|
167
|
-
print(f"Returns: '{makeSelection(['interactive', 'cli', 'menu'], 'make_selection')}'")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{make_selection-1.0.1 → make_selection-1.0.3}/src/make_selection.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|