PyTermGUI 7.7.0__tar.gz → 7.7.2__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.
- {pytermgui-7.7.0 → pytermgui-7.7.2}/CHANGELOG.md +13 -3
- {pytermgui-7.7.0 → pytermgui-7.7.2}/PKG-INFO +14 -26
- {pytermgui-7.7.0 → pytermgui-7.7.2}/README.md +8 -11
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pyproject.toml +1 -1
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/__init__.py +1 -1
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/parsing.py +1 -2
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/palettes.py +2 -2
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/regex.py +3 -1
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/collapsible.py +8 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/inline.py +1 -1
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/input_field.py +10 -4
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/layouts.py +3 -2
- pytermgui-7.7.0/pytermgui/cell.py +0 -320
- pytermgui-7.7.0/tests/not_test_cell.py +0 -125
- {pytermgui-7.7.0 → pytermgui-7.7.2}/.gitignore +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/LICENSE +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/examples/README.md +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/animations.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/ansi_interface.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/cmd.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/color_info.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/colors.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/context_managers.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/enums.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/exceptions.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/exporters.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/fancy_repr.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/file_loaders.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/helpers.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/highlighters.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/input.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/inspector.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/__init__.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/aliases.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/language.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/macros.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/style_maps.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/tokens.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/prettifiers.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/pretty.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/py.typed +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/serialization.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/term.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/__init__.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/base.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/boxes.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/button.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/checkbox.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/color_picker.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/containers.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/fancy_repr.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/frames.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/keyboard_button.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/pixel_matrix.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/slider.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/styles.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/toggle.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/win32console.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/__init__.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/compositor.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/manager.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/window.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/__init__.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/_exporter_targets.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/colorgrids.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test.json +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test.yaml +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_animations.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_auto.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_colors.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_dump_n_load.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_exporters.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_fancy_repr.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_helpers.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_highlight_markup_literal.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_layouts.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_parser.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_regex.py +0 -0
- {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_styles.py +0 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [7.7.1] - 2024-02-24
|
|
2
|
+
|
|
3
|
+
### Additions
|
|
4
|
+
|
|
5
|
+
- Add support for wide characters
|
|
6
|
+
|
|
7
|
+
### Bugfixes
|
|
8
|
+
|
|
9
|
+
- Fix `inline` widget referring to `hover` mouse mode
|
|
10
|
+
|
|
11
|
+
<!-- HATCH README END -->
|
|
12
|
+
|
|
1
13
|
## [7.7.0] - 2023-09-06
|
|
2
14
|
|
|
3
15
|
### Additions
|
|
@@ -8,7 +20,6 @@
|
|
|
8
20
|
|
|
9
21
|
- Fix sluggish and unreliable text input caused by `getch_timeout`.
|
|
10
22
|
|
|
11
|
-
|
|
12
23
|
## [7.6.0] - 2023-07-25
|
|
13
24
|
|
|
14
25
|
### Bugfixes
|
|
@@ -19,8 +30,6 @@
|
|
|
19
30
|
|
|
20
31
|
- Remove (undocumented) shortening behaviour from `Button`
|
|
21
32
|
|
|
22
|
-
<!-- HATCH README END -->
|
|
23
|
-
|
|
24
33
|
## [7.5.0] - 2023-07-24
|
|
25
34
|
|
|
26
35
|
### Additions
|
|
@@ -394,6 +403,7 @@
|
|
|
394
403
|
|
|
395
404
|
<!-- HATCH URI DEFINITIONS START -->
|
|
396
405
|
|
|
406
|
+
[7.7.1]: https://github.com/bczsalba/pytermgui/compare/v7.7.0...v7.7.1
|
|
397
407
|
[7.7.0]: https://github.com/bczsalba/pytermgui/compare/v7.6.0...v7.7.0
|
|
398
408
|
[7.6.0]: https://github.com/bczsalba/pytermgui/compare/v7.5.0...v7.6.0
|
|
399
409
|
[7.5.0]: https://github.com/bczsalba/pytermgui/compare/v7.4.0...v7.5.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PyTermGUI
|
|
3
|
-
Version: 7.7.
|
|
3
|
+
Version: 7.7.2
|
|
4
4
|
Summary: Python TUI framework with mouse support, modular widget system, customizable and rapid terminal markup language and more!
|
|
5
5
|
Project-URL: homepage, https://github.com/bczsalba/PyTermGUI
|
|
6
6
|
Project-URL: repository, https://github.com/bczsalba/PyTermGUI
|
|
@@ -26,6 +26,7 @@ Classifier: Topic :: Text Processing :: Markup
|
|
|
26
26
|
Classifier: Typing :: Typed
|
|
27
27
|
Requires-Python: >=3.8
|
|
28
28
|
Requires-Dist: typing-extensions
|
|
29
|
+
Requires-Dist: wcwidth
|
|
29
30
|
Provides-Extra: yaml
|
|
30
31
|
Requires-Dist: pyyaml; extra == 'yaml'
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
@@ -47,7 +48,7 @@ pip3 install pytermgui
|
|
|
47
48
|
<a href="https://github.com/bczsalba/pytermgui/blob/master/utils/create_badge.py">
|
|
48
49
|
<img alt="Code quality" src="https://raw.githubusercontent.com/bczsalba/pytermgui/master/assets/badges/quality.svg">
|
|
49
50
|
</a>
|
|
50
|
-
<a href="http://ptg.bczsalba.com/
|
|
51
|
+
<a href="http://ptg.bczsalba.com/">
|
|
51
52
|
<img src="https://img.shields.io/badge/documentation-up%20to%20date-brightgreen">
|
|
52
53
|
</a>
|
|
53
54
|
<a href="https://github.com/bczsalba/pytermgui/actions/workflows/pytest.yml">
|
|
@@ -55,21 +56,18 @@ pip3 install pytermgui
|
|
|
55
56
|
</a>
|
|
56
57
|
</p>
|
|
57
58
|
<p align=center>
|
|
58
|
-
<a href="https://twitter.com/bczsalba">
|
|
59
|
-
<img alt="Twitter"
|
|
60
|
-
src="https://img.shields.io/twitter/url/https/twitter.com/bczsalba.svg?style=social&label=Follow%20%40bczsalba">
|
|
61
|
-
</a>
|
|
62
|
-
<a href="https://reddit.com/r/pytermgui">
|
|
63
|
-
<img src="https://img.shields.io/reddit/subreddit-subscribers/pytermgui?style=flat&color=bright-green">
|
|
64
|
-
</a>
|
|
65
59
|
<a href="https://discord.gg/g4bqMvpG4U">
|
|
66
60
|
<img src="https://img.shields.io/discord/999374285686706367?label=join%20our%20discord">
|
|
67
61
|
</a>
|
|
68
|
-
<a href="https://ko-fi.com/bczsalba" title="Donate to this project using Ko-Fi">
|
|
69
|
-
<img src="https://img.shields.io/badge/ko--fi%20-donate-%232aabe0" alt="Buy Me A Coffee donate button" />
|
|
70
|
-
</a>
|
|
71
62
|
</p>
|
|
72
63
|
|
|
64
|
+
## Notice
|
|
65
|
+
|
|
66
|
+
A much better, more complete version of PTG's core ideas now exists over at [Shade 40](https://github.com/shade40). While PTG is not yet fully obsolete,
|
|
67
|
+
those libraries will be the primary focus of development going forward.
|
|
68
|
+
|
|
69
|
+
<hr>
|
|
70
|
+
|
|
73
71
|
## Why?
|
|
74
72
|
|
|
75
73
|
Mostly because terminals are cool, but creating terminal apps has historically been difficult. PyTermGUI aims to provide a simple, readable and modular way to make the app of your dreams!
|
|
@@ -101,32 +99,22 @@ Additionally, there are a couple of neat tools to make your general Python devel
|
|
|
101
99
|
|
|
102
100
|
## Latest release
|
|
103
101
|
|
|
104
|
-
### [7.7.
|
|
102
|
+
### [7.7.1] - 2024-02-24
|
|
105
103
|
|
|
106
104
|
### Additions
|
|
107
105
|
|
|
108
|
-
- Add support for
|
|
109
|
-
|
|
110
|
-
### Bugfixes:
|
|
111
|
-
|
|
112
|
-
- Fix sluggish and unreliable text input caused by `getch_timeout`.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
## [7.6.0] - 2023-07-25
|
|
106
|
+
- Add support for wide characters
|
|
116
107
|
|
|
117
108
|
### Bugfixes
|
|
118
109
|
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
### Removals
|
|
122
|
-
|
|
123
|
-
- Remove (undocumented) shortening behaviour from `Button`
|
|
110
|
+
- Fix `inline` widget referring to `hover` mouse mode
|
|
124
111
|
|
|
125
112
|
|
|
126
113
|
Read the full changelog [here](https://github.com/bczsalba/pytermgui/blob/master/CHANGELOG.md).
|
|
127
114
|
|
|
128
115
|
|
|
129
116
|
|
|
117
|
+
[7.7.1]: https://github.com/bczsalba/pytermgui/compare/v7.7.0...v7.7.1
|
|
130
118
|
[7.7.0]: https://github.com/bczsalba/pytermgui/compare/v7.6.0...v7.7.0
|
|
131
119
|
[7.6.0]: https://github.com/bczsalba/pytermgui/compare/v7.5.0...v7.6.0
|
|
132
120
|
[7.5.0]: https://github.com/bczsalba/pytermgui/compare/v7.4.0...v7.5.0
|
|
@@ -15,7 +15,7 @@ pip3 install pytermgui
|
|
|
15
15
|
<a href="https://github.com/bczsalba/pytermgui/blob/master/utils/create_badge.py">
|
|
16
16
|
<img alt="Code quality" src="https://raw.githubusercontent.com/bczsalba/pytermgui/master/assets/badges/quality.svg">
|
|
17
17
|
</a>
|
|
18
|
-
<a href="http://ptg.bczsalba.com/
|
|
18
|
+
<a href="http://ptg.bczsalba.com/">
|
|
19
19
|
<img src="https://img.shields.io/badge/documentation-up%20to%20date-brightgreen">
|
|
20
20
|
</a>
|
|
21
21
|
<a href="https://github.com/bczsalba/pytermgui/actions/workflows/pytest.yml">
|
|
@@ -23,21 +23,18 @@ pip3 install pytermgui
|
|
|
23
23
|
</a>
|
|
24
24
|
</p>
|
|
25
25
|
<p align=center>
|
|
26
|
-
<a href="https://twitter.com/bczsalba">
|
|
27
|
-
<img alt="Twitter"
|
|
28
|
-
src="https://img.shields.io/twitter/url/https/twitter.com/bczsalba.svg?style=social&label=Follow%20%40bczsalba">
|
|
29
|
-
</a>
|
|
30
|
-
<a href="https://reddit.com/r/pytermgui">
|
|
31
|
-
<img src="https://img.shields.io/reddit/subreddit-subscribers/pytermgui?style=flat&color=bright-green">
|
|
32
|
-
</a>
|
|
33
26
|
<a href="https://discord.gg/g4bqMvpG4U">
|
|
34
27
|
<img src="https://img.shields.io/discord/999374285686706367?label=join%20our%20discord">
|
|
35
28
|
</a>
|
|
36
|
-
<a href="https://ko-fi.com/bczsalba" title="Donate to this project using Ko-Fi">
|
|
37
|
-
<img src="https://img.shields.io/badge/ko--fi%20-donate-%232aabe0" alt="Buy Me A Coffee donate button" />
|
|
38
|
-
</a>
|
|
39
29
|
</p>
|
|
40
30
|
|
|
31
|
+
## Notice
|
|
32
|
+
|
|
33
|
+
A much better, more complete version of PTG's core ideas now exists over at [Shade 40](https://github.com/shade40). While PTG is not yet fully obsolete,
|
|
34
|
+
those libraries will be the primary focus of development going forward.
|
|
35
|
+
|
|
36
|
+
<hr>
|
|
37
|
+
|
|
41
38
|
## Why?
|
|
42
39
|
|
|
43
40
|
Mostly because terminals are cool, but creating terminal apps has historically been difficult. PyTermGUI aims to provide a simple, readable and modular way to make the app of your dreams!
|
|
@@ -134,8 +134,7 @@ def consume_tag(tag: str) -> Token: # pylint: disable=too-many-return-statement
|
|
|
134
134
|
except ColorSyntaxError:
|
|
135
135
|
token = AliasToken(tag)
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
return token # pylint: disable=lost-exception
|
|
137
|
+
return token
|
|
139
138
|
|
|
140
139
|
|
|
141
140
|
def tokenize_markup(text: str) -> Iterator[Token]:
|
|
@@ -127,7 +127,7 @@ class Palette:
|
|
|
127
127
|
|
|
128
128
|
data: dict[str, str]
|
|
129
129
|
|
|
130
|
-
def __init__( # pylint: disable=too-many-locals
|
|
130
|
+
def __init__( # pylint: disable=too-many-locals,too-many-arguments
|
|
131
131
|
self,
|
|
132
132
|
*,
|
|
133
133
|
primary: str,
|
|
@@ -167,7 +167,7 @@ class Palette:
|
|
|
167
167
|
strategy=strategy,
|
|
168
168
|
)
|
|
169
169
|
|
|
170
|
-
def _generate_map( # pylint: disable=too-many-locals
|
|
170
|
+
def _generate_map( # pylint: disable=too-many-locals,too-many-arguments
|
|
171
171
|
self,
|
|
172
172
|
*,
|
|
173
173
|
primary: str,
|
|
@@ -4,6 +4,8 @@ import re
|
|
|
4
4
|
from functools import lru_cache
|
|
5
5
|
from typing import Match
|
|
6
6
|
|
|
7
|
+
from wcwidth import wcswidth
|
|
8
|
+
|
|
7
9
|
RE_LINK = re.compile(r"(?:\x1b\]8;;([^\\]*)\x1b\\([^\\]*?)\x1b\]8;;\x1b\\)")
|
|
8
10
|
RE_ANSI_NEW = re.compile(rf"(\x1b\[(.*?)[mH])|{RE_LINK.pattern}|(\x1b_G(.*?)\x1b\\)")
|
|
9
11
|
RE_ANSI = re.compile(r"(?:\x1b\[(.*?)[mH])|(?:\x1b\](.*?)\x1b\\)|(?:\x1b_G(.*?)\x1b\\)")
|
|
@@ -69,7 +71,7 @@ def real_length(text: str) -> int:
|
|
|
69
71
|
The display-length of text.
|
|
70
72
|
"""
|
|
71
73
|
|
|
72
|
-
return
|
|
74
|
+
return max(wcswidth(strip_ansi(text)), 0)
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
def escape_markup(text: str) -> str:
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
from ..enums import Overflow
|
|
8
8
|
from ..input import keys
|
|
9
9
|
from .containers import Container
|
|
10
|
+
from .base import Widget
|
|
10
11
|
from .toggle import Toggle
|
|
11
12
|
|
|
12
13
|
__all__ = ["Collapsible"]
|
|
@@ -53,6 +54,13 @@ class Collapsible(Container):
|
|
|
53
54
|
|
|
54
55
|
self._is_expanded = False
|
|
55
56
|
|
|
57
|
+
@property
|
|
58
|
+
def selectables(self) -> list[tuple[Widget, int]]:
|
|
59
|
+
if self._is_expanded:
|
|
60
|
+
return super().selectables
|
|
61
|
+
|
|
62
|
+
return [(self.trigger, 0)]
|
|
63
|
+
|
|
56
64
|
def toggle(self) -> Collapsible:
|
|
57
65
|
"""Toggles expanded state.
|
|
58
66
|
|
|
@@ -7,6 +7,8 @@ from collections.abc import Iterable
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from typing import Any, Iterator, Literal
|
|
9
9
|
|
|
10
|
+
from wcwidth import wcwidth
|
|
11
|
+
|
|
10
12
|
from ..ansi_interface import MouseAction, MouseEvent
|
|
11
13
|
from ..enums import HorizontalAlignment
|
|
12
14
|
from ..helpers import break_line
|
|
@@ -66,7 +68,7 @@ class InputField(Widget): # pylint: disable=too-many-instance-attributes
|
|
|
66
68
|
|
|
67
69
|
parent_align = HorizontalAlignment.LEFT
|
|
68
70
|
|
|
69
|
-
def __init__(
|
|
71
|
+
def __init__( # pylint: disable=too-many-arguments
|
|
70
72
|
self,
|
|
71
73
|
value: str = "",
|
|
72
74
|
*,
|
|
@@ -83,6 +85,9 @@ class InputField(Widget): # pylint: disable=too-many-instance-attributes
|
|
|
83
85
|
if "width" not in attrs:
|
|
84
86
|
self.width = len(value)
|
|
85
87
|
|
|
88
|
+
if any(wcwidth(char) > 1 for char in value):
|
|
89
|
+
raise ValueError("InputField doesn't support wide unicode characters.")
|
|
90
|
+
|
|
86
91
|
self.prompt = prompt
|
|
87
92
|
self.height = 1
|
|
88
93
|
self.tablength = tablength
|
|
@@ -431,6 +436,7 @@ class InputField(Widget): # pylint: disable=too-many-instance-attributes
|
|
|
431
436
|
row, col = self.cursor
|
|
432
437
|
|
|
433
438
|
line = self._lines[row]
|
|
439
|
+
width = len(line)
|
|
434
440
|
|
|
435
441
|
# Going left, possibly upwards
|
|
436
442
|
if col < 0:
|
|
@@ -440,17 +446,17 @@ class InputField(Widget): # pylint: disable=too-many-instance-attributes
|
|
|
440
446
|
else:
|
|
441
447
|
self.cursor.row -= 1
|
|
442
448
|
line = self._lines[self.cursor.row]
|
|
443
|
-
self.cursor.col =
|
|
449
|
+
self.cursor.col = width
|
|
444
450
|
|
|
445
451
|
# Going right, possibly downwards
|
|
446
|
-
elif col >
|
|
452
|
+
elif col > width and line != "":
|
|
447
453
|
if len(self._lines) > row + 1:
|
|
448
454
|
self.cursor.row += 1
|
|
449
455
|
self.cursor.col = 0
|
|
450
456
|
|
|
451
457
|
line = self._lines[self.cursor.row]
|
|
452
458
|
|
|
453
|
-
self.cursor.col = max(0, min(self.cursor.col,
|
|
459
|
+
self.cursor.col = max(0, min(self.cursor.col, width))
|
|
454
460
|
|
|
455
461
|
def get_lines(self) -> list[str]:
|
|
456
462
|
"""Builds the input field's lines."""
|
|
@@ -273,7 +273,7 @@ class Layout:
|
|
|
273
273
|
|
|
274
274
|
return rows
|
|
275
275
|
|
|
276
|
-
def add_slot(
|
|
276
|
+
def add_slot( # pylint: disable=too-many-arguments
|
|
277
277
|
self,
|
|
278
278
|
name: str = "Slot",
|
|
279
279
|
*,
|
|
@@ -285,7 +285,8 @@ class Layout:
|
|
|
285
285
|
"""Adds a new slot to the layout.
|
|
286
286
|
|
|
287
287
|
Args:
|
|
288
|
-
name: The name of the slot.
|
|
288
|
+
name: The **snakeified** name of the slot. A non-snake case name
|
|
289
|
+
will cause issues when trying to retrieve the slot (see GH#147).
|
|
289
290
|
slot: An already instantiated `Slot` instance. If this is given,
|
|
290
291
|
the additional width & height arguments will be ignored.
|
|
291
292
|
width: The width for the new slot. See below for special types.
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
|
|
5
|
-
STYLE_START_CHAR = "\x1b"
|
|
6
|
-
STYLE_END_CHAR = "m"
|
|
7
|
-
|
|
8
|
-
UNSETTER_LOOKUP = {
|
|
9
|
-
**{"38": {"39"}, "48": {"49"}, "54": {"53"}},
|
|
10
|
-
**{str(index + 20): {str(index)} for index in range(1, 10)},
|
|
11
|
-
**{"22": {"1", "2"}},
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _is_color(style: str) -> bool:
|
|
16
|
-
"""Determines whether the given style string is a color."""
|
|
17
|
-
|
|
18
|
-
if style.startswith(("38", "48")):
|
|
19
|
-
return True
|
|
20
|
-
|
|
21
|
-
if style.isdigit():
|
|
22
|
-
index = int(style)
|
|
23
|
-
|
|
24
|
-
if 30 <= index <= 38 or 40 <= index <= 48:
|
|
25
|
-
return True
|
|
26
|
-
|
|
27
|
-
return False
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _process_styles(styles: list[str]) -> tuple[list[str], str, str]:
|
|
31
|
-
"""Processes the given styles, cleaning them up along the way.
|
|
32
|
-
|
|
33
|
-
This does 2 major tasks:
|
|
34
|
-
|
|
35
|
-
- Remove styles that are unset by later ones
|
|
36
|
-
- Extract the fore and background colors, returning them
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
style: The list of styles to process.
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
A tuple containing:
|
|
43
|
-
|
|
44
|
-
- The processed list of styles
|
|
45
|
-
- The foreground color
|
|
46
|
-
- The background color
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
# TODO: Split up compound styles before handling them.
|
|
50
|
-
|
|
51
|
-
applied = []
|
|
52
|
-
foreground = ""
|
|
53
|
-
background = ""
|
|
54
|
-
|
|
55
|
-
for style in styles:
|
|
56
|
-
unset_styles = UNSETTER_LOOKUP.get(style)
|
|
57
|
-
|
|
58
|
-
if _is_color(style):
|
|
59
|
-
if style.startswith("3"):
|
|
60
|
-
foreground = style
|
|
61
|
-
else:
|
|
62
|
-
background = style
|
|
63
|
-
|
|
64
|
-
continue
|
|
65
|
-
|
|
66
|
-
if style == "39":
|
|
67
|
-
foreground = ""
|
|
68
|
-
|
|
69
|
-
elif style == "49":
|
|
70
|
-
background = ""
|
|
71
|
-
|
|
72
|
-
elif style == "0":
|
|
73
|
-
applied = []
|
|
74
|
-
foreground = ""
|
|
75
|
-
background = ""
|
|
76
|
-
|
|
77
|
-
elif unset_styles is not None:
|
|
78
|
-
for unset_style in unset_styles:
|
|
79
|
-
if unset_style in applied:
|
|
80
|
-
applied.remove(unset_style)
|
|
81
|
-
|
|
82
|
-
if style not in applied:
|
|
83
|
-
applied.append(style)
|
|
84
|
-
|
|
85
|
-
if foreground:
|
|
86
|
-
applied.append(foreground)
|
|
87
|
-
|
|
88
|
-
if background:
|
|
89
|
-
applied.append(background)
|
|
90
|
-
|
|
91
|
-
return applied, foreground, background
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _split(styles: str) -> list[str]:
|
|
95
|
-
"""Splits styles into a list of their content.
|
|
96
|
-
|
|
97
|
-
For example, `\x1b[38;5;141;1m` would become `38;5;141` and `1`.
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
return [style.rstrip("m") for style in styles.split("\x1b[")[1:]]
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _combine(styles: list[str]) -> str:
|
|
104
|
-
"""Combines a list of style content into a compound style setter.
|
|
105
|
-
|
|
106
|
-
Instead of doing individual tags, it puts all of the styles into
|
|
107
|
-
a single unit, i.e. instead of `\x1b[38;5;141m\x1b[1m` it returns
|
|
108
|
-
`\x1b[38;5;141;1m`.
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
|
-
if len(styles) == 0:
|
|
112
|
-
return ""
|
|
113
|
-
|
|
114
|
-
return f"\x1b[{';'.join(styles)}{STYLE_END_CHAR}"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@dataclass
|
|
118
|
-
class Cell:
|
|
119
|
-
"""A unit that represents a single, possibly ANSI-styled character."""
|
|
120
|
-
|
|
121
|
-
char: str = ""
|
|
122
|
-
styles: str = ""
|
|
123
|
-
|
|
124
|
-
_foreground: str | None = None
|
|
125
|
-
_background: str | None = None
|
|
126
|
-
|
|
127
|
-
def __str__(self) -> str:
|
|
128
|
-
"""Gets the content of this cell."""
|
|
129
|
-
|
|
130
|
-
return self.styles + self.char
|
|
131
|
-
|
|
132
|
-
@property
|
|
133
|
-
def foreground(self) -> str | None:
|
|
134
|
-
"""Returns the currently set foreground color in this cell."""
|
|
135
|
-
|
|
136
|
-
return self._foreground
|
|
137
|
-
|
|
138
|
-
@foreground.setter
|
|
139
|
-
def foreground(self, new: str | None) -> None:
|
|
140
|
-
"""Allows setting the foreground color, while unsetting the current one."""
|
|
141
|
-
|
|
142
|
-
current = self.foreground
|
|
143
|
-
|
|
144
|
-
if current is not None:
|
|
145
|
-
self.styles = self.styles.replace(current, "")
|
|
146
|
-
|
|
147
|
-
self.styles += new
|
|
148
|
-
|
|
149
|
-
if new is not None:
|
|
150
|
-
self._foreground = f"\x1b[{new}m"
|
|
151
|
-
|
|
152
|
-
else:
|
|
153
|
-
self._foreground = None
|
|
154
|
-
|
|
155
|
-
@property
|
|
156
|
-
def background(self) -> str | None:
|
|
157
|
-
"""Returns the currently set background color in this cell."""
|
|
158
|
-
|
|
159
|
-
return self._background
|
|
160
|
-
|
|
161
|
-
@background.setter
|
|
162
|
-
def background(self, new: str | None) -> None:
|
|
163
|
-
"""Allows setting the background color, while unsetting the current one."""
|
|
164
|
-
|
|
165
|
-
current = self.background
|
|
166
|
-
|
|
167
|
-
if current is not None:
|
|
168
|
-
self.styles = self.styles.replace(current, "")
|
|
169
|
-
|
|
170
|
-
self.styles += new
|
|
171
|
-
|
|
172
|
-
if new is not None:
|
|
173
|
-
self._background = f"\x1b[{new}m"
|
|
174
|
-
|
|
175
|
-
else:
|
|
176
|
-
self._background = None
|
|
177
|
-
|
|
178
|
-
@classmethod
|
|
179
|
-
def from_line(cls, line: str, styles: str = "") -> Generator[None, None, Cell]:
|
|
180
|
-
"""Yields Cells generated from a line of text.
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
line: The input line. May or may not contain ANSI styles.
|
|
184
|
-
styles: Styles that are applied to the line from before the line started.
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
A Cell generator.
|
|
188
|
-
"""
|
|
189
|
-
|
|
190
|
-
in_style = False
|
|
191
|
-
current_style = styles
|
|
192
|
-
current_styles = []
|
|
193
|
-
|
|
194
|
-
for char in line:
|
|
195
|
-
if char == STYLE_START_CHAR:
|
|
196
|
-
in_style = True
|
|
197
|
-
current_style = ""
|
|
198
|
-
continue
|
|
199
|
-
|
|
200
|
-
if in_style:
|
|
201
|
-
if char == "[":
|
|
202
|
-
continue
|
|
203
|
-
|
|
204
|
-
if char == STYLE_END_CHAR:
|
|
205
|
-
current_styles.append(current_style)
|
|
206
|
-
in_style = False
|
|
207
|
-
continue
|
|
208
|
-
|
|
209
|
-
current_style += char
|
|
210
|
-
continue
|
|
211
|
-
|
|
212
|
-
current_styles, fore, back = _process_styles(current_styles)
|
|
213
|
-
|
|
214
|
-
yield Cell(char or " ", _combine(current_styles), fore, back)
|
|
215
|
-
|
|
216
|
-
if current_style != "":
|
|
217
|
-
current_styles, fore, back = _process_styles(
|
|
218
|
-
current_styles + [current_style]
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
yield Cell("", _combine(current_styles), fore, back)
|
|
222
|
-
|
|
223
|
-
@classmethod
|
|
224
|
-
def first_of(cls, line: str) -> Cell:
|
|
225
|
-
"""Returns the first cell yielded by `from_line`."""
|
|
226
|
-
|
|
227
|
-
for cell in cls.from_line(line):
|
|
228
|
-
return cell
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
class CellMatrix:
|
|
232
|
-
def __init__(self, width: int, height: int, default: str = " ") -> None:
|
|
233
|
-
self._cells = []
|
|
234
|
-
self._changes: list[tuple[int, int, Cell]] = []
|
|
235
|
-
|
|
236
|
-
for _ in range(height):
|
|
237
|
-
row = []
|
|
238
|
-
|
|
239
|
-
for _ in range(width):
|
|
240
|
-
row.append(Cell(default))
|
|
241
|
-
|
|
242
|
-
self._cells.append(row)
|
|
243
|
-
|
|
244
|
-
self.width = width
|
|
245
|
-
self.height = height
|
|
246
|
-
|
|
247
|
-
def resize(self, width: int, height: int, default: str = " ") -> None:
|
|
248
|
-
diff_cols = width - self.width
|
|
249
|
-
diff_rows = height - self.height
|
|
250
|
-
|
|
251
|
-
default_cell = list(Cell.from_line(default))[0]
|
|
252
|
-
|
|
253
|
-
for row in range(self.height):
|
|
254
|
-
self._cells[row].extend([default_cell] * diff_cols)
|
|
255
|
-
|
|
256
|
-
for _ in range(diff_rows):
|
|
257
|
-
self._cells.append([default_cell] * (width))
|
|
258
|
-
|
|
259
|
-
self.width, self.height = width, height
|
|
260
|
-
|
|
261
|
-
def add_text(
|
|
262
|
-
self, text: str, offset: tuple[int, int] = (0, 0), register_change: bool = True
|
|
263
|
-
) -> None:
|
|
264
|
-
x, y = offset
|
|
265
|
-
|
|
266
|
-
if y >= self.height:
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
for i, cell in enumerate(Cell.from_line(text)):
|
|
270
|
-
# Sometimes some styles (usually the global unsetter 0) are left over at
|
|
271
|
-
# the end of a line of text. These are dealt with here; overflowing either
|
|
272
|
-
# into the next cell in the same row, or the first cell of the next one.
|
|
273
|
-
if cell.char == "":
|
|
274
|
-
if x + i < self.width:
|
|
275
|
-
self._cells[y][x + i].styles += cell.styles
|
|
276
|
-
|
|
277
|
-
else:
|
|
278
|
-
if y + 1 >= self.height:
|
|
279
|
-
continue
|
|
280
|
-
|
|
281
|
-
self._cells[y + 1][0].styles += cell.styles
|
|
282
|
-
|
|
283
|
-
continue
|
|
284
|
-
|
|
285
|
-
if self.width <= x + i:
|
|
286
|
-
break
|
|
287
|
-
|
|
288
|
-
if self._cells[y][x + i] != cell:
|
|
289
|
-
self._cells[y][x + i] = cell
|
|
290
|
-
|
|
291
|
-
if register_change:
|
|
292
|
-
self._changes.append((x + i, y, cell))
|
|
293
|
-
|
|
294
|
-
def fill(self, content: str) -> None:
|
|
295
|
-
for row in range(self.height):
|
|
296
|
-
for col in range(self.width):
|
|
297
|
-
self.add_text(content, (col, row), register_change=False)
|
|
298
|
-
|
|
299
|
-
def build_lines(self) -> list[str]:
|
|
300
|
-
lines = []
|
|
301
|
-
for row in self._cells:
|
|
302
|
-
buff = ""
|
|
303
|
-
|
|
304
|
-
for cell in row:
|
|
305
|
-
buff += str(cell)
|
|
306
|
-
|
|
307
|
-
lines.append(buff + "")
|
|
308
|
-
|
|
309
|
-
return lines
|
|
310
|
-
|
|
311
|
-
def get_diff(self, origin: tuple[int, int] = (1, 1)) -> tuple[str, int]:
|
|
312
|
-
buff = ""
|
|
313
|
-
|
|
314
|
-
for x, y, cell in self._changes:
|
|
315
|
-
buff += f"\x1b[{y + origin[1]};{x + origin[0]}H{cell}"
|
|
316
|
-
|
|
317
|
-
change_count = len(self._changes)
|
|
318
|
-
self._changes.clear()
|
|
319
|
-
|
|
320
|
-
return buff, change_count
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
from pytermgui.cell import (
|
|
2
|
-
Cell,
|
|
3
|
-
CellMatrix,
|
|
4
|
-
_is_color,
|
|
5
|
-
_combine,
|
|
6
|
-
_split,
|
|
7
|
-
_process_styles,
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_is_color():
|
|
12
|
-
assert _is_color("31")
|
|
13
|
-
assert _is_color("45")
|
|
14
|
-
assert _is_color("38;5;141")
|
|
15
|
-
assert _is_color("48;5;61")
|
|
16
|
-
assert _is_color("38;2;22;33;44")
|
|
17
|
-
assert _is_color("48;2;22;33;44")
|
|
18
|
-
|
|
19
|
-
assert not _is_color("1")
|
|
20
|
-
assert not _is_color("9")
|
|
21
|
-
assert not _is_color("22")
|
|
22
|
-
assert not _is_color("1")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_combine():
|
|
26
|
-
assert _combine(["41", "38;5;141", "1"]) == "\x1b[41;38;5;141;1m"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_split():
|
|
30
|
-
assert _split("\x1b[38;2;53;12;123m\x1b[1m\x1b[9m\x1b[48;2;21;21;21m") == [
|
|
31
|
-
"38;2;53;12;123",
|
|
32
|
-
"1",
|
|
33
|
-
"9",
|
|
34
|
-
"48;2;21;21;21",
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_process_styles():
|
|
39
|
-
tests = [
|
|
40
|
-
# Bold & Dim both get unset by the same unsetter
|
|
41
|
-
(["38;5;141", "1", "9", "2", "22"], {"38;5;141", "9", "22"}),
|
|
42
|
-
# 0 unsets everything
|
|
43
|
-
(["38;2;66;66;66", "1", "0"], {"0"}),
|
|
44
|
-
# Duplicates are ignored
|
|
45
|
-
(["1", "1", "9"], {"1", "9"}),
|
|
46
|
-
# Foreground colors work; background ones unset
|
|
47
|
-
(["38;5;131", "1", "41", "49"], {"38;5;131", "1", "49"}),
|
|
48
|
-
# Background colors work; foreground ones unset
|
|
49
|
-
(["48;2;33;44;55", "7", "35", "39"], {"48;2;33;44;55", "7", "39"}),
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
for target, expected in tests:
|
|
53
|
-
output = _process_styles(target)[0]
|
|
54
|
-
|
|
55
|
-
if len(set(output)) != len(output):
|
|
56
|
-
raise ValueError(f"Output {output} contains duplicates.")
|
|
57
|
-
|
|
58
|
-
assert set(output) == expected
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_cell_from_line():
|
|
62
|
-
tests = [
|
|
63
|
-
# General example
|
|
64
|
-
(
|
|
65
|
-
"\x1b[38;5;141\x1b[1mHello\x1b[7mThere",
|
|
66
|
-
[
|
|
67
|
-
Cell("H", "\x1b[38;5;141;1m", "38;5;141"),
|
|
68
|
-
Cell("e", "\x1b[38;5;141;1m", "38;5;141"),
|
|
69
|
-
Cell("l", "\x1b[38;5;141;1m", "38;5;141"),
|
|
70
|
-
Cell("l", "\x1b[38;5;141;1m", "38;5;141"),
|
|
71
|
-
Cell("o", "\x1b[38;5;141;1m", "38;5;141"),
|
|
72
|
-
Cell("T", "\x1b[7;38;5;141;1m", "38;5;141"),
|
|
73
|
-
Cell("h", "\x1b[7;38;5;141;1m", "38;5;141"),
|
|
74
|
-
Cell("e", "\x1b[7;38;5;141;1m", "38;5;141"),
|
|
75
|
-
Cell("r", "\x1b[7;38;5;141;1m", "38;5;141"),
|
|
76
|
-
Cell("e", "\x1b[7;38;5;141;1m", "38;5;141"),
|
|
77
|
-
# Trailing style
|
|
78
|
-
Cell("", "\x1b[7;38;5;141;1m", "38;5;141"),
|
|
79
|
-
],
|
|
80
|
-
),
|
|
81
|
-
# Achy breaky code (chode)
|
|
82
|
-
# Unsetters & trailing
|
|
83
|
-
(
|
|
84
|
-
"\x1b[1m\x1b[33m1\x1b[22m2\x1b[0m",
|
|
85
|
-
[
|
|
86
|
-
Cell(char="1", styles="\x1b[1;33m"),
|
|
87
|
-
Cell(char="2", styles="\x1b[22;33m"),
|
|
88
|
-
Cell(char="", styles="\x1b[0m"),
|
|
89
|
-
],
|
|
90
|
-
),
|
|
91
|
-
# (Future) Compound styles
|
|
92
|
-
# (
|
|
93
|
-
# "\x1b[1;33m1\x1b[22m2\x1b[0m",
|
|
94
|
-
# [
|
|
95
|
-
# Cell(char="1", styles="\x1b[1;33m"),
|
|
96
|
-
# Cell(char="2", styles="\x1b[22;33m"),
|
|
97
|
-
# Cell(char="", styles="\x1b[0m"),
|
|
98
|
-
# ],
|
|
99
|
-
# ),
|
|
100
|
-
]
|
|
101
|
-
|
|
102
|
-
for target, expected in tests:
|
|
103
|
-
assert list(Cell.from_line(target)) == expected
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_cell_foreground():
|
|
107
|
-
assert Cell.first_of("\x1b[38;5;141mHello there").foreground == "\x1b[38;5;141m"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_cell_matrix_resize():
|
|
111
|
-
mat = CellMatrix(5, 5, default="x")
|
|
112
|
-
mat.resize(10, 10, default="O")
|
|
113
|
-
|
|
114
|
-
assert mat.build_lines() == [
|
|
115
|
-
"xxxxxOOOOO",
|
|
116
|
-
"xxxxxOOOOO",
|
|
117
|
-
"xxxxxOOOOO",
|
|
118
|
-
"xxxxxOOOOO",
|
|
119
|
-
"xxxxxOOOOO",
|
|
120
|
-
"OOOOOOOOOO",
|
|
121
|
-
"OOOOOOOOOO",
|
|
122
|
-
"OOOOOOOOOO",
|
|
123
|
-
"OOOOOOOOOO",
|
|
124
|
-
"OOOOOOOOOO",
|
|
125
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|