xulbux 1.8.0__py3-none-any.whl → 1.8.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.

Potentially problematic release.


This version of xulbux might be problematic. Click here for more details.

xulbux/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.8.0"
1
+ __version__ = "1.8.2"
2
2
 
3
3
  __author__ = "XulbuX"
4
4
  __email__ = "xulbux.real@gmail.com"
@@ -7,11 +7,26 @@ __copyright__ = "Copyright (c) 2024 XulbuX"
7
7
  __url__ = "https://github.com/XulbuX/PythonLibraryXulbuX"
8
8
  __description__ = "A Python library which includes lots of helpful classes, types, and functions aiming to make common programming tasks simpler."
9
9
 
10
- __all__ = ["Code", "Color", "Console", "Data", "EnvPath", "File", "FormatCodes", "Json", "Path", "Regex", "String", "System"]
10
+ __all__ = [
11
+ "Code",
12
+ "Color",
13
+ "Console",
14
+ "Data",
15
+ "EnvPath",
16
+ "File",
17
+ "FormatCodes",
18
+ "Json",
19
+ "Path",
20
+ "ProgressBar",
21
+ "Regex",
22
+ "String",
23
+ "System",
24
+ ]
11
25
 
12
26
  from .code import Code
13
27
  from .color import Color
14
28
  from .console import Console
29
+ from .console import ProgressBar
15
30
  from .data import Data
16
31
  from .env_path import EnvPath
17
32
  from .file import File
xulbux/base/consts.py ADDED
@@ -0,0 +1,173 @@
1
+ from typing import TypeAlias
2
+
3
+
4
+ FormattableString: TypeAlias = str
5
+ """A `str` object that is made to be formatted with the `.format()` method."""
6
+
7
+
8
+ class COLOR:
9
+ """Hexa color presets."""
10
+
11
+ WHITE = "#F1F2FF"
12
+ LIGHT_GRAY = "#B6B7C0"
13
+ GRAY = "#7B7C8D"
14
+ DARK_GRAY = "#67686C"
15
+ BLACK = "#202125"
16
+ RED = "#FF606A"
17
+ CORAL = "#FF7069"
18
+ ORANGE = "#FF876A"
19
+ TANGERINE = "#FF9962"
20
+ GOLD = "#FFAF60"
21
+ YELLOW = "#FFD260"
22
+ LIME = "#C9F16E"
23
+ GREEN = "#7EE787"
24
+ NEON_GREEN = "#4CFF85"
25
+ TEAL = "#50EAAF"
26
+ CYAN = "#3EDEE6"
27
+ ICE = "#77DBEF"
28
+ LIGHT_BLUE = "#60AAFF"
29
+ BLUE = "#8085FF"
30
+ LAVENDER = "#9B7DFF"
31
+ PURPLE = "#AD68FF"
32
+ MAGENTA = "#C860FF"
33
+ PINK = "#F162EF"
34
+ ROSE = "#FF609F"
35
+
36
+
37
+ class CHARS:
38
+ """Text character sets."""
39
+
40
+ class _AllTextChars:
41
+ pass
42
+
43
+ ALL = _AllTextChars
44
+ """Code to signal that all characters are allowed."""
45
+
46
+ DIGITS = "0123456789"
47
+ """Digits: `0`-`9`"""
48
+ FLOAT_DIGITS = DIGITS + "."
49
+ """Digits: `0`-`9` with decimal point `.`"""
50
+ HEX_DIGITS = DIGITS + "#abcdefABCDEF"
51
+ """Digits: `0`-`9` Letters: `a`-`f` `A`-`F` and a hashtag `#`"""
52
+
53
+ LOWERCASE = "abcdefghijklmnopqrstuvwxyz"
54
+ """Lowercase letters `a`-`z`"""
55
+ LOWERCASE_EXTENDED = LOWERCASE + "äëïöüÿàèìòùáéíóúýâêîôûãñõåæç"
56
+ """Lowercase letters `a`-`z` with all lowercase diacritic letters."""
57
+ UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
58
+ """Uppercase letters `A`-`Z`"""
59
+ UPPERCASE_EXTENDED = UPPERCASE + "ÄËÏÖÜÀÈÌÒÙÁÉÍÓÚÝÂÊÎÔÛÃÑÕÅÆÇß"
60
+ """Uppercase letters `A`-`Z` with all uppercase diacritic letters."""
61
+
62
+ LETTERS = LOWERCASE + UPPERCASE
63
+ """Lowercase and uppercase letters `a`-`z` and `A`-`Z`"""
64
+ LETTERS_EXTENDED = LOWERCASE_EXTENDED + UPPERCASE_EXTENDED
65
+ """Lowercase and uppercase letters `a`-`z` `A`-`Z` and all diacritic letters."""
66
+
67
+ SPECIAL_ASCII = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
68
+ """All ASCII special characters."""
69
+ SPECIAL_ASCII_EXTENDED = SPECIAL_ASCII + "ø£Ø×ƒªº¿®¬½¼¡«»░▒▓│┤©╣║╗╝¢¥┐└┴┬├─┼╚╔╩╦╠═╬¤ðÐı┘┌█▄¦▀µþÞ¯´≡­±‗¾¶§÷¸°¨·¹³²■ "
70
+ """All ASCII special characters with the extended ASCII special characters."""
71
+ STANDARD_ASCII = SPECIAL_ASCII + DIGITS + LETTERS
72
+ """All standard ASCII characters."""
73
+ FULL_ASCII = SPECIAL_ASCII_EXTENDED + DIGITS + LETTERS_EXTENDED
74
+ """All characters in the ASCII table."""
75
+
76
+
77
+ class ANSI:
78
+ """Constants and methods for use of ANSI escape codes"""
79
+
80
+ ESCAPED_CHAR = "\\x1b"
81
+ """The printable ANSI escape character."""
82
+ CHAR = char = "\x1b"
83
+ """The ANSI escape character."""
84
+ START = start = "["
85
+ """The start of an ANSI escape sequence."""
86
+ SEP = sep = ";"
87
+ """The separator between ANSI escape sequence parts."""
88
+ END = end = "m"
89
+ """The end of an ANSI escape sequence."""
90
+
91
+ @classmethod
92
+ def seq(cls, parts: int = 1) -> FormattableString:
93
+ """Generate an ANSI sequence with `parts` amount of placeholders."""
94
+ return cls.CHAR + cls.START + cls.SEP.join(["{}" for _ in range(parts)]) + cls.END
95
+
96
+ SEQ_COLOR: FormattableString = CHAR + START + "38" + SEP + "2" + SEP + "{}" + SEP + "{}" + SEP + "{}" + END
97
+ """The ANSI escape sequence for setting the text RGB color."""
98
+ SEQ_BG_COLOR: FormattableString = CHAR + START + "48" + SEP + "2" + SEP + "{}" + SEP + "{}" + SEP + "{}" + END
99
+ """The ANSI escape sequence for setting the background RGB color."""
100
+
101
+ COLOR_MAP: tuple[str, ...] = (
102
+ ########### DEFAULT CONSOLE COLOR NAMES ############
103
+ "black",
104
+ "red",
105
+ "green",
106
+ "yellow",
107
+ "blue",
108
+ "magenta",
109
+ "cyan",
110
+ "white",
111
+ )
112
+ """The console default color names."""
113
+
114
+ CODES_MAP: dict[str | tuple[str, ...], int] = {
115
+ ################# SPECIFIC RESETS ##################
116
+ "_": 0,
117
+ ("_bold", "_b"): 22,
118
+ ("_dim", "_d"): 22,
119
+ ("_italic", "_i"): 23,
120
+ ("_underline", "_u"): 24,
121
+ ("_double-underline", "_du"): 24,
122
+ ("_inverse", "_invert", "_in"): 27,
123
+ ("_hidden", "_hide", "_h"): 28,
124
+ ("_strikethrough", "_s"): 29,
125
+ ("_color", "_c"): 39,
126
+ ("_background", "_bg"): 49,
127
+ ################### TEXT STYLES ####################
128
+ ("bold", "b"): 1,
129
+ ("dim", "d"): 2,
130
+ ("italic", "i"): 3,
131
+ ("underline", "u"): 4,
132
+ ("inverse", "invert", "in"): 7,
133
+ ("hidden", "hide", "h"): 8,
134
+ ("strikethrough", "s"): 9,
135
+ ("double-underline", "du"): 21,
136
+ ################## DEFAULT COLORS ##################
137
+ "black": 30,
138
+ "red": 31,
139
+ "green": 32,
140
+ "yellow": 33,
141
+ "blue": 34,
142
+ "magenta": 35,
143
+ "cyan": 36,
144
+ "white": 37,
145
+ ############## BRIGHT DEFAULT COLORS ###############
146
+ "br:black": 90,
147
+ "br:red": 91,
148
+ "br:green": 92,
149
+ "br:yellow": 93,
150
+ "br:blue": 94,
151
+ "br:magenta": 95,
152
+ "br:cyan": 96,
153
+ "br:white": 97,
154
+ ############ DEFAULT BACKGROUND COLORS #############
155
+ "bg:black": 40,
156
+ "bg:red": 41,
157
+ "bg:green": 42,
158
+ "bg:yellow": 43,
159
+ "bg:blue": 44,
160
+ "bg:magenta": 45,
161
+ "bg:cyan": 46,
162
+ "bg:white": 47,
163
+ ######### BRIGHT DEFAULT BACKGROUND COLORS #########
164
+ "bg:br:black": 100,
165
+ "bg:br:red": 101,
166
+ "bg:br:green": 102,
167
+ "bg:br:yellow": 103,
168
+ "bg:br:blue": 104,
169
+ "bg:br:magenta": 105,
170
+ "bg:br:cyan": 106,
171
+ "bg:br:white": 107,
172
+ }
173
+ """All custom format keys and their corresponding ANSI format number codes."""
xulbux/cli/help.py ADDED
@@ -0,0 +1,73 @@
1
+ from .. import __version__
2
+ from ..base.consts import COLOR
3
+ from ..format_codes import FormatCodes
4
+ from ..console import Console
5
+
6
+ from urllib.error import HTTPError
7
+ from typing import Optional
8
+ import urllib.request as _request
9
+ import json as _json
10
+
11
+
12
+ def get_latest_version() -> Optional[str]:
13
+ with _request.urlopen(URL) as response:
14
+ if response.status == 200:
15
+ data = _json.load(response)
16
+ return data["info"]["version"]
17
+ else:
18
+ raise HTTPError(URL, response.status, "Failed to fetch latest version info", response.headers, None)
19
+
20
+
21
+ def is_latest_version() -> Optional[bool]:
22
+ try:
23
+ latest = get_latest_version()
24
+ if latest in ("", None): return None
25
+ latest_v_parts = tuple(int(part) for part in latest.lower().lstrip("v").split('.'))
26
+ installed_v_parts = tuple(int(part) for part in __version__.lower().lstrip("v").split('.'))
27
+ return latest_v_parts <= installed_v_parts
28
+ except Exception:
29
+ return None
30
+
31
+
32
+ URL = "https://pypi.org/pypi/xulbux/json"
33
+ IS_LATEST_VERSION = is_latest_version()
34
+ CLR = {
35
+ "class": COLOR.TANGERINE,
36
+ "code_border": COLOR.GRAY,
37
+ "const": COLOR.RED,
38
+ "func": COLOR.CYAN,
39
+ "import": COLOR.NEON_GREEN,
40
+ "lib": COLOR.ORANGE,
41
+ "notice": COLOR.YELLOW,
42
+ "punctuators": COLOR.DARK_GRAY,
43
+ }
44
+ HELP = FormatCodes.to_ansi(
45
+ rf""" [_|b|#7075FF] __ __
46
+ [b|#7075FF] _ __ __ __/ / / /_ __ ___ __
47
+ [b|#7075FF] | |/ // / / / / / __ \/ / / | |/ /
48
+ [b|#7075FF] > , </ /_/ / /_/ /_/ / /_/ /> , <
49
+ [b|#7075FF]/_/|_|\____/\__/\____/\____//_/|_| [*|BG:{COLOR.GRAY}|#000] v[b]{__version__} [*|dim|{CLR['notice']}]({'' if IS_LATEST_VERSION else ' (newer available)'})[*]
50
+
51
+ [i|{COLOR.CORAL}]A TON OF COOL FUNCTIONS, YOU NEED![*]
52
+
53
+ [b|#FCFCFF]Usage:[*]
54
+ [dim|{CLR['code_border']}](╭────────────────────────────────────────────────────╮)
55
+ [dim|{CLR['code_border']}](│) [{CLR['punctuators']}]# LIBRARY CONSTANTS[*] [dim|{CLR['code_border']}](│)
56
+ [dim|{CLR['code_border']}](│) [{CLR['import']}]from [{CLR['lib']}]xulbux[{CLR['punctuators']}].[{CLR['lib']}]base[{CLR['punctuators']}].[{CLR['lib']}]consts [{CLR['import']}]import [{CLR['const']}]COLOR[{CLR['punctuators']}], [{CLR['const']}]CHARS[{CLR['punctuators']}], [{CLR['const']}]ANSI[*] [dim|{CLR['code_border']}](│)
57
+ [dim|{CLR['code_border']}](│) [{CLR['punctuators']}]# Main Classes[*] [dim|{CLR['code_border']}](│)
58
+ [dim|{CLR['code_border']}](│) [{CLR['import']}]from [{CLR['lib']}]xulbux [{CLR['import']}]import [{CLR['class']}]Code[{CLR['punctuators']}], [{CLR['class']}]Color[{CLR['punctuators']}], [{CLR['class']}]Console[{CLR['punctuators']}], ...[*] [dim|{CLR['code_border']}](│)
59
+ [dim|{CLR['code_border']}](│) [{CLR['punctuators']}]# module specific imports[*] [dim|{CLR['code_border']}](│)
60
+ [dim|{CLR['code_border']}](│) [{CLR['import']}]from [{CLR['lib']}]xulbux[{CLR['punctuators']}].[{CLR['lib']}]color [{CLR['import']}]import [{CLR['func']}]rgba[{CLR['punctuators']}], [{CLR['func']}]hsla[{CLR['punctuators']}], [{CLR['func']}]hexa[*] [dim|{CLR['code_border']}](│)
61
+ [dim|{CLR['code_border']}](╰────────────────────────────────────────────────────╯)
62
+ [b|#FCFCFF]Documentation:[*]
63
+ [dim|{CLR['code_border']}](╭────────────────────────────────────────────────────╮)
64
+ [dim|{CLR['code_border']}](│) [#DADADD]For more information see the GitHub page. [dim|{CLR['code_border']}](│)
65
+ [dim|{CLR['code_border']}](│) [u|#8085FF](https://github.com/XulbuX/PythonLibraryXulbuX/wiki) [dim|{CLR['code_border']}](│)
66
+ [dim|{CLR['code_border']}](╰────────────────────────────────────────────────────╯)
67
+ [_]"""
68
+ )
69
+
70
+
71
+ def show_help() -> None:
72
+ print(HELP)
73
+ Console.pause_exit(pause=True, prompt=" [dim](Press any key to exit...)\n\n")
xulbux/color.py CHANGED
@@ -92,9 +92,13 @@ class rgba:
92
92
 
93
93
  def __init__(self, r: int, g: int, b: int, a: Optional[float] = None, _validate: bool = True):
94
94
  self.r: int
95
+ """The red channel (`0`–`255`)"""
95
96
  self.g: int
97
+ """The green channel (`0`–`255`)"""
96
98
  self.b: int
99
+ """The blue channel (`0`–`255`)"""
97
100
  self.a: Optional[float]
101
+ """The alpha channel (`0.0`–`1.0`) or `None` if not set"""
98
102
  if not _validate:
99
103
  self.r, self.g, self.b, self.a = r, g, b, a
100
104
  return
@@ -291,9 +295,13 @@ class hsla:
291
295
 
292
296
  def __init__(self, h: int, s: int, l: int, a: Optional[float] = None, _validate: bool = True):
293
297
  self.h: int
298
+ """The hue channel (`0`–`360`)"""
294
299
  self.s: int
300
+ """The saturation channel (`0`–`100`)"""
295
301
  self.l: int
302
+ """The lightness channel (`0`–`100`)"""
296
303
  self.a: Optional[float]
304
+ """The alpha channel (`0.0`–`1.0`) or `None` if not set"""
297
305
  if not _validate:
298
306
  self.h, self.s, self.l, self.a = h, s, l, a
299
307
  return
@@ -496,9 +504,13 @@ class hexa:
496
504
  _a: Optional[float] = None,
497
505
  ):
498
506
  self.r: int
507
+ """The red channel (`0`–`255`)"""
499
508
  self.g: int
509
+ """The green channel (`0`–`255`)"""
500
510
  self.b: int
511
+ """The blue channel (`0`–`255`)"""
501
512
  self.a: Optional[float]
513
+ """The alpha channel (`0.0`–`1.0`) or `None` if not set"""
502
514
  if all(x is not None for x in (_r, _g, _b)):
503
515
  self.r, self.g, self.b, self.a = cast(int, _r), cast(int, _g), cast(int, _b), _a
504
516
  return
xulbux/console.py CHANGED
@@ -5,15 +5,16 @@ You can also use special formatting codes directly inside the log message to cha
5
5
  For more detailed information about formatting codes, see the the `format_codes` module documentation.
6
6
  """
7
7
 
8
- from .base.consts import COLOR, CHARS
9
- from .format_codes import FormatCodes, _COMPILED
8
+ from .base.consts import COLOR, CHARS, ANSI
9
+ from .format_codes import FormatCodes, _COMPILED as _FC_COMPILED
10
10
  from .string import String
11
11
  from .color import Color, Rgba, Hexa
12
12
 
13
- from typing import Callable, Optional, Literal, Mapping, Any, cast
13
+ from typing import Generator, Callable, Optional, Literal, Mapping, Pattern, TypeVar, TextIO, Any, cast
14
14
  from prompt_toolkit.key_binding import KeyPressEvent, KeyBindings
15
15
  from prompt_toolkit.validation import ValidationError, Validator
16
16
  from prompt_toolkit.styles import Style
17
+ from contextlib import contextmanager
17
18
  from prompt_toolkit.keys import Keys
18
19
  import prompt_toolkit as _pt
19
20
  import keyboard as _keyboard
@@ -21,6 +22,17 @@ import getpass as _getpass
21
22
  import shutil as _shutil
22
23
  import sys as _sys
23
24
  import os as _os
25
+ import re as _re
26
+ import io as _io
27
+
28
+
29
+ _COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
30
+ "label": _re.compile(r"(?i)\{(?:label|l)\}"),
31
+ "bar": _re.compile(r"(?i)\{(?:bar|b)\}"),
32
+ "current": _re.compile(r"(?i)\{(?:current|c)\}"),
33
+ "total": _re.compile(r"(?i)\{(?:total|t)\}"),
34
+ "percentage": _re.compile(r"(?i)\{(?:percentage|percent|p)\}"),
35
+ }
24
36
 
25
37
 
26
38
  class _ConsoleWidth:
@@ -66,7 +78,9 @@ class ArgResult:
66
78
 
67
79
  def __init__(self, exists: bool, value: Any | list[Any]):
68
80
  self.exists: bool = exists
81
+ """Whether the argument was found or not."""
69
82
  self.value: Any = value
83
+ """The value given with the found argument."""
70
84
 
71
85
  def __bool__(self):
72
86
  return self.exists
@@ -366,9 +380,12 @@ class Console:
366
380
  end: str = "\n",
367
381
  title_bg_color: Optional[Rgba | Hexa] = None,
368
382
  default_color: Optional[Rgba | Hexa] = None,
369
- _console_tabsize: int = 8,
383
+ tab_size: int = 8,
384
+ title_px: int = 1,
385
+ title_mx: int = 2,
370
386
  ) -> None:
371
- """Will print a formatted log message:
387
+ """Prints a nicely formatted log message.\n
388
+ -------------------------------------------------------------------------------------------
372
389
  - `title` -⠀the title of the log message (e.g. `DEBUG`, `WARN`, `FAIL`, etc.)
373
390
  - `prompt` -⠀the log message
374
391
  - `format_linebreaks` -⠀whether to format (indent after) the line breaks or not
@@ -376,38 +393,40 @@ class Console:
376
393
  - `end` -⠀something to print after the log is printed (e.g. `\\n`)
377
394
  - `title_bg_color` -⠀the background color of the `title`
378
395
  - `default_color` -⠀the default text color of the `prompt`
379
- - `_console_tabsize` -⠀the tab size of the console (default is 8)\n
380
- -----------------------------------------------------------------------------------
396
+ - `tab_size` -⠀the tab size used for the log (default is 8 like console tabs)
397
+ - `title_px` -⠀the horizontal padding (in chars) to the title (if `title_bg_color` is set)
398
+ - `title_mx` -⠀the horizontal margin (in chars) to the title\n
399
+ -------------------------------------------------------------------------------------------
381
400
  The log message can be formatted with special formatting codes. For more detailed
382
401
  information about formatting codes, see `format_codes` module documentation."""
402
+ has_title_bg = title_bg_color is not None and Color.is_valid(title_bg_color)
383
403
  title = "" if title is None else title.strip().upper()
384
- title_len, tab_len = len(title) + 4, _console_tabsize - ((len(title) + 4) % _console_tabsize)
385
- if title_bg_color is not None and Color.is_valid(title_bg_color):
386
- title_bg_color = Color.to_hexa(title_bg_color)
387
- title_color = Color.text_color_for_on_bg(title_bg_color)
388
- else:
389
- title_color = "_color" if title_bg_color is None else "#000"
404
+ title_fg = Color.text_color_for_on_bg(
405
+ Color.to_hexa(title_bg_color) # type: ignore[assignment]
406
+ ) if has_title_bg else "_color"
407
+ px, mx = (" " * title_px) if has_title_bg else "", " " * title_mx
408
+ tab = " " * (tab_size - 1 - ((len(mx) + (title_len := len(title) + 2 * len(px))) % tab_size))
390
409
  if format_linebreaks:
391
410
  clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True, _ignore_linebreaks=True)
392
- prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(clean_prompt).splitlines())
411
+ prompt_lst = (
412
+ String.split_count(l, Console.w - (title_len + len(tab) + 2 * len(mx))) for l in str(clean_prompt).splitlines()
413
+ )
393
414
  prompt_lst = (
394
415
  item for lst in prompt_lst for item in ([""] if lst == [] else (lst if isinstance(lst, list) else [lst]))
395
416
  )
396
- prompt = f"\n{' ' * title_len}\t".join(
417
+ prompt = f"\n{mx}{' ' * title_len}{mx}{tab}".join(
397
418
  Console.__add_back_removed_parts(list(prompt_lst), cast(tuple[tuple[int, str], ...], removals))
398
419
  )
399
- else:
400
- prompt = str(prompt)
401
420
  if title == "":
402
421
  FormatCodes.print(
403
- f'{start} {f"[{default_color}]" if default_color else ""}{str(prompt)}[_]',
422
+ f'{start} {f"[{default_color}]" if default_color else ""}{prompt}[_]',
404
423
  default_color=default_color,
405
424
  end=end,
406
425
  )
407
426
  else:
408
427
  FormatCodes.print(
409
- f'{start} [bold][{title_color}]{f"[BG:{title_bg_color}]" if title_bg_color else ""} {title} [_]'
410
- + f'\t{f"[{default_color}]" if default_color else ""}{prompt}[_]',
428
+ f"{start}{mx}[bold][{title_fg}]{f'[BG:{title_bg_color}]' if title_bg_color else ''}{px}{title}{px}[_]{mx}"
429
+ + f"{tab}{f'[{default_color}]' if default_color else ''}{prompt}[_]",
411
430
  default_color=default_color,
412
431
  end=end,
413
432
  )
@@ -569,7 +588,7 @@ class Console:
569
588
  spaces_l = " " * indent
570
589
  lines = [
571
590
  f"{spaces_l}[bg:{box_bg_color}]{' ' * w_padding}"
572
- + _COMPILED["formatting"].sub(lambda m: f"{m.group(0)}[bg:{box_bg_color}]", line) +
591
+ + _FC_COMPILED["formatting"].sub(lambda m: f"{m.group(0)}[bg:{box_bg_color}]", line) +
573
592
  (" " * ((w_padding + max_line_len - len(unfmt)) + pad_w_full)) + "[*]" for line, unfmt in zip(lines, unfmt_lines)
574
593
  ]
575
594
  pady = " " * (Console.w if w_full else max_line_len + (2 * w_padding))
@@ -724,6 +743,8 @@ class Console:
724
743
  FormatCodes.print("[_]" if reset_ansi else "", end=end[1:] if end.startswith("\n") else end)
725
744
  return input_string
726
745
 
746
+ T = TypeVar("T")
747
+
727
748
  @staticmethod
728
749
  def input(
729
750
  prompt: object = "",
@@ -737,7 +758,9 @@ class Console:
737
758
  allowed_chars: str = CHARS.ALL, #type: ignore[assignment]
738
759
  allow_paste: bool = True,
739
760
  validator: Optional[Callable[[str], Optional[str]]] = None,
740
- ) -> str:
761
+ default_val: Optional[T] = None,
762
+ output_type: type[T] = str, # type: ignore[assignment]
763
+ ) -> T:
741
764
  """Acts like a standard Python `input()` a bunch of cool extra features.\n
742
765
  ------------------------------------------------------------------------------------
743
766
  - `prompt` -⠀the input prompt
@@ -753,12 +776,15 @@ class Console:
753
776
  - `allow_paste` -⠀whether to allow pasting text into the input or not
754
777
  - `validator` -⠀a function that takes the input string and returns a string error
755
778
  message if invalid, or nothing if valid
779
+ - `default_val` -⠀the default value to return if the input is empty
780
+ - `output_type` -⠀the type (class) to convert the input to before returning it\n
756
781
  ------------------------------------------------------------------------------------
757
782
  The input prompt can be formatted with special formatting codes. For more detailed
758
783
  information about formatting codes, see the `format_codes` module documentation."""
759
784
  result_text = ""
760
785
  tried_pasting = False
761
786
  filtered_chars = set()
787
+ has_default = default_val is not None
762
788
 
763
789
  class InputValidator(Validator):
764
790
 
@@ -900,4 +926,282 @@ class Console:
900
926
  FormatCodes.print(start, end="")
901
927
  session.prompt()
902
928
  FormatCodes.print(end, end="")
903
- return result_text
929
+
930
+ if result_text in ("", None):
931
+ if has_default: return default_val
932
+ result_text = ""
933
+
934
+ if output_type == str:
935
+ return result_text # type: ignore[return-value]
936
+ else:
937
+ try:
938
+ return output_type(result_text) # type: ignore[call-arg]
939
+ except (ValueError, TypeError):
940
+ if has_default: return default_val
941
+ raise
942
+
943
+
944
+ class ProgressBar:
945
+ """A console progress bar with smooth transitions and customizable appearance.\n
946
+ -------------------------------------------------------------------------------------------------
947
+ - `min_width` -⠀the min width of the progress bar in chars
948
+ - `max_width` -⠀the max width of the progress bar in chars
949
+ - `bar_format` -⠀the format string used to render the progress bar, containing placeholders:
950
+ * `{label}` `{l}`
951
+ * `{bar}` `{b}`
952
+ * `{current}` `{c}`
953
+ * `{total}` `{t}`
954
+ * `{percentage}` `{percent}` `{p}`
955
+ - `limited_bar_format` -⠀a simplified format string used when the console width is too small
956
+ - `chars` -⠀a tuple of characters ordered from full to empty progress<br>
957
+ The first character represents completely filled sections, intermediate
958
+ characters create smooth transitions, and the last character represents
959
+ empty sections. Default is a set of Unicode block characters.
960
+ --------------------------------------------------------------------------------------------------
961
+ The bar format (also limited) can additionally be formatted with special formatting codes. For
962
+ more detailed information about formatting codes, see the `format_codes` module documentation."""
963
+
964
+ def __init__(
965
+ self,
966
+ min_width: int = 10,
967
+ max_width: int = 50,
968
+ bar_format: str = "{l} |{b}| [b]({c})/{t} [dim](([i]({p}%)))",
969
+ limited_bar_format: str = "|{b}|",
970
+ chars: tuple[str, ...] = ("█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", " "),
971
+ ):
972
+ self.active: bool = False
973
+ """Whether the progress bar is currently active (intercepting stdout) or not."""
974
+ self.min_width: int
975
+ """The min width of the progress bar in chars."""
976
+ self.max_width: int
977
+ """The max width of the progress bar in chars."""
978
+ self.bar_format: str
979
+ """The format string used to render the progress bar."""
980
+ self.limited_bar_format: str
981
+ """The simplified format string used when the console width is too small."""
982
+ self.chars: tuple[str, ...]
983
+ """A tuple of characters ordered from full to empty progress."""
984
+
985
+ self.set_width(min_width, max_width)
986
+ self.set_bar_format(bar_format, limited_bar_format)
987
+ self.set_chars(chars)
988
+
989
+ self._buffer: list[str] = []
990
+ self._original_stdout: Optional[TextIO] = None
991
+ self._current_progress_str: str = ""
992
+ self._last_line_len: int = 0
993
+
994
+ def set_width(self, min_width: Optional[int] = None, max_width: Optional[int] = None) -> None:
995
+ """Set the width of the progress bar.\n
996
+ --------------------------------------------------------------
997
+ - `min_width` -⠀the min width of the progress bar in chars
998
+ - `max_width` -⠀the max width of the progress bar in chars"""
999
+ if min_width is not None:
1000
+ if min_width < 1:
1001
+ raise ValueError("Minimum width must be at least 1.")
1002
+ self.min_width = max(1, min_width)
1003
+ if max_width is not None:
1004
+ if max_width < 1:
1005
+ raise ValueError("Maximum width must be at least 1.")
1006
+ self.max_width = max(self.min_width, max_width)
1007
+
1008
+ def set_bar_format(self, bar_format: Optional[str] = None, limited_bar_format: Optional[str] = None) -> None:
1009
+ """Set the format string used to render the progress bar.\n
1010
+ --------------------------------------------------------------------------------------------------
1011
+ - `bar_format` -⠀the format string used to render the progress bar, containing placeholders:
1012
+ * `{label}` `{l}`
1013
+ * `{bar}` `{b}`
1014
+ * `{current}` `{c}`
1015
+ * `{total}` `{t}`
1016
+ * `{percentage}` `{percent}` `{p}`
1017
+ - `limited_bar_format` -⠀a simplified format string used when the console width is too small
1018
+ --------------------------------------------------------------------------------------------------
1019
+ The bar format (also limited) can additionally be formatted with special formatting codes. For
1020
+ more detailed information about formatting codes, see the `format_codes` module documentation."""
1021
+ if bar_format is not None:
1022
+ if not _COMPILED["bar"].search(bar_format):
1023
+ raise ValueError("'bar_format' must contain the '{bar}' or '{b}' placeholder.")
1024
+ self.bar_format = bar_format
1025
+ if limited_bar_format is not None:
1026
+ if not _COMPILED["bar"].search(limited_bar_format):
1027
+ raise ValueError("'limited_bar_format' must contain the '{bar}' or '{b}' placeholder.")
1028
+ self.limited_bar_format = limited_bar_format
1029
+
1030
+ def set_chars(self, chars: tuple[str, ...]) -> None:
1031
+ """Set the characters used to render the progress bar.\n
1032
+ --------------------------------------------------------------------------
1033
+ - `chars` -⠀a tuple of characters ordered from full to empty progress<br>
1034
+ The first character represents completely filled sections, intermediate
1035
+ characters create smooth transitions, and the last character represents
1036
+ empty sections. If None, uses default Unicode block characters."""
1037
+ if len(chars) < 2:
1038
+ raise ValueError("'chars' must contain at least two characters (full and empty).")
1039
+ if not all(len(c) == 1 for c in chars if isinstance(c, str)):
1040
+ raise ValueError("All 'chars' items must be single-character strings.")
1041
+ self.chars = chars
1042
+
1043
+ def show_progress(self, current: int, total: int, label: Optional[str] = None) -> None:
1044
+ """Show or update the progress bar.\n
1045
+ -------------------------------------------------------------------------------------------
1046
+ - `current` -⠀the current progress value (below `0` or greater than `total` hides the bar)
1047
+ - `total` -⠀the total value representing 100% progress (must be greater than `0`)
1048
+ - `label` -⠀an optional label which is inserted at the `{label}` or `{l}` placeholder"""
1049
+ if total <= 0:
1050
+ raise ValueError("Total must be greater than 0.")
1051
+
1052
+ try:
1053
+ if not self.active:
1054
+ self._start_intercepting()
1055
+ self._flush_buffer()
1056
+ self._draw_progress_bar(current, total, label or "")
1057
+ if current < 0 or current > total:
1058
+ self.hide_progress()
1059
+ except Exception:
1060
+ self._emergency_cleanup()
1061
+ raise
1062
+
1063
+ def hide_progress(self) -> None:
1064
+ """Hide the progress bar and restore normal console output."""
1065
+ if self.active:
1066
+ self._clear_progress_line()
1067
+ self._stop_intercepting()
1068
+
1069
+ @contextmanager
1070
+ def progress_context(self, total: int, label: Optional[str] = None) -> Generator[Callable[[int], None], None, None]:
1071
+ """Context manager for automatic cleanup. Returns a function to update progress.\n
1072
+ --------------------------------------------------------------------------------------
1073
+ - `total` -⠀the total value representing 100% progress (must be greater than `0`)
1074
+ - `label` -⠀an optional label which is inserted at the `{label}` or `{l}` placeholder
1075
+ --------------------------------------------------------------------------------------
1076
+ Example usage:
1077
+ ```python
1078
+ with ProgressBar().progress_context(500, "Loading") as update_progress:
1079
+ for i in range(500):
1080
+ # Do some work...
1081
+ update_progress(i) # Update progress
1082
+ ```"""
1083
+ try:
1084
+
1085
+ def update_progress(current: int) -> None:
1086
+ self.show_progress(current, total, label)
1087
+
1088
+ yield update_progress
1089
+ except Exception:
1090
+ self._emergency_cleanup()
1091
+ raise
1092
+ finally:
1093
+ self.hide_progress()
1094
+
1095
+ def _start_intercepting(self) -> None:
1096
+ self.active = True
1097
+ self._original_stdout = _sys.stdout
1098
+ _sys.stdout = _InterceptedOutput(self)
1099
+
1100
+ def _stop_intercepting(self) -> None:
1101
+ if self._original_stdout:
1102
+ _sys.stdout = self._original_stdout
1103
+ self._original_stdout = None
1104
+ self.active = False
1105
+ self._buffer.clear()
1106
+ self._last_line_len = 0
1107
+ self._current_progress_str = ""
1108
+
1109
+ def _emergency_cleanup(self) -> None:
1110
+ """Emergency cleanup to restore stdout in case of exceptions."""
1111
+ try:
1112
+ self._stop_intercepting()
1113
+ except Exception:
1114
+ pass
1115
+
1116
+ def _flush_buffer(self) -> None:
1117
+ if self._buffer and self._original_stdout:
1118
+ self._clear_progress_line()
1119
+ for content in self._buffer:
1120
+ self._original_stdout.write(content)
1121
+ self._original_stdout.flush()
1122
+ self._buffer.clear()
1123
+
1124
+ def _draw_progress_bar(self, current: int, total: int, label: Optional[str] = None) -> None:
1125
+ if total <= 0 or not self._original_stdout:
1126
+ return
1127
+ percentage = min(100, (current / total) * 100)
1128
+ formatted, bar_width = self._get_formatted_info_and_bar_width(self.bar_format, current, total, percentage, label)
1129
+ if bar_width < self.min_width:
1130
+ formatted, bar_width = self._get_formatted_info_and_bar_width(
1131
+ self.limited_bar_format, current, total, percentage, label
1132
+ )
1133
+ bar = self._create_bar(current, total, max(1, bar_width)) + "[*]"
1134
+ progress_text = _COMPILED["bar"].sub(FormatCodes.to_ansi(bar), formatted)
1135
+ self._current_progress_str = progress_text
1136
+ self._last_line_len = len(progress_text)
1137
+ self._original_stdout.write(f"\r{progress_text}")
1138
+ self._original_stdout.flush()
1139
+
1140
+ def _get_formatted_info_and_bar_width(
1141
+ self,
1142
+ bar_format: str,
1143
+ current: int,
1144
+ total: int,
1145
+ percentage: float,
1146
+ label: Optional[str] = None,
1147
+ ) -> tuple[str, int]:
1148
+ formatted = _COMPILED["label"].sub(label or "", bar_format)
1149
+ formatted = _COMPILED["current"].sub(str(current), formatted)
1150
+ formatted = _COMPILED["total"].sub(str(total), formatted)
1151
+ formatted = _COMPILED["percentage"].sub(f"{percentage:.1f}", formatted)
1152
+ formatted = FormatCodes.to_ansi(formatted)
1153
+ bar_space = Console.w - len(FormatCodes.remove_ansi(_COMPILED["bar"].sub("", formatted)))
1154
+ bar_width = min(bar_space, self.max_width) if bar_space > 0 else 0
1155
+ return formatted, bar_width
1156
+
1157
+ def _create_bar(self, current: int, total: int, bar_width: int) -> str:
1158
+ progress = current / total if total > 0 else 0
1159
+ bar = []
1160
+
1161
+ for i in range(bar_width):
1162
+ pos_progress = (i + 1) / bar_width
1163
+ if progress >= pos_progress:
1164
+ bar.append(self.chars[0])
1165
+ elif progress >= pos_progress - (1 / bar_width):
1166
+ remainder = (progress - (pos_progress - (1 / bar_width))) * bar_width
1167
+ char_idx = len(self.chars) - 1 - min(int(remainder * len(self.chars)), len(self.chars) - 1)
1168
+ bar.append(self.chars[char_idx])
1169
+ else:
1170
+ bar.append(self.chars[-1])
1171
+ return "".join(bar)
1172
+
1173
+ def _clear_progress_line(self) -> None:
1174
+ if self._last_line_len > 0 and self._original_stdout:
1175
+ self._original_stdout.write(f"{ANSI.CHAR}[2K\r")
1176
+ self._original_stdout.flush()
1177
+
1178
+ def _redraw_progress_bar(self) -> None:
1179
+ if self._current_progress_str and self._original_stdout:
1180
+ self._original_stdout.write(f"{self._current_progress_str}")
1181
+ self._original_stdout.flush()
1182
+
1183
+
1184
+ class _InterceptedOutput(_io.StringIO):
1185
+ """Custom StringIO that captures output and stores it in the progress bar buffer."""
1186
+
1187
+ def __init__(self, progress_bar: ProgressBar):
1188
+ super().__init__()
1189
+ self.progress_bar = progress_bar
1190
+
1191
+ def write(self, content: str) -> int:
1192
+ try:
1193
+ if content and content != "\r":
1194
+ self.progress_bar._buffer.append(content)
1195
+ return len(content)
1196
+ except Exception:
1197
+ self.progress_bar._emergency_cleanup()
1198
+ raise
1199
+
1200
+ def flush(self) -> None:
1201
+ try:
1202
+ if self.progress_bar.active and self.progress_bar._buffer:
1203
+ self.progress_bar._flush_buffer()
1204
+ self.progress_bar._redraw_progress_bar()
1205
+ except Exception:
1206
+ self.progress_bar._emergency_cleanup()
1207
+ raise
xulbux/format_codes.py CHANGED
@@ -190,7 +190,7 @@ _COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
190
190
  "ansi_seq": _re.compile(ANSI.CHAR + r"(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"),
191
191
  "formatting": _rx.compile(
192
192
  Regex.brackets("[", "]", is_group=True, ignore_in_strings=False)
193
- + r"(?:\s*([/\\]?)\s*"
193
+ + r"(?:([/\\]?)"
194
194
  + Regex.brackets("(", ")", is_group=True, strip_spaces=False, ignore_in_strings=False)
195
195
  + r")?"
196
196
  ),
@@ -317,6 +317,7 @@ class FormatCodes:
317
317
  ]
318
318
  if auto_reset_txt and not auto_reset_escaped:
319
319
  reset_keys = []
320
+ default_color_resets = ("_bg", "default") if use_default else ("_bg", "_c")
320
321
  for k in format_keys:
321
322
  k_lower = k.lower()
322
323
  k_set = set(k_lower.split(":"))
@@ -324,7 +325,7 @@ class FormatCodes:
324
325
  if k_set & _PREFIX["BR"]:
325
326
  for i in range(len(k)):
326
327
  if is_valid_color(k[i:]):
327
- reset_keys.extend(["_bg", "default"] if use_default else ["_bg", "_c"])
328
+ reset_keys.extend(default_color_resets)
328
329
  break
329
330
  else:
330
331
  for i in range(len(k)):
@@ -334,7 +335,7 @@ class FormatCodes:
334
335
  elif is_valid_color(k) or any(
335
336
  k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon):])
336
337
  for prefix in _PREFIX["BR"]):
337
- reset_keys.append("default" if use_default else "_c")
338
+ reset_keys.append(default_color_resets[1])
338
339
  else:
339
340
  reset_keys.append(f"_{k}")
340
341
  ansi_resets = [
xulbux/system.py CHANGED
@@ -12,7 +12,7 @@ class _IsElevated:
12
12
  def __get__(self, obj, owner=None):
13
13
  try:
14
14
  if _os.name == "nt":
15
- return _ctypes.windll.shell32.IsUserAnAdmin() != 0
15
+ return _ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined]
16
16
  elif _os.name == "posix":
17
17
  return _os.geteuid() == 0 # type: ignore[attr-defined]
18
18
  except Exception:
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xulbux
3
- Version: 1.8.0
4
- Summary: A Python library which includes lots of helpful classes, types and functions aiming to make common programming tasks simpler.
3
+ Version: 1.8.2
4
+ Summary: A Python library which includes lots of helpful classes, types, and functions aiming to make common programming tasks simpler.
5
5
  Author-email: XulbuX <xulbux.real@gmail.com>
6
6
  Maintainer-email: XulbuX <xulbux.real@gmail.com>
7
7
  License-Expression: MIT
@@ -0,0 +1,20 @@
1
+ xulbux/__init__.py,sha256=NnqeQz_8_t4bkghp5qN3vQEm_Dff6AJBghJsTUVUaks,935
2
+ xulbux/code.py,sha256=CLA3wh6mTvcXTlQgBeFaaLIBciHvXWqVM56rAtfO-JE,6102
3
+ xulbux/color.py,sha256=tTNH30Wf2QNmyopKz4LADdels_TDzDMCptCtm76KkNo,50375
4
+ xulbux/console.py,sha256=b9B1lQzAl5OEhmIlCbTyCyu_WRZwMVOuI-v1GfGtxzo,57308
5
+ xulbux/data.py,sha256=hB9JxrSC7_6kelJ_TwOaapNrlig-jiyNZS3YoiJX8F8,30884
6
+ xulbux/env_path.py,sha256=HGOSffdIDubzczsXe6umyqotrzqhIW83QBkISAamXT8,4157
7
+ xulbux/file.py,sha256=7pa0-WS_DpXq7HRB1fLS6Acd9CM-ozXPpNJvMqCW4fw,2624
8
+ xulbux/format_codes.py,sha256=UwJOH3JJebylYd-MclDvKOG72fhuku63JzZU_eUzMtA,24764
9
+ xulbux/json.py,sha256=Ei5FdCjfM0FrrAEBmuuTcexl7mUY4eirXr-QPct2OS0,7448
10
+ xulbux/path.py,sha256=lLAEVZrW0TAwCewlONFVQcQ_8tVn9LTJZVOZpeGvE5s,7673
11
+ xulbux/regex.py,sha256=_BtMHRDNcD9zF4SL87dQuUVZcYGfZx9H5YNSDiEtzm8,8059
12
+ xulbux/string.py,sha256=QaTo0TQ9m_2USNgQNaVw5ivQt-A1E-e5x8OpIB3xIlY,5561
13
+ xulbux/system.py,sha256=uCjQhqlfOoLkEBdjWn5cLCEnaI4Ac3o9cwRKev07eWI,6612
14
+ xulbux/base/consts.py,sha256=HwgI_Cr_U2QznezN17SP1j-gpyf3tsbu_KHMd8aKzyw,5918
15
+ xulbux/cli/help.py,sha256=wlDTjFhyWQucywMGDmrYIjYn1TV__TwMlYzeSCkQ__U,4403
16
+ xulbux-1.8.2.dist-info/METADATA,sha256=h7DU1OI_NtZNXoq6pFPP1dQhYnDlcbdEbaSsgYajXdc,11042
17
+ xulbux-1.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ xulbux-1.8.2.dist-info/entry_points.txt,sha256=aYh89GfiBOB8vw2VPgC6rhBinhnJoAL1kig-3lq_zkg,58
19
+ xulbux-1.8.2.dist-info/top_level.txt,sha256=FkK4EZajwfP36fnlrPaR98OrEvZpvdEOdW1T5zTj6og,7
20
+ xulbux-1.8.2.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- xulbux/__init__.py,sha256=ee9vPsQXG50BOKoVLqUmcClEs-94eYHN3572x0AC3BY,817
2
- xulbux/code.py,sha256=CLA3wh6mTvcXTlQgBeFaaLIBciHvXWqVM56rAtfO-JE,6102
3
- xulbux/color.py,sha256=XCB05xBSEz6-dpX9yVkMB2zTSdTH01MfUazJEthL_zM,49741
4
- xulbux/console.py,sha256=J--5Ma2H3-YPT21pT92J4mNgu-0h_vJJ8M1BLOKbHWc,42773
5
- xulbux/data.py,sha256=hB9JxrSC7_6kelJ_TwOaapNrlig-jiyNZS3YoiJX8F8,30884
6
- xulbux/env_path.py,sha256=HGOSffdIDubzczsXe6umyqotrzqhIW83QBkISAamXT8,4157
7
- xulbux/file.py,sha256=7pa0-WS_DpXq7HRB1fLS6Acd9CM-ozXPpNJvMqCW4fw,2624
8
- xulbux/format_codes.py,sha256=94VyxnUZI2dS1glteNs7izKYrTI_W2pukFGF5f9k72E,24720
9
- xulbux/json.py,sha256=Ei5FdCjfM0FrrAEBmuuTcexl7mUY4eirXr-QPct2OS0,7448
10
- xulbux/path.py,sha256=lLAEVZrW0TAwCewlONFVQcQ_8tVn9LTJZVOZpeGvE5s,7673
11
- xulbux/regex.py,sha256=_BtMHRDNcD9zF4SL87dQuUVZcYGfZx9H5YNSDiEtzm8,8059
12
- xulbux/string.py,sha256=QaTo0TQ9m_2USNgQNaVw5ivQt-A1E-e5x8OpIB3xIlY,5561
13
- xulbux/system.py,sha256=Y5x719Ocwi93VJ6DVE8NR_Js7FQ6QT5wmtjX9FAsw9U,6582
14
- xulbux-1.8.0.dist-info/METADATA,sha256=Gk3Oj08lJRZuRWrUZ5bkPVjjWy0HIGS4h9UazzDrzP8,11041
15
- xulbux-1.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- xulbux-1.8.0.dist-info/entry_points.txt,sha256=aYh89GfiBOB8vw2VPgC6rhBinhnJoAL1kig-3lq_zkg,58
17
- xulbux-1.8.0.dist-info/top_level.txt,sha256=FkK4EZajwfP36fnlrPaR98OrEvZpvdEOdW1T5zTj6og,7
18
- xulbux-1.8.0.dist-info/RECORD,,
File without changes