xulbux 1.7.3__py3-none-any.whl → 1.8.1__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 xulbux might be problematic. Click here for more details.
- xulbux/__init__.py +17 -19
- xulbux/{_consts_.py → base/consts.py} +51 -58
- xulbux/cli/help.py +45 -0
- xulbux/{xx_code.py → code.py} +3 -3
- xulbux/{xx_color.py → color.py} +1 -1
- xulbux/{xx_console.py → console.py} +358 -200
- xulbux/{xx_data.py → data.py} +14 -14
- xulbux/{xx_env_path.py → env_path.py} +1 -1
- xulbux/{xx_file.py → file.py} +1 -1
- xulbux/{xx_format_codes.py → format_codes.py} +21 -21
- xulbux/{xx_json.py → json.py} +3 -3
- xulbux/{xx_system.py → system.py} +11 -11
- xulbux-1.8.1.dist-info/METADATA +190 -0
- xulbux-1.8.1.dist-info/RECORD +20 -0
- xulbux-1.8.1.dist-info/entry_points.txt +2 -0
- xulbux/_cli_.py +0 -46
- xulbux-1.7.3.dist-info/METADATA +0 -173
- xulbux-1.7.3.dist-info/RECORD +0 -21
- xulbux-1.7.3.dist-info/entry_points.txt +0 -3
- xulbux-1.7.3.dist-info/licenses/LICENSE +0 -21
- /xulbux/{xx_path.py → path.py} +0 -0
- /xulbux/{xx_regex.py → regex.py} +0 -0
- /xulbux/{xx_string.py → string.py} +0 -0
- {xulbux-1.7.3.dist-info → xulbux-1.8.1.dist-info}/WHEEL +0 -0
- {xulbux-1.7.3.dist-info → xulbux-1.8.1.dist-info}/top_level.txt +0 -0
|
@@ -2,22 +2,23 @@
|
|
|
2
2
|
Functions for logging and other small actions within the console.\n
|
|
3
3
|
----------------------------------------------------------------------------------------------------------
|
|
4
4
|
You can also use special formatting codes directly inside the log message to change their appearance.
|
|
5
|
-
For more detailed information about formatting codes, see the the `
|
|
5
|
+
For more detailed information about formatting codes, see the the `format_codes` module documentation.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
12
|
-
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
from .base.consts import COLOR, CHARS
|
|
9
|
+
from .format_codes import FormatCodes, _COMPILED
|
|
10
|
+
from .string import String
|
|
11
|
+
from .color import Color, Rgba, Hexa
|
|
12
|
+
|
|
13
|
+
from typing import Callable, Optional, Literal, Mapping, Any, cast
|
|
14
|
+
from prompt_toolkit.key_binding import KeyPressEvent, KeyBindings
|
|
15
|
+
from prompt_toolkit.validation import ValidationError, Validator
|
|
16
|
+
from prompt_toolkit.styles import Style
|
|
17
|
+
from prompt_toolkit.keys import Keys
|
|
18
|
+
import prompt_toolkit as _pt
|
|
17
19
|
import keyboard as _keyboard
|
|
18
20
|
import getpass as _getpass
|
|
19
21
|
import shutil as _shutil
|
|
20
|
-
import mouse as _mouse
|
|
21
22
|
import sys as _sys
|
|
22
23
|
import os as _os
|
|
23
24
|
|
|
@@ -63,7 +64,7 @@ class ArgResult:
|
|
|
63
64
|
--------------------------------------------------------------------------------------------------------
|
|
64
65
|
When the `ArgResult` instance is accessed as a boolean it will correspond to the `exists` attribute."""
|
|
65
66
|
|
|
66
|
-
def __init__(self, exists: bool, value: Any):
|
|
67
|
+
def __init__(self, exists: bool, value: Any | list[Any]):
|
|
67
68
|
self.exists: bool = exists
|
|
68
69
|
self.value: Any = value
|
|
69
70
|
|
|
@@ -76,12 +77,11 @@ class Args:
|
|
|
76
77
|
For example, if an argument `foo` was parsed, it can be accessed via `args.foo`.
|
|
77
78
|
Each such attribute (e.g. `args.foo`) is an instance of `ArgResult`."""
|
|
78
79
|
|
|
79
|
-
def __init__(self, **kwargs: dict[str, Any]):
|
|
80
|
+
def __init__(self, **kwargs: dict[str, Any | list[Any]]):
|
|
80
81
|
for alias_name, data_dict in kwargs.items():
|
|
81
82
|
if not alias_name.isidentifier():
|
|
82
83
|
raise TypeError(f"Argument alias '{alias_name}' is invalid. It must be a valid Python variable name.")
|
|
83
|
-
|
|
84
|
-
setattr(self, alias_name, arg_result_instance)
|
|
84
|
+
setattr(self, alias_name, ArgResult(exists=cast(bool, data_dict["exists"]), value=data_dict["value"]))
|
|
85
85
|
|
|
86
86
|
def __len__(self):
|
|
87
87
|
return len(vars(self))
|
|
@@ -132,7 +132,8 @@ class Console:
|
|
|
132
132
|
|
|
133
133
|
@staticmethod
|
|
134
134
|
def get_args(
|
|
135
|
-
find_args: Mapping[str, list[str] | tuple[str, ...] | dict[str, list[str] | tuple[str, ...] | Any]
|
|
135
|
+
find_args: Mapping[str, list[str] | tuple[str, ...] | dict[str, list[str] | tuple[str, ...] | Any]
|
|
136
|
+
| Literal["before", "after"]],
|
|
136
137
|
allow_spaces: bool = False
|
|
137
138
|
) -> Args:
|
|
138
139
|
"""Will search for the specified arguments in the command line
|
|
@@ -150,24 +151,31 @@ class Console:
|
|
|
150
151
|
"default": "some_value" # Optional
|
|
151
152
|
}
|
|
152
153
|
```
|
|
154
|
+
3. Positional argument collection (string value):
|
|
155
|
+
```python
|
|
156
|
+
"alias_name": "before" # Collects non-flagged args before first flag
|
|
157
|
+
"alias_name": "after" # Collects non-flagged args after last flag
|
|
158
|
+
```
|
|
153
159
|
Example `find_args`:
|
|
154
160
|
```python
|
|
155
161
|
find_args={
|
|
156
|
-
"
|
|
162
|
+
"text": "before", # Positional args before flags
|
|
163
|
+
"arg1": { # With default
|
|
157
164
|
"flags": ["-a1", "--arg1"],
|
|
158
165
|
"default": "default_val"
|
|
159
166
|
},
|
|
160
|
-
"arg2": ("-a2", "--arg2"),
|
|
161
|
-
"arg3": ["-a3"],
|
|
162
|
-
"arg4": {
|
|
167
|
+
"arg2": ("-a2", "--arg2"), # Without default (original format)
|
|
168
|
+
"arg3": ["-a3"], # Without default (list format)
|
|
169
|
+
"arg4": { # Flag with default True
|
|
163
170
|
"flags": ["-f"],
|
|
164
171
|
"default": True
|
|
165
172
|
}
|
|
166
173
|
}
|
|
167
174
|
```
|
|
168
175
|
If the script is called via the command line:\n
|
|
169
|
-
`python script.py -a1 "value1" --arg2 -f`\n
|
|
176
|
+
`python script.py Hello World -a1 "value1" --arg2 -f`\n
|
|
170
177
|
...it would return an `Args` object where:
|
|
178
|
+
- `args.text.exists` is `True`, `args.text.value` is `["Hello", "World"]`
|
|
171
179
|
- `args.arg1.exists` is `True`, `args.arg1.value` is `"value1"`
|
|
172
180
|
- `args.arg2.exists` is `True`, `args.arg2.value` is `True` (flag present without value)
|
|
173
181
|
- `args.arg3.exists` is `False`, `args.arg3.value` is `None` (not present, no default)
|
|
@@ -176,19 +184,46 @@ class Console:
|
|
|
176
184
|
- `exists` will be `False`
|
|
177
185
|
- `value` will be the specified `default` value, or `None` if no default was specified.\n
|
|
178
186
|
----------------------------------------------------------------
|
|
187
|
+
For positional arguments:
|
|
188
|
+
- `"before"`: Collects all non-flagged arguments that appear before the first flag
|
|
189
|
+
- `"after"`: Collects all non-flagged arguments that appear after the last flag's value
|
|
190
|
+
----------------------------------------------------------------
|
|
179
191
|
Normally if `allow_spaces` is false, it will take a space as
|
|
180
192
|
the end of an args value. If it is true, it will take spaces as
|
|
181
|
-
part of the value until the next arg is found.
|
|
193
|
+
part of the value up until the next arg-flag is found.
|
|
182
194
|
(Multiple spaces will become one space in the value.)"""
|
|
183
195
|
args = _sys.argv[1:]
|
|
184
196
|
args_len = len(args)
|
|
185
197
|
arg_lookup = {}
|
|
186
198
|
results = {}
|
|
199
|
+
positional_configs = {}
|
|
200
|
+
before_count = 0
|
|
201
|
+
after_count = 0
|
|
202
|
+
|
|
203
|
+
# PARSE "find_args" CONFIGURATION
|
|
187
204
|
for alias, config in find_args.items():
|
|
188
205
|
flags = None
|
|
189
206
|
default_value = None
|
|
190
|
-
|
|
207
|
+
|
|
208
|
+
if isinstance(config, str):
|
|
209
|
+
# HANDLE POSITIONAL ARGUMENT COLLECTION
|
|
210
|
+
if config not in ("before", "after"):
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"Invalid positional argument type '{config}' for alias '{alias}'. Must be 'before' or 'after'."
|
|
213
|
+
)
|
|
214
|
+
if config == "before":
|
|
215
|
+
before_count += 1
|
|
216
|
+
if before_count > 1:
|
|
217
|
+
raise ValueError("Only one alias can have the value 'before' for positional argument collection.")
|
|
218
|
+
elif config == "after":
|
|
219
|
+
after_count += 1
|
|
220
|
+
if after_count > 1:
|
|
221
|
+
raise ValueError("Only one alias can have the value 'after' for positional argument collection.")
|
|
222
|
+
positional_configs[alias] = config
|
|
223
|
+
results[alias] = {"exists": False, "value": []}
|
|
224
|
+
elif isinstance(config, (list, tuple)):
|
|
191
225
|
flags = config
|
|
226
|
+
results[alias] = {"exists": False, "value": default_value}
|
|
192
227
|
elif isinstance(config, dict):
|
|
193
228
|
if "flags" not in config:
|
|
194
229
|
raise ValueError(f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'flags' key.")
|
|
@@ -196,15 +231,54 @@ class Console:
|
|
|
196
231
|
default_value = config.get("default")
|
|
197
232
|
if not isinstance(flags, (list, tuple)):
|
|
198
233
|
raise ValueError(f"Invalid 'flags' for alias '{alias}'. Must be a list or tuple.")
|
|
234
|
+
results[alias] = {"exists": False, "value": default_value}
|
|
199
235
|
else:
|
|
200
|
-
raise TypeError(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
236
|
+
raise TypeError(
|
|
237
|
+
f"Invalid configuration type for alias '{alias}'. Must be a list, tuple, dict or literal 'before' / 'after'."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# BUILD FLAG LOOKUP FOR NON-POSITIONAL ARGUMENTS
|
|
241
|
+
if flags is not None:
|
|
242
|
+
for flag in flags:
|
|
243
|
+
if flag in arg_lookup:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Duplicate flag '{flag}' found. It's assigned to both '{arg_lookup[flag]}' and '{alias}'."
|
|
246
|
+
)
|
|
247
|
+
arg_lookup[flag] = alias
|
|
248
|
+
|
|
249
|
+
# FIND POSITIONS OF FIRST AND LAST FLAGS FOR POSITIONAL ARGUMENT COLLECTION
|
|
250
|
+
first_flag_pos = None
|
|
251
|
+
last_flag_with_value_pos = None
|
|
252
|
+
|
|
253
|
+
for i, arg in enumerate(args):
|
|
254
|
+
if arg in arg_lookup:
|
|
255
|
+
if first_flag_pos is None:
|
|
256
|
+
first_flag_pos = i
|
|
257
|
+
# CHECK IF THIS FLAG HAS A VALUE FOLLOWING IT
|
|
258
|
+
flag_has_value = (i + 1 < args_len and not args[i + 1].startswith("-") and args[i + 1] not in arg_lookup)
|
|
259
|
+
if flag_has_value:
|
|
260
|
+
if not allow_spaces:
|
|
261
|
+
last_flag_with_value_pos = i + 1
|
|
262
|
+
else:
|
|
263
|
+
# FIND THE END OF THE MULTI-WORD VALUE
|
|
264
|
+
j = i + 1
|
|
265
|
+
while j < args_len and not args[j].startswith("-") and args[j] not in arg_lookup:
|
|
266
|
+
j += 1
|
|
267
|
+
last_flag_with_value_pos = j - 1
|
|
268
|
+
|
|
269
|
+
# COLLECT "before" POSITIONAL ARGUMENTS
|
|
270
|
+
for alias, pos_type in positional_configs.items():
|
|
271
|
+
if pos_type == "before":
|
|
272
|
+
before_args = []
|
|
273
|
+
end_pos = first_flag_pos if first_flag_pos is not None else args_len
|
|
274
|
+
for i in range(end_pos):
|
|
275
|
+
if not args[i].startswith("-"):
|
|
276
|
+
before_args.append(String.to_type(args[i]))
|
|
277
|
+
if before_args:
|
|
278
|
+
results[alias]["value"] = before_args
|
|
279
|
+
results[alias]["exists"] = len(before_args) > 0
|
|
280
|
+
|
|
281
|
+
# PROCESS FLAGGED ARGUMENTS
|
|
208
282
|
i = 0
|
|
209
283
|
while i < args_len:
|
|
210
284
|
arg = args[i]
|
|
@@ -230,19 +304,43 @@ class Console:
|
|
|
230
304
|
if not value_found_after_flag:
|
|
231
305
|
results[alias]["value"] = True
|
|
232
306
|
i += 1
|
|
307
|
+
|
|
308
|
+
# COLLECT "after" POSITIONAL ARGUMENTS
|
|
309
|
+
for alias, pos_type in positional_configs.items():
|
|
310
|
+
if pos_type == "after":
|
|
311
|
+
after_args = []
|
|
312
|
+
start_pos = (last_flag_with_value_pos + 1) if last_flag_with_value_pos is not None else 0
|
|
313
|
+
# IF NO FLAGS WERE FOUND WITH VALUES, START AFTER THE LAST FLAG
|
|
314
|
+
if last_flag_with_value_pos is None and first_flag_pos is not None:
|
|
315
|
+
# FIND THE LAST FLAG POSITION
|
|
316
|
+
last_flag_pos = None
|
|
317
|
+
for i, arg in enumerate(args):
|
|
318
|
+
if arg in arg_lookup:
|
|
319
|
+
last_flag_pos = i
|
|
320
|
+
if last_flag_pos is not None:
|
|
321
|
+
start_pos = last_flag_pos + 1
|
|
322
|
+
|
|
323
|
+
for i in range(start_pos, args_len):
|
|
324
|
+
if not args[i].startswith("-") and args[i] not in arg_lookup:
|
|
325
|
+
after_args.append(String.to_type(args[i]))
|
|
326
|
+
|
|
327
|
+
if after_args:
|
|
328
|
+
results[alias]["value"] = after_args
|
|
329
|
+
results[alias]["exists"] = len(after_args) > 0
|
|
330
|
+
|
|
233
331
|
return Args(**results)
|
|
234
332
|
|
|
235
333
|
@staticmethod
|
|
236
334
|
def pause_exit(
|
|
237
|
-
pause: bool =
|
|
335
|
+
pause: bool = True,
|
|
238
336
|
exit: bool = False,
|
|
239
337
|
prompt: object = "",
|
|
240
338
|
exit_code: int = 0,
|
|
241
339
|
reset_ansi: bool = False,
|
|
242
340
|
) -> None:
|
|
243
|
-
"""Will print the `prompt` and then pause the program if `pause` is
|
|
244
|
-
|
|
245
|
-
print(prompt, end="", flush=True)
|
|
341
|
+
"""Will print the `prompt` and then pause the program if `pause` is
|
|
342
|
+
true and after the pause, exit the program if `exit` is set true."""
|
|
343
|
+
FormatCodes.print(prompt, end="", flush=True)
|
|
246
344
|
if reset_ansi:
|
|
247
345
|
FormatCodes.print("[_]", end="")
|
|
248
346
|
if pause:
|
|
@@ -268,9 +366,12 @@ class Console:
|
|
|
268
366
|
end: str = "\n",
|
|
269
367
|
title_bg_color: Optional[Rgba | Hexa] = None,
|
|
270
368
|
default_color: Optional[Rgba | Hexa] = None,
|
|
271
|
-
|
|
369
|
+
tab_size: int = 8,
|
|
370
|
+
title_px: int = 1,
|
|
371
|
+
title_mx: int = 2,
|
|
272
372
|
) -> None:
|
|
273
|
-
"""
|
|
373
|
+
"""Prints a nicely formatted log message.\n
|
|
374
|
+
-------------------------------------------------------------------------------------------
|
|
274
375
|
- `title` -⠀the title of the log message (e.g. `DEBUG`, `WARN`, `FAIL`, etc.)
|
|
275
376
|
- `prompt` -⠀the log message
|
|
276
377
|
- `format_linebreaks` -⠀whether to format (indent after) the line breaks or not
|
|
@@ -278,38 +379,40 @@ class Console:
|
|
|
278
379
|
- `end` -⠀something to print after the log is printed (e.g. `\\n`)
|
|
279
380
|
- `title_bg_color` -⠀the background color of the `title`
|
|
280
381
|
- `default_color` -⠀the default text color of the `prompt`
|
|
281
|
-
- `
|
|
282
|
-
|
|
382
|
+
- `tab_size` -⠀the tab size used for the log (default is 8 like console tabs)
|
|
383
|
+
- `title_px` -⠀the horizontal padding (in chars) to the title (if `title_bg_color` is set)
|
|
384
|
+
- `title_mx` -⠀the horizontal margin (in chars) to the title\n
|
|
385
|
+
-------------------------------------------------------------------------------------------
|
|
283
386
|
The log message can be formatted with special formatting codes. For more detailed
|
|
284
|
-
information about formatting codes, see `
|
|
387
|
+
information about formatting codes, see `format_codes` module documentation."""
|
|
388
|
+
has_title_bg = title_bg_color is not None and Color.is_valid(title_bg_color)
|
|
285
389
|
title = "" if title is None else title.strip().upper()
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
title_color = "_color" if title_bg_color is None else "#000"
|
|
390
|
+
title_fg = Color.text_color_for_on_bg(
|
|
391
|
+
Color.to_hexa(title_bg_color) # type: ignore[assignment]
|
|
392
|
+
) if has_title_bg else "_color"
|
|
393
|
+
px, mx = (" " * title_px) if has_title_bg else "", " " * title_mx
|
|
394
|
+
tab = " " * (tab_size - 1 - ((len(mx) + (title_len := len(title) + 2 * len(px))) % tab_size))
|
|
292
395
|
if format_linebreaks:
|
|
293
396
|
clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True, _ignore_linebreaks=True)
|
|
294
|
-
prompt_lst = (
|
|
397
|
+
prompt_lst = (
|
|
398
|
+
String.split_count(l, Console.w - (title_len + len(tab) + 2 * len(mx))) for l in str(clean_prompt).splitlines()
|
|
399
|
+
)
|
|
295
400
|
prompt_lst = (
|
|
296
401
|
item for lst in prompt_lst for item in ([""] if lst == [] else (lst if isinstance(lst, list) else [lst]))
|
|
297
402
|
)
|
|
298
|
-
prompt = f"\n{' ' * title_len}
|
|
403
|
+
prompt = f"\n{mx}{' ' * title_len}{mx}{tab}".join(
|
|
299
404
|
Console.__add_back_removed_parts(list(prompt_lst), cast(tuple[tuple[int, str], ...], removals))
|
|
300
405
|
)
|
|
301
|
-
else:
|
|
302
|
-
prompt = str(prompt)
|
|
303
406
|
if title == "":
|
|
304
407
|
FormatCodes.print(
|
|
305
|
-
f'{start} {f"[{default_color}]" if default_color else ""}{
|
|
408
|
+
f'{start} {f"[{default_color}]" if default_color else ""}{prompt}[_]',
|
|
306
409
|
default_color=default_color,
|
|
307
410
|
end=end,
|
|
308
411
|
)
|
|
309
412
|
else:
|
|
310
413
|
FormatCodes.print(
|
|
311
|
-
f
|
|
312
|
-
+ f
|
|
414
|
+
f"{start}{mx}[bold][{title_fg}]{f'[BG:{title_bg_color}]' if title_bg_color else ''}{px}{title}{px}[_]{mx}"
|
|
415
|
+
+ f"{tab}{f'[{default_color}]' if default_color else ''}{prompt}[_]",
|
|
313
416
|
default_color=default_color,
|
|
314
417
|
end=end,
|
|
315
418
|
)
|
|
@@ -342,7 +445,7 @@ class Console:
|
|
|
342
445
|
i = find_string_part(pos)
|
|
343
446
|
adjusted_pos = (pos - cumulative_pos[i]) + offset_adjusts[i]
|
|
344
447
|
parts = [result[i][:adjusted_pos], removal, result[i][adjusted_pos:]]
|
|
345
|
-
result[i] =
|
|
448
|
+
result[i] = "".join(parts)
|
|
346
449
|
offset_adjusts[i] += len(removal)
|
|
347
450
|
return result
|
|
348
451
|
|
|
@@ -353,7 +456,7 @@ class Console:
|
|
|
353
456
|
format_linebreaks: bool = True,
|
|
354
457
|
start: str = "",
|
|
355
458
|
end: str = "\n",
|
|
356
|
-
default_color: Optional[Rgba | Hexa] =
|
|
459
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
357
460
|
pause: bool = False,
|
|
358
461
|
exit: bool = False,
|
|
359
462
|
) -> None:
|
|
@@ -361,7 +464,7 @@ class Console:
|
|
|
361
464
|
at the message and exit the program after the message was printed.
|
|
362
465
|
If `active` is false, no debug message will be printed."""
|
|
363
466
|
if active:
|
|
364
|
-
Console.log("DEBUG", prompt, format_linebreaks, start, end, COLOR.
|
|
467
|
+
Console.log("DEBUG", prompt, format_linebreaks, start, end, COLOR.YELLOW, default_color)
|
|
365
468
|
Console.pause_exit(pause, exit)
|
|
366
469
|
|
|
367
470
|
@staticmethod
|
|
@@ -370,13 +473,13 @@ class Console:
|
|
|
370
473
|
format_linebreaks: bool = True,
|
|
371
474
|
start: str = "",
|
|
372
475
|
end: str = "\n",
|
|
373
|
-
default_color: Optional[Rgba | Hexa] =
|
|
476
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
374
477
|
pause: bool = False,
|
|
375
478
|
exit: bool = False,
|
|
376
479
|
) -> None:
|
|
377
480
|
"""A preset for `log()`: `INFO` log message with the options to pause
|
|
378
481
|
at the message and exit the program after the message was printed."""
|
|
379
|
-
Console.log("INFO", prompt, format_linebreaks, start, end, COLOR.
|
|
482
|
+
Console.log("INFO", prompt, format_linebreaks, start, end, COLOR.BLUE, default_color)
|
|
380
483
|
Console.pause_exit(pause, exit)
|
|
381
484
|
|
|
382
485
|
@staticmethod
|
|
@@ -385,13 +488,13 @@ class Console:
|
|
|
385
488
|
format_linebreaks: bool = True,
|
|
386
489
|
start: str = "",
|
|
387
490
|
end: str = "\n",
|
|
388
|
-
default_color: Optional[Rgba | Hexa] =
|
|
491
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
389
492
|
pause: bool = False,
|
|
390
493
|
exit: bool = False,
|
|
391
494
|
) -> None:
|
|
392
495
|
"""A preset for `log()`: `DONE` log message with the options to pause
|
|
393
496
|
at the message and exit the program after the message was printed."""
|
|
394
|
-
Console.log("DONE", prompt, format_linebreaks, start, end, COLOR.
|
|
497
|
+
Console.log("DONE", prompt, format_linebreaks, start, end, COLOR.TEAL, default_color)
|
|
395
498
|
Console.pause_exit(pause, exit)
|
|
396
499
|
|
|
397
500
|
@staticmethod
|
|
@@ -400,13 +503,13 @@ class Console:
|
|
|
400
503
|
format_linebreaks: bool = True,
|
|
401
504
|
start: str = "",
|
|
402
505
|
end: str = "\n",
|
|
403
|
-
default_color: Optional[Rgba | Hexa] =
|
|
506
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
404
507
|
pause: bool = False,
|
|
405
508
|
exit: bool = False,
|
|
406
509
|
) -> None:
|
|
407
510
|
"""A preset for `log()`: `WARN` log message with the options to pause
|
|
408
511
|
at the message and exit the program after the message was printed."""
|
|
409
|
-
Console.log("WARN", prompt, format_linebreaks, start, end, COLOR.
|
|
512
|
+
Console.log("WARN", prompt, format_linebreaks, start, end, COLOR.ORANGE, default_color)
|
|
410
513
|
Console.pause_exit(pause, exit)
|
|
411
514
|
|
|
412
515
|
@staticmethod
|
|
@@ -415,14 +518,14 @@ class Console:
|
|
|
415
518
|
format_linebreaks: bool = True,
|
|
416
519
|
start: str = "",
|
|
417
520
|
end: str = "\n",
|
|
418
|
-
default_color: Optional[Rgba | Hexa] =
|
|
521
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
419
522
|
pause: bool = False,
|
|
420
523
|
exit: bool = True,
|
|
421
524
|
reset_ansi: bool = True,
|
|
422
525
|
) -> None:
|
|
423
526
|
"""A preset for `log()`: `FAIL` log message with the options to pause
|
|
424
527
|
at the message and exit the program after the message was printed."""
|
|
425
|
-
Console.log("FAIL", prompt, format_linebreaks, start, end, COLOR.
|
|
528
|
+
Console.log("FAIL", prompt, format_linebreaks, start, end, COLOR.RED, default_color)
|
|
426
529
|
Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
|
|
427
530
|
|
|
428
531
|
@staticmethod
|
|
@@ -431,14 +534,14 @@ class Console:
|
|
|
431
534
|
format_linebreaks: bool = True,
|
|
432
535
|
start: str = "",
|
|
433
536
|
end: str = "\n",
|
|
434
|
-
default_color: Optional[Rgba | Hexa] =
|
|
537
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
435
538
|
pause: bool = False,
|
|
436
539
|
exit: bool = True,
|
|
437
540
|
reset_ansi: bool = True,
|
|
438
541
|
) -> None:
|
|
439
542
|
"""A preset for `log()`: `EXIT` log message with the options to pause
|
|
440
543
|
at the message and exit the program after the message was printed."""
|
|
441
|
-
Console.log("EXIT", prompt, format_linebreaks, start, end, COLOR.
|
|
544
|
+
Console.log("EXIT", prompt, format_linebreaks, start, end, COLOR.MAGENTA, default_color)
|
|
442
545
|
Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
|
|
443
546
|
|
|
444
547
|
@staticmethod
|
|
@@ -463,7 +566,7 @@ class Console:
|
|
|
463
566
|
- `indent` -⠀the indentation of the box (in chars)\n
|
|
464
567
|
-----------------------------------------------------------------------------------
|
|
465
568
|
The box content can be formatted with special formatting codes. For more detailed
|
|
466
|
-
information about formatting codes, see `
|
|
569
|
+
information about formatting codes, see `format_codes` module documentation."""
|
|
467
570
|
lines, unfmt_lines, max_line_len = Console.__prepare_log_box(values, default_color)
|
|
468
571
|
pad_w_full = (Console.w - (max_line_len + (2 * w_padding))) if w_full else 0
|
|
469
572
|
if box_bg_color is not None and Color.is_valid(box_bg_color):
|
|
@@ -489,7 +592,7 @@ class Console:
|
|
|
489
592
|
start: str = "",
|
|
490
593
|
end: str = "\n",
|
|
491
594
|
border_type: Literal["standard", "rounded", "strong", "double"] = "rounded",
|
|
492
|
-
border_style: str | Rgba | Hexa = f"dim|{COLOR.
|
|
595
|
+
border_style: str | Rgba | Hexa = f"dim|{COLOR.GRAY}",
|
|
493
596
|
default_color: Optional[Rgba | Hexa] = None,
|
|
494
597
|
w_padding: int = 1,
|
|
495
598
|
w_full: bool = False,
|
|
@@ -509,7 +612,7 @@ class Console:
|
|
|
509
612
|
- `_border_chars` -⠀define your own border characters set (overwrites `border_type`)\n
|
|
510
613
|
---------------------------------------------------------------------------------------
|
|
511
614
|
The box content can be formatted with special formatting codes. For more detailed
|
|
512
|
-
information about formatting codes, see `
|
|
615
|
+
information about formatting codes, see `format_codes` module documentation.\n
|
|
513
616
|
---------------------------------------------------------------------------------------
|
|
514
617
|
The `border_type` can be one of the following:
|
|
515
618
|
- `"standard" = ('┌', '─', '┐', '│', '┘', '─', '└', '│')`
|
|
@@ -567,22 +670,28 @@ class Console:
|
|
|
567
670
|
def confirm(
|
|
568
671
|
prompt: object = "Do you want to continue?",
|
|
569
672
|
start="",
|
|
570
|
-
end="
|
|
571
|
-
default_color: Optional[Rgba | Hexa] =
|
|
673
|
+
end="",
|
|
674
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
572
675
|
default_is_yes: bool = True,
|
|
573
676
|
) -> bool:
|
|
574
677
|
"""Ask a yes/no question.\n
|
|
575
678
|
---------------------------------------------------------------------------------------
|
|
679
|
+
- `prompt` -⠀the input prompt
|
|
680
|
+
- `start` -⠀something to print before the input
|
|
681
|
+
- `end` -⠀something to print after the input (e.g. `\\n`)
|
|
682
|
+
- `default_color` -⠀the default text color of the `prompt`
|
|
683
|
+
- `default_is_yes` -⠀the default answer if the user just presses enter
|
|
684
|
+
---------------------------------------------------------------------------------------
|
|
576
685
|
The prompt can be formatted with special formatting codes. For more detailed
|
|
577
|
-
information about formatting codes, see the `
|
|
686
|
+
information about formatting codes, see the `format_codes` module documentation."""
|
|
578
687
|
confirmed = input(
|
|
579
688
|
FormatCodes.to_ansi(
|
|
580
|
-
f'{start}
|
|
689
|
+
f'{start}{str(prompt)} [_|dim](({"Y" if default_is_yes else "y"}/{"n" if default_is_yes else "N"}): )',
|
|
581
690
|
default_color=default_color,
|
|
582
691
|
)
|
|
583
692
|
).strip().lower() in (("", "y", "yes") if default_is_yes else ("y", "yes"))
|
|
584
693
|
if end:
|
|
585
|
-
|
|
694
|
+
FormatCodes.print(end, end="")
|
|
586
695
|
return confirmed
|
|
587
696
|
|
|
588
697
|
@staticmethod
|
|
@@ -590,13 +699,13 @@ class Console:
|
|
|
590
699
|
prompt: object = "",
|
|
591
700
|
start="",
|
|
592
701
|
end="\n",
|
|
593
|
-
default_color: Optional[Rgba | Hexa] =
|
|
702
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
594
703
|
show_keybindings=True,
|
|
595
704
|
input_prefix=" ⮡ ",
|
|
596
705
|
reset_ansi=True,
|
|
597
706
|
) -> str:
|
|
598
|
-
"""An input where users can
|
|
599
|
-
|
|
707
|
+
"""An input where users can write (and paste) text over multiple lines.\n
|
|
708
|
+
---------------------------------------------------------------------------------------
|
|
600
709
|
- `prompt` -⠀the input prompt
|
|
601
710
|
- `start` -⠀something to print before the input
|
|
602
711
|
- `end` -⠀something to print after the input (e.g. `\\n`)
|
|
@@ -604,9 +713,9 @@ class Console:
|
|
|
604
713
|
- `show_keybindings` -⠀whether to show the special keybindings or not
|
|
605
714
|
- `input_prefix` -⠀the prefix of the input line
|
|
606
715
|
- `reset_ansi` -⠀whether to reset the ANSI codes after the input or not
|
|
607
|
-
|
|
716
|
+
---------------------------------------------------------------------------------------
|
|
608
717
|
The input prompt can be formatted with special formatting codes. For more detailed
|
|
609
|
-
information about formatting codes, see `
|
|
718
|
+
information about formatting codes, see the `format_codes` module documentation."""
|
|
610
719
|
kb = KeyBindings()
|
|
611
720
|
|
|
612
721
|
@kb.add("c-d", eager=True) # CTRL+D
|
|
@@ -616,135 +725,184 @@ class Console:
|
|
|
616
725
|
FormatCodes.print(start + str(prompt), default_color=default_color)
|
|
617
726
|
if show_keybindings:
|
|
618
727
|
FormatCodes.print("[dim][[b](CTRL+D)[dim] : end of input][_dim]")
|
|
619
|
-
input_string =
|
|
728
|
+
input_string = _pt.prompt(input_prefix, multiline=True, wrap_lines=True, key_bindings=kb)
|
|
620
729
|
FormatCodes.print("[_]" if reset_ansi else "", end=end[1:] if end.startswith("\n") else end)
|
|
621
730
|
return input_string
|
|
622
731
|
|
|
623
732
|
@staticmethod
|
|
624
|
-
def
|
|
733
|
+
def input(
|
|
625
734
|
prompt: object = "",
|
|
626
735
|
start="",
|
|
627
|
-
end="
|
|
628
|
-
default_color: Optional[Rgba | Hexa] =
|
|
629
|
-
|
|
736
|
+
end="",
|
|
737
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
738
|
+
placeholder: Optional[str] = None,
|
|
739
|
+
mask_char: Optional[str] = None,
|
|
630
740
|
min_len: Optional[int] = None,
|
|
631
741
|
max_len: Optional[int] = None,
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
-
|
|
639
|
-
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
742
|
+
allowed_chars: str = CHARS.ALL, #type: ignore[assignment]
|
|
743
|
+
allow_paste: bool = True,
|
|
744
|
+
validator: Optional[Callable[[str], Optional[str]]] = None,
|
|
745
|
+
) -> str:
|
|
746
|
+
"""Acts like a standard Python `input()` a bunch of cool extra features.\n
|
|
747
|
+
------------------------------------------------------------------------------------
|
|
748
|
+
- `prompt` -⠀the input prompt
|
|
749
|
+
- `start` -⠀something to print before the input
|
|
750
|
+
- `end` -⠀something to print after the input (e.g. `\\n`)
|
|
751
|
+
- `default_color` -⠀the default text color of the `prompt`
|
|
752
|
+
- `placeholder` -⠀a placeholder text that is shown when the input is empty
|
|
753
|
+
- `mask_char` -⠀if set, the input will be masked with this character
|
|
754
|
+
- `min_len` -⠀the minimum length of the input (required to submit)
|
|
755
|
+
- `max_len` -⠀the maximum length of the input (can't write further if reached)
|
|
756
|
+
- `allowed_chars` -⠀a string of characters that are allowed to be inputted
|
|
757
|
+
(default allows all characters)
|
|
758
|
+
- `allow_paste` -⠀whether to allow pasting text into the input or not
|
|
759
|
+
- `validator` -⠀a function that takes the input string and returns a string error
|
|
760
|
+
message if invalid, or nothing if valid
|
|
761
|
+
------------------------------------------------------------------------------------
|
|
762
|
+
The input prompt can be formatted with special formatting codes. For more detailed
|
|
763
|
+
information about formatting codes, see the `format_codes` module documentation."""
|
|
764
|
+
result_text = ""
|
|
765
|
+
tried_pasting = False
|
|
766
|
+
filtered_chars = set()
|
|
767
|
+
|
|
768
|
+
class InputValidator(Validator):
|
|
769
|
+
|
|
770
|
+
def validate(self, document) -> None:
|
|
771
|
+
text_to_validate = result_text if mask_char else document.text
|
|
772
|
+
if min_len and len(text_to_validate) < min_len:
|
|
773
|
+
raise ValidationError(message="", cursor_position=len(document.text))
|
|
774
|
+
if validator and validator(text_to_validate) not in ("", None):
|
|
775
|
+
raise ValidationError(message="", cursor_position=len(document.text))
|
|
776
|
+
|
|
777
|
+
def bottom_toolbar() -> _pt.formatted_text.ANSI:
|
|
778
|
+
nonlocal tried_pasting
|
|
779
|
+
try:
|
|
780
|
+
if mask_char:
|
|
781
|
+
text_to_check = result_text
|
|
782
|
+
else:
|
|
783
|
+
app = _pt.application.get_app()
|
|
784
|
+
text_to_check = app.current_buffer.text
|
|
785
|
+
toolbar_msgs = []
|
|
786
|
+
if max_len and len(text_to_check) > max_len:
|
|
787
|
+
toolbar_msgs.append("[b|#FFF|bg:red]( Text too long! )")
|
|
788
|
+
if validator and text_to_check and (validation_error_msg := validator(text_to_check)) not in ("", None):
|
|
789
|
+
toolbar_msgs.append(f"[b|#000|bg:br:red] {validation_error_msg} [_bg]")
|
|
790
|
+
if filtered_chars:
|
|
791
|
+
plural = "" if len(char_list := "".join(sorted(filtered_chars))) == 1 else "s"
|
|
792
|
+
toolbar_msgs.append(f"[b|#000|bg:yellow]( Char{plural} '{char_list}' not allowed )")
|
|
793
|
+
filtered_chars.clear()
|
|
794
|
+
if min_len and len(text_to_check) < min_len:
|
|
795
|
+
toolbar_msgs.append(f"[b|#000|bg:yellow]( Need {min_len - len(text_to_check)} more chars )")
|
|
796
|
+
if tried_pasting:
|
|
797
|
+
toolbar_msgs.append("[b|#000|bg:br:yellow]( Pasting disabled )")
|
|
798
|
+
tried_pasting = False
|
|
799
|
+
if max_len and len(text_to_check) == max_len:
|
|
800
|
+
toolbar_msgs.append("[b|#000|bg:br:yellow]( Maximum length reached )")
|
|
801
|
+
return _pt.formatted_text.ANSI(FormatCodes.to_ansi(" ".join(toolbar_msgs)))
|
|
802
|
+
except Exception:
|
|
803
|
+
return _pt.formatted_text.ANSI("")
|
|
804
|
+
|
|
805
|
+
def process_insert_text(text: str) -> tuple[str, set[str]]:
|
|
806
|
+
removed_chars = set()
|
|
807
|
+
if not text: return "", removed_chars
|
|
808
|
+
processed_text = "".join(c for c in text if ord(c) >= 32)
|
|
809
|
+
if allowed_chars != CHARS.ALL:
|
|
810
|
+
filtered_text = ""
|
|
811
|
+
for char in processed_text:
|
|
812
|
+
if char in allowed_chars:
|
|
813
|
+
filtered_text += char
|
|
814
|
+
else:
|
|
815
|
+
removed_chars.add(char)
|
|
816
|
+
processed_text = filtered_text
|
|
817
|
+
if max_len:
|
|
818
|
+
if (remaining_space := max_len - len(result_text)) > 0:
|
|
819
|
+
if len(processed_text) > remaining_space:
|
|
820
|
+
processed_text = processed_text[:remaining_space]
|
|
821
|
+
else:
|
|
822
|
+
processed_text = ""
|
|
823
|
+
return processed_text, removed_chars
|
|
824
|
+
|
|
825
|
+
def insert_text_event(event: KeyPressEvent) -> None:
|
|
826
|
+
nonlocal result_text, filtered_chars
|
|
827
|
+
try:
|
|
828
|
+
insert_text = event.data
|
|
829
|
+
if not insert_text: return
|
|
830
|
+
buffer = event.app.current_buffer
|
|
831
|
+
cursor_pos = buffer.cursor_position
|
|
832
|
+
insert_text, filtered_chars = process_insert_text(insert_text)
|
|
833
|
+
if insert_text:
|
|
834
|
+
result_text = result_text[:cursor_pos] + insert_text + result_text[cursor_pos:]
|
|
835
|
+
if mask_char:
|
|
836
|
+
buffer.insert_text(mask_char[0] * len(insert_text))
|
|
837
|
+
else:
|
|
838
|
+
buffer.insert_text(insert_text)
|
|
839
|
+
except Exception:
|
|
840
|
+
pass
|
|
841
|
+
|
|
842
|
+
def remove_text_event(event: KeyPressEvent, is_backspace: bool = False) -> None:
|
|
843
|
+
nonlocal result_text
|
|
844
|
+
try:
|
|
845
|
+
buffer = event.app.current_buffer
|
|
846
|
+
cursor_pos = buffer.cursor_position
|
|
847
|
+
has_selection = buffer.selection_state is not None
|
|
848
|
+
if has_selection:
|
|
849
|
+
start, end = buffer.document.selection_range()
|
|
850
|
+
result_text = result_text[:start] + result_text[end:]
|
|
851
|
+
buffer.cursor_position = start
|
|
852
|
+
buffer.delete(end - start)
|
|
723
853
|
else:
|
|
724
|
-
|
|
725
|
-
|
|
854
|
+
if is_backspace:
|
|
855
|
+
if cursor_pos > 0:
|
|
856
|
+
result_text = result_text[:cursor_pos - 1] + result_text[cursor_pos:]
|
|
857
|
+
buffer.delete_before_cursor(1)
|
|
858
|
+
else:
|
|
859
|
+
if cursor_pos < len(result_text):
|
|
860
|
+
result_text = result_text[:cursor_pos] + result_text[cursor_pos + 1:]
|
|
861
|
+
buffer.delete(1)
|
|
862
|
+
except Exception:
|
|
863
|
+
pass
|
|
726
864
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
865
|
+
kb = KeyBindings()
|
|
866
|
+
|
|
867
|
+
@kb.add(Keys.Delete)
|
|
868
|
+
def _(event: KeyPressEvent) -> None:
|
|
869
|
+
remove_text_event(event)
|
|
870
|
+
|
|
871
|
+
@kb.add(Keys.Backspace)
|
|
872
|
+
def _(event: KeyPressEvent) -> None:
|
|
873
|
+
remove_text_event(event, is_backspace=True)
|
|
874
|
+
|
|
875
|
+
@kb.add(Keys.ControlA)
|
|
876
|
+
def _(event: KeyPressEvent) -> None:
|
|
877
|
+
buffer = event.app.current_buffer
|
|
878
|
+
buffer.cursor_position = 0
|
|
879
|
+
buffer.start_selection()
|
|
880
|
+
buffer.cursor_position = len(buffer.text)
|
|
881
|
+
|
|
882
|
+
@kb.add(Keys.BracketedPaste)
|
|
883
|
+
def _(event: KeyPressEvent) -> None:
|
|
884
|
+
if allow_paste:
|
|
885
|
+
insert_text_event(event)
|
|
886
|
+
else:
|
|
887
|
+
nonlocal tried_pasting
|
|
888
|
+
tried_pasting = True
|
|
889
|
+
|
|
890
|
+
@kb.add(Keys.Any)
|
|
891
|
+
def _(event: KeyPressEvent) -> None:
|
|
892
|
+
insert_text_event(event)
|
|
893
|
+
|
|
894
|
+
custom_style = Style.from_dict({'bottom-toolbar': 'noreverse'})
|
|
895
|
+
session = _pt.PromptSession(
|
|
896
|
+
message=_pt.formatted_text.ANSI(FormatCodes.to_ansi(str(prompt), default_color=default_color)),
|
|
897
|
+
validator=InputValidator(),
|
|
898
|
+
validate_while_typing=True,
|
|
899
|
+
key_bindings=kb,
|
|
900
|
+
bottom_toolbar=bottom_toolbar,
|
|
901
|
+
placeholder=_pt.formatted_text.ANSI(FormatCodes.to_ansi(f"[i|br:black]{placeholder}[_i|_c]"))
|
|
902
|
+
if placeholder else "",
|
|
903
|
+
style=custom_style,
|
|
750
904
|
)
|
|
905
|
+
FormatCodes.print(start, end="")
|
|
906
|
+
session.prompt()
|
|
907
|
+
FormatCodes.print(end, end="")
|
|
908
|
+
return result_text
|