escape-colors 0.1.0__tar.gz
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.
- escape_colors-0.1.0/PKG-INFO +241 -0
- escape_colors-0.1.0/README.md +230 -0
- escape_colors-0.1.0/pyproject.toml +19 -0
- escape_colors-0.1.0/pyproject.toml.orig +18 -0
- escape_colors-0.1.0/src/escape_colors/__init__.py +84 -0
- escape_colors-0.1.0/src/escape_colors/_codes.py +55 -0
- escape_colors-0.1.0/src/escape_colors/_notation.py +39 -0
- escape_colors-0.1.0/src/escape_colors/_parser.py +73 -0
- escape_colors-0.1.0/src/escape_colors/_terminal.py +59 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: escape-colors
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ANSI colors, nested text styles, and portable terminal printing
|
|
5
|
+
Author: ellainng
|
|
6
|
+
Author-email: ellainng <299958882+ellainng@users.noreply.github.com>
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Project-URL: Repository, https://github.com/outside-labs/escape_colors
|
|
9
|
+
Project-URL: Issues, https://github.com/outside-labs/escape_colors/issues
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Escape Colors
|
|
13
|
+
|
|
14
|
+
Readable ANSI colors and nested text styles for Python 3.14+, with no runtime
|
|
15
|
+
dependencies.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from escape_colors import ansi_print, blue, escape, escape_ansi, init_windows
|
|
19
|
+
|
|
20
|
+
ansi_print("blue(hello world)")
|
|
21
|
+
ansi_print("bold(blue(hello red(world) again))")
|
|
22
|
+
|
|
23
|
+
# For direct print/f-string output on Windows:
|
|
24
|
+
init_windows()
|
|
25
|
+
print(escape_ansi("blue(hello red(world))"))
|
|
26
|
+
print(f"{blue}hello world{escape}")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`escape_ansi` returns a string containing real ANSI SGR codes. `ansi_print` renders
|
|
30
|
+
markup and chooses whether to use color for the destination stream. Redirected
|
|
31
|
+
output is plain by default; use `color=True` to force ANSI sequences.
|
|
32
|
+
|
|
33
|
+
## Install and develop
|
|
34
|
+
|
|
35
|
+
Install from PyPI with Python 3.14 or later:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
python -m pip install escape-colors
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
From this checkout:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uv sync --locked
|
|
45
|
+
uv run python examples/basic.py
|
|
46
|
+
python tools/review_source.py
|
|
47
|
+
uv run python -m unittest discover -s tests -v
|
|
48
|
+
uv build
|
|
49
|
+
uv run python tools/verify_distribution.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Install a built wheel into another Python environment with
|
|
53
|
+
`uv pip install /path/to/escape_colors-0.1.0-py3-none-any.whl`.
|
|
54
|
+
|
|
55
|
+
## Releases
|
|
56
|
+
|
|
57
|
+
Publish a GitHub release with a tag matching the package version, such as
|
|
58
|
+
`v0.1.0`. `.github/workflows/release.yaml` tests and verifies the distributions
|
|
59
|
+
before publishing to PyPI through the `pypi` environment using Trusted Publishing.
|
|
60
|
+
The workflow can also be run manually against an existing release tag.
|
|
61
|
+
|
|
62
|
+
## Colors and styles
|
|
63
|
+
|
|
64
|
+
The 16 named foreground colors are `black`, `red`, `green`, `yellow`, `blue`,
|
|
65
|
+
`magenta`, `cyan`, and `white`, plus `bright_black`, `bright_red`, `bright_green`,
|
|
66
|
+
`bright_yellow`, `bright_blue`, `bright_magenta`, `bright_cyan`, and `bright_white`.
|
|
67
|
+
Add `bg_` for a background: `bg_blue`, `bg_bright_white`, and so on. Each name is
|
|
68
|
+
both a markup expression and an importable ANSI string constant.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from escape_colors import background, color, escape, escape_ansi
|
|
72
|
+
|
|
73
|
+
text = escape_ansi("bright_blue(hello bg_white(world))")
|
|
74
|
+
indexed = escape_ansi("15(white) 196(red) bg_232(dark background)")
|
|
75
|
+
direct = f"{color(196)}red {background(232)}on dark{escape}"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Numeric markup selects a **256-color palette index**, not an SGR parameter:
|
|
79
|
+
`15(text)` means `ESC[38;5;15m`, and `bg_15(text)` means `ESC[48;5;15m`.
|
|
80
|
+
Indices 0–15 address the base palette, 16–231 the color cube, and 232–255 the
|
|
81
|
+
grayscale ramp. The terminal controls the actual palette and appearance.
|
|
82
|
+
`color(index)` and `background(index)` accept integers 0–255; booleans, floats,
|
|
83
|
+
strings, and values outside that range are rejected.
|
|
84
|
+
|
|
85
|
+
| Expression / constant | SGR | Effect |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `bold` | 1 | Bold/increased intensity |
|
|
88
|
+
| `faint`, `dim` | 2 | Decreased intensity |
|
|
89
|
+
| `italic`, `italics` | 3 | Italic |
|
|
90
|
+
| `underline` | 4 | Underline |
|
|
91
|
+
| `double_underline` | 21 | Double underline |
|
|
92
|
+
| `blink` | 5 | Slow blink |
|
|
93
|
+
| `reverse` | 7 | Swap foreground/background |
|
|
94
|
+
| `conceal` | 8 | Conceal text |
|
|
95
|
+
| `strike`, `strikethrough` | 9 | Strike through |
|
|
96
|
+
| `overline` | 53 | Overline |
|
|
97
|
+
| `font0` … `font9` | 10 … 19 | Default/alternative terminal font |
|
|
98
|
+
| `escape`, `reset` | 0 | Reset constant for direct output |
|
|
99
|
+
|
|
100
|
+
Use `bold(text)`, `underline(text)`, `italics(text)`, or combine them by nesting.
|
|
101
|
+
When an inner expression closes, its enclosing styles are restored. A top-level
|
|
102
|
+
expression ends with a reset. Font selection, italics, blink, and other attributes
|
|
103
|
+
depend on terminal support. SGR does not select arbitrary installed typefaces or
|
|
104
|
+
font sizes; older terminals may treat SGR 21 as an intensity change.
|
|
105
|
+
`escape` and `reset` are constants, rather than markup expressions.
|
|
106
|
+
|
|
107
|
+
## Grammar and literal text
|
|
108
|
+
|
|
109
|
+
Markup is case-sensitive. A standalone ASCII name or decimal index immediately
|
|
110
|
+
followed by `(` opens an expression. Whitespace between a name and `(` makes it
|
|
111
|
+
ordinary text. Unknown names and ordinary parentheses remain in the output;
|
|
112
|
+
recognized expressions inside unknown parentheses still work.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from escape_colors import escape_ansi, strip_ansi
|
|
116
|
+
|
|
117
|
+
assert strip_ansi(escape_ansi("blue(hello (world))")) == "hello (world)"
|
|
118
|
+
assert escape_ansi("unknown(hello)") == "unknown(hello)"
|
|
119
|
+
assert escape_ansi(r"blue\(literal\)") == "blue(literal)"
|
|
120
|
+
assert strip_ansi(escape_ansi(r"blue(a\) b\( c\\d)")) == "a) b( c\\d"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`\(` and `\)` quote parentheses; `\\` quotes a backslash. Other backslashes,
|
|
124
|
+
Unicode characters, line breaks, and tabs are preserved. Recognized unclosed
|
|
125
|
+
expressions and invalid numeric color indices raise `ValueError` with a
|
|
126
|
+
zero-based character position. Unmatched ordinary parentheses remain literal.
|
|
127
|
+
The parser is iterative and does not evaluate Python or depend on its recursion
|
|
128
|
+
limit. Existing raw SGR codes pass through; scope restoration tracks markup
|
|
129
|
+
styles, rather than arbitrary control sequences embedded in the input.
|
|
130
|
+
|
|
131
|
+
## Unicode, hex, and octal notation
|
|
132
|
+
|
|
133
|
+
These three Python literals hold exactly the same one-character string:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
assert "\u001b" == "\x1b" == "\033" == chr(27)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The spelling is consumed by Python when it creates the string. No format warning
|
|
140
|
+
is emitted, and normal terminal output is identical for all three choices.
|
|
141
|
+
`ESCAPE_COLOR_FORMAT` controls the visible representation produced by
|
|
142
|
+
`literal=True` or `format_ansi`, and defaults to `"unicode"`.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import escape_colors
|
|
146
|
+
|
|
147
|
+
escape_colors.ESCAPE_COLOR_FORMAT = "octal"
|
|
148
|
+
assert escape_colors.escape_ansi("blue(hello)", literal=True) == r"\033[34mhello\033[0m"
|
|
149
|
+
assert escape_colors.escape_ansi("blue(hello)", literal=True, format="hex") == r"\x1b[34mhello\x1b[0m"
|
|
150
|
+
|
|
151
|
+
# The live runtime string always contains actual ESC characters.
|
|
152
|
+
assert escape_colors.escape_ansi("blue(hello)") == "\u001b[34mhello\u001b[0m"
|
|
153
|
+
escape_colors.ESCAPE_COLOR_FORMAT = "unicode"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Alternatively set the environment variable **before importing**:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
ESCAPE_COLOR_FORMAT=octal uv run python examples/basic.py
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
On PowerShell, use `$env:ESCAPE_COLOR_FORMAT = 'octal'` before starting Python.
|
|
163
|
+
Accepted values are exactly `unicode`, `hex`, and `octal`. A per-call `format`
|
|
164
|
+
overrides the package setting. Assign `escape_colors.ESCAPE_COLOR_FORMAT` on the
|
|
165
|
+
module; assigning a separately imported local name does not update the module.
|
|
166
|
+
Invalid settings raise an error when rendering or serializing.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from escape_colors import blue, escape, escape_ansi, format_ansi, normalize_ansi
|
|
170
|
+
|
|
171
|
+
assert format_ansi(f"{blue}café{escape}", format="unicode") == r"\u001b[34mcafé\u001b[0m"
|
|
172
|
+
assert normalize_ansi(r"\x1b[34mcafé\033[0m") == f"{blue}café{escape}"
|
|
173
|
+
assert escape_ansi(r"\u001b[34mcafé\033[0m", decode=True) == f"{blue}café{escape}"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Textual notation decoding is opt-in. `normalize_ansi` and `decode=True` recognize
|
|
177
|
+
only `\u001b`, `\x1b` (also with uppercase `B`), and `\033` followed by complete
|
|
178
|
+
numeric SGR sequences. They do not decode general Python escapes, change Unicode,
|
|
179
|
+
or decode a backslash-prefixed spelling. Literal output replaces ESC only: it is
|
|
180
|
+
readable escape notation, not a complete quoted/escaped Python string literal.
|
|
181
|
+
|
|
182
|
+
## Printing and Windows
|
|
183
|
+
|
|
184
|
+
`ansi_print(*objects, sep=" ", end="\n", file=None, flush=False, color=None)`
|
|
185
|
+
renders each object's string representation and supports the usual `print`
|
|
186
|
+
arguments, including `sep=None` and `end=None`. It returns `None`.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
import io
|
|
190
|
+
from escape_colors import ansi_print
|
|
191
|
+
|
|
192
|
+
capture = io.StringIO()
|
|
193
|
+
ansi_print("blue(hello)", "bold(world)", sep=" / ", end="!", file=capture)
|
|
194
|
+
assert capture.getvalue() == "hello / world!"
|
|
195
|
+
ansi_print("196(forced ANSI)", color=True)
|
|
196
|
+
ansi_print("blue(plain text)", color=False)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- `color=None`: use color for a TTY unless `NO_COLOR` is nonempty or `TERM=dumb`.
|
|
200
|
+
Streams without a working `isatty()` get plain text. Windows also needs
|
|
201
|
+
successful VT initialization.
|
|
202
|
+
- `color=True`: attempt Windows initialization and emit ANSI, even on redirection
|
|
203
|
+
or if initialization fails. This overrides environment color preferences.
|
|
204
|
+
- `color=False`: remove rendered SGR codes, including existing SGR in objects,
|
|
205
|
+
separators, and endings.
|
|
206
|
+
|
|
207
|
+
`ansi_print` initializes Windows automatically for the destination stream.
|
|
208
|
+
`init_windows(file=None)` is available for direct `print`/f-string output and can
|
|
209
|
+
also initialize stderr or another console stream. It preserves existing console
|
|
210
|
+
mode bits and enables processed output and virtual-terminal processing. Repeated
|
|
211
|
+
calls are safe. It returns `True` on Windows success and `False` on other systems,
|
|
212
|
+
unsupported consoles, or redirected/invalid handles. Importing the module does
|
|
213
|
+
not change console state. Obsolete Windows consoles without VT support are not
|
|
214
|
+
emulated; automatic printing uses plain text there.
|
|
215
|
+
|
|
216
|
+
`strip_ansi` removes numeric SGR sequences only. These APIs format text and do not
|
|
217
|
+
sanitize arbitrary terminal control commands in untrusted input.
|
|
218
|
+
|
|
219
|
+
## Verification and references
|
|
220
|
+
|
|
221
|
+
`python tools/review_source.py` performs a static review using only Python's
|
|
222
|
+
standard library. It checks first-party Python files for syntax errors and
|
|
223
|
+
`SyntaxWarning`s, checks runtime imports against the standard library and local
|
|
224
|
+
modules, and reports missing internal modules. It compiles source for validation
|
|
225
|
+
without executing the inspected code. Use `--format json` for structured results
|
|
226
|
+
or `--root /path/to/checkout` for another source tree. Findings exit with status 1;
|
|
227
|
+
invalid invocation exits with status 2. Review covers ordinary import statements,
|
|
228
|
+
including conditional imports; dynamic imports and imported attributes are
|
|
229
|
+
outside these checks. Runtime behavior is covered by the test suite.
|
|
230
|
+
|
|
231
|
+
CI tests Python 3.14 on Linux, macOS, and Windows, builds the wheel and source
|
|
232
|
+
distribution, and runs the suite from freshly installed wheel artifacts outside
|
|
233
|
+
the checkout. The verification script also rebuilds a wheel from the source
|
|
234
|
+
archive. Tests cover nested restoration, every indexed color, notation, Unicode,
|
|
235
|
+
printing, and Windows API behavior. Visual support still depends on the terminal.
|
|
236
|
+
|
|
237
|
+
- [Python string escapes](https://docs.python.org/3.14/reference/lexical_analysis.html#escape-sequences)
|
|
238
|
+
- [ANSI escape code overview](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
|
239
|
+
- [Xterm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
|
240
|
+
- [Windows VT sequences](https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
|
|
241
|
+
- [SetConsoleMode](https://learn.microsoft.com/en-us/windows/console/setconsolemode)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Escape Colors
|
|
2
|
+
|
|
3
|
+
Readable ANSI colors and nested text styles for Python 3.14+, with no runtime
|
|
4
|
+
dependencies.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from escape_colors import ansi_print, blue, escape, escape_ansi, init_windows
|
|
8
|
+
|
|
9
|
+
ansi_print("blue(hello world)")
|
|
10
|
+
ansi_print("bold(blue(hello red(world) again))")
|
|
11
|
+
|
|
12
|
+
# For direct print/f-string output on Windows:
|
|
13
|
+
init_windows()
|
|
14
|
+
print(escape_ansi("blue(hello red(world))"))
|
|
15
|
+
print(f"{blue}hello world{escape}")
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`escape_ansi` returns a string containing real ANSI SGR codes. `ansi_print` renders
|
|
19
|
+
markup and chooses whether to use color for the destination stream. Redirected
|
|
20
|
+
output is plain by default; use `color=True` to force ANSI sequences.
|
|
21
|
+
|
|
22
|
+
## Install and develop
|
|
23
|
+
|
|
24
|
+
Install from PyPI with Python 3.14 or later:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
python -m pip install escape-colors
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
From this checkout:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uv sync --locked
|
|
34
|
+
uv run python examples/basic.py
|
|
35
|
+
python tools/review_source.py
|
|
36
|
+
uv run python -m unittest discover -s tests -v
|
|
37
|
+
uv build
|
|
38
|
+
uv run python tools/verify_distribution.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Install a built wheel into another Python environment with
|
|
42
|
+
`uv pip install /path/to/escape_colors-0.1.0-py3-none-any.whl`.
|
|
43
|
+
|
|
44
|
+
## Releases
|
|
45
|
+
|
|
46
|
+
Publish a GitHub release with a tag matching the package version, such as
|
|
47
|
+
`v0.1.0`. `.github/workflows/release.yaml` tests and verifies the distributions
|
|
48
|
+
before publishing to PyPI through the `pypi` environment using Trusted Publishing.
|
|
49
|
+
The workflow can also be run manually against an existing release tag.
|
|
50
|
+
|
|
51
|
+
## Colors and styles
|
|
52
|
+
|
|
53
|
+
The 16 named foreground colors are `black`, `red`, `green`, `yellow`, `blue`,
|
|
54
|
+
`magenta`, `cyan`, and `white`, plus `bright_black`, `bright_red`, `bright_green`,
|
|
55
|
+
`bright_yellow`, `bright_blue`, `bright_magenta`, `bright_cyan`, and `bright_white`.
|
|
56
|
+
Add `bg_` for a background: `bg_blue`, `bg_bright_white`, and so on. Each name is
|
|
57
|
+
both a markup expression and an importable ANSI string constant.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from escape_colors import background, color, escape, escape_ansi
|
|
61
|
+
|
|
62
|
+
text = escape_ansi("bright_blue(hello bg_white(world))")
|
|
63
|
+
indexed = escape_ansi("15(white) 196(red) bg_232(dark background)")
|
|
64
|
+
direct = f"{color(196)}red {background(232)}on dark{escape}"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Numeric markup selects a **256-color palette index**, not an SGR parameter:
|
|
68
|
+
`15(text)` means `ESC[38;5;15m`, and `bg_15(text)` means `ESC[48;5;15m`.
|
|
69
|
+
Indices 0–15 address the base palette, 16–231 the color cube, and 232–255 the
|
|
70
|
+
grayscale ramp. The terminal controls the actual palette and appearance.
|
|
71
|
+
`color(index)` and `background(index)` accept integers 0–255; booleans, floats,
|
|
72
|
+
strings, and values outside that range are rejected.
|
|
73
|
+
|
|
74
|
+
| Expression / constant | SGR | Effect |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `bold` | 1 | Bold/increased intensity |
|
|
77
|
+
| `faint`, `dim` | 2 | Decreased intensity |
|
|
78
|
+
| `italic`, `italics` | 3 | Italic |
|
|
79
|
+
| `underline` | 4 | Underline |
|
|
80
|
+
| `double_underline` | 21 | Double underline |
|
|
81
|
+
| `blink` | 5 | Slow blink |
|
|
82
|
+
| `reverse` | 7 | Swap foreground/background |
|
|
83
|
+
| `conceal` | 8 | Conceal text |
|
|
84
|
+
| `strike`, `strikethrough` | 9 | Strike through |
|
|
85
|
+
| `overline` | 53 | Overline |
|
|
86
|
+
| `font0` … `font9` | 10 … 19 | Default/alternative terminal font |
|
|
87
|
+
| `escape`, `reset` | 0 | Reset constant for direct output |
|
|
88
|
+
|
|
89
|
+
Use `bold(text)`, `underline(text)`, `italics(text)`, or combine them by nesting.
|
|
90
|
+
When an inner expression closes, its enclosing styles are restored. A top-level
|
|
91
|
+
expression ends with a reset. Font selection, italics, blink, and other attributes
|
|
92
|
+
depend on terminal support. SGR does not select arbitrary installed typefaces or
|
|
93
|
+
font sizes; older terminals may treat SGR 21 as an intensity change.
|
|
94
|
+
`escape` and `reset` are constants, rather than markup expressions.
|
|
95
|
+
|
|
96
|
+
## Grammar and literal text
|
|
97
|
+
|
|
98
|
+
Markup is case-sensitive. A standalone ASCII name or decimal index immediately
|
|
99
|
+
followed by `(` opens an expression. Whitespace between a name and `(` makes it
|
|
100
|
+
ordinary text. Unknown names and ordinary parentheses remain in the output;
|
|
101
|
+
recognized expressions inside unknown parentheses still work.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from escape_colors import escape_ansi, strip_ansi
|
|
105
|
+
|
|
106
|
+
assert strip_ansi(escape_ansi("blue(hello (world))")) == "hello (world)"
|
|
107
|
+
assert escape_ansi("unknown(hello)") == "unknown(hello)"
|
|
108
|
+
assert escape_ansi(r"blue\(literal\)") == "blue(literal)"
|
|
109
|
+
assert strip_ansi(escape_ansi(r"blue(a\) b\( c\\d)")) == "a) b( c\\d"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`\(` and `\)` quote parentheses; `\\` quotes a backslash. Other backslashes,
|
|
113
|
+
Unicode characters, line breaks, and tabs are preserved. Recognized unclosed
|
|
114
|
+
expressions and invalid numeric color indices raise `ValueError` with a
|
|
115
|
+
zero-based character position. Unmatched ordinary parentheses remain literal.
|
|
116
|
+
The parser is iterative and does not evaluate Python or depend on its recursion
|
|
117
|
+
limit. Existing raw SGR codes pass through; scope restoration tracks markup
|
|
118
|
+
styles, rather than arbitrary control sequences embedded in the input.
|
|
119
|
+
|
|
120
|
+
## Unicode, hex, and octal notation
|
|
121
|
+
|
|
122
|
+
These three Python literals hold exactly the same one-character string:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
assert "\u001b" == "\x1b" == "\033" == chr(27)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The spelling is consumed by Python when it creates the string. No format warning
|
|
129
|
+
is emitted, and normal terminal output is identical for all three choices.
|
|
130
|
+
`ESCAPE_COLOR_FORMAT` controls the visible representation produced by
|
|
131
|
+
`literal=True` or `format_ansi`, and defaults to `"unicode"`.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import escape_colors
|
|
135
|
+
|
|
136
|
+
escape_colors.ESCAPE_COLOR_FORMAT = "octal"
|
|
137
|
+
assert escape_colors.escape_ansi("blue(hello)", literal=True) == r"\033[34mhello\033[0m"
|
|
138
|
+
assert escape_colors.escape_ansi("blue(hello)", literal=True, format="hex") == r"\x1b[34mhello\x1b[0m"
|
|
139
|
+
|
|
140
|
+
# The live runtime string always contains actual ESC characters.
|
|
141
|
+
assert escape_colors.escape_ansi("blue(hello)") == "\u001b[34mhello\u001b[0m"
|
|
142
|
+
escape_colors.ESCAPE_COLOR_FORMAT = "unicode"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Alternatively set the environment variable **before importing**:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
ESCAPE_COLOR_FORMAT=octal uv run python examples/basic.py
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
On PowerShell, use `$env:ESCAPE_COLOR_FORMAT = 'octal'` before starting Python.
|
|
152
|
+
Accepted values are exactly `unicode`, `hex`, and `octal`. A per-call `format`
|
|
153
|
+
overrides the package setting. Assign `escape_colors.ESCAPE_COLOR_FORMAT` on the
|
|
154
|
+
module; assigning a separately imported local name does not update the module.
|
|
155
|
+
Invalid settings raise an error when rendering or serializing.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from escape_colors import blue, escape, escape_ansi, format_ansi, normalize_ansi
|
|
159
|
+
|
|
160
|
+
assert format_ansi(f"{blue}café{escape}", format="unicode") == r"\u001b[34mcafé\u001b[0m"
|
|
161
|
+
assert normalize_ansi(r"\x1b[34mcafé\033[0m") == f"{blue}café{escape}"
|
|
162
|
+
assert escape_ansi(r"\u001b[34mcafé\033[0m", decode=True) == f"{blue}café{escape}"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Textual notation decoding is opt-in. `normalize_ansi` and `decode=True` recognize
|
|
166
|
+
only `\u001b`, `\x1b` (also with uppercase `B`), and `\033` followed by complete
|
|
167
|
+
numeric SGR sequences. They do not decode general Python escapes, change Unicode,
|
|
168
|
+
or decode a backslash-prefixed spelling. Literal output replaces ESC only: it is
|
|
169
|
+
readable escape notation, not a complete quoted/escaped Python string literal.
|
|
170
|
+
|
|
171
|
+
## Printing and Windows
|
|
172
|
+
|
|
173
|
+
`ansi_print(*objects, sep=" ", end="\n", file=None, flush=False, color=None)`
|
|
174
|
+
renders each object's string representation and supports the usual `print`
|
|
175
|
+
arguments, including `sep=None` and `end=None`. It returns `None`.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import io
|
|
179
|
+
from escape_colors import ansi_print
|
|
180
|
+
|
|
181
|
+
capture = io.StringIO()
|
|
182
|
+
ansi_print("blue(hello)", "bold(world)", sep=" / ", end="!", file=capture)
|
|
183
|
+
assert capture.getvalue() == "hello / world!"
|
|
184
|
+
ansi_print("196(forced ANSI)", color=True)
|
|
185
|
+
ansi_print("blue(plain text)", color=False)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- `color=None`: use color for a TTY unless `NO_COLOR` is nonempty or `TERM=dumb`.
|
|
189
|
+
Streams without a working `isatty()` get plain text. Windows also needs
|
|
190
|
+
successful VT initialization.
|
|
191
|
+
- `color=True`: attempt Windows initialization and emit ANSI, even on redirection
|
|
192
|
+
or if initialization fails. This overrides environment color preferences.
|
|
193
|
+
- `color=False`: remove rendered SGR codes, including existing SGR in objects,
|
|
194
|
+
separators, and endings.
|
|
195
|
+
|
|
196
|
+
`ansi_print` initializes Windows automatically for the destination stream.
|
|
197
|
+
`init_windows(file=None)` is available for direct `print`/f-string output and can
|
|
198
|
+
also initialize stderr or another console stream. It preserves existing console
|
|
199
|
+
mode bits and enables processed output and virtual-terminal processing. Repeated
|
|
200
|
+
calls are safe. It returns `True` on Windows success and `False` on other systems,
|
|
201
|
+
unsupported consoles, or redirected/invalid handles. Importing the module does
|
|
202
|
+
not change console state. Obsolete Windows consoles without VT support are not
|
|
203
|
+
emulated; automatic printing uses plain text there.
|
|
204
|
+
|
|
205
|
+
`strip_ansi` removes numeric SGR sequences only. These APIs format text and do not
|
|
206
|
+
sanitize arbitrary terminal control commands in untrusted input.
|
|
207
|
+
|
|
208
|
+
## Verification and references
|
|
209
|
+
|
|
210
|
+
`python tools/review_source.py` performs a static review using only Python's
|
|
211
|
+
standard library. It checks first-party Python files for syntax errors and
|
|
212
|
+
`SyntaxWarning`s, checks runtime imports against the standard library and local
|
|
213
|
+
modules, and reports missing internal modules. It compiles source for validation
|
|
214
|
+
without executing the inspected code. Use `--format json` for structured results
|
|
215
|
+
or `--root /path/to/checkout` for another source tree. Findings exit with status 1;
|
|
216
|
+
invalid invocation exits with status 2. Review covers ordinary import statements,
|
|
217
|
+
including conditional imports; dynamic imports and imported attributes are
|
|
218
|
+
outside these checks. Runtime behavior is covered by the test suite.
|
|
219
|
+
|
|
220
|
+
CI tests Python 3.14 on Linux, macOS, and Windows, builds the wheel and source
|
|
221
|
+
distribution, and runs the suite from freshly installed wheel artifacts outside
|
|
222
|
+
the checkout. The verification script also rebuilds a wheel from the source
|
|
223
|
+
archive. Tests cover nested restoration, every indexed color, notation, Unicode,
|
|
224
|
+
printing, and Windows API behavior. Visual support still depends on the terminal.
|
|
225
|
+
|
|
226
|
+
- [Python string escapes](https://docs.python.org/3.14/reference/lexical_analysis.html#escape-sequences)
|
|
227
|
+
- [ANSI escape code overview](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
|
228
|
+
- [Xterm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html)
|
|
229
|
+
- [Windows VT sequences](https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
|
|
230
|
+
- [SetConsoleMode](https://learn.microsoft.com/en-us/windows/console/setconsolemode)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "escape-colors"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "ANSI colors, nested text styles, and portable terminal printing"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[[project.authors]]
|
|
10
|
+
name = "ellainng"
|
|
11
|
+
email = "299958882+ellainng@users.noreply.github.com"
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
Repository = "https://github.com/outside-labs/escape_colors"
|
|
15
|
+
Issues = "https://github.com/outside-labs/escape_colors/issues"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.12.19,<0.13.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "escape-colors"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "ANSI colors, nested text styles, and portable terminal printing"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "ellainng", email = "299958882+ellainng@users.noreply.github.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Repository = "https://github.com/outside-labs/escape_colors"
|
|
14
|
+
Issues = "https://github.com/outside-labs/escape_colors/issues"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.12.19,<0.13.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""ANSI colors and formatting without runtime dependencies."""
|
|
2
|
+
|
|
3
|
+
import os as _os
|
|
4
|
+
import sys as _sys
|
|
5
|
+
from typing import TextIO as _TextIO
|
|
6
|
+
|
|
7
|
+
from ._codes import NAMED_CODES as _NAMED_CODES
|
|
8
|
+
from ._codes import RESET as escape
|
|
9
|
+
from ._codes import background, color
|
|
10
|
+
from ._notation import normalize_ansi, strip_ansi
|
|
11
|
+
from ._notation import require_text as _require_text
|
|
12
|
+
from ._notation import serialize as _serialize
|
|
13
|
+
from ._notation import validate_format as _validate_format
|
|
14
|
+
from ._parser import render as _render
|
|
15
|
+
from ._terminal import color_enabled as _color_enabled
|
|
16
|
+
from ._terminal import init_windows
|
|
17
|
+
|
|
18
|
+
ESCAPE_COLOR_FORMAT = _os.environ.get("ESCAPE_COLOR_FORMAT", "unicode")
|
|
19
|
+
reset = escape
|
|
20
|
+
|
|
21
|
+
# Named constants are ordinary strings, suitable for direct f-string use.
|
|
22
|
+
globals().update(_NAMED_CODES)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_ansi(text: str, *, format: str | None = None) -> str:
|
|
26
|
+
"""Write ESC using unicode, hex, or octal notation, preserving other text."""
|
|
27
|
+
return _serialize(text, ESCAPE_COLOR_FORMAT if format is None else format)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def escape_ansi(
|
|
31
|
+
text: str,
|
|
32
|
+
*,
|
|
33
|
+
format: str | None = None,
|
|
34
|
+
literal: bool = False,
|
|
35
|
+
decode: bool = False,
|
|
36
|
+
) -> str:
|
|
37
|
+
"""Render nested styles; optionally decode or serialize textual ESC notation."""
|
|
38
|
+
_require_text(text)
|
|
39
|
+
selected = _validate_format(ESCAPE_COLOR_FORMAT if format is None else format)
|
|
40
|
+
if not isinstance(literal, bool) or not isinstance(decode, bool):
|
|
41
|
+
raise TypeError("literal and decode must be bools")
|
|
42
|
+
rendered = _render(normalize_ansi(text) if decode else text)
|
|
43
|
+
return _serialize(rendered, selected) if literal else rendered
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ansi_print(
|
|
47
|
+
*objects: object,
|
|
48
|
+
sep: str | None = " ",
|
|
49
|
+
end: str | None = "\n",
|
|
50
|
+
file: _TextIO | None = None,
|
|
51
|
+
flush: bool = False,
|
|
52
|
+
color: bool | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Print nested markup, choosing color automatically unless overridden."""
|
|
55
|
+
if sep is not None and not isinstance(sep, str):
|
|
56
|
+
raise TypeError("sep must be None or a string")
|
|
57
|
+
if end is not None and not isinstance(end, str):
|
|
58
|
+
raise TypeError("end must be None or a string")
|
|
59
|
+
stream = _sys.stdout if file is None else file
|
|
60
|
+
enabled = _color_enabled(stream, color)
|
|
61
|
+
if stream is None:
|
|
62
|
+
return
|
|
63
|
+
rendered = [escape_ansi(str(obj)) for obj in objects]
|
|
64
|
+
if not enabled:
|
|
65
|
+
rendered = [strip_ansi(text) for text in rendered]
|
|
66
|
+
sep = strip_ansi(sep) if sep is not None else None
|
|
67
|
+
end = strip_ansi(end) if end is not None else None
|
|
68
|
+
print(*rendered, sep=sep, end=end, file=stream, flush=flush)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"ESCAPE_COLOR_FORMAT",
|
|
73
|
+
"ansi_print",
|
|
74
|
+
"background",
|
|
75
|
+
"color",
|
|
76
|
+
"escape",
|
|
77
|
+
"escape_ansi",
|
|
78
|
+
"format_ansi",
|
|
79
|
+
"init_windows",
|
|
80
|
+
"normalize_ansi",
|
|
81
|
+
"reset",
|
|
82
|
+
"strip_ansi",
|
|
83
|
+
*_NAMED_CODES,
|
|
84
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Select Graphic Rendition codes and indexed palette helpers."""
|
|
2
|
+
|
|
3
|
+
ESC = "\u001b"
|
|
4
|
+
RESET = f"{ESC}[0m"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _sgr(*parameters: int) -> str:
|
|
8
|
+
return f"{ESC}[{';'.join(map(str, parameters))}m"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _palette_index(index: int) -> int:
|
|
12
|
+
if isinstance(index, bool) or not isinstance(index, int):
|
|
13
|
+
raise TypeError("color index must be an integer")
|
|
14
|
+
if not 0 <= index <= 255:
|
|
15
|
+
raise ValueError("color index must be between 0 and 255")
|
|
16
|
+
return index
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def color(index: int) -> str:
|
|
20
|
+
"""Return the foreground SGR sequence for palette index 0 through 255."""
|
|
21
|
+
return _sgr(38, 5, _palette_index(index))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def background(index: int) -> str:
|
|
25
|
+
"""Return the background SGR sequence for palette index 0 through 255."""
|
|
26
|
+
return _sgr(48, 5, _palette_index(index))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
COLOR_NAMES = ("black", "red", "green", "yellow", "blue", "magenta", "cyan", "white")
|
|
30
|
+
NAMED_CODES = {}
|
|
31
|
+
for _offset, _name in enumerate(COLOR_NAMES):
|
|
32
|
+
NAMED_CODES[_name] = _sgr(30 + _offset)
|
|
33
|
+
NAMED_CODES[f"bright_{_name}"] = _sgr(90 + _offset)
|
|
34
|
+
NAMED_CODES[f"bg_{_name}"] = _sgr(40 + _offset)
|
|
35
|
+
NAMED_CODES[f"bg_bright_{_name}"] = _sgr(100 + _offset)
|
|
36
|
+
|
|
37
|
+
for _name, _parameter in {
|
|
38
|
+
"bold": 1,
|
|
39
|
+
"faint": 2,
|
|
40
|
+
"dim": 2,
|
|
41
|
+
"italic": 3,
|
|
42
|
+
"italics": 3,
|
|
43
|
+
"underline": 4,
|
|
44
|
+
"double_underline": 21,
|
|
45
|
+
"blink": 5,
|
|
46
|
+
"reverse": 7,
|
|
47
|
+
"conceal": 8,
|
|
48
|
+
"strike": 9,
|
|
49
|
+
"strikethrough": 9,
|
|
50
|
+
"overline": 53,
|
|
51
|
+
}.items():
|
|
52
|
+
NAMED_CODES[_name] = _sgr(_parameter)
|
|
53
|
+
|
|
54
|
+
for _font in range(10):
|
|
55
|
+
NAMED_CODES[f"font{_font}"] = _sgr(10 + _font)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Represent ESC without decoding unrelated user text."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ._codes import ESC
|
|
6
|
+
|
|
7
|
+
SPELLINGS = {"unicode": r"\u001b", "hex": r"\x1b", "octal": r"\033"}
|
|
8
|
+
SGR_PATTERN = re.compile(r"\u001b\[[0-9;]*m")
|
|
9
|
+
_LITERAL_ESC = re.compile(r"(?<!\\)(?:\\u001[bB]|\\x1[bB]|\\033)(?=\[[0-9;]*m)")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_format(format: str) -> str:
|
|
13
|
+
if not isinstance(format, str):
|
|
14
|
+
raise TypeError("escape format must be a string")
|
|
15
|
+
if format not in SPELLINGS:
|
|
16
|
+
raise ValueError("escape format must be 'unicode', 'hex', or 'octal'")
|
|
17
|
+
return format
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def require_text(text: str) -> None:
|
|
21
|
+
if not isinstance(text, str):
|
|
22
|
+
raise TypeError("text must be a string")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def serialize(text: str, format: str) -> str:
|
|
26
|
+
require_text(text)
|
|
27
|
+
return text.replace(ESC, SPELLINGS[validate_format(format)])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_ansi(text: str) -> str:
|
|
31
|
+
"""Decode textual ESC spellings only before complete numeric SGR codes."""
|
|
32
|
+
require_text(text)
|
|
33
|
+
return _LITERAL_ESC.sub(lambda match: ESC, text)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def strip_ansi(text: str) -> str:
|
|
37
|
+
"""Remove numeric SGR codes; leave other control sequences untouched."""
|
|
38
|
+
require_text(text)
|
|
39
|
+
return SGR_PATTERN.sub("", text)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""An iterative parser for nested style expressions."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ._codes import NAMED_CODES, RESET, background, color
|
|
6
|
+
|
|
7
|
+
_OPEN = re.compile(r"(bg_-?[0-9]+|[A-Za-z_][A-Za-z_0-9]*|-?[0-9]+)\(")
|
|
8
|
+
_INDEX = re.compile(r"-?[0-9]+\Z")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _indexed_code(token: str, position: int, *, bg: bool = False) -> str:
|
|
12
|
+
# Avoid Python's large integer string limit and unnecessary conversion work.
|
|
13
|
+
digits = token.lstrip("0") or "0"
|
|
14
|
+
if token.startswith("-") or len(digits) > 3 or int(digits) > 255:
|
|
15
|
+
raise ValueError(f"color index must be between 0 and 255 at position {position}")
|
|
16
|
+
return background(int(digits)) if bg else color(int(digits))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _style_code(token: str, position: int) -> str | None:
|
|
20
|
+
if token in NAMED_CODES:
|
|
21
|
+
return NAMED_CODES[token]
|
|
22
|
+
if _INDEX.fullmatch(token):
|
|
23
|
+
return _indexed_code(token, position)
|
|
24
|
+
if token.startswith("bg_") and _INDEX.fullmatch(token[3:]):
|
|
25
|
+
return _indexed_code(token[3:], position, bg=True)
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def render(text: str) -> str:
|
|
30
|
+
output = []
|
|
31
|
+
frames: list[tuple[str | None, int]] = []
|
|
32
|
+
active: list[str] = []
|
|
33
|
+
position = 0
|
|
34
|
+
|
|
35
|
+
while position < len(text):
|
|
36
|
+
char = text[position]
|
|
37
|
+
if char == "\\" and position + 1 < len(text) and text[position + 1] in "()\\":
|
|
38
|
+
output.append(text[position + 1])
|
|
39
|
+
position += 2
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
standalone = position == 0 or not (text[position - 1].isalnum() or text[position - 1] == "_")
|
|
43
|
+
opening = _OPEN.match(text, position) if standalone else None
|
|
44
|
+
if opening:
|
|
45
|
+
code = _style_code(opening[1], position)
|
|
46
|
+
frames.append((code, position))
|
|
47
|
+
if code is None:
|
|
48
|
+
output.append(opening[0])
|
|
49
|
+
else:
|
|
50
|
+
output.append(code)
|
|
51
|
+
active.append(code)
|
|
52
|
+
position = opening.end()
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
if char == "(":
|
|
56
|
+
frames.append((None, position))
|
|
57
|
+
output.append(char)
|
|
58
|
+
elif char == ")" and frames:
|
|
59
|
+
code, _ = frames.pop()
|
|
60
|
+
if code is None:
|
|
61
|
+
output.append(char)
|
|
62
|
+
else:
|
|
63
|
+
active.pop()
|
|
64
|
+
output.append(RESET)
|
|
65
|
+
output.extend(active)
|
|
66
|
+
else:
|
|
67
|
+
output.append(char)
|
|
68
|
+
position += 1
|
|
69
|
+
|
|
70
|
+
for code, start in frames:
|
|
71
|
+
if code is not None:
|
|
72
|
+
raise ValueError(f"unclosed style expression at position {start}")
|
|
73
|
+
return "".join(output)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Destination-specific terminal detection and Windows VT initialization."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TextIO
|
|
7
|
+
|
|
8
|
+
_PROCESSED_OUTPUT = 0x0001
|
|
9
|
+
_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def init_windows(file: TextIO | None = None) -> bool:
|
|
13
|
+
"""Enable Windows VT output on a stream; return False when unavailable."""
|
|
14
|
+
if os.name != "nt":
|
|
15
|
+
return False
|
|
16
|
+
stream = sys.stdout if file is None else file
|
|
17
|
+
try:
|
|
18
|
+
import msvcrt
|
|
19
|
+
from ctypes import wintypes
|
|
20
|
+
|
|
21
|
+
handle = msvcrt.get_osfhandle(stream.fileno())
|
|
22
|
+
if handle in (-1, 0):
|
|
23
|
+
return False
|
|
24
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
25
|
+
get_mode = kernel.GetConsoleMode
|
|
26
|
+
get_mode.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
|
|
27
|
+
get_mode.restype = wintypes.BOOL
|
|
28
|
+
set_mode = kernel.SetConsoleMode
|
|
29
|
+
set_mode.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
30
|
+
set_mode.restype = wintypes.BOOL
|
|
31
|
+
|
|
32
|
+
mode = wintypes.DWORD()
|
|
33
|
+
if not get_mode(handle, ctypes.byref(mode)):
|
|
34
|
+
return False
|
|
35
|
+
desired = mode.value | _PROCESSED_OUTPUT | _VIRTUAL_TERMINAL_PROCESSING
|
|
36
|
+
if desired == mode.value:
|
|
37
|
+
return True
|
|
38
|
+
return bool(set_mode(handle, desired))
|
|
39
|
+
except (AttributeError, ImportError, OSError, TypeError, ValueError, OverflowError):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def color_enabled(stream: TextIO, color: bool | None) -> bool:
|
|
44
|
+
if color is not None and not isinstance(color, bool):
|
|
45
|
+
raise TypeError("color must be True, False, or None")
|
|
46
|
+
if color is False:
|
|
47
|
+
return False
|
|
48
|
+
if color is True:
|
|
49
|
+
if os.name == "nt":
|
|
50
|
+
init_windows(stream)
|
|
51
|
+
return True
|
|
52
|
+
if os.environ.get("NO_COLOR") or os.environ.get("TERM") == "dumb":
|
|
53
|
+
return False
|
|
54
|
+
try:
|
|
55
|
+
if not stream.isatty():
|
|
56
|
+
return False
|
|
57
|
+
except (AttributeError, OSError, ValueError):
|
|
58
|
+
return False
|
|
59
|
+
return init_windows(stream) if os.name == "nt" else True
|