-
Notifications
You must be signed in to change notification settings - Fork 13
/
cutie.py
executable file
·368 lines (339 loc) · 13.4 KB
/
cutie.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#! /usr/bin/env python3
"""
Commandline User Tools for Input Easification
"""
__version__ = "0.3.2"
__author__ = "Hans / Kamik423"
__license__ = "MIT"
import getpass
from typing import List, Optional
import readchar
from colorama import init
init()
class DefaultKeys:
"""List of default keybindings.
Attributes:
interrupt(List[str]): Keys that cause a keyboard interrupt.
select(List[str]): Keys that trigger list element selection.
confirm(List[str]): Keys that trigger list confirmation.
delete(List[str]): Keys that trigger character deletion.
down(List[str]): Keys that select the element below.
up(List[str]): Keys that select the element above.
"""
interrupt: List[str] = [readchar.key.CTRL_C, readchar.key.CTRL_D]
select: List[str] = [readchar.key.SPACE]
confirm: List[str] = [readchar.key.ENTER]
delete: List[str] = [readchar.key.BACKSPACE]
down: List[str] = [readchar.key.DOWN, "j"]
up: List[str] = [readchar.key.UP, "k"]
def get_number(
prompt: str,
min_value: Optional[float] = None,
max_value: Optional[float] = None,
allow_float: bool = True,
) -> float:
"""Get a number from user input.
If an invalid number is entered the user will be prompted again.
Args:
prompt (str): The prompt asking the user to input.
min_value (float, optional): The [inclusive] minimum value.
max_value (float, optional): The [inclusive] maximum value.
allow_float (bool, optional): Allow floats or force integers.
Returns:
float: The number input by the user.
"""
return_value: Optional[float] = None
while return_value is None:
input_value = input(prompt + " ")
try:
return_value = float(input_value)
except ValueError:
print("Not a valid number.\033[K\033[1A\r\033[K", end="")
if not allow_float and return_value is not None:
if return_value != int(return_value):
print("Has to be an integer.\033[K\033[1A\r\033[K", end="")
return_value = None
if min_value is not None and return_value is not None:
if return_value < min_value:
print(f"Has to be at least {min_value}.\033[K\033[1A\r\033[K", end="")
return_value = None
if max_value is not None and return_value is not None:
if return_value > max_value:
print(f"Has to be at most {max_value}.\033[1A\r\033[K", end="")
return_value = None
if return_value is not None:
break
print("\033[K", end="")
if allow_float:
return return_value
return int(return_value)
def secure_input(prompt: str) -> str:
"""Get secure input without showing it in the command line.
Args:
prompt (str): The prompt asking the user to input.
Returns:
str: The secure input.
"""
return getpass.getpass(prompt + " ")
def select(
options: List[str],
caption_indices: Optional[List[int]] = None,
deselected_prefix: str = "\033[1m[ ]\033[0m ",
selected_prefix: str = "\033[1m[\033[32;1mx\033[0;1m]\033[0m ",
caption_prefix: str = "",
selected_index: int = 0,
confirm_on_select: bool = True,
) -> int:
"""Select an option from a list.
Args:
options (List[str]): The options to select from.
caption_indices (List[int], optional): Non-selectable indices.
deselected_prefix (str, optional): Prefix for deselected option ([ ]).
selected_prefix (str, optional): Prefix for selected option ([x]).
caption_prefix (str, optional): Prefix for captions ().
selected_index (int, optional): The index to be selected at first.
confirm_on_select (bool, optional): Select keys also confirm.
Returns:
int: The index that has been selected.
"""
print("\n" * (len(options) - 1))
if caption_indices is None:
caption_indices = []
while True:
print(f"\033[{len(options) + 1}A")
for i, option in enumerate(options):
if i not in caption_indices:
print(
"\033[K{}{}".format(
selected_prefix if i == selected_index else deselected_prefix,
option,
)
)
elif i in caption_indices:
print("\033[K{}{}".format(caption_prefix, options[i]))
keypress = readchar.readkey()
if keypress in DefaultKeys.up:
new_index = selected_index
while new_index > 0:
new_index -= 1
if new_index not in caption_indices:
selected_index = new_index
break
elif keypress in DefaultKeys.down:
new_index = selected_index
while new_index < len(options) - 1:
new_index += 1
if new_index not in caption_indices:
selected_index = new_index
break
elif (
keypress in DefaultKeys.confirm
or confirm_on_select
and keypress in DefaultKeys.select
):
break
elif keypress in DefaultKeys.interrupt:
raise KeyboardInterrupt
return selected_index
def select_multiple(
options: List[str],
caption_indices: Optional[List[int]] = None,
deselected_unticked_prefix: str = "\033[1m( )\033[0m ",
deselected_ticked_prefix: str = "\033[1m(\033[32mx\033[0;1m)\033[0m ",
selected_unticked_prefix: str = "\033[32;1m{ }\033[0m ",
selected_ticked_prefix: str = "\033[32;1m{x}\033[0m ",
caption_prefix: str = "",
ticked_indices: Optional[List[int]] = None,
cursor_index: int = 0,
minimal_count: int = 0,
maximal_count: Optional[int] = None,
hide_confirm: bool = True,
deselected_confirm_label: str = "\033[1m(( confirm ))\033[0m",
selected_confirm_label: str = "\033[1;32m{{ confirm }}\033[0m",
) -> List[int]:
"""Select multiple options from a list.
Args:
options (List[str]): The options to select from.
caption_indices (List[int], optional): Non-selectable indices.
deselected_unticked_prefix (str, optional): Prefix for lines that are
not selected and not ticked (( )).
deselected_ticked_prefix (str, optional): Prefix for lines that are
not selected but ticked ((x)).
selected_unticked_prefix (str, optional): Prefix for lines that are
selected but not ticked ({ }).
selected_ticked_prefix (str, optional): Prefix for lines that are
selected and ticked ({x}).
caption_prefix (str, optional): Prefix for captions ().
ticked_indices (List[int], optional): Indices that are
ticked initially.
cursor_index (int, optional): The index the cursor starts at.
minimal_count (int, optional): The minimal amount of lines
that have to be ticked.
maximal_count (int, optional): The maximal amount of lines
that have to be ticked.
hide_confirm (bool, optional): Hide the confirm button.
This causes <ENTER> to confirm the entire selection and not just
tick the line.
deselected_confirm_label (str, optional): The confirm label
if not selected ((( confirm ))).
selected_confirm_label (str, optional): The confirm label
if selected ({{ confirm }}).
Returns:
List[int]: The indices that have been selected
"""
print("\n" * (len(options) - 1))
if caption_indices is None:
caption_indices = []
if ticked_indices is None:
ticked_indices = []
max_index = len(options) - (1 if hide_confirm else 0)
error_message = ""
while True:
print(f"\033[{len(options) + 1}A")
for i, option in enumerate(options):
prefix = ""
if i in caption_indices:
prefix = caption_prefix
elif i == cursor_index:
if i in ticked_indices:
prefix = selected_ticked_prefix
else:
prefix = selected_unticked_prefix
else:
if i in ticked_indices:
prefix = deselected_ticked_prefix
else:
prefix = deselected_unticked_prefix
print("\033[K{}{}".format(prefix, option))
if hide_confirm:
print(f"{error_message}\033[K", end="", flush=True)
else:
if cursor_index == max_index:
print(
f"{selected_confirm_label} {error_message}\033[K",
end="",
flush=True,
)
else:
print(
f"{deselected_confirm_label} {error_message}\033[K",
end="",
flush=True,
)
error_message = ""
keypress = readchar.readkey()
if keypress in DefaultKeys.up:
new_index = cursor_index
while new_index > 0:
new_index -= 1
if new_index not in caption_indices:
cursor_index = new_index
break
elif keypress in DefaultKeys.down:
new_index = cursor_index
while new_index + 1 <= max_index:
new_index += 1
if new_index not in caption_indices:
cursor_index = new_index
break
elif (
hide_confirm
and keypress in DefaultKeys.confirm
or not hide_confirm
and cursor_index == max_index
):
if minimal_count > len(ticked_indices):
error_message = f"Must select at least {minimal_count} options"
elif maximal_count is not None and maximal_count < len(ticked_indices):
error_message = f"Must select at most {maximal_count} options"
else:
break
elif (
keypress in DefaultKeys.select
or not hide_confirm
and keypress in DefaultKeys.confirm
):
if cursor_index in ticked_indices:
ticked_indices.remove(cursor_index)
else:
ticked_indices.append(cursor_index)
elif keypress in DefaultKeys.interrupt:
raise KeyboardInterrupt
print("\r\033[K", end="", flush=True)
return ticked_indices
def prompt_yes_or_no(
question: str,
yes_text: str = "Yes",
no_text: str = "No",
has_to_match_case: bool = False,
enter_empty_confirms: bool = True,
default_is_yes: bool = False,
deselected_prefix: str = " ",
selected_prefix: str = "\033[31m>\033[0m ",
char_prompt: bool = True,
) -> Optional[bool]:
"""Prompt the user to input yes or no.
Args:
question (str): The prompt asking the user to input.
yes_text (str, optional): The text corresponding to 'yes'.
no_text (str, optional): The text corresponding to 'no'.
has_to_match_case (bool, optional): Does the case have to match.
enter_empty_confirms (bool, optional): Does enter on empty string work.
default_is_yes (bool, optional): Is yes selected by default (no).
deselected_prefix (str, optional): Prefix if something is deselected.
selected_prefix (str, optional): Prefix if something is selected (> )
char_prompt (bool, optional): Add a [Y/N] to the prompt.
Returns:
Optional[bool]: The bool what has been selected.
"""
is_yes = default_is_yes
is_selected = enter_empty_confirms
current_message = ""
yn_prompt = f" ({yes_text[0]}/{no_text[0]}) " if char_prompt else ": "
print()
while True:
yes = is_yes and is_selected
no = not is_yes and is_selected
print("\033[K" f"{selected_prefix if yes else deselected_prefix}{yes_text}")
print("\033[K" f"{selected_prefix if no else deselected_prefix}{no_text}")
print(
"\033[3A\r\033[K" f"{question}{yn_prompt}{current_message}",
end="",
flush=True,
)
keypress = readchar.readkey()
if keypress in DefaultKeys.down or keypress in DefaultKeys.up:
is_yes = not is_yes
is_selected = True
current_message = yes_text if is_yes else no_text
elif keypress in DefaultKeys.delete:
if current_message:
current_message = current_message[:-1]
elif keypress in DefaultKeys.interrupt:
raise KeyboardInterrupt
elif keypress in DefaultKeys.confirm:
if is_selected:
break
elif keypress in "\t":
if is_selected:
current_message = yes_text if is_yes else no_text
else:
current_message += keypress
match_yes = yes_text
match_no = no_text
match_text = current_message
if not has_to_match_case:
match_yes = match_yes.upper()
match_no = match_no.upper()
match_text = match_text.upper()
if match_no.startswith(match_text):
is_selected = True
is_yes = False
elif match_yes.startswith(match_text):
is_selected = True
is_yes = True
else:
is_selected = False
print()
print("\033[K\n\033[K\n\033[K\n\033[3A")
return is_selected and is_yes