PyTermGUI 7.7.4__py3-none-any.whl → 7.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- CHANGELOG.md +38 -2
- README.md +19 -17
- pytermgui/__init__.py +1 -1
- pytermgui/animations.py +1 -1
- pytermgui/ansi_interface.py +1 -1
- pytermgui/colors.py +3 -3
- pytermgui/exporters.py +2 -2
- pytermgui/file_loaders.py +4 -4
- pytermgui/helpers.py +21 -74
- pytermgui/input.py +1 -5
- pytermgui/inspector.py +1 -1
- pytermgui/markup/macros.py +1 -1
- pytermgui/markup/parsing.py +11 -5
- pytermgui/palettes.py +5 -10
- pytermgui/regex.py +6 -2
- pytermgui/term.py +50 -19
- pytermgui/widgets/base.py +0 -12
- pytermgui/widgets/containers.py +47 -4
- pytermgui/widgets/input_field.py +13 -6
- pytermgui/window_manager/compositor.py +3 -1
- pytermgui/window_manager/manager.py +10 -3
- pytermgui/window_manager/window.py +3 -1
- {pytermgui-7.7.4.dist-info → pytermgui-7.8.1.dist-info}/METADATA +50 -18
- pytermgui-7.8.1.dist-info/RECORD +60 -0
- examples/README.md +0 -19
- pytermgui-7.7.4.dist-info/RECORD +0 -78
- tests/__init__.py +0 -0
- tests/_exporter_targets.py +0 -1646
- tests/colorgrids.py +0 -122
- tests/test.json +0 -45
- tests/test.yaml +0 -103
- tests/test_animations.py +0 -114
- tests/test_auto.py +0 -68
- tests/test_colors.py +0 -113
- tests/test_dump_n_load.py +0 -34
- tests/test_exporters.py +0 -1063
- tests/test_fancy_repr.py +0 -17
- tests/test_helpers.py +0 -53
- tests/test_highlight_markup_literal.py +0 -8
- tests/test_layouts.py +0 -99
- tests/test_parser.py +0 -179
- tests/test_regex.py +0 -40
- tests/test_styles.py +0 -56
- {pytermgui-7.7.4.dist-info → pytermgui-7.8.1.dist-info}/WHEEL +0 -0
- {pytermgui-7.7.4.dist-info → pytermgui-7.8.1.dist-info}/entry_points.txt +0 -0
- {pytermgui-7.7.4.dist-info → pytermgui-7.8.1.dist-info}/licenses/LICENSE +0 -0
CHANGELOG.md
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
|
-
## [7.
|
|
1
|
+
## [7.8.1] - 2026-09-10
|
|
2
2
|
|
|
3
3
|
### Bugfixes
|
|
4
4
|
|
|
5
|
-
- Fix
|
|
5
|
+
- Fix multiline input navigation and joining lines with Backspace
|
|
6
|
+
|
|
7
|
+
## [7.8.0] - 2026-08-10
|
|
8
|
+
|
|
9
|
+
### Additions
|
|
10
|
+
|
|
11
|
+
- Add grapheme-aware wrapping with correct widths for CJK and emoji sequences
|
|
12
|
+
- Preserve ANSI styles and hyperlinks when wrapping text
|
|
13
|
+
- Document terminal compatibility and the project's archived status
|
|
14
|
+
|
|
15
|
+
### Bugfixes
|
|
16
|
+
|
|
17
|
+
- Fix terminal clearing, synchronized frame recording, and resize handling
|
|
18
|
+
- Fix multiline input cursor placement and nested container scrolling
|
|
19
|
+
- Fix uneven splitter columns and automatically sized scrollable windows
|
|
20
|
+
- Fix ANSI bright magenta, cyan, and white color mappings
|
|
21
|
+
- Fix cursor-home and erase sequences in ANSI exports
|
|
22
|
+
- Avoid busy-waiting for input on Windows
|
|
23
|
+
|
|
24
|
+
### Maintenance
|
|
25
|
+
|
|
26
|
+
- Modernize linting and packaging configuration
|
|
27
|
+
- Remove obsolete project assets and tooling
|
|
6
28
|
|
|
7
29
|
<!-- HATCH README END -->
|
|
8
30
|
|
|
31
|
+
## [7.7.4] - 2025-03-31
|
|
32
|
+
|
|
33
|
+
### Bugfixes
|
|
34
|
+
|
|
35
|
+
- Fix rapid input to \_GetchLinux
|
|
36
|
+
|
|
37
|
+
## [7.7.3] - 2024-12-29
|
|
38
|
+
|
|
39
|
+
### Bugfixes
|
|
40
|
+
|
|
41
|
+
- Fix getch mapping for Windows keys
|
|
42
|
+
|
|
9
43
|
## [7.7.2] - 2024-08-31
|
|
10
44
|
|
|
11
45
|
### Additions
|
|
@@ -416,6 +450,8 @@
|
|
|
416
450
|
|
|
417
451
|
<!-- HATCH URI DEFINITIONS START -->
|
|
418
452
|
|
|
453
|
+
[7.8.0]: https://github.com/bczsalba/pytermgui/compare/v7.7.4...v7.8.0
|
|
454
|
+
[7.7.4]: https://github.com/bczsalba/pytermgui/compare/v7.7.3...v7.7.4
|
|
419
455
|
[7.7.3]: https://github.com/bczsalba/pytermgui/compare/v7.7.2...v7.7.3
|
|
420
456
|
[7.7.2]: https://github.com/bczsalba/pytermgui/compare/v7.7.1...v7.7.2
|
|
421
457
|
[7.7.1]: https://github.com/bczsalba/pytermgui/compare/v7.7.0...v7.7.1
|
README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<!---->
|
|
2
|
-
|
|
3
1
|

|
|
4
2
|
|
|
5
3
|
> Python TUI framework with mouse support, modular widget system, customizable and rapid terminal markup language and more!
|
|
@@ -12,26 +10,20 @@ pip3 install pytermgui
|
|
|
12
10
|
<a href="https://pypi.org/project/pytermgui">
|
|
13
11
|
<img alt="PyPi project" src="https://img.shields.io/pypi/v/pytermgui?color=brightgreen">
|
|
14
12
|
</a>
|
|
15
|
-
|
|
16
|
-
<img
|
|
17
|
-
</a>
|
|
18
|
-
<a href="http://ptg.bczsalba.com/">
|
|
19
|
-
<img src="https://img.shields.io/badge/documentation-up%20to%20date-brightgreen">
|
|
13
|
+
<a href="https://ptg.bczsalba.com/">
|
|
14
|
+
<img src="https://img.shields.io/badge/documentation-gray">
|
|
20
15
|
</a>
|
|
21
16
|
<a href="https://github.com/bczsalba/pytermgui/actions/workflows/pytest.yml">
|
|
22
17
|
<img src="https://github.com/bczsalba/pytermgui/actions/workflows/pytest.yml/badge.svg">
|
|
23
18
|
</a>
|
|
24
19
|
</p>
|
|
25
|
-
<p align=center>
|
|
26
|
-
<a href="https://discord.gg/g4bqMvpG4U">
|
|
27
|
-
<img src="https://img.shields.io/discord/999374285686706367?label=join%20our%20discord">
|
|
28
|
-
</a>
|
|
29
20
|
</p>
|
|
30
21
|
|
|
31
|
-
|
|
22
|
+
<hr>
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
## Archival notice
|
|
25
|
+
|
|
26
|
+
PyTermGUI is no longer in development. The latest release will remain available, alongside its documentation. The MIT license allows for free modification of the library, as long as you include credit to its original author. Thank you for everything.
|
|
35
27
|
|
|
36
28
|
<hr>
|
|
37
29
|
|
|
@@ -64,6 +56,16 @@ Additionally, there are a couple of neat tools to make your general Python devel
|
|
|
64
56
|
- A pretty printer for both the REPL and IPython
|
|
65
57
|
- A way to create SVG and HTML screenshots of your terminal
|
|
66
58
|
|
|
59
|
+
### Terminal compatibility
|
|
60
|
+
|
|
61
|
+
PyTermGUI relies on ANSI virtual-terminal sequences for rendering, cursor reports,
|
|
62
|
+
keyboard input, and mouse input. On Windows, support depends on the terminal emulator's
|
|
63
|
+
ANSI compatibility. [Windows Terminal](https://github.com/microsoft/terminal) supports
|
|
64
|
+
the expected cursor and mouse protocols, while the legacy Windows Console Host
|
|
65
|
+
(`conhost.exe`) may provide reduced functionality. This does not depend on whether
|
|
66
|
+
PowerShell or Command Prompt is running inside the emulator. Mouse support also depends
|
|
67
|
+
on the emulator forwarding the requested events.
|
|
68
|
+
|
|
67
69
|
<!-- HATCH README END -->
|
|
68
70
|
|
|
69
71
|
## Examples
|
|
@@ -181,9 +183,9 @@ We use algorithms based on human vision to convert and downgrade colors when the
|
|
|
181
183
|
|
|
182
184
|
## Questions? See the docs!
|
|
183
185
|
|
|
184
|
-
Pretty much every
|
|
186
|
+
Pretty much every name in the library, private or public, has an insightful docstring attached to it, alongside walkthrough-based documentation articles. See them all on the [documentation website](https://ptg.bczsalba.com)!
|
|
185
187
|
|
|
186
188
|
|
|
187
|
-
##
|
|
189
|
+
## Project status
|
|
188
190
|
|
|
189
|
-
|
|
191
|
+
PyTermGUI has reached its final release and is no longer under active development. The repository and its documentation are retained as an archive for existing users.
|
pytermgui/__init__.py
CHANGED
pytermgui/animations.py
CHANGED
|
@@ -23,7 +23,7 @@ from typing import TYPE_CHECKING, Any, Callable
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
24
|
from .widgets import Widget
|
|
25
25
|
else:
|
|
26
|
-
Widget = Any
|
|
26
|
+
Widget = Any # pylint: disable=invalid-name
|
|
27
27
|
|
|
28
28
|
__all__ = ["Animator", "FloatAnimation", "AttrAnimation", "animator", "is_animated"]
|
|
29
29
|
|
pytermgui/ansi_interface.py
CHANGED
pytermgui/colors.py
CHANGED
|
@@ -68,9 +68,9 @@ XTERM_NAMED_COLORS = {
|
|
|
68
68
|
10: "ansi-bright-green",
|
|
69
69
|
11: "ansi-bright-yellow",
|
|
70
70
|
12: "ansi-bright-blue",
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
13: "ansi-bright-magenta",
|
|
72
|
+
14: "ansi-bright-cyan",
|
|
73
|
+
15: "ansi-bright-white",
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
NAMED_COLORS = {
|
pytermgui/exporters.py
CHANGED
|
@@ -273,7 +273,7 @@ def token_to_css(token: Token, invert: bool = False) -> str:
|
|
|
273
273
|
|
|
274
274
|
# We take this many arguments for future proofing and customization, not much we can
|
|
275
275
|
# do about it.
|
|
276
|
-
def to_html( # pylint: disable=too-many-arguments, too-many-locals
|
|
276
|
+
def to_html( # pylint: disable=too-many-arguments, too-many-locals, R0917
|
|
277
277
|
obj: Widget | StyledText | str,
|
|
278
278
|
prefix: str | None = None,
|
|
279
279
|
inline_styles: bool = False,
|
|
@@ -441,7 +441,7 @@ def _make_tag(tagname: str, content: str = "", **attrs) -> str:
|
|
|
441
441
|
|
|
442
442
|
# This is a bit of a beast of a function, but it does the job and IMO reducing it
|
|
443
443
|
# into parts would just make our lives more complicated.
|
|
444
|
-
def to_svg( # pylint: disable=too-many-locals, too-many-arguments, too-many-statements, R0912
|
|
444
|
+
def to_svg( # pylint: disable=too-many-locals, too-many-arguments, too-many-statements, R0912, R0917
|
|
445
445
|
obj: Widget | StyledText | str,
|
|
446
446
|
prefix: str | None = None,
|
|
447
447
|
chrome: bool = True,
|
pytermgui/file_loaders.py
CHANGED
|
@@ -131,14 +131,14 @@ from . import widgets as widgets_m
|
|
|
131
131
|
from .markup import tim
|
|
132
132
|
from .serialization import Serializer
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
_yaml_error = None
|
|
135
135
|
|
|
136
136
|
try:
|
|
137
137
|
import yaml
|
|
138
138
|
except ImportError as import_error:
|
|
139
139
|
# yaml is explicitly checked to be None later
|
|
140
140
|
yaml = None # type: ignore
|
|
141
|
-
|
|
141
|
+
_yaml_error = import_error
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
__all__ = ["WidgetNamespace", "FileLoader", "YamlLoader", "JsonLoader"]
|
|
@@ -412,10 +412,10 @@ class YamlLoader(FileLoader):
|
|
|
412
412
|
def __init__(self, serializer: Serializer | None = None) -> None:
|
|
413
413
|
"""Initialize object, check for installation of PyYAML."""
|
|
414
414
|
|
|
415
|
-
if
|
|
415
|
+
if _yaml_error is not None:
|
|
416
416
|
raise RuntimeError(
|
|
417
417
|
"YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
|
|
418
|
-
) from
|
|
418
|
+
) from _yaml_error
|
|
419
419
|
|
|
420
420
|
super().__init__()
|
|
421
421
|
|
pytermgui/helpers.py
CHANGED
|
@@ -4,8 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Iterator
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
|
|
7
|
+
from wcwidth import wrap as wcwidth_wrap
|
|
8
|
+
|
|
9
9
|
from .regex import real_length
|
|
10
10
|
|
|
11
11
|
__all__ = [
|
|
@@ -13,17 +13,14 @@ __all__ = [
|
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def break_line(
|
|
16
|
+
def break_line(
|
|
17
17
|
line: str, limit: int, non_first_limit: int | None = None, fill: str | None = None
|
|
18
18
|
) -> Iterator[str]:
|
|
19
19
|
"""Breaks a line into a `list[str]` with maximum `limit` length per line.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
At the moment it splits strings exactly on the limit, and not on word
|
|
25
|
-
boundaries. That functionality would be preferred, so it will end up being
|
|
26
|
-
implemented at some point.
|
|
21
|
+
Uses wcwidth.wrap() for proper word-boundary breaking, grapheme cluster
|
|
22
|
+
handling, and wide character support. ANSI sequences are preserved and
|
|
23
|
+
propagated across line breaks.
|
|
27
24
|
|
|
28
25
|
Args:
|
|
29
26
|
line: The line to split. May or may not contain ANSI sequences.
|
|
@@ -31,83 +28,33 @@ def break_line( # pylint: disable=too-many-branches
|
|
|
31
28
|
non-printing sequences.
|
|
32
29
|
non_first_limit: The limit after the first line. If not given, defaults
|
|
33
30
|
to `limit`.
|
|
31
|
+
fill: Optional character to pad lines to the limit width.
|
|
34
32
|
"""
|
|
35
33
|
|
|
36
34
|
if line in ["", "\x1b[0m"]:
|
|
37
35
|
yield ""
|
|
38
36
|
return
|
|
39
37
|
|
|
40
|
-
def
|
|
41
|
-
count = limit - real_length(line)
|
|
42
|
-
|
|
43
|
-
if link is not None:
|
|
44
|
-
line = LINK_TEMPLATE.format(uri=link, label=line)
|
|
45
|
-
|
|
38
|
+
def _pad_line(text: str, width: int) -> str:
|
|
46
39
|
if fill is None:
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
line += count * fill
|
|
50
|
-
|
|
51
|
-
return line
|
|
40
|
+
return text
|
|
52
41
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
count = width - real_length(text)
|
|
43
|
+
if count > 0:
|
|
44
|
+
return text + count * fill
|
|
45
|
+
return text
|
|
56
46
|
|
|
57
47
|
if non_first_limit is None:
|
|
58
48
|
non_first_limit = limit
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if token.is_plain():
|
|
65
|
-
for char in token.value:
|
|
66
|
-
if char == "\n" or used >= limit:
|
|
67
|
-
if sequences != "":
|
|
68
|
-
current += "\x1b[0m"
|
|
69
|
-
|
|
70
|
-
yield _pad_and_link(current, link)
|
|
71
|
-
link = None
|
|
72
|
-
|
|
73
|
-
current = sequences
|
|
74
|
-
used = 0
|
|
75
|
-
|
|
76
|
-
limit = non_first_limit
|
|
77
|
-
|
|
78
|
-
if char != "\n":
|
|
79
|
-
current += char
|
|
80
|
-
used += 1
|
|
81
|
-
|
|
82
|
-
# If the link wasn't yielded along with its token, remove and add it
|
|
83
|
-
# to current manually.
|
|
84
|
-
if link is not None:
|
|
85
|
-
current = current[: -len(token.value)]
|
|
86
|
-
current += LINK_TEMPLATE.format(uri=link, label=token.value)
|
|
87
|
-
link = None
|
|
88
|
-
|
|
89
|
-
continue
|
|
90
|
-
|
|
91
|
-
if token.value == "/":
|
|
92
|
-
sequences = "\x1b[0m"
|
|
93
|
-
|
|
94
|
-
if len(current) > 0:
|
|
95
|
-
current += sequences
|
|
96
|
-
|
|
50
|
+
for segment in line.split("\n"):
|
|
51
|
+
if not segment:
|
|
52
|
+
yield _pad_line("", limit)
|
|
53
|
+
limit = non_first_limit
|
|
97
54
|
continue
|
|
98
55
|
|
|
99
|
-
|
|
100
|
-
link = token.value
|
|
101
|
-
continue
|
|
102
|
-
|
|
103
|
-
sequence = parsers[type(token)](token, {}, lambda: line) # type: ignore
|
|
104
|
-
sequences += sequence
|
|
105
|
-
current += sequence
|
|
106
|
-
|
|
107
|
-
if current == "":
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
if sequences != "" and not current.endswith("\x1b[0m"):
|
|
111
|
-
current += "\x1b[0m"
|
|
56
|
+
wrapped = wcwidth_wrap(segment, limit)
|
|
112
57
|
|
|
113
|
-
|
|
58
|
+
for wrapped_line in wrapped:
|
|
59
|
+
yield _pad_line(wrapped_line, limit)
|
|
60
|
+
limit = non_first_limit
|
pytermgui/input.py
CHANGED
|
@@ -5,10 +5,6 @@ Credits:
|
|
|
5
5
|
|
|
6
6
|
- Original getch implementation: [Danny Yoo](https://code.activestate.com/recipes/134892)
|
|
7
7
|
- Modern additions & idea: [kcsaff](https://github.com/kcsaff/getkey)
|
|
8
|
-
|
|
9
|
-
Note that the original link seems to no longer be active, but an archive can be found
|
|
10
|
-
on [GitHub](https://github.com/ActiveState/code/tree/master/recipes/Python/
|
|
11
|
-
134892_getchlike_unbuffered_character_reading_stdboth).
|
|
12
8
|
"""
|
|
13
9
|
|
|
14
10
|
# pylint doesn't see the C source
|
|
@@ -134,7 +130,7 @@ class _GetchUnix:
|
|
|
134
130
|
|
|
135
131
|
descriptor = sys.stdin.fileno()
|
|
136
132
|
old_settings = termios.tcgetattr(descriptor)
|
|
137
|
-
tty.setcbreak(descriptor)
|
|
133
|
+
tty.setcbreak(descriptor, termios.TCSANOW)
|
|
138
134
|
|
|
139
135
|
try:
|
|
140
136
|
yield self._read(1)
|
pytermgui/inspector.py
CHANGED
|
@@ -175,7 +175,7 @@ def inspect(target: object, **inspector_args: Any) -> Inspector:
|
|
|
175
175
|
class Inspector(Container):
|
|
176
176
|
"""A widget to inspect any Python object."""
|
|
177
177
|
|
|
178
|
-
def __init__( # pylint: disable=too-many-arguments
|
|
178
|
+
def __init__( # pylint: disable=too-many-arguments, R0917
|
|
179
179
|
self,
|
|
180
180
|
target: object = None,
|
|
181
181
|
show_private: bool = False,
|
pytermgui/markup/macros.py
CHANGED
pytermgui/markup/parsing.py
CHANGED
|
@@ -211,6 +211,11 @@ def tokenize_ansi( # pylint: disable=too-many-locals, too-many-branches, too-ma
|
|
|
211
211
|
if cursor < start:
|
|
212
212
|
yield PlainToken(text[cursor:start])
|
|
213
213
|
|
|
214
|
+
if matchobj.groups()[4] is not None:
|
|
215
|
+
cursor = end
|
|
216
|
+
yield ClearToken("/~")
|
|
217
|
+
continue
|
|
218
|
+
|
|
214
219
|
if link_osc != (None, None):
|
|
215
220
|
cursor = end
|
|
216
221
|
uri, label = link_osc
|
|
@@ -225,6 +230,9 @@ def tokenize_ansi( # pylint: disable=too-many-locals, too-many-branches, too-ma
|
|
|
225
230
|
|
|
226
231
|
cursor = end
|
|
227
232
|
|
|
233
|
+
if full.endswith(("J", "K")):
|
|
234
|
+
continue
|
|
235
|
+
|
|
228
236
|
code = ""
|
|
229
237
|
|
|
230
238
|
# Position
|
|
@@ -233,9 +241,7 @@ def tokenize_ansi( # pylint: disable=too-many-locals, too-many-branches, too-ma
|
|
|
233
241
|
if posmatch is not None:
|
|
234
242
|
ypos, xpos = posmatch.groups()
|
|
235
243
|
if not ypos and not xpos:
|
|
236
|
-
|
|
237
|
-
f"Cannot parse cursor when no position is supplied. Match: {posmatch!r}"
|
|
238
|
-
)
|
|
244
|
+
ypos = xpos = "1"
|
|
239
245
|
|
|
240
246
|
yield CursorToken(content, int(ypos) or None, int(xpos) or None)
|
|
241
247
|
continue
|
|
@@ -481,7 +487,7 @@ def optimize_tokens(tokens: list[Token]) -> Iterator[Token]:
|
|
|
481
487
|
applied.append(tkn)
|
|
482
488
|
yield tkn
|
|
483
489
|
|
|
484
|
-
def _remove_redundant_color(token: Token) -> None:
|
|
490
|
+
def _remove_redundant_color(token: Token, new: Color) -> None:
|
|
485
491
|
"""Removes non-functional colors.
|
|
486
492
|
|
|
487
493
|
These happen in the following ways:
|
|
@@ -513,7 +519,7 @@ def optimize_tokens(tokens: list[Token]) -> Iterator[Token]:
|
|
|
513
519
|
if Token.is_color(token):
|
|
514
520
|
new = token.color
|
|
515
521
|
|
|
516
|
-
_remove_redundant_color(token)
|
|
522
|
+
_remove_redundant_color(token, new)
|
|
517
523
|
|
|
518
524
|
if not any(token.markup == applied.markup for applied in current_tag_group):
|
|
519
525
|
current_tag_group.append(token)
|
pytermgui/palettes.py
CHANGED
|
@@ -241,23 +241,18 @@ class Palette:
|
|
|
241
241
|
for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
|
|
242
242
|
if shadenumber > 0:
|
|
243
243
|
shadeindex = f"+{shadenumber}"
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
blended = color.blend(
|
|
245
|
+
white, SHADE_INCREMENT * shadenumber
|
|
246
|
+
)
|
|
246
247
|
|
|
247
248
|
elif shadenumber == 0:
|
|
248
249
|
shadeindex = ""
|
|
249
|
-
|
|
250
|
-
else:
|
|
251
|
-
shadeindex = str(shadenumber)
|
|
252
|
-
blend_color = black
|
|
253
|
-
blend_multiplier = -1
|
|
254
|
-
|
|
255
|
-
if shadenumber == 0:
|
|
256
250
|
blended = color
|
|
257
251
|
|
|
258
252
|
else:
|
|
253
|
+
shadeindex = str(shadenumber)
|
|
259
254
|
blended = color.blend(
|
|
260
|
-
|
|
255
|
+
black, -SHADE_INCREMENT * shadenumber
|
|
261
256
|
)
|
|
262
257
|
|
|
263
258
|
data[f"{name}{shadeindex}"] = blended
|
pytermgui/regex.py
CHANGED
|
@@ -7,8 +7,12 @@ from typing import Match
|
|
|
7
7
|
from wcwidth import wcswidth
|
|
8
8
|
|
|
9
9
|
RE_LINK = re.compile(r"(?:\x1b\]8;;([^\\]*)\x1b\\([^\\]*?)\x1b\]8;;\x1b\\)")
|
|
10
|
-
RE_ANSI_NEW = re.compile(
|
|
11
|
-
|
|
10
|
+
RE_ANSI_NEW = re.compile(
|
|
11
|
+
rf"(\x1b\[(.*?)[mHJK])|{RE_LINK.pattern}|(\x1b\]8;;\x1b\\)|(\x1b_G(.*?)\x1b\\)"
|
|
12
|
+
)
|
|
13
|
+
RE_ANSI = re.compile(
|
|
14
|
+
r"(?:\x1b\[(.*?)[mHJK])|(?:\x1b\](.*?)\x1b\\)|(?:\x1b_G(.*?)\x1b\\)"
|
|
15
|
+
)
|
|
12
16
|
RE_MACRO = re.compile(r"(![a-z0-9_\-]+)(?:\(([\w\/\.?\-=:]+)\))?")
|
|
13
17
|
RE_MARKUP = re.compile(r"((\\*)\[([^\[\]]*)\])")
|
|
14
18
|
RE_POSITION = re.compile(r"\x1b\[(\d*?)(?:;(\d*))?H")
|
pytermgui/term.py
CHANGED
|
@@ -8,6 +8,7 @@ import errno
|
|
|
8
8
|
import os
|
|
9
9
|
import signal
|
|
10
10
|
import sys
|
|
11
|
+
import threading
|
|
11
12
|
import time
|
|
12
13
|
from contextlib import contextmanager
|
|
13
14
|
from datetime import datetime
|
|
@@ -126,7 +127,7 @@ class Recorder:
|
|
|
126
127
|
with open(filename, "w", encoding="utf-8") as file:
|
|
127
128
|
file.write(self.export_html(prefix=prefix, inline_styles=inline_styles))
|
|
128
129
|
|
|
129
|
-
def save_svg( # pylint: disable=too-many-arguments
|
|
130
|
+
def save_svg( # pylint: disable=too-many-arguments, R0917
|
|
130
131
|
self,
|
|
131
132
|
filename: str | None = None,
|
|
132
133
|
prefix: str | None = None,
|
|
@@ -263,6 +264,9 @@ class Terminal: # pylint: disable=too-many-instance-attributes
|
|
|
263
264
|
|
|
264
265
|
self._listeners: dict[int, list[Callable[..., Any]]] = {}
|
|
265
266
|
|
|
267
|
+
# Async-signal-safe resize mechanism
|
|
268
|
+
self._resize_pending = threading.Event()
|
|
269
|
+
|
|
266
270
|
if hasattr(signal, "SIGWINCH"):
|
|
267
271
|
signal.signal(signal.SIGWINCH, self._update_size)
|
|
268
272
|
else:
|
|
@@ -278,16 +282,16 @@ class Terminal: # pylint: disable=too-many-instance-attributes
|
|
|
278
282
|
["" for _ in range(self.width)] for y in range(self.height)
|
|
279
283
|
]
|
|
280
284
|
|
|
281
|
-
def _window_terminal_resize(self):
|
|
285
|
+
def _window_terminal_resize(self) -> None:
|
|
282
286
|
from time import sleep # pylint: disable=import-outside-toplevel
|
|
283
287
|
|
|
284
288
|
_previous = get_terminal_size()
|
|
285
289
|
while True:
|
|
286
290
|
_next = get_terminal_size()
|
|
287
291
|
if _previous != _next:
|
|
288
|
-
self.
|
|
292
|
+
self._resize_pending.set()
|
|
289
293
|
_previous = _next
|
|
290
|
-
sleep(0.
|
|
294
|
+
sleep(0.01)
|
|
291
295
|
|
|
292
296
|
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
|
|
293
297
|
"""Returns a cool looking repr."""
|
|
@@ -348,17 +352,34 @@ class Terminal: # pylint: disable=too-many-instance-attributes
|
|
|
348
352
|
return (size[0], size[1])
|
|
349
353
|
|
|
350
354
|
def _update_size(self, *_: Any) -> None:
|
|
351
|
-
"""
|
|
355
|
+
"""Signal handler for SIGWINCH - ONLY sets flag (async-signal-safe)."""
|
|
352
356
|
|
|
353
|
-
|
|
354
|
-
del self.resolution
|
|
357
|
+
self._resize_pending.set()
|
|
355
358
|
|
|
356
|
-
|
|
359
|
+
def process_pending_resize(self) -> bool:
|
|
360
|
+
"""Process pending resize event if one is queued.
|
|
361
|
+
|
|
362
|
+
Call this periodically from the main event loop.
|
|
363
|
+
|
|
364
|
+
:returns: True if a resize was processed.
|
|
365
|
+
"""
|
|
366
|
+
if not self._resize_pending.is_set():
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
self._resize_pending.clear()
|
|
370
|
+
|
|
371
|
+
# Check __dict__ directly to avoid triggering the cached_property getter,
|
|
372
|
+
# which uses signals and can only run in the main thread
|
|
373
|
+
if "resolution" in self.__dict__:
|
|
374
|
+
del self.__dict__["resolution"]
|
|
357
375
|
|
|
376
|
+
self.size = self._get_size()
|
|
358
377
|
self._call_listener(self.RESIZE, self.size)
|
|
359
378
|
|
|
360
379
|
# Wipe the screen in case anything got messed up
|
|
361
|
-
self.write("\x1b[2J")
|
|
380
|
+
self.write("\x1b[H\x1b[2J")
|
|
381
|
+
|
|
382
|
+
return True
|
|
362
383
|
|
|
363
384
|
@property
|
|
364
385
|
def width(self) -> int:
|
|
@@ -454,15 +475,21 @@ class Terminal: # pylint: disable=too-many-instance-attributes
|
|
|
454
475
|
buffer = StringIO()
|
|
455
476
|
|
|
456
477
|
try:
|
|
457
|
-
|
|
458
|
-
|
|
478
|
+
# Write directly to stream to avoid write()'s auto-clear behavior
|
|
479
|
+
self._stream.write("\x1b[?2026h")
|
|
459
480
|
yield buffer
|
|
460
481
|
|
|
461
482
|
finally:
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
483
|
+
content = buffer.getvalue()
|
|
484
|
+
self._stream.write(content)
|
|
485
|
+
|
|
486
|
+
# Frame contents bypass write() so they are sent atomically to the output
|
|
487
|
+
# stream. Forward the visual content to an active recorder explicitly.
|
|
488
|
+
if self._recorder is not None:
|
|
489
|
+
self._recorder.write(content)
|
|
490
|
+
|
|
491
|
+
self._stream.write("\x1b[?2026l")
|
|
492
|
+
self._stream.flush()
|
|
466
493
|
|
|
467
494
|
@staticmethod
|
|
468
495
|
def isatty() -> bool:
|
|
@@ -532,6 +559,7 @@ class Terminal: # pylint: disable=too-many-instance-attributes
|
|
|
532
559
|
|
|
533
560
|
return sliced
|
|
534
561
|
|
|
562
|
+
# Truncate pending buffer on clear (may help on Windows)
|
|
535
563
|
if "\x1b[2J" in data:
|
|
536
564
|
self.clear_stream()
|
|
537
565
|
|
|
@@ -546,8 +574,7 @@ class Terminal: # pylint: disable=too-many-instance-attributes
|
|
|
546
574
|
|
|
547
575
|
maximum = self.width - xpos
|
|
548
576
|
|
|
549
|
-
|
|
550
|
-
xpos = self.origin[0]
|
|
577
|
+
xpos = max(xpos, self.origin[0])
|
|
551
578
|
|
|
552
579
|
sliced = _slice(data, maximum) if len(data) > maximum else data
|
|
553
580
|
|
|
@@ -565,7 +592,11 @@ class Terminal: # pylint: disable=too-many-instance-attributes
|
|
|
565
592
|
self._stream.flush()
|
|
566
593
|
|
|
567
594
|
def clear_stream(self) -> None:
|
|
568
|
-
"""Clears
|
|
595
|
+
"""Clears the terminal screen.
|
|
596
|
+
|
|
597
|
+
Attempts to truncate any buffered stream data (may work on Windows),
|
|
598
|
+
then moves cursor to home position and clears the entire screen.
|
|
599
|
+
"""
|
|
569
600
|
|
|
570
601
|
try:
|
|
571
602
|
self._stream.truncate(0)
|
|
@@ -574,7 +605,7 @@ class Terminal: # pylint: disable=too-many-instance-attributes
|
|
|
574
605
|
if error.errno != errno.EINVAL and os.name != "nt":
|
|
575
606
|
raise
|
|
576
607
|
|
|
577
|
-
self._stream.write("\x1b[2J")
|
|
608
|
+
self._stream.write("\x1b[H\x1b[2J")
|
|
578
609
|
|
|
579
610
|
def print(
|
|
580
611
|
self,
|