xulbux 1.9.5__cp311-cp311-macosx_11_0_arm64.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.
- 455848faf89d8974b22a__mypyc.cpython-311-darwin.so +0 -0
- xulbux/__init__.cpython-311-darwin.so +0 -0
- xulbux/__init__.py +46 -0
- xulbux/base/consts.cpython-311-darwin.so +0 -0
- xulbux/base/consts.py +172 -0
- xulbux/base/decorators.cpython-311-darwin.so +0 -0
- xulbux/base/decorators.py +28 -0
- xulbux/base/exceptions.cpython-311-darwin.so +0 -0
- xulbux/base/exceptions.py +23 -0
- xulbux/base/types.cpython-311-darwin.so +0 -0
- xulbux/base/types.py +118 -0
- xulbux/cli/help.cpython-311-darwin.so +0 -0
- xulbux/cli/help.py +77 -0
- xulbux/code.cpython-311-darwin.so +0 -0
- xulbux/code.py +137 -0
- xulbux/color.cpython-311-darwin.so +0 -0
- xulbux/color.py +1331 -0
- xulbux/console.cpython-311-darwin.so +0 -0
- xulbux/console.py +2069 -0
- xulbux/data.cpython-311-darwin.so +0 -0
- xulbux/data.py +798 -0
- xulbux/env_path.cpython-311-darwin.so +0 -0
- xulbux/env_path.py +123 -0
- xulbux/file.cpython-311-darwin.so +0 -0
- xulbux/file.py +74 -0
- xulbux/file_sys.cpython-311-darwin.so +0 -0
- xulbux/file_sys.py +266 -0
- xulbux/format_codes.cpython-311-darwin.so +0 -0
- xulbux/format_codes.py +722 -0
- xulbux/json.cpython-311-darwin.so +0 -0
- xulbux/json.py +200 -0
- xulbux/regex.cpython-311-darwin.so +0 -0
- xulbux/regex.py +247 -0
- xulbux/string.cpython-311-darwin.so +0 -0
- xulbux/string.py +161 -0
- xulbux/system.cpython-311-darwin.so +0 -0
- xulbux/system.py +313 -0
- xulbux-1.9.5.dist-info/METADATA +271 -0
- xulbux-1.9.5.dist-info/RECORD +43 -0
- xulbux-1.9.5.dist-info/WHEEL +6 -0
- xulbux-1.9.5.dist-info/entry_points.txt +2 -0
- xulbux-1.9.5.dist-info/licenses/LICENSE +21 -0
- xulbux-1.9.5.dist-info/top_level.txt +2 -0
xulbux/format_codes.py
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the `FormatCodes` class, which includes methods to print and work with strings that
|
|
3
|
+
contain special formatting codes, which are then converted to ANSI codes for pretty console output.
|
|
4
|
+
|
|
5
|
+
------------------------------------------------------------------------------------------------------------------------------------
|
|
6
|
+
### The Easy Formatting
|
|
7
|
+
|
|
8
|
+
First, let's take a look at a small example of what a highly styled print string with formatting could look like using this module:
|
|
9
|
+
```
|
|
10
|
+
This here is just unformatted text. [b|u|br:blue](Next we have text that is bright blue + bold + underlined.)\\n
|
|
11
|
+
[#000|bg:#F67](Then there's also black text with a red background.) And finally the ([i](boring)) plain text again.
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
How all of this exactly works is explained in the sections below. 🠫
|
|
15
|
+
|
|
16
|
+
------------------------------------------------------------------------------------------------------------------------------------
|
|
17
|
+
#### Formatting Codes and Keys
|
|
18
|
+
|
|
19
|
+
In this module, you can apply styles and colors using simple formatting codes.
|
|
20
|
+
These formatting codes consist of one or multiple different formatting keys in between square brackets.
|
|
21
|
+
|
|
22
|
+
If a formatting code is placed in a print-string, the formatting of that code will be applied to everything behind it until its
|
|
23
|
+
formatting is reset. If applying multiple styles and colors in the same place, instead of writing the formatting keys all into
|
|
24
|
+
separate brackets (e.g. `[x][y][z]`), they can also be put in a single pair of brackets, separated by pipes (e.g. `[x|y|z]`).
|
|
25
|
+
|
|
26
|
+
A list of all possible formatting keys can be found under all possible formatting keys.
|
|
27
|
+
|
|
28
|
+
------------------------------------------------------------------------------------------------------------------------------------
|
|
29
|
+
#### Auto Resetting Formatting Codes
|
|
30
|
+
|
|
31
|
+
Certain formatting can automatically be reset, behind a certain amount of text, just like shown in the following example:
|
|
32
|
+
```
|
|
33
|
+
This is plain text, [br:blue](which is bright blue now.) Now it was automatically reset to plain again.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This will only reset formatting codes, that have a specific reset listed below.
|
|
37
|
+
That means if you use it where another formatting is already applied, that formatting is still there after the automatic reset:
|
|
38
|
+
```
|
|
39
|
+
[cyan]This is cyan text, [dim](which is dimmed now.) Now it's not dimmed any more but still cyan.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If you want to ignore the auto-reset functionality of `()` brackets, you can put a `\\` or `/` between them and
|
|
43
|
+
the formatting code:
|
|
44
|
+
```
|
|
45
|
+
[cyan]This is cyan text, [u]/(which is underlined now.) And now it is still underlined and cyan.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
------------------------------------------------------------------------------------------------------------------------------------
|
|
49
|
+
#### All possible Formatting Keys
|
|
50
|
+
|
|
51
|
+
- RGB colors:
|
|
52
|
+
Change the text color directly with an RGB color inside the square brackets. (With or without `rgb()` brackets doesn't matter.)
|
|
53
|
+
Examples:
|
|
54
|
+
- `[rgb(115, 117, 255)]`
|
|
55
|
+
- `[(255, 0, 136)]`
|
|
56
|
+
- `[255, 0, 136]`
|
|
57
|
+
- HEX colors:
|
|
58
|
+
Change the text color directly with a HEX color inside the square brackets. (Whether the `RGB` or `RRGGBB` HEX format is used,
|
|
59
|
+
and if there's a `#` or `0x` prefix, doesn't matter.)
|
|
60
|
+
Examples:
|
|
61
|
+
- `[0x7788FF]`
|
|
62
|
+
- `[#7788FF]`
|
|
63
|
+
- `[7788FF]`
|
|
64
|
+
- `[0x78F]`
|
|
65
|
+
- `[#78F]`
|
|
66
|
+
- `[78F]`
|
|
67
|
+
- background RGB / HEX colors:
|
|
68
|
+
Change the background color directly with an RGB or HEX color inside the square brackets, using the `background:` `BG:` prefix.
|
|
69
|
+
(Same RGB / HEX formatting code rules as for text color.)
|
|
70
|
+
Examples:
|
|
71
|
+
- `[background:rgb(115, 117, 255)]`
|
|
72
|
+
- `[BG:(255, 0, 136)]`
|
|
73
|
+
- `[background:#7788FF]`
|
|
74
|
+
- `[BG:#78F]`
|
|
75
|
+
- standard console colors:
|
|
76
|
+
Change the text color to one of the standard console colors by just writing the color name in the square brackets.
|
|
77
|
+
- `[black]`
|
|
78
|
+
- `[red]`
|
|
79
|
+
- `[green]`
|
|
80
|
+
- `[yellow]`
|
|
81
|
+
- `[blue]`
|
|
82
|
+
- `[magenta]`
|
|
83
|
+
- `[cyan]`
|
|
84
|
+
- `[white]`
|
|
85
|
+
- bright console colors:
|
|
86
|
+
Use the prefix `bright:` `BR:` to use the bright variant of the standard console color.
|
|
87
|
+
Examples:
|
|
88
|
+
- `[bright:black]` `[BR:black]`
|
|
89
|
+
- `[bright:red]` `[BR:red]`
|
|
90
|
+
- …
|
|
91
|
+
- Background console colors:
|
|
92
|
+
Use the prefix `background:` `BG:` to set the background to a standard console color. (Not all consoles support bright
|
|
93
|
+
standard colors.)
|
|
94
|
+
Examples:
|
|
95
|
+
- `[background:black]` `[BG:black]`
|
|
96
|
+
- `[background:red]` `[BG:red]`
|
|
97
|
+
- …
|
|
98
|
+
- Bright background console colors:
|
|
99
|
+
Combine the prefixes `background:` / `BG:` and `bright:` / `BR:` to set the background to a bright console color.
|
|
100
|
+
(The order of the prefixes doesn't matter.)
|
|
101
|
+
Examples:
|
|
102
|
+
- `[background:bright:black]` `[BG:BR:black]`
|
|
103
|
+
- `[background:bright:red]` `[BG:BR:red]`
|
|
104
|
+
- …
|
|
105
|
+
- Text styles:
|
|
106
|
+
Use the built-in text formatting to change the style of the text. There are long and short forms for each formatting code.
|
|
107
|
+
(Not all consoles support all text styles.)
|
|
108
|
+
- `[bold]` `[b]`
|
|
109
|
+
- `[dim]`
|
|
110
|
+
- `[italic]` `[i]`
|
|
111
|
+
- `[underline]` `[u]`
|
|
112
|
+
- `[inverse]` `[invert]` `[in]`
|
|
113
|
+
- `[hidden]` `[hide]` `[h]`
|
|
114
|
+
- `[strikethrough]` `[s]`
|
|
115
|
+
- `[double-underline]` `[du]`
|
|
116
|
+
- Specific reset:
|
|
117
|
+
Use these reset codes to remove a specific style, color or background. Again, there are long and
|
|
118
|
+
short forms for each reset code.
|
|
119
|
+
- `[_bold]` `[_b]`
|
|
120
|
+
- `[_dim]`
|
|
121
|
+
- `[_italic]` `[_i]`
|
|
122
|
+
- `[_underline]` `[_u]`
|
|
123
|
+
- `[_inverse]` `[_invert]` `[_in]`
|
|
124
|
+
- `[_hidden]` `[_hide]` `[_h]`
|
|
125
|
+
- `[_strikethrough]` `[_s]`
|
|
126
|
+
- `[_double-underline]` `[_du]`
|
|
127
|
+
- `[_color]` `[_c]`
|
|
128
|
+
- `[_background]` `[_bg]`
|
|
129
|
+
- Total reset:
|
|
130
|
+
This will reset all previously applied formatting codes.
|
|
131
|
+
- `[_]`
|
|
132
|
+
|
|
133
|
+
------------------------------------------------------------------------------------------------------------------------------------
|
|
134
|
+
#### Additional Formatting Codes when a `default_color` is set
|
|
135
|
+
|
|
136
|
+
1. `[*]` resets everything, just like `[_]`, but the text color will remain in `default_color`
|
|
137
|
+
(if no `default_color` is set, it resets everything, exactly like `[_]`)
|
|
138
|
+
2. `[default]` will just color the text in `default_color`
|
|
139
|
+
(if no `default_color` is set, it's treated as an invalid formatting code)
|
|
140
|
+
3. `[background:default]` `[BG:default]` will color the background in `default_color`
|
|
141
|
+
(if no `default_color` is set, both are treated as invalid formatting codes)\n
|
|
142
|
+
|
|
143
|
+
Unlike the standard console colors, the default color can be changed by using the following modifiers:
|
|
144
|
+
|
|
145
|
+
- `[l]` will lighten the `default_color` text by `brightness_steps`%
|
|
146
|
+
- `[ll]` will lighten the `default_color` text by `2 × brightness_steps`%
|
|
147
|
+
- `[lll]` will lighten the `default_color` text by `3 × brightness_steps`%
|
|
148
|
+
- … etc. Same thing for darkening:
|
|
149
|
+
- `[d]` will darken the `default_color` text by `brightness_steps`%
|
|
150
|
+
- `[dd]` will darken the `default_color` text by `2 × brightness_steps`%
|
|
151
|
+
- `[ddd]` will darken the `default_color` text by `3 × brightness_steps`%
|
|
152
|
+
- … etc.
|
|
153
|
+
Per default, you can also use `+` and `-` to get lighter and darker `default_color` versions.
|
|
154
|
+
All of these lighten/darken formatting codes are treated as invalid if no `default_color` is set.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
from .base.types import FormattableString, Rgba, Hexa
|
|
158
|
+
from .base.consts import ANSI
|
|
159
|
+
|
|
160
|
+
from .string import String
|
|
161
|
+
from .regex import LazyRegex, Regex
|
|
162
|
+
from .color import Color, rgba, hexa
|
|
163
|
+
|
|
164
|
+
from typing import Optional, Literal, Final, cast
|
|
165
|
+
import ctypes as _ctypes
|
|
166
|
+
import regex as _rx
|
|
167
|
+
import sys as _sys
|
|
168
|
+
import os as _os
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
_CONSOLE_ANSI_CONFIGURED: bool = False
|
|
172
|
+
"""Whether the console was already configured to be able to interpret and render ANSI formatting."""
|
|
173
|
+
|
|
174
|
+
_ANSI_SEQ_1: Final[FormattableString] = ANSI.seq(1)
|
|
175
|
+
"""ANSI escape sequence with a single placeholder."""
|
|
176
|
+
_DEFAULT_COLOR_MODS: Final[dict[str, str]] = {
|
|
177
|
+
"lighten": "+l",
|
|
178
|
+
"darken": "-d",
|
|
179
|
+
}
|
|
180
|
+
"""Formatting codes for lightening and darkening the `default_color`."""
|
|
181
|
+
_PREFIX: Final[dict[str, set[str]]] = {
|
|
182
|
+
"BG": {"background", "bg"},
|
|
183
|
+
"BR": {"bright", "br"},
|
|
184
|
+
}
|
|
185
|
+
"""Formatting code prefixes for setting background- and bright-colors."""
|
|
186
|
+
_PREFIX_RX: Final[dict[str, str]] = {
|
|
187
|
+
"BG": rf"(?:{'|'.join(_PREFIX['BG'])})\s*:",
|
|
188
|
+
"BR": rf"(?:{'|'.join(_PREFIX['BR'])})\s*:",
|
|
189
|
+
}
|
|
190
|
+
"""Regex patterns for matching background- and bright-color prefixes."""
|
|
191
|
+
|
|
192
|
+
_PATTERNS = LazyRegex(
|
|
193
|
+
star_reset=r"\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]",
|
|
194
|
+
star_reset_inside=r"([^|]*?)\s*\*\s*([^|]*)",
|
|
195
|
+
ansi_seq=ANSI.CHAR + r"(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])",
|
|
196
|
+
formatting=(
|
|
197
|
+
Regex.brackets("[", "]", is_group=True, ignore_in_strings=False) + r"(?:([/\\]?)"
|
|
198
|
+
+ Regex.brackets("(", ")", is_group=True, strip_spaces=False, ignore_in_strings=False) + r")?"
|
|
199
|
+
),
|
|
200
|
+
escape_char=r"(\s*)(\/|\\)",
|
|
201
|
+
escape_char_cond=r"(\s*\[\s*)(\/|\\)(?!\2+)",
|
|
202
|
+
bg_opt_default=r"(?i)((?:" + _PREFIX_RX["BG"] + r")?)\s*default",
|
|
203
|
+
bg_default=r"(?i)" + _PREFIX_RX["BG"] + r"\s*default",
|
|
204
|
+
modifier=(
|
|
205
|
+
r"(?i)^((?:BG\s*:)?)\s*("
|
|
206
|
+
+ "|".join([f"{_rx.escape(m)}+" for m in _DEFAULT_COLOR_MODS["lighten"] + _DEFAULT_COLOR_MODS["darken"]]) + r")$"
|
|
207
|
+
),
|
|
208
|
+
rgb=r"(?i)^\s*(" + _PREFIX_RX["BG"] + r")?\s*(?:rgb|rgba)?\s*\(?\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)?\s*$",
|
|
209
|
+
hex=r"(?i)^\s*(" + _PREFIX_RX["BG"] + r")?\s*(?:#|0x)?([0-9A-F]{6}|[0-9A-F]{3})\s*$",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class FormatCodes:
|
|
214
|
+
"""This class provides methods to print and work with strings that contain special formatting codes,
|
|
215
|
+
which are then converted to ANSI codes for pretty console output."""
|
|
216
|
+
|
|
217
|
+
@classmethod
|
|
218
|
+
def print(
|
|
219
|
+
cls,
|
|
220
|
+
*values: object,
|
|
221
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
222
|
+
brightness_steps: int = 20,
|
|
223
|
+
sep: str = " ",
|
|
224
|
+
end: str = "\n",
|
|
225
|
+
flush: bool = True,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""A print function, whose print `values` can be formatted using formatting codes.\n
|
|
228
|
+
--------------------------------------------------------------------------------------------------
|
|
229
|
+
- `values` -⠀the values to print
|
|
230
|
+
- `default_color` -⠀the default text color to use if no other text color was applied
|
|
231
|
+
- `brightness_steps` -⠀the amount to increase/decrease default-color brightness per modifier code
|
|
232
|
+
- `sep` -⠀the separator to use between multiple values
|
|
233
|
+
- `end` -⠀the string to append at the end of the printed values
|
|
234
|
+
- `flush` -⠀whether to flush the output buffer after printing\n
|
|
235
|
+
--------------------------------------------------------------------------------------------------
|
|
236
|
+
For exact information about how to use special formatting codes,
|
|
237
|
+
see the `format_codes` module documentation."""
|
|
238
|
+
cls._config_console()
|
|
239
|
+
_sys.stdout.write(cls.to_ansi(sep.join(map(str, values)) + end, default_color, brightness_steps))
|
|
240
|
+
|
|
241
|
+
if flush:
|
|
242
|
+
_sys.stdout.flush()
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def input(
|
|
246
|
+
cls,
|
|
247
|
+
prompt: object = "",
|
|
248
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
249
|
+
brightness_steps: int = 20,
|
|
250
|
+
reset_ansi: bool = False,
|
|
251
|
+
) -> str:
|
|
252
|
+
"""An input, whose `prompt` can be formatted using formatting codes.\n
|
|
253
|
+
--------------------------------------------------------------------------------------------------
|
|
254
|
+
- `prompt` -⠀the prompt to show to the user
|
|
255
|
+
- `default_color` -⠀the default text color to use if no other text color was applied
|
|
256
|
+
- `brightness_steps` -⠀the amount to increase/decrease default-color brightness per modifier code
|
|
257
|
+
- `reset_ansi` -⠀if true, all ANSI formatting will be reset, after the user confirmed the input
|
|
258
|
+
and the program continues to run\n
|
|
259
|
+
--------------------------------------------------------------------------------------------------
|
|
260
|
+
For exact information about how to use special formatting codes, see the
|
|
261
|
+
`format_codes` module documentation."""
|
|
262
|
+
cls._config_console()
|
|
263
|
+
user_input = input(cls.to_ansi(str(prompt), default_color, brightness_steps))
|
|
264
|
+
|
|
265
|
+
if reset_ansi:
|
|
266
|
+
_sys.stdout.write(f"{ANSI.CHAR}[0m")
|
|
267
|
+
return user_input
|
|
268
|
+
|
|
269
|
+
@classmethod
|
|
270
|
+
def to_ansi(
|
|
271
|
+
cls,
|
|
272
|
+
string: str,
|
|
273
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
274
|
+
brightness_steps: int = 20,
|
|
275
|
+
_default_start: bool = True,
|
|
276
|
+
_validate_default: bool = True,
|
|
277
|
+
) -> str:
|
|
278
|
+
"""Convert the formatting codes inside a string to ANSI formatting.\n
|
|
279
|
+
--------------------------------------------------------------------------------------------------
|
|
280
|
+
- `string` -⠀the string that contains the formatting codes to convert
|
|
281
|
+
- `default_color` -⠀the default text color to use if no other text color was applied
|
|
282
|
+
- `brightness_steps` -⠀the amount to increase/decrease default-color brightness per modifier code
|
|
283
|
+
- `_default_start` -⠀whether to start the string with the `default_color` ANSI code, if set
|
|
284
|
+
- `_validate_default` -⠀whether to validate the `default_color` before use
|
|
285
|
+
(expects valid RGBA color or None, if not validated)\n
|
|
286
|
+
--------------------------------------------------------------------------------------------------
|
|
287
|
+
For exact information about how to use special formatting codes,
|
|
288
|
+
see the `format_codes` module documentation."""
|
|
289
|
+
if not (0 < brightness_steps <= 100):
|
|
290
|
+
raise ValueError("The 'brightness_steps' parameter must be between 1 and 100.")
|
|
291
|
+
|
|
292
|
+
if _validate_default:
|
|
293
|
+
use_default, default_color = cls._validate_default_color(default_color)
|
|
294
|
+
else:
|
|
295
|
+
use_default = default_color is not None
|
|
296
|
+
default_color = cast(Optional[rgba], default_color)
|
|
297
|
+
|
|
298
|
+
if use_default:
|
|
299
|
+
string = _PATTERNS.star_reset.sub(r"[\1_|default\2]", string) # REPLACE `[…|*|…]` WITH `[…|_|default|…]`
|
|
300
|
+
else:
|
|
301
|
+
string = _PATTERNS.star_reset.sub(r"[\1_\2]", string) # REPLACE `[…|*|…]` WITH `[…|_|…]`
|
|
302
|
+
|
|
303
|
+
string = "\n".join(
|
|
304
|
+
_PATTERNS.formatting.sub(_ReplaceKeysHelper(cls, use_default, default_color, brightness_steps), line)
|
|
305
|
+
for line in string.split("\n")
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
((cls._get_default_ansi(default_color) or "") if _default_start else "") \
|
|
310
|
+
+ string
|
|
311
|
+
) if default_color is not None else string
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def escape(
|
|
315
|
+
cls,
|
|
316
|
+
string: str,
|
|
317
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
318
|
+
_escape_char: Literal["/", "\\"] = "/",
|
|
319
|
+
) -> str:
|
|
320
|
+
"""Escapes all valid formatting codes in the string, so they are visible when output
|
|
321
|
+
to the console using `FormatCodes.print()`. Invalid formatting codes remain unchanged.\n
|
|
322
|
+
-----------------------------------------------------------------------------------------
|
|
323
|
+
- `string` -⠀the string that contains the formatting codes to escape
|
|
324
|
+
- `default_color` -⠀the default text color to use if no other text color was applied
|
|
325
|
+
- `_escape_char` -⠀the character to use to escape formatting codes (`/` or `\\`)\n
|
|
326
|
+
-----------------------------------------------------------------------------------------
|
|
327
|
+
For exact information about how to use special formatting codes,
|
|
328
|
+
see the `format_codes` module documentation."""
|
|
329
|
+
use_default, default_color = cls._validate_default_color(default_color)
|
|
330
|
+
|
|
331
|
+
return "\n".join(
|
|
332
|
+
_PATTERNS.formatting.sub(_EscapeFormatCodeHelper(cls, use_default, default_color, _escape_char), line)
|
|
333
|
+
for line in string.split("\n")
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def escape_ansi(cls, ansi_string: str) -> str:
|
|
338
|
+
"""Escapes all ANSI codes in the string, so they are visible when output to the console.\n
|
|
339
|
+
-------------------------------------------------------------------------------------------
|
|
340
|
+
- `ansi_string` -⠀the string that contains the ANSI codes to escape"""
|
|
341
|
+
return ansi_string.replace(ANSI.CHAR, ANSI.CHAR_ESCAPED)
|
|
342
|
+
|
|
343
|
+
@classmethod
|
|
344
|
+
def remove(
|
|
345
|
+
cls,
|
|
346
|
+
string: str,
|
|
347
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
348
|
+
get_removals: bool = False,
|
|
349
|
+
_ignore_linebreaks: bool = False,
|
|
350
|
+
) -> str | tuple[str, tuple[tuple[int, str], ...]]:
|
|
351
|
+
"""Removes all formatting codes from the string with optional tracking of removed codes.\n
|
|
352
|
+
--------------------------------------------------------------------------------------------------------
|
|
353
|
+
- `string` -⠀the string that contains the formatting codes to remove
|
|
354
|
+
- `default_color` -⠀the default text color to use if no other text color was applied
|
|
355
|
+
- `get_removals` -⠀if true, additionally to the cleaned string, a list of tuples will be returned,
|
|
356
|
+
where each tuple contains the position of the removed formatting code and the removed formatting code
|
|
357
|
+
- `_ignore_linebreaks` -⠀whether to ignore line breaks for the removal positions"""
|
|
358
|
+
return cls.remove_ansi(
|
|
359
|
+
cls.to_ansi(string, default_color=default_color),
|
|
360
|
+
get_removals=get_removals,
|
|
361
|
+
_ignore_linebreaks=_ignore_linebreaks,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def remove_ansi(
|
|
366
|
+
cls,
|
|
367
|
+
ansi_string: str,
|
|
368
|
+
get_removals: bool = False,
|
|
369
|
+
_ignore_linebreaks: bool = False,
|
|
370
|
+
) -> str | tuple[str, tuple[tuple[int, str], ...]]:
|
|
371
|
+
"""Removes all ANSI codes from the string with optional tracking of removed codes.\n
|
|
372
|
+
---------------------------------------------------------------------------------------------------
|
|
373
|
+
- `ansi_string` -⠀the string that contains the ANSI codes to remove
|
|
374
|
+
- `get_removals` -⠀if true, additionally to the cleaned string, a list of tuples will be returned,
|
|
375
|
+
where each tuple contains the position of the removed ansi code and the removed ansi code
|
|
376
|
+
- `_ignore_linebreaks` -⠀whether to ignore line breaks for the removal positions"""
|
|
377
|
+
if get_removals:
|
|
378
|
+
removals: list[tuple[int, str]] = []
|
|
379
|
+
|
|
380
|
+
clean_string = _PATTERNS.ansi_seq.sub(
|
|
381
|
+
_RemAnsiSeqHelper(removals),
|
|
382
|
+
ansi_string.replace("\n", "") if _ignore_linebreaks else ansi_string # REMOVE LINEBREAKS FOR POSITIONS
|
|
383
|
+
)
|
|
384
|
+
if _ignore_linebreaks:
|
|
385
|
+
clean_string = _PATTERNS.ansi_seq.sub("", ansi_string) # BUT KEEP LINEBREAKS IN RETURNED CLEAN STRING
|
|
386
|
+
|
|
387
|
+
return clean_string, tuple(removals)
|
|
388
|
+
|
|
389
|
+
else:
|
|
390
|
+
return _PATTERNS.ansi_seq.sub("", ansi_string)
|
|
391
|
+
|
|
392
|
+
@classmethod
|
|
393
|
+
def _config_console(cls) -> None:
|
|
394
|
+
"""Internal method which configure the console to be able to interpret and render ANSI formatting.\n
|
|
395
|
+
-----------------------------------------------------------------------------------------------------
|
|
396
|
+
This method will only do something the first time it's called. Subsequent calls will do nothing."""
|
|
397
|
+
global _CONSOLE_ANSI_CONFIGURED
|
|
398
|
+
if not _CONSOLE_ANSI_CONFIGURED:
|
|
399
|
+
_sys.stdout.flush()
|
|
400
|
+
if _os.name == "nt":
|
|
401
|
+
try:
|
|
402
|
+
# ENABLE VT100 MODE ON WINDOWS TO BE ABLE TO USE ANSI CODES
|
|
403
|
+
kernel32 = getattr(_ctypes, "windll").kernel32
|
|
404
|
+
h = kernel32.GetStdHandle(-11)
|
|
405
|
+
mode = _ctypes.c_ulong()
|
|
406
|
+
kernel32.GetConsoleMode(h, _ctypes.byref(mode))
|
|
407
|
+
kernel32.SetConsoleMode(h, mode.value | 0x0004)
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
_CONSOLE_ANSI_CONFIGURED = True
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _validate_default_color(default_color: Optional[Rgba | Hexa]) -> tuple[bool, Optional[rgba]]:
|
|
414
|
+
"""Internal method to validate and convert `default_color` to a `rgba` color object."""
|
|
415
|
+
if default_color is None:
|
|
416
|
+
return False, None
|
|
417
|
+
if Color.is_valid_hexa(default_color, False):
|
|
418
|
+
return True, hexa(cast(str | int, default_color)).to_rgba()
|
|
419
|
+
elif Color.is_valid_rgba(default_color, False):
|
|
420
|
+
return True, Color._parse_rgba(default_color)
|
|
421
|
+
raise TypeError("The 'default_color' parameter must be either a valid RGBA or HEXA color, or None.")
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _formats_to_keys(formats: str) -> list[str]:
|
|
425
|
+
"""Internal method to convert a string of multiple format keys
|
|
426
|
+
to a list of individual, stripped format keys."""
|
|
427
|
+
return [k.strip() for k in formats.split("|") if k.strip()]
|
|
428
|
+
|
|
429
|
+
@classmethod
|
|
430
|
+
def _get_replacement(cls, format_key: str, default_color: Optional[rgba], brightness_steps: int = 20) -> str:
|
|
431
|
+
"""Internal method that gives you the corresponding ANSI code for the given format key.
|
|
432
|
+
If `default_color` is not `None`, the text color will be `default_color` if all formats
|
|
433
|
+
are reset or you can get lighter or darker version of `default_color` (also as BG)"""
|
|
434
|
+
_format_key, format_key = format_key, cls._normalize_key(format_key) # NORMALIZE KEY AND SAVE ORIGINAL
|
|
435
|
+
if default_color and (new_default_color := cls._get_default_ansi(default_color, format_key, brightness_steps)):
|
|
436
|
+
return new_default_color
|
|
437
|
+
for map_key in ANSI.CODES_MAP:
|
|
438
|
+
if (isinstance(map_key, tuple) and format_key in map_key) or format_key == map_key:
|
|
439
|
+
return _ANSI_SEQ_1.format(
|
|
440
|
+
next((
|
|
441
|
+
v for k, v in ANSI.CODES_MAP.items() if format_key == k or (isinstance(k, tuple) and format_key in k)
|
|
442
|
+
), None)
|
|
443
|
+
)
|
|
444
|
+
rgb_match = _PATTERNS.rgb.match(format_key)
|
|
445
|
+
hex_match = _PATTERNS.hex.match(format_key)
|
|
446
|
+
try:
|
|
447
|
+
if rgb_match:
|
|
448
|
+
is_bg = rgb_match.group(1)
|
|
449
|
+
r, g, b = map(int, rgb_match.groups()[1:])
|
|
450
|
+
if Color.is_valid_rgba((r, g, b)):
|
|
451
|
+
return ANSI.SEQ_BG_COLOR.format(r, g, b) if is_bg else ANSI.SEQ_COLOR.format(r, g, b)
|
|
452
|
+
elif hex_match:
|
|
453
|
+
is_bg = hex_match.group(1)
|
|
454
|
+
rgb = Color.to_rgba(hex_match.group(2))
|
|
455
|
+
return (
|
|
456
|
+
ANSI.SEQ_BG_COLOR.format(rgb[0], rgb[1], rgb[2])
|
|
457
|
+
if is_bg else ANSI.SEQ_COLOR.format(rgb[0], rgb[1], rgb[2])
|
|
458
|
+
)
|
|
459
|
+
except Exception:
|
|
460
|
+
pass
|
|
461
|
+
return _format_key
|
|
462
|
+
|
|
463
|
+
@staticmethod
|
|
464
|
+
def _get_default_ansi(
|
|
465
|
+
default_color: rgba,
|
|
466
|
+
format_key: Optional[str] = None,
|
|
467
|
+
brightness_steps: Optional[int] = None,
|
|
468
|
+
_modifiers: tuple[str, str] = (_DEFAULT_COLOR_MODS["lighten"], _DEFAULT_COLOR_MODS["darken"]),
|
|
469
|
+
) -> Optional[str]:
|
|
470
|
+
"""Internal method to get the `default_color` and lighter/darker versions of it as ANSI code."""
|
|
471
|
+
if not isinstance(default_color, rgba):
|
|
472
|
+
return None
|
|
473
|
+
_default_color: tuple[int, int, int] = tuple(default_color)[:3]
|
|
474
|
+
if brightness_steps is None or (format_key and _PATTERNS.bg_opt_default.search(format_key)):
|
|
475
|
+
return (ANSI.SEQ_BG_COLOR if format_key and _PATTERNS.bg_default.search(format_key) else ANSI.SEQ_COLOR).format(
|
|
476
|
+
*_default_color
|
|
477
|
+
)
|
|
478
|
+
if format_key is None or not (match := _PATTERNS.modifier.match(format_key)):
|
|
479
|
+
return None
|
|
480
|
+
is_bg, modifiers = match.groups()
|
|
481
|
+
adjust = 0
|
|
482
|
+
for mod in _modifiers[0] + _modifiers[1]:
|
|
483
|
+
adjust = String.single_char_repeats(modifiers, mod)
|
|
484
|
+
if adjust and adjust > 0:
|
|
485
|
+
modifiers = mod
|
|
486
|
+
break
|
|
487
|
+
new_rgb = _default_color
|
|
488
|
+
if adjust == 0:
|
|
489
|
+
return None
|
|
490
|
+
elif modifiers in _modifiers[0]:
|
|
491
|
+
new_rgb = tuple(Color.adjust_lightness(default_color, (brightness_steps / 100) * adjust))
|
|
492
|
+
elif modifiers in _modifiers[1]:
|
|
493
|
+
new_rgb = tuple(Color.adjust_lightness(default_color, -(brightness_steps / 100) * adjust))
|
|
494
|
+
return (ANSI.SEQ_BG_COLOR if is_bg else ANSI.SEQ_COLOR).format(*new_rgb[:3])
|
|
495
|
+
|
|
496
|
+
@staticmethod
|
|
497
|
+
def _normalize_key(format_key: str) -> str:
|
|
498
|
+
"""Internal method to normalize the given format key."""
|
|
499
|
+
k_parts = format_key.replace(" ", "").lower().split(":")
|
|
500
|
+
prefix_str = "".join(
|
|
501
|
+
f"{prefix_key.lower()}:" for prefix_key, prefix_values in _PREFIX.items()
|
|
502
|
+
if any(k_part in prefix_values for k_part in k_parts)
|
|
503
|
+
)
|
|
504
|
+
return prefix_str + ":".join(
|
|
505
|
+
part for part in k_parts \
|
|
506
|
+
if part not in {val for values in _PREFIX.values() for val in values}
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class _EscapeFormatCodeHelper:
|
|
511
|
+
"""Internal, callable helper class to escape formatting codes."""
|
|
512
|
+
|
|
513
|
+
def __init__(
|
|
514
|
+
self,
|
|
515
|
+
cls: type[FormatCodes],
|
|
516
|
+
use_default: bool,
|
|
517
|
+
default_color: Optional[rgba],
|
|
518
|
+
escape_char: Literal["/", "\\"],
|
|
519
|
+
):
|
|
520
|
+
self.cls = cls
|
|
521
|
+
self.use_default = use_default
|
|
522
|
+
self.default_color = default_color
|
|
523
|
+
self.escape_char: Literal["/", "\\"] = escape_char
|
|
524
|
+
|
|
525
|
+
def __call__(self, match: _rx.Match[str]) -> str:
|
|
526
|
+
formats, auto_reset_txt = match.group(1), match.group(3)
|
|
527
|
+
|
|
528
|
+
# CHECK IF ALREADY ESCAPED OR CONTAINS NO FORMATTING
|
|
529
|
+
if not formats or _PATTERNS.escape_char_cond.match(match.group(0)):
|
|
530
|
+
return match.group(0)
|
|
531
|
+
|
|
532
|
+
# TEMPORARILY REPLACE `*` FOR VALIDATION
|
|
533
|
+
_formats = formats
|
|
534
|
+
if self.use_default:
|
|
535
|
+
_formats = _PATTERNS.star_reset_inside.sub(r"\1_|default\2", formats)
|
|
536
|
+
else:
|
|
537
|
+
_formats = _PATTERNS.star_reset_inside.sub(r"\1_\2", formats)
|
|
538
|
+
|
|
539
|
+
if all((self.cls._get_replacement(k, self.default_color) != k) for k in self.cls._formats_to_keys(_formats)):
|
|
540
|
+
# ESCAPE THE FORMATTING CODE
|
|
541
|
+
escaped = f"[{self.escape_char}{formats}]"
|
|
542
|
+
if auto_reset_txt:
|
|
543
|
+
# RECURSIVELY ESCAPE FORMATTING IN AUTO-RESET TEXT
|
|
544
|
+
escaped_auto_reset = self.cls.escape(auto_reset_txt, self.default_color, self.escape_char)
|
|
545
|
+
escaped += f"({escaped_auto_reset})"
|
|
546
|
+
return escaped
|
|
547
|
+
else:
|
|
548
|
+
# KEEP INVALID FORMATTING CODES AS-IS
|
|
549
|
+
result = f"[{formats}]"
|
|
550
|
+
if auto_reset_txt:
|
|
551
|
+
# STILL RECURSIVELY PROCESS AUTO-RESET TEXT
|
|
552
|
+
escaped_auto_reset = self.cls.escape(auto_reset_txt, self.default_color, self.escape_char)
|
|
553
|
+
result += f"({escaped_auto_reset})"
|
|
554
|
+
return result
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class _RemAnsiSeqHelper:
|
|
558
|
+
"""Internal, callable helper class to remove ANSI sequences and track their removal positions."""
|
|
559
|
+
|
|
560
|
+
def __init__(self, removals: list[tuple[int, str]]):
|
|
561
|
+
self.removals = removals
|
|
562
|
+
|
|
563
|
+
def __call__(self, match: _rx.Match[str]) -> str:
|
|
564
|
+
start_pos = match.start() - sum(len(removed) for _, removed in self.removals)
|
|
565
|
+
if self.removals and self.removals[-1][0] == start_pos:
|
|
566
|
+
start_pos = self.removals[-1][0]
|
|
567
|
+
self.removals.append((start_pos, match.group()))
|
|
568
|
+
return ""
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
class _ReplaceKeysHelper:
|
|
572
|
+
"""Internal, callable helper class to replace formatting keys with their respective ANSI codes."""
|
|
573
|
+
|
|
574
|
+
def __init__(
|
|
575
|
+
self,
|
|
576
|
+
cls: type[FormatCodes],
|
|
577
|
+
use_default: bool,
|
|
578
|
+
default_color: Optional[rgba],
|
|
579
|
+
brightness_steps: int,
|
|
580
|
+
):
|
|
581
|
+
self.cls = cls
|
|
582
|
+
self.use_default = use_default
|
|
583
|
+
self.default_color = default_color
|
|
584
|
+
self.brightness_steps = brightness_steps
|
|
585
|
+
|
|
586
|
+
# INSTANCE VARIABLES FOR CURRENT PROCESSING STATE
|
|
587
|
+
self.formats: str = ""
|
|
588
|
+
self.original_formats: str = ""
|
|
589
|
+
self.formats_escaped: bool = False
|
|
590
|
+
self.auto_reset_escaped: bool = False
|
|
591
|
+
self.auto_reset_txt: Optional[str] = None
|
|
592
|
+
self.format_keys: list[str] = []
|
|
593
|
+
self.ansi_formats: list[str] = []
|
|
594
|
+
self.ansi_resets: list[str] = []
|
|
595
|
+
|
|
596
|
+
def __call__(self, match: _rx.Match[str]) -> str:
|
|
597
|
+
self.original_formats = self.formats = match.group(1)
|
|
598
|
+
self.auto_reset_escaped = bool(match.group(2))
|
|
599
|
+
self.auto_reset_txt = match.group(3)
|
|
600
|
+
|
|
601
|
+
# CHECK IF THERE'S ESCAPED FORMAT CODES
|
|
602
|
+
self.formats_escaped = bool(_PATTERNS.escape_char_cond.match(match.group(0)))
|
|
603
|
+
if self.formats_escaped:
|
|
604
|
+
self.original_formats = self.formats = _PATTERNS.escape_char.sub(r"\1", self.formats)
|
|
605
|
+
|
|
606
|
+
self.process_formats_and_auto_reset()
|
|
607
|
+
|
|
608
|
+
if not self.formats:
|
|
609
|
+
return match.group(0)
|
|
610
|
+
|
|
611
|
+
self.convert_to_ansi()
|
|
612
|
+
return self.build_output(match)
|
|
613
|
+
|
|
614
|
+
def process_formats_and_auto_reset(self) -> None:
|
|
615
|
+
"""Process nested formatting in both formats and auto-reset text."""
|
|
616
|
+
# PROCESS AUTO-RESET TEXT IF IT CONTAINS NESTED FORMATTING
|
|
617
|
+
if self.auto_reset_txt and self.auto_reset_txt.count("[") > 0 and self.auto_reset_txt.count("]") > 0:
|
|
618
|
+
self.auto_reset_txt = self.cls.to_ansi(
|
|
619
|
+
self.auto_reset_txt,
|
|
620
|
+
self.default_color,
|
|
621
|
+
self.brightness_steps,
|
|
622
|
+
_default_start=False,
|
|
623
|
+
_validate_default=False,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# PROCESS NESTED FORMATTING IN FORMATS
|
|
627
|
+
if self.formats and self.formats.count("[") > 0 and self.formats.count("]") > 0:
|
|
628
|
+
self.formats = self.cls.to_ansi(
|
|
629
|
+
self.formats,
|
|
630
|
+
self.default_color,
|
|
631
|
+
self.brightness_steps,
|
|
632
|
+
_default_start=False,
|
|
633
|
+
_validate_default=False,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
def convert_to_ansi(self) -> None:
|
|
637
|
+
"""Convert format keys to ANSI codes and generate resets if needed."""
|
|
638
|
+
self.format_keys = self.cls._formats_to_keys(self.formats)
|
|
639
|
+
self.ansi_formats = [
|
|
640
|
+
r if (r := self.cls._get_replacement(k, self.default_color, self.brightness_steps)) != k else f"[{k}]"
|
|
641
|
+
for k in self.format_keys
|
|
642
|
+
]
|
|
643
|
+
|
|
644
|
+
# GENERATE RESET CODES IF AUTO-RESET IS ACTIVE
|
|
645
|
+
if self.auto_reset_txt and not self.auto_reset_escaped:
|
|
646
|
+
self.gen_reset_codes()
|
|
647
|
+
else:
|
|
648
|
+
self.ansi_resets = []
|
|
649
|
+
|
|
650
|
+
def gen_reset_codes(self) -> None:
|
|
651
|
+
"""Generate appropriate ANSI reset codes for each format key."""
|
|
652
|
+
default_color_resets = ("_bg", "default") if self.use_default else ("_bg", "_c")
|
|
653
|
+
reset_keys: list[str] = []
|
|
654
|
+
|
|
655
|
+
for k in self.format_keys:
|
|
656
|
+
k_lower = k.lower()
|
|
657
|
+
k_set = set(k_lower.split(":"))
|
|
658
|
+
|
|
659
|
+
# BACKGROUND COLOR FORMAT
|
|
660
|
+
if _PREFIX["BG"] & k_set and len(k_set) <= 3:
|
|
661
|
+
if k_set & _PREFIX["BR"]:
|
|
662
|
+
# BRIGHT BACKGROUND COLOR - RESET BOTH BG AND COLOR
|
|
663
|
+
for i in range(len(k)):
|
|
664
|
+
if self.is_valid_color(k[i:]):
|
|
665
|
+
reset_keys.extend(default_color_resets)
|
|
666
|
+
break
|
|
667
|
+
else:
|
|
668
|
+
# REGULAR BACKGROUND COLOR - RESET ONLY BG
|
|
669
|
+
for i in range(len(k)):
|
|
670
|
+
if self.is_valid_color(k[i:]):
|
|
671
|
+
reset_keys.append("_bg")
|
|
672
|
+
break
|
|
673
|
+
|
|
674
|
+
# TEXT COLOR FORMAT
|
|
675
|
+
elif self.is_valid_color(k) or any(
|
|
676
|
+
k_lower.startswith(pref_colon := f"{prefix}:") and self.is_valid_color(k[len(pref_colon):]) \
|
|
677
|
+
for prefix in _PREFIX["BR"]
|
|
678
|
+
):
|
|
679
|
+
reset_keys.append(default_color_resets[1])
|
|
680
|
+
|
|
681
|
+
# TEXT STYLE FORMAT
|
|
682
|
+
else:
|
|
683
|
+
reset_keys.append(f"_{k}")
|
|
684
|
+
|
|
685
|
+
# CONVERT RESET KEYS TO ANSI CODES
|
|
686
|
+
self.ansi_resets = [
|
|
687
|
+
r for k in reset_keys if ( \
|
|
688
|
+
r := self.cls._get_replacement(k, self.default_color, self.brightness_steps)
|
|
689
|
+
).startswith(f"{ANSI.CHAR}{ANSI.START}")
|
|
690
|
+
]
|
|
691
|
+
|
|
692
|
+
def build_output(self, match: _rx.Match[str]) -> str:
|
|
693
|
+
"""Build the final output string based on processed formats and resets."""
|
|
694
|
+
# CHECK IF ALL FORMATS WERE VALID
|
|
695
|
+
has_single_valid_ansi = len(self.ansi_formats) == 1 and self.ansi_formats[0].count(f"{ANSI.CHAR}{ANSI.START}") >= 1
|
|
696
|
+
all_formats_valid = all(ansi_format.startswith(f"{ANSI.CHAR}{ANSI.START}") for ansi_format in self.ansi_formats)
|
|
697
|
+
|
|
698
|
+
if not has_single_valid_ansi and not all_formats_valid:
|
|
699
|
+
return match.group(0)
|
|
700
|
+
|
|
701
|
+
# HANDLE ESCAPED FORMATTING
|
|
702
|
+
if self.formats_escaped:
|
|
703
|
+
return f"[{self.original_formats}]({self.auto_reset_txt})" if self.auto_reset_txt else f"[{self.original_formats}]"
|
|
704
|
+
|
|
705
|
+
# BUILD NORMAL OUTPUT WITH FORMATS AND RESETS
|
|
706
|
+
output = "".join(self.ansi_formats)
|
|
707
|
+
|
|
708
|
+
# ADD AUTO-RESET TEXT
|
|
709
|
+
if self.auto_reset_escaped and self.auto_reset_txt:
|
|
710
|
+
output += f"({self.cls.to_ansi(self.auto_reset_txt, self.default_color, self.brightness_steps, _default_start=False, _validate_default=False)})"
|
|
711
|
+
elif self.auto_reset_txt:
|
|
712
|
+
output += self.auto_reset_txt
|
|
713
|
+
|
|
714
|
+
# ADD RESET CODES IF NOT ESCAPED
|
|
715
|
+
if not self.auto_reset_escaped:
|
|
716
|
+
output += "".join(self.ansi_resets)
|
|
717
|
+
|
|
718
|
+
return output
|
|
719
|
+
|
|
720
|
+
def is_valid_color(self, color: str) -> bool:
|
|
721
|
+
"""Check whether the given color string is a valid formatting-key color."""
|
|
722
|
+
return bool((color in ANSI.COLOR_MAP) or Color.is_valid_rgba(color) or Color.is_valid_hexa(color))
|