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.
Files changed (79) hide show
  1. {pytermgui-7.7.0 → pytermgui-7.7.2}/CHANGELOG.md +13 -3
  2. {pytermgui-7.7.0 → pytermgui-7.7.2}/PKG-INFO +14 -26
  3. {pytermgui-7.7.0 → pytermgui-7.7.2}/README.md +8 -11
  4. {pytermgui-7.7.0 → pytermgui-7.7.2}/pyproject.toml +1 -1
  5. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/__init__.py +1 -1
  6. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/parsing.py +1 -2
  7. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/palettes.py +2 -2
  8. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/regex.py +3 -1
  9. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/collapsible.py +8 -0
  10. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/inline.py +1 -1
  11. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/input_field.py +10 -4
  12. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/layouts.py +3 -2
  13. pytermgui-7.7.0/pytermgui/cell.py +0 -320
  14. pytermgui-7.7.0/tests/not_test_cell.py +0 -125
  15. {pytermgui-7.7.0 → pytermgui-7.7.2}/.gitignore +0 -0
  16. {pytermgui-7.7.0 → pytermgui-7.7.2}/LICENSE +0 -0
  17. {pytermgui-7.7.0 → pytermgui-7.7.2}/examples/README.md +0 -0
  18. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/animations.py +0 -0
  19. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/ansi_interface.py +0 -0
  20. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/cmd.py +0 -0
  21. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/color_info.py +0 -0
  22. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/colors.py +0 -0
  23. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/context_managers.py +0 -0
  24. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/enums.py +0 -0
  25. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/exceptions.py +0 -0
  26. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/exporters.py +0 -0
  27. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/fancy_repr.py +0 -0
  28. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/file_loaders.py +0 -0
  29. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/helpers.py +0 -0
  30. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/highlighters.py +0 -0
  31. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/input.py +0 -0
  32. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/inspector.py +0 -0
  33. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/__init__.py +0 -0
  34. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/aliases.py +0 -0
  35. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/language.py +0 -0
  36. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/macros.py +0 -0
  37. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/style_maps.py +0 -0
  38. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/markup/tokens.py +0 -0
  39. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/prettifiers.py +0 -0
  40. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/pretty.py +0 -0
  41. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/py.typed +0 -0
  42. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/serialization.py +0 -0
  43. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/term.py +0 -0
  44. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/__init__.py +0 -0
  45. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/base.py +0 -0
  46. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/boxes.py +0 -0
  47. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/button.py +0 -0
  48. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/checkbox.py +0 -0
  49. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/color_picker.py +0 -0
  50. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/containers.py +0 -0
  51. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/fancy_repr.py +0 -0
  52. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/frames.py +0 -0
  53. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/keyboard_button.py +0 -0
  54. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/pixel_matrix.py +0 -0
  55. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/slider.py +0 -0
  56. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/styles.py +0 -0
  57. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/widgets/toggle.py +0 -0
  58. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/win32console.py +0 -0
  59. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/__init__.py +0 -0
  60. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/compositor.py +0 -0
  61. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/manager.py +0 -0
  62. {pytermgui-7.7.0 → pytermgui-7.7.2}/pytermgui/window_manager/window.py +0 -0
  63. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/__init__.py +0 -0
  64. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/_exporter_targets.py +0 -0
  65. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/colorgrids.py +0 -0
  66. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test.json +0 -0
  67. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test.yaml +0 -0
  68. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_animations.py +0 -0
  69. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_auto.py +0 -0
  70. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_colors.py +0 -0
  71. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_dump_n_load.py +0 -0
  72. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_exporters.py +0 -0
  73. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_fancy_repr.py +0 -0
  74. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_helpers.py +0 -0
  75. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_highlight_markup_literal.py +0 -0
  76. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_layouts.py +0 -0
  77. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_parser.py +0 -0
  78. {pytermgui-7.7.0 → pytermgui-7.7.2}/tests/test_regex.py +0 -0
  79. {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.0
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/pytermgui.html">
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.0] - 2023-09-06
102
+ ### [7.7.1] - 2024-02-24
105
103
 
106
104
  ### Additions
107
105
 
108
- - Add support for using carriage returns for button submissions
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
- - (temporarily) Fix hyperlinks breaking SVG exports
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/pytermgui.html">
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!
@@ -15,7 +15,7 @@ description = """\
15
15
  license = {text = "MIT"}
16
16
 
17
17
  requires-python = ">=3.8"
18
- dependencies = ["typing_extensions"]
18
+ dependencies = ["wcwidth", "typing_extensions"]
19
19
 
20
20
  keywords = [
21
21
  "terminal",
@@ -40,7 +40,7 @@ if "-m" in sys.argv: # pragma: no cover
40
40
 
41
41
  warnings.filterwarnings("ignore")
42
42
 
43
- __version__ = "7.7.0"
43
+ __version__ = "7.7.2"
44
44
 
45
45
 
46
46
  def auto(data: Any, **widget_args: Any) -> Optional[Widget | list[Splitter]]:
@@ -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
- finally:
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 len(strip_ansi(text))
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
 
@@ -87,7 +87,7 @@ def inline(
87
87
 
88
88
  _print_widget()
89
89
 
90
- with mouse_handler(["press_hold", "hover"], "decimal_xterm") as translate:
90
+ with mouse_handler(["all"], "decimal_xterm") as translate:
91
91
  while True:
92
92
  key = getch(interrupts=False)
93
93
 
@@ -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 = len(line)
449
+ self.cursor.col = width
444
450
 
445
451
  # Going right, possibly downwards
446
- elif col > len(line) and line != "":
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, len(line)))
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. Used for display purposes.
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