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.

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

Set instance attributes

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

Set border & corner chars of cls_or_obj to self values

def debug(self) -> str:
168    def debug(self) -> str:
169        """Return identifiable information about object"""
170
171        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=['╭', '╮', '╯', '╰'])