wizlib 2.0.14__py3-none-any.whl → 2.0.16__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.
Potentially problematic release.
This version of wizlib might be problematic. Click here for more details.
wizlib/ui/__init__.py
CHANGED
|
@@ -2,21 +2,12 @@
|
|
|
2
2
|
# interfaces (shell, curses, Slack, etc) to drive the same data model without
|
|
3
3
|
# the data model needing specific UI knowledge.
|
|
4
4
|
#
|
|
5
|
-
# The UI keeps a reference to the Handler. The Handler will bring a Command
|
|
6
|
-
# with it, and this is the main root command. In the case of a Shell-type UI,
|
|
7
|
-
# that is the command to be executed. In the case of an interactive UI, it's
|
|
8
|
-
# really just a placeholder; the UI will instantiate other commands as it
|
|
9
|
-
# proceeds. But the Handler won't know about those.
|
|
10
|
-
#
|
|
11
5
|
# Commands might call back to the UI for confirmations or arguments that are
|
|
12
6
|
# previously omitted, using the get_ methods.
|
|
13
7
|
|
|
14
8
|
#
|
|
15
9
|
# UI end classes must implement the following interface:
|
|
16
10
|
#
|
|
17
|
-
# __init__(handler): Takes the handler and hangs on to it. Implemented in the
|
|
18
|
-
# main base class.
|
|
19
|
-
#
|
|
20
11
|
# start(): No arguments. Actually performs the operation of the UI. It might be
|
|
21
12
|
# short running (in the case of a shell UI) or long-running (in the case of an
|
|
22
13
|
# interactive UI).
|
|
@@ -48,41 +39,52 @@ from wizlib.class_family import ClassFamily
|
|
|
48
39
|
|
|
49
40
|
@dataclass
|
|
50
41
|
class Choice:
|
|
51
|
-
"""
|
|
42
|
+
"""A single option, equivalent to a single radio button or select option in
|
|
43
|
+
HTML.
|
|
44
|
+
|
|
45
|
+
text - text of the option
|
|
46
|
+
|
|
47
|
+
keys - keystrokes that might indicate this option in a keyboard-driven UI
|
|
48
|
+
|
|
49
|
+
action - what to do when chosen
|
|
50
|
+
"""
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
text: str = ''
|
|
53
|
+
keys: str = ''
|
|
55
54
|
action: object = None
|
|
56
55
|
|
|
57
56
|
@property
|
|
58
57
|
def key(self):
|
|
59
|
-
return self.keys[0]
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def word(self):
|
|
63
|
-
return self.words[0]
|
|
58
|
+
return self.keys[0] if self.keys else ''
|
|
64
59
|
|
|
65
60
|
def hit_key(self, key):
|
|
66
61
|
return key in self.keys
|
|
67
62
|
|
|
68
|
-
def
|
|
69
|
-
return
|
|
63
|
+
def hit_text(self, text):
|
|
64
|
+
return self.text.startswith(text)
|
|
70
65
|
|
|
66
|
+
@property
|
|
71
67
|
def value(self):
|
|
72
|
-
return self.
|
|
68
|
+
return self.text if (self.action is None) else self.action
|
|
73
69
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
@property
|
|
71
|
+
def key_prompt(self):
|
|
72
|
+
"""Text with keystroke in parens"""
|
|
73
|
+
for key in self.keys:
|
|
74
|
+
if key in self.text:
|
|
75
|
+
pre, it, post = self.text.partition(key)
|
|
76
|
+
return f"{pre}({it}){post}"
|
|
77
|
+
return f"({self.keys[0]}){self.text}"
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
# TODO: Decide whether the Prompt/Chooser division makes sense
|
|
81
|
+
|
|
80
82
|
class Prompt:
|
|
81
83
|
"""Tell the user what kind of input to provide"""
|
|
82
84
|
|
|
83
|
-
intro: str =
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
def __init__(self, intro: str = '', default: str = ''):
|
|
86
|
+
self.intro = intro
|
|
87
|
+
self.default = default
|
|
86
88
|
|
|
87
89
|
@property
|
|
88
90
|
def prompt_string(self):
|
|
@@ -93,35 +95,50 @@ class Prompt:
|
|
|
93
95
|
if self.default:
|
|
94
96
|
result.append(f"[{self.default}]")
|
|
95
97
|
for choice in self.choices:
|
|
96
|
-
if choice.
|
|
98
|
+
if choice.text == self.default:
|
|
97
99
|
continue
|
|
98
|
-
|
|
99
|
-
result.append(f"{pre}({it}){post}")
|
|
100
|
+
result.append(choice.key_prompt)
|
|
100
101
|
return " ".join(result) + ": "
|
|
101
102
|
|
|
102
103
|
|
|
103
|
-
@dataclass
|
|
104
104
|
class Chooser(Prompt):
|
|
105
105
|
"""Hold a set of choices and get the result"""
|
|
106
106
|
|
|
107
|
+
def __init__(self, intro: str = '', default: str = '', choices=None):
|
|
108
|
+
super().__init__(intro, default)
|
|
109
|
+
if isinstance(choices, dict):
|
|
110
|
+
self.choices = [Choice(choices[k], k) for k in choices]
|
|
111
|
+
elif isinstance(choices, list):
|
|
112
|
+
self.choices = [(c if isinstance(c, Choice) else Choice(c))
|
|
113
|
+
for c in choices]
|
|
114
|
+
else:
|
|
115
|
+
self.choices = []
|
|
116
|
+
|
|
107
117
|
def add_choice(self, *args, **kwargs):
|
|
108
118
|
choice = Choice(*args, **kwargs)
|
|
109
119
|
self.choices.append(choice)
|
|
110
120
|
|
|
111
121
|
def choice_by_key(self, key):
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
if key == '\n':
|
|
123
|
+
choice = self.default
|
|
124
|
+
else:
|
|
125
|
+
choice = next(
|
|
126
|
+
(o.value for o in self.choices if key in o.keys), None)
|
|
127
|
+
return choice() if callable(choice) else choice
|
|
114
128
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
return self._choose(chosenlist)
|
|
129
|
+
def choices_by_text(self, text):
|
|
130
|
+
return [o.value for o in self.choices if o.hit_text(text)]
|
|
118
131
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
# def choice_by_word(self, word):
|
|
133
|
+
# chosenlist = [o.value for o in self.choices if o.hit_word(word)]
|
|
134
|
+
# return self._choose(chosenlist)
|
|
135
|
+
|
|
136
|
+
# def _choose(self, chosenlist):
|
|
137
|
+
# choice = next(iter(chosenlist), None)
|
|
138
|
+
# if callable(choice):
|
|
139
|
+
# return choice()
|
|
140
|
+
# else:
|
|
141
|
+
# return choice
|
|
125
142
|
|
|
126
143
|
|
|
127
144
|
@dataclass
|
|
@@ -134,8 +151,8 @@ class UI(ClassFamily):
|
|
|
134
151
|
# def start(self):
|
|
135
152
|
# pass
|
|
136
153
|
|
|
137
|
-
# # TODO: Make
|
|
138
|
-
# def
|
|
154
|
+
# # TODO: Make get_option_mini an abstract method
|
|
155
|
+
# def get_option_mini(self, chooser: Chooser):
|
|
139
156
|
# """Get an option from a list from the user"""
|
|
140
157
|
# pass
|
|
141
158
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from readchar import readkey
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
if (sys.platform == "win32"):
|
|
9
|
+
import ctypes
|
|
10
|
+
from ctypes import wintypes
|
|
11
|
+
else:
|
|
12
|
+
import termios
|
|
13
|
+
|
|
14
|
+
# https://stackoverflow.com/questions/35526014/how-can-i-get-the-cursors-position-in-an-ansi-terminal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cursorPos(): # pragma: nocover
|
|
18
|
+
if (sys.platform == "win32"):
|
|
19
|
+
OldStdinMode = ctypes.wintypes.DWORD()
|
|
20
|
+
OldStdoutMode = ctypes.wintypes.DWORD()
|
|
21
|
+
kernel32 = ctypes.windll.kernel32
|
|
22
|
+
kernel32.GetConsoleMode(
|
|
23
|
+
kernel32.GetStdHandle(-10), ctypes.byref(OldStdinMode))
|
|
24
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 0)
|
|
25
|
+
kernel32.GetConsoleMode(
|
|
26
|
+
kernel32.GetStdHandle(-11), ctypes.byref(OldStdoutMode))
|
|
27
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
28
|
+
else:
|
|
29
|
+
OldStdinMode = termios.tcgetattr(sys.stdin)
|
|
30
|
+
_ = termios.tcgetattr(sys.stdin)
|
|
31
|
+
_[3] = _[3] & ~(termios.ECHO | termios.ICANON)
|
|
32
|
+
termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, _)
|
|
33
|
+
try:
|
|
34
|
+
_ = ""
|
|
35
|
+
sys.stdout.write("\x1b[6n")
|
|
36
|
+
sys.stdout.flush()
|
|
37
|
+
while not (_ := _ + sys.stdin.read(1)).endswith('R'):
|
|
38
|
+
True
|
|
39
|
+
res = re.match(r".*\[(?P<y>\d*);(?P<x>\d*)R", _)
|
|
40
|
+
finally:
|
|
41
|
+
if (sys.platform == "win32"):
|
|
42
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), OldStdinMode)
|
|
43
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), OldStdoutMode)
|
|
44
|
+
else:
|
|
45
|
+
termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, OldStdinMode)
|
|
46
|
+
if (res):
|
|
47
|
+
return (res.group("x"), res.group("y"))
|
|
48
|
+
return (-1, -1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sequence(hexes: str) -> str:
|
|
52
|
+
return bytes.fromhex(hexes).decode()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
LEFT = sequence("1b5b44")
|
|
56
|
+
RIGHT = sequence("1b5b43")
|
|
57
|
+
BACKSPACE = sequence("7f")
|
|
58
|
+
BEGINNING = sequence("01")
|
|
59
|
+
END = sequence("05")
|
|
60
|
+
RETURN = sequence("0a")
|
|
61
|
+
ESC = sequence("1b")
|
|
62
|
+
TAB = sequence("09")
|
|
63
|
+
SHIFT_TAB = sequence("1b5b5a")
|
|
64
|
+
LEFT_WORD = sequence("1b62")
|
|
65
|
+
RIGHT_WORD = sequence("1b66")
|
|
66
|
+
KILL = sequence("0b")
|
|
67
|
+
|
|
68
|
+
RESET = ESC + "[0m"
|
|
69
|
+
FAINT = ESC + "[2m"
|
|
70
|
+
BOLD = ESC + "[1m"
|
|
71
|
+
|
|
72
|
+
WORD_SEPARATORS = ' -_.,'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write(key):
|
|
76
|
+
sys.stderr.write(key)
|
|
77
|
+
sys.stderr.flush()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# "Fill" refers to the lighter text in the editor, to the right of (or instead
|
|
81
|
+
# of) user-typed text.
|
|
82
|
+
#
|
|
83
|
+
# States:
|
|
84
|
+
# - USER: User has typed something, so no fill
|
|
85
|
+
# - TAB: User has hit tab or shift-tab, show tab completion if any
|
|
86
|
+
# - DEFAULT: editor has a default value and user has typed nothing, so show
|
|
87
|
+
# default if any
|
|
88
|
+
# - BLANK: user hit backspace to clear the fill
|
|
89
|
+
|
|
90
|
+
FillState = Enum('FillState', 'USER TAB DEFAULT BLANK')
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ShellLineEditor: # pragma: nocover
|
|
94
|
+
|
|
95
|
+
buf = ''
|
|
96
|
+
pos = 0
|
|
97
|
+
index = -1
|
|
98
|
+
|
|
99
|
+
def __init__(self, choices=[], default=''):
|
|
100
|
+
"""Parameters:
|
|
101
|
+
|
|
102
|
+
choices: List of string options for tab completion
|
|
103
|
+
|
|
104
|
+
default: Starting string value, can be accepted by user pressing return
|
|
105
|
+
"""
|
|
106
|
+
self.choices = choices
|
|
107
|
+
self.default = default
|
|
108
|
+
self.fillstate = FillState.DEFAULT
|
|
109
|
+
|
|
110
|
+
def edit(self):
|
|
111
|
+
write(RESET)
|
|
112
|
+
while True:
|
|
113
|
+
self.write_fill()
|
|
114
|
+
key = readkey()
|
|
115
|
+
self.clear_fill()
|
|
116
|
+
if key == RETURN:
|
|
117
|
+
break
|
|
118
|
+
if key.isprintable():
|
|
119
|
+
write(BOLD + key + self.buf[self.pos:] +
|
|
120
|
+
('\b' * (len(self.buf) - self.pos)) + RESET)
|
|
121
|
+
self.buf = self.buf[:self.pos] + key + self.buf[self.pos:]
|
|
122
|
+
self.pos += 1
|
|
123
|
+
self.fillstate = FillState.USER
|
|
124
|
+
elif (key in [BACKSPACE, KILL]) and self.has_fill:
|
|
125
|
+
# Backspace clears the fill
|
|
126
|
+
self.fillstate = FillState.BLANK
|
|
127
|
+
elif (key == BACKSPACE) and (self.pos > 0):
|
|
128
|
+
write(BOLD + '\b' + self.buf[self.pos:] + ' ' +
|
|
129
|
+
('\b' * (1 + len(self.buf) - self.pos)) + RESET)
|
|
130
|
+
self.buf = self.buf[:self.pos-1] + self.buf[self.pos:]
|
|
131
|
+
self.pos -= 1
|
|
132
|
+
self.fillstate = FillState.USER if (
|
|
133
|
+
self.pos > 0) else FillState.DEFAULT
|
|
134
|
+
elif key == LEFT and self.pos > 0:
|
|
135
|
+
self.move_left()
|
|
136
|
+
self.fillstate = FillState.USER
|
|
137
|
+
elif key == RIGHT and self.pos < len(self.buf):
|
|
138
|
+
self.move_right()
|
|
139
|
+
self.fillstate = FillState.USER
|
|
140
|
+
elif key == BEGINNING and self.pos > 0:
|
|
141
|
+
self.move_beginning()
|
|
142
|
+
self.fillstate = FillState.USER
|
|
143
|
+
elif key == END and self.has_fill:
|
|
144
|
+
self.accept_fill()
|
|
145
|
+
self.fillstate = FillState.USER
|
|
146
|
+
elif key == END and self.pos < len(self.buf):
|
|
147
|
+
self.move_end_buf()
|
|
148
|
+
self.fillstate = FillState.USER
|
|
149
|
+
elif key == TAB and (choices := self.valid_choices):
|
|
150
|
+
self.index = (self.index + 1) % len(choices)
|
|
151
|
+
self.fillstate = FillState.TAB
|
|
152
|
+
elif key == SHIFT_TAB and self.index > -1:
|
|
153
|
+
self.index = (self.index - 1) % len(self.valid_choices)
|
|
154
|
+
self.fillstate = FillState.TAB
|
|
155
|
+
elif key == LEFT_WORD and self.pos > 0:
|
|
156
|
+
while (self.pos > 0) and self.is_sep(self.pos - 1):
|
|
157
|
+
self.move_left()
|
|
158
|
+
while (self.pos > 0) and not self.is_sep(self.pos - 1):
|
|
159
|
+
self.move_left()
|
|
160
|
+
self.fillstate = FillState.USER
|
|
161
|
+
elif key == RIGHT_WORD and self.pos < len(self.buf):
|
|
162
|
+
while (self.pos < len(self.buf)) and self.is_sep(self.pos):
|
|
163
|
+
self.move_right()
|
|
164
|
+
while (self.pos < len(self.buf)) and not self.is_sep(self.pos):
|
|
165
|
+
self.move_right()
|
|
166
|
+
self.fillstate = FillState.USER
|
|
167
|
+
elif key == KILL:
|
|
168
|
+
chars = len(self.buf) - self.pos
|
|
169
|
+
write(' ' * chars + '\b' * chars)
|
|
170
|
+
self.buf = self.buf[:self.pos]
|
|
171
|
+
self.fillstate = FillState.USER
|
|
172
|
+
else:
|
|
173
|
+
pass
|
|
174
|
+
self.accept_fill()
|
|
175
|
+
write(RETURN)
|
|
176
|
+
return self.buf
|
|
177
|
+
|
|
178
|
+
def is_sep(self, pos):
|
|
179
|
+
return (self.buf[pos] in WORD_SEPARATORS)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def has_fill(self):
|
|
183
|
+
return self.fillstate in [FillState.DEFAULT, FillState.TAB]
|
|
184
|
+
|
|
185
|
+
def accept_fill(self):
|
|
186
|
+
if self.has_fill:
|
|
187
|
+
write(BOLD + self.fill + RESET)
|
|
188
|
+
self.buf += self.fill
|
|
189
|
+
self.pos = len(self.buf)
|
|
190
|
+
|
|
191
|
+
def move_left(self):
|
|
192
|
+
write(LEFT)
|
|
193
|
+
self.pos -= 1
|
|
194
|
+
|
|
195
|
+
def move_right(self):
|
|
196
|
+
write(RIGHT)
|
|
197
|
+
self.pos += 1
|
|
198
|
+
|
|
199
|
+
def move_beginning(self):
|
|
200
|
+
while self.pos > 0:
|
|
201
|
+
self.move_left()
|
|
202
|
+
|
|
203
|
+
def move_end_buf(self):
|
|
204
|
+
while self.pos < len(self.buf):
|
|
205
|
+
self.move_right()
|
|
206
|
+
|
|
207
|
+
def write_fill(self):
|
|
208
|
+
if self.fillstate in [FillState.DEFAULT, FillState.TAB]:
|
|
209
|
+
self.move_end_buf()
|
|
210
|
+
write(FAINT + self.fill + '\b' * len(self.fill) + RESET)
|
|
211
|
+
|
|
212
|
+
def clear_fill(self):
|
|
213
|
+
if self.has_fill:
|
|
214
|
+
self.move_end_buf()
|
|
215
|
+
write(' ' * len(self.fill) + '\b' * len(self.fill))
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def fill(self):
|
|
219
|
+
if self.fillstate == FillState.TAB:
|
|
220
|
+
choice = self.valid_choices[self.index]
|
|
221
|
+
return choice[len(self.last_word):]
|
|
222
|
+
elif self.fillstate == FillState.DEFAULT:
|
|
223
|
+
return self.default
|
|
224
|
+
else:
|
|
225
|
+
return ''
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def last_word(self):
|
|
229
|
+
index = next((c for c in reversed(range(len(self.buf)))
|
|
230
|
+
if self.buf[c] in WORD_SEPARATORS), -1)
|
|
231
|
+
return self.buf[index+1:]
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def valid_choices(self):
|
|
235
|
+
# return [c for c in self.choices if c.startswith(self.buf)]
|
|
236
|
+
return [c for c in self.choices if c.startswith(self.last_word)]
|
wizlib/ui/shell_ui.py
CHANGED
|
@@ -3,6 +3,7 @@ import sys
|
|
|
3
3
|
from readchar import readkey
|
|
4
4
|
from wizlib.rlinput import rlinput
|
|
5
5
|
from wizlib.ui import UI
|
|
6
|
+
from wizlib.ui.shell_line_editor import ShellLineEditor
|
|
6
7
|
|
|
7
8
|
INTERACTIVE = sys.stdin.isatty()
|
|
8
9
|
|
|
@@ -21,7 +22,7 @@ class ShellUI(UI):
|
|
|
21
22
|
if value:
|
|
22
23
|
print(value, file=sys.stderr)
|
|
23
24
|
|
|
24
|
-
def
|
|
25
|
+
def get_option_mini(self, chooser):
|
|
25
26
|
"""Get a choice from the user with a single keystroke"""
|
|
26
27
|
# key = rlinput(chooser.prompt_string)
|
|
27
28
|
print(chooser.prompt_string, end='', file=sys.stderr, flush=True)
|
|
@@ -31,3 +32,11 @@ class ShellUI(UI):
|
|
|
31
32
|
key = sys.stdin.read(1)
|
|
32
33
|
print(file=sys.stderr, flush=True)
|
|
33
34
|
return chooser.choice_by_key(key)
|
|
35
|
+
|
|
36
|
+
def get_text(self, prompt):
|
|
37
|
+
"""Allow the user to input an arbitrary line of text"""
|
|
38
|
+
sys.stderr.write(prompt)
|
|
39
|
+
sys.stderr.flush()
|
|
40
|
+
value = ShellLineEditor().edit()
|
|
41
|
+
# value = input()
|
|
42
|
+
return value
|
|
@@ -9,9 +9,10 @@ wizlib/input_handler.py,sha256=QFiR9CC5bkW7HofNAKZEq_hL089hbWzyrFGAjAjvozg,624
|
|
|
9
9
|
wizlib/parser.py,sha256=O34azN4ttVfwwAsza0hujxGxDpzc4xUEVAf26DXJS5g,1505
|
|
10
10
|
wizlib/rlinput.py,sha256=l00Pa3rxNeY6LJgz8Aws_rTKoEchw33fuL8yqHF9_-o,1754
|
|
11
11
|
wizlib/super_wrapper.py,sha256=F834ytHqA7zegTD1ezk_uxlF9PLygh84wReuiqcI7BI,272
|
|
12
|
-
wizlib/ui/__init__.py,sha256=
|
|
13
|
-
wizlib/ui/
|
|
12
|
+
wizlib/ui/__init__.py,sha256=55tzM_XphYFqm_jVxQc5vkfPLgD1Z3aqSjDl50GM44A,6139
|
|
13
|
+
wizlib/ui/shell_line_editor.py,sha256=HQNMUijdf-qU2kbzU_iOrK5gT-nEMkxweld2fP8yvPo,7818
|
|
14
|
+
wizlib/ui/shell_ui.py,sha256=phgrFFMZGY9jJB_QwoOXhI6P6VK3W740g4rkVsFbRSc,1234
|
|
14
15
|
wizlib/ui_handler.py,sha256=nvMRuGVCwJ-_H59iZQ72--_tiiqXAsSHP7kYnd43P68,716
|
|
15
|
-
wizlib-2.0.
|
|
16
|
-
wizlib-2.0.
|
|
17
|
-
wizlib-2.0.
|
|
16
|
+
wizlib-2.0.16.dist-info/METADATA,sha256=RfenANTrwBZ7LGRZhMLnSgMjI8Jmm4lVqtxuD1oRNfk,1781
|
|
17
|
+
wizlib-2.0.16.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
18
|
+
wizlib-2.0.16.dist-info/RECORD,,
|