pytermgui.widgets.boxes

Convenience objects for Container corner & border styles.

They can be used as:

from pytermgui import Container, boxes

boxes.DOUBLE_TOP.set_chars_of(Container)
c = Container() # this will now use the style chosen

Boxes are also settable as a property of pytermgui.widgets.Container, and can be referenced & defined in markup file definitions. For more info, check out pytermgui.file_loaders.

View Source
  0"""
  1Convenience objects for Container corner & border styles.
  2
  3They can be used as:
  4```python3
  5from pytermgui import Container, boxes
  6
  7boxes.DOUBLE_TOP.set_chars_of(Container)
  8c = Container() # this will now use the style chosen
  9```
 10
 11Boxes are also settable as a property of `pytermgui.widgets.Container`, and can
 12be referenced & defined in markup file definitions. For more info, check out
 13`pytermgui.file_loaders`.
 14"""
 15
 16from __future__ import annotations
 17
 18from typing import Tuple
 19
 20from .base import WidgetType
 21from ..regex import real_length
 22
 23
 24class Box:
 25    """Class for defining border & corner styles
 26
 27    `lines` should be `list[str]` of length 3, such as:
 28
 29    ```python3
 30    lines = [
 31        ".---.",
 32        "| x |",
 33        "`---`",
 34    ]
 35    ```
 36
 37    The length of individual lines is arbitrary, only limitation is
 38    that the top & bottom border characters should occur most often in
 39    their respective lines.
 40
 41    You can set corners to be of any length, their end is calculated by
 42    finding the index of the most often occuring character, which is assumed
 43    to be the border character.
 44
 45    Top & bottom borders are currently limited in length to 1, but sides
 46    operate similarly to corners. They are separated by finding the index
 47    of the fill char from the start or end. The content char is "x" by
 48    default, however it can be set to anything else by giving the "content_char"
 49    construction parameter.
 50
 51    As such, this:
 52
 53    ```python3
 54    boxes.Box(
 55       [
 56           "corner1 ________________ corner2",
 57           "xleft   ################ rightxx",
 58           "corner3 ---------------- corner4",
 59       ],
 60       content_char="#",
 61    )
 62    ```
 63
 64    Will result in:
 65
 66    ```python3
 67    Box(
 68        borders=['xleft   ', '_', ' rightxx', '-'],
 69        corners=['corner1 ', ' corner2', ' corner4', 'corner3 ']
 70    )
 71    ```
 72    """
 73
 74    CharType = Tuple[str, str, str, str]
 75
 76    def __init__(self, lines: list[str], content_char: str = "x"):
 77        """Set instance attributes"""
 78
 79        super().__init__()
 80        self.content_char = content_char
 81
 82        top, _, bottom = lines
 83        top_left, top_right = self._get_corners(top)
 84        bottom_left, bottom_right = self._get_corners(bottom)
 85
 86        self.borders = list(self._get_borders(lines))
 87        self.corners = [
 88            top_left,
 89            top_right,
 90            bottom_right,
 91            bottom_left,
 92        ]
 93
 94    def __repr__(self) -> str:
 95        """Return string of self"""
 96
 97        return self.debug()
 98
 99    @staticmethod
100    def _find_mode_char(line: str) -> str:
101        """Find most often consecutively occuring character in string"""
102
103        instances = 0
104        current_char = ""
105
106        results: list[tuple[str, int]] = []
107        for char in line:
108            if current_char == char:
109                instances += 1
110            else:
111                if len(current_char) > 0:
112                    results.append((current_char, instances))
113
114                instances = 1
115                current_char = char
116
117        results.append((current_char, instances))
118
119        results.sort(key=lambda item: item[1])
120        if len(results) == 0:
121            print(line, instances, current_char)
122
123        return results[-1][0]
124
125    def _get_corners(self, line: str) -> tuple[str, str]:
126        """Get corners from a line"""
127
128        mode_char = self._find_mode_char(line)
129        left = line[: line.index(mode_char)]
130        right = line[real_length(line) - (line[::-1].index(mode_char)) :]
131
132        return left, right
133
134    def _get_borders(self, lines: list[str]) -> tuple[str, str, str, str]:
135        """Get borders from all lines"""
136
137        top, middle, bottom = lines
138        middle_reversed = middle[::-1]
139
140        top_border = self._find_mode_char(top)
141        left_border = middle[: middle.index(self.content_char)]
142
143        right_border = middle[
144            real_length(middle) - middle_reversed.index(self.content_char) :
145        ]
146        bottom_border = self._find_mode_char(bottom)
147
148        # return top_border, left_border, right_border, bottom_border
149        return left_border, top_border, right_border, bottom_border
150
151    def set_chars_of(self, cls_or_obj: WidgetType) -> WidgetType:
152        """Set border & corner chars of cls_or_obj to self values"""
153
154        # We cannot import any widgets into here due to cyclic imports,
155        # so we have to "hack" around it.
156        if not hasattr(cls_or_obj, "set_char"):
157            raise NotImplementedError(
158                f"Object of type {cls_or_obj} does not support `set_char`"
159            )
160
161        cls_or_obj.set_char("border", self.borders)
162        cls_or_obj.set_char("corner", self.corners)
163
164        return cls_or_obj
165
166    def debug(self) -> str:
167        """Return identifiable information about object"""
168
169        return f"Box(borders={self.borders}, corners={self.corners})"
170
171
172BASIC = Box(
173    [
174        "-----",
175        "| x |",
176        "-----",
177    ]
178)
179
180HEAVY = Box(
181    [
182        "┏━━━┓",
183        "┃ x ┃",
184        "┗━━━┛",
185    ]
186)
187EMPTY = Box(
188    [
189        "",
190        "x",
191        "",
192    ]
193)
194EMPTY_VERTICAL = Box(
195    [
196        "─────",
197        "  x  ",
198        "─────",
199    ]
200)
201EMPTY_HORIZONTAL = Box(
202    [
203        "│   │",
204        "│ x │",
205        "│   │",
206    ]
207)
208ROUNDED = Box(
209    [
210        "╭───╮",
211        "│ x │",
212        "╰───╯",
213    ]
214)
215
216SINGLE = Box(
217    [
218        "┌───┐",
219        "│ x │",
220        "└───┘",
221    ]
222)
223
224SINGLE_VERTICAL = Box(
225    [
226        "╔───╗",
227        "║ x ║",
228        "╚───╝",
229    ]
230)
231SINGLE_HORIZONTAL = Box(
232    [
233        "╔═══╗",
234        "│ x │",
235        "╚═══╝",
236    ]
237)
238DOUBLE_HORIZONTAL = Box(
239    [
240        "╭═══╮",
241        "│ x │",
242        "╰═══╯",
243    ]
244)
245DOUBLE_VERTICAL = Box(
246    [
247        "╭───╮",
248        "║ x ║",
249        "╰───╯",
250    ]
251)
252DOUBLE_SIDES = Box(
253    [
254        "╭═══╮",
255        "║ x ║",
256        "╰═══╯",
257    ]
258)
259DOUBLE = Box(
260    [
261        "╔═══╗",
262        "║ x ║",
263        "╚═══╝",
264    ]
265)
266DOUBLE_TOP = Box(
267    [
268        "╭═══╮",
269        "│ x │",
270        "╰───╯",
271    ]
272)
273DOUBLE_BOTTOM = Box(
274    [
275        "╭───╮",
276        "│ x │",
277        "╰═══╯",
278    ]
279)
#   class Box:
View Source
 25class Box:
 26    """Class for defining border & corner styles
 27
 28    `lines` should be `list[str]` of length 3, such as:
 29
 30    ```python3
 31    lines = [
 32        ".---.",
 33        "| x |",
 34        "`---`",
 35    ]
 36    ```
 37
 38    The length of individual lines is arbitrary, only limitation is
 39    that the top & bottom border characters should occur most often in
 40    their respective lines.
 41
 42    You can set corners to be of any length, their end is calculated by
 43    finding the index of the most often occuring character, which is assumed
 44    to be the border character.
 45
 46    Top & bottom borders are currently limited in length to 1, but sides
 47    operate similarly to corners. They are separated by finding the index
 48    of the fill char from the start or end. The content char is "x" by
 49    default, however it can be set to anything else by giving the "content_char"
 50    construction parameter.
 51
 52    As such, this:
 53
 54    ```python3
 55    boxes.Box(
 56       [
 57           "corner1 ________________ corner2",
 58           "xleft   ################ rightxx",
 59           "corner3 ---------------- corner4",
 60       ],
 61       content_char="#",
 62    )
 63    ```
 64
 65    Will result in:
 66
 67    ```python3
 68    Box(
 69        borders=['xleft   ', '_', ' rightxx', '-'],
 70        corners=['corner1 ', ' corner2', ' corner4', 'corner3 ']
 71    )
 72    ```
 73    """
 74
 75    CharType = Tuple[str, str, str, str]
 76
 77    def __init__(self, lines: list[str], content_char: str = "x"):
 78        """Set instance attributes"""
 79
 80        super().__init__()
 81        self.content_char = content_char
 82
 83        top, _, bottom = lines
 84        top_left, top_right = self._get_corners(top)
 85        bottom_left, bottom_right = self._get_corners(bottom)
 86
 87        self.borders = list(self._get_borders(lines))
 88        self.corners = [
 89            top_left,
 90            top_right,
 91            bottom_right,
 92            bottom_left,
 93        ]
 94
 95    def __repr__(self) -> str:
 96        """Return string of self"""
 97
 98        return self.debug()
 99
100    @staticmethod
101    def _find_mode_char(line: str) -> str:
102        """Find most often consecutively occuring character in string"""
103
104        instances = 0
105        current_char = ""
106
107        results: list[tuple[str, int]] = []
108        for char in line:
109            if current_char == char:
110                instances += 1
111            else:
112                if len(current_char) > 0:
113                    results.append((current_char, instances))
114
115                instances = 1
116                current_char = char
117
118        results.append((current_char, instances))
119
120        results.sort(key=lambda item: item[1])
121        if len(results) == 0:
122            print(line, instances, current_char)
123
124        return results[-1][0]
125
126    def _get_corners(self, line: str) -> tuple[str, str]:
127        """Get corners from a line"""
128
129        mode_char = self._find_mode_char(line)
130        left = line[: line.index(mode_char)]
131        right = line[real_length(line) - (line[::-1].index(mode_char)) :]
132
133        return left, right
134
135    def _get_borders(self, lines: list[str]) -> tuple[str, str, str, str]:
136        """Get borders from all lines"""
137
138        top, middle, bottom = lines
139        middle_reversed = middle[::-1]
140
141        top_border = self._find_mode_char(top)
142        left_border = middle[: middle.index(self.content_char)]
143
144        right_border = middle[
145            real_length(middle) - middle_reversed.index(self.content_char) :
146        ]
147        bottom_border = self._find_mode_char(bottom)
148
149        # return top_border, left_border, right_border, bottom_border
150        return left_border, top_border, right_border, bottom_border
151
152    def set_chars_of(self, cls_or_obj: WidgetType) -> WidgetType:
153        """Set border & corner chars of cls_or_obj to self values"""
154
155        # We cannot import any widgets into here due to cyclic imports,
156        # so we have to "hack" around it.
157        if not hasattr(cls_or_obj, "set_char"):
158            raise NotImplementedError(
159                f"Object of type {cls_or_obj} does not support `set_char`"
160            )
161
162        cls_or_obj.set_char("border", self.borders)
163        cls_or_obj.set_char("corner", self.corners)
164
165        return cls_or_obj
166
167    def debug(self) -> str:
168        """Return identifiable information about object"""
169
170        return f"Box(borders={self.borders}, corners={self.corners})"

Class for defining border & corner styles

lines should be list[str] of length 3, such as:

lines = [
    ".---.",
    "| x |",
    "`---`",
]

The length of individual lines is arbitrary, only limitation is that the top & bottom border characters should occur most often in their respective lines.

You can set corners to be of any length, their end is calculated by finding the index of the most often occuring character, which is assumed to be the border character.

Top & bottom borders are currently limited in length to 1, but sides operate similarly to corners. They are separated by finding the index of the fill char from the start or end. The content char is "x" by default, however it can be set to anything else by giving the "content_char" construction parameter.

As such, this:

boxes.Box(
   [
       "corner1 ________________ corner2",
       "xleft   ################ rightxx",
       "corner3 ---------------- corner4",
   ],
   content_char="#",
)

Will result in:

Box(
    borders=['xleft   ', '_', ' rightxx', '-'],
    corners=['corner1 ', ' corner2', ' corner4', 'corner3 ']
)
#   Box(lines: list[str], content_char: str = 'x')
View Source
77    def __init__(self, lines: list[str], content_char: str = "x"):
78        """Set instance attributes"""
79
80        super().__init__()
81        self.content_char = content_char
82
83        top, _, bottom = lines
84        top_left, top_right = self._get_corners(top)
85        bottom_left, bottom_right = self._get_corners(bottom)
86
87        self.borders = list(self._get_borders(lines))
88        self.corners = [
89            top_left,
90            top_right,
91            bottom_right,
92            bottom_left,
93        ]

Set instance attributes

#   CharType = typing.Tuple[str, str, str, str]
#   def set_chars_of(self, cls_or_obj: 'WidgetType') -> 'WidgetType':
View Source
152    def set_chars_of(self, cls_or_obj: WidgetType) -> WidgetType:
153        """Set border & corner chars of cls_or_obj to self values"""
154
155        # We cannot import any widgets into here due to cyclic imports,
156        # so we have to "hack" around it.
157        if not hasattr(cls_or_obj, "set_char"):
158            raise NotImplementedError(
159                f"Object of type {cls_or_obj} does not support `set_char`"
160            )
161
162        cls_or_obj.set_char("border", self.borders)
163        cls_or_obj.set_char("corner", self.corners)
164
165        return cls_or_obj

Set border & corner chars of cls_or_obj to self values

#   def debug(self) -> str:
View Source
167    def debug(self) -> str:
168        """Return identifiable information about object"""
169
170        return f"Box(borders={self.borders}, corners={self.corners})"

Return identifiable information about object

#   BASIC = Box(borders=['| ', '-', ' |', '-'], corners=['', '', '', ''])
#   HEAVY = Box(borders=['┃ ', '━', ' ┃', '━'], corners=['┏', '┓', '┛', '┗'])
#   EMPTY = Box(borders=['', '', '', ''], corners=['', '', '', ''])
#   EMPTY_VERTICAL = Box(borders=[' ', '─', ' ', '─'], corners=['', '', '', ''])
#   EMPTY_HORIZONTAL = Box(borders=['│ ', ' ', ' │', ' '], corners=['│', '│', '│', '│'])
#   ROUNDED = Box(borders=['│ ', '─', ' │', '─'], corners=['╭', '╮', '╯', '╰'])
#   SINGLE = Box(borders=['│ ', '─', ' │', '─'], corners=['┌', '┐', '┘', '└'])
#   SINGLE_VERTICAL = Box(borders=['║ ', '─', ' ║', '─'], corners=['╔', '╗', '╝', '╚'])
#   SINGLE_HORIZONTAL = Box(borders=['│ ', '═', ' │', '═'], corners=['╔', '╗', '╝', '╚'])
#   DOUBLE_HORIZONTAL = Box(borders=['│ ', '═', ' │', '═'], corners=['╭', '╮', '╯', '╰'])
#   DOUBLE_VERTICAL = Box(borders=['║ ', '─', ' ║', '─'], corners=['╭', '╮', '╯', '╰'])
#   DOUBLE_SIDES = Box(borders=['║ ', '═', ' ║', '═'], corners=['╭', '╮', '╯', '╰'])
#   DOUBLE = Box(borders=['║ ', '═', ' ║', '═'], corners=['╔', '╗', '╝', '╚'])
#   DOUBLE_TOP = Box(borders=['│ ', '═', ' │', '─'], corners=['╭', '╮', '╯', '╰'])
#   DOUBLE_BOTTOM = Box(borders=['│ ', '─', ' │', '═'], corners=['╭', '╮', '╯', '╰'])