Skip to content

layouts

Layouts for the WindowManager.

ROW_BREAK = Slot('Row Break', Static(0), Static(0)) module-attribute

When encountered in Layout.build_rows, a new row will be started at the next element.

Auto dataclass

Bases: Dimension

An automatically calculated dimension.

The value of this dimension is overwritten on Layout.apply.

Generally, the way calculations are done is by looking at the available size of the layout by subtracting the sum of all the non-auto dimensions from the terminal's width or height, and dividing it by the number of Auto-type dimensions in the current context.

An additional offset is applied to the first dimension (left-most or top-most) of the context when the division has a remainder.

Source code in pytermgui/window_manager/layouts.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
@dataclass
class Auto(Dimension):
    """An automatically calculated dimension.

    The value of this dimension is overwritten on `Layout.apply`.

    Generally, the way calculations are done is by looking at the available
    size of the layout by subtracting the sum of all the non-auto dimensions
    from the terminal's width or height, and dividing it by the number of
    Auto-type dimensions in the current context.

    An additional offset is applied to the first dimension (left-most or top-most)
    of the context when the division has a remainder.
    """

    _value = 0

    def __repr__(self) -> str:
        return f"{type(self).__name__}(value={self.value})"

Dimension

The base class for layout dimensions.

Each dimension has a value property. This returns an integer, and is essentially the meaning of the object.

Source code in pytermgui/window_manager/layouts.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Dimension:
    """The base class for layout dimensions.

    Each dimension has a `value` property. This returns an integer,
    and is essentially the *meaning* of the object.
    """

    _value: int

    @property
    def value(self) -> int:
        """Returns the value of the object.

        Override this for custom behaviour."""

        return self._value

    @value.setter
    def value(self, new: int) -> None:
        """Sets a new value."""

        self._value = new

    def __repr__(self) -> str:
        """Returns `{typename}(value={value})`.

        We use this over the dataclasses one as that used `_value`, and it's
        a bit ugly.
        """

        return f"{type(self).__name__}(value={self.value})"

__repr__()

Returns {typename}(value={value}).

We use this over the dataclasses one as that used _value, and it's a bit ugly.

Source code in pytermgui/window_manager/layouts.py
35
36
37
38
39
40
41
42
def __repr__(self) -> str:
    """Returns `{typename}(value={value})`.

    We use this over the dataclasses one as that used `_value`, and it's
    a bit ugly.
    """

    return f"{type(self).__name__}(value={self.value})"

value() property writable

Returns the value of the object.

Override this for custom behaviour.

Source code in pytermgui/window_manager/layouts.py
21
22
23
24
25
26
27
@property
def value(self) -> int:
    """Returns the value of the object.

    Override this for custom behaviour."""

    return self._value

Layout

Defines a layout of Widgets, used by WindowManager.

Internally, it keeps track of a list of Slot. This list is then turned into a list of rows, all containing slots. This is done either when the current row has run out of the terminal's width, or ROW_BREAK is encountered.

Source code in pytermgui/window_manager/layouts.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
class Layout:
    """Defines a layout of Widgets, used by WindowManager.

    Internally, it keeps track of a list of `Slot`. This list is then turned into a list
    of rows, all containing slots. This is done either when the current row has run out
    of the terminal's width, or `ROW_BREAK` is encountered.
    """

    name: str

    def __init__(self, name: str = "Layout") -> None:
        self.name = name
        self.slots: list[Slot] = []

    @property
    def terminal(self) -> Terminal:
        """Returns the current global terminal instance."""

        return get_terminal()

    def _to_rows(self) -> list[list[Slot]]:
        """Breaks `self.slots` into a list of list of slots.

        The terminal's remaining width is kept track of, and when a slot doesn't have enough
        space left it is pushed to a new row. Additionally, `ROW_BREAK` will force a new
        row to be created, starting with the next slot.
        """

        rows: list[list[Slot]] = []
        available = self.terminal.width

        row: list[Slot] = []
        for slot in self.slots:
            if available <= 0 or slot is ROW_BREAK:
                rows.append(row)

                row = []
                available = self.terminal.width - slot.width.value

            if slot is ROW_BREAK:
                continue

            available -= slot.width.value
            row.append(slot)

        if len(row) > 0:
            rows.append(row)

        return rows

    def build_rows(self) -> list[list[Slot]]:
        """Builds a list of slot rows, breaking them & applying automatic dimensions.

        Returns:
            A list[list[Slot]], aka. a list of slot-rows.
        """

        def _get_height(row: list[Slot]) -> int:
            defined = list(filter(lambda slot: not isinstance(slot.height, Auto), row))

            if len(defined) > 0:
                return max(slot.height.value for slot in defined)

            return 0

        def _calculate_widths(row: list[Slot]) -> tuple[int, int]:
            defined: list[Slot] = list(
                filter(lambda slt: not isinstance(slt.width, Auto), row)
            )
            undefined = list(filter(lambda slt: slt not in defined, row))

            available = self.terminal.width - sum(slot.width.value for slot in defined)

            return divmod(available, len(undefined) or 1)

        rows = self._to_rows()
        heights = [_get_height(row) for row in rows]

        occupied = sum(heights)
        auto_height, extra_height = divmod(
            self.terminal.height - occupied, heights.count(0) or 1
        )

        for row, height in zip(rows, heights):
            height = height or auto_height

            auto_width, extra_width = _calculate_widths(row)
            for slot in row:
                width = auto_width if isinstance(slot.width, Auto) else slot.width.value

                if isinstance(slot.height, Auto):
                    slot.height.value = height + extra_height
                    extra_height = 0

                if isinstance(slot.width, Auto):
                    slot.width.value = width + extra_width
                    extra_width = 0

        return rows

    def add_slot(
        self,
        name: str = "Slot",
        *,
        slot: Slot | None = None,
        width: Dimension | int | float | None = None,
        height: Dimension | int | float | None = None,
        index: int = -1,
    ) -> Slot:
        """Adds a new slot to the layout.

        Args:
            name: The name of the slot. Used for display purposes.
            slot: An already instantiated `Slot` instance. If this is given,
                the additional width & height arguments will be ignored.
            width: The width for the new slot. See below for special types.
            height: The height for the new slot. See below for special types.
            index: The index to add the new slot to.

        Returns:
            The just-added slot.

        When defining dimensions, either width or height, some special value
        types can be given:
        - `Dimension`: Passed directly to the new slot.
        - `None`: An `Auto` dimension is created with no value.
        - `int`: A `Static` dimension is created with the given value.
        - `float`: A `Relative` dimension is created with the given value as its
            scale. Its `bound` attribute will default to the relevant part of the
            terminal's size.
        """

        if slot is None:
            if width is None:
                width = Auto()

            elif isinstance(width, int):
                width = Static(width)

            elif isinstance(width, float):
                width = Relative(width, bound=lambda: self.terminal.width)

            if height is None:
                height = Auto()

            elif isinstance(height, int):
                height = Static(height)

            elif isinstance(height, float):
                height = Relative(height, bound=lambda: self.terminal.height)

            slot = Slot(name, width=width, height=height)

        if index == -1:
            self.slots.append(slot)
            return slot

        self.slots.insert(index, slot)

        return slot

    def add_break(self, *, index: int = -1) -> None:
        """Adds `ROW_BREAK` to the given index.

        This special slot is ignored for all intents and purposes, other than when
        breaking the slots into rows. In that context, when encountered, the current
        row is deemed completed, and the next slot will go into a new row list.
        """

        self.add_slot(slot=ROW_BREAK, index=index)

    def assign(self, widget: Widget, *, index: int = -1, apply: bool = True) -> None:
        """Assigns a widget to the slot at the specified index.

        Args:
            widget: The widget to assign.
            index: The target slot's index.
            apply: If set, `apply` will be called once the widget has been assigned.
        """

        slots = [slot for slot in self.slots if slot is not ROW_BREAK]
        if index > len(slots) - 1:
            return

        slot = slots[index]

        slot.content = widget

        if apply:
            self.apply()

    def apply(self) -> None:
        """Applies the layout to each slot."""

        position = list(self.terminal.origin)
        for row in self.build_rows():
            position[0] = 1

            for slot in row:
                slot.apply((position[0], position[1]))

                position[0] += slot.width.value

            position[1] += max(slot.height.value for slot in row)

    def __getattr__(self, attr: str) -> Slot:
        """Gets a slot by its (slugified) name."""

        def _snakeify(name: str) -> str:
            return name.lower().replace(" ", "_")

        for slot in self.slots:
            if _snakeify(slot.name) == attr:
                return slot

        raise AttributeError(f"Slot with name {attr!r} could not be found.")

__getattr__(attr)

Gets a slot by its (slugified) name.

Source code in pytermgui/window_manager/layouts.py
376
377
378
379
380
381
382
383
384
385
386
def __getattr__(self, attr: str) -> Slot:
    """Gets a slot by its (slugified) name."""

    def _snakeify(name: str) -> str:
        return name.lower().replace(" ", "_")

    for slot in self.slots:
        if _snakeify(slot.name) == attr:
            return slot

    raise AttributeError(f"Slot with name {attr!r} could not be found.")

add_break(*, index=-1)

Adds ROW_BREAK to the given index.

This special slot is ignored for all intents and purposes, other than when breaking the slots into rows. In that context, when encountered, the current row is deemed completed, and the next slot will go into a new row list.

Source code in pytermgui/window_manager/layouts.py
332
333
334
335
336
337
338
339
340
def add_break(self, *, index: int = -1) -> None:
    """Adds `ROW_BREAK` to the given index.

    This special slot is ignored for all intents and purposes, other than when
    breaking the slots into rows. In that context, when encountered, the current
    row is deemed completed, and the next slot will go into a new row list.
    """

    self.add_slot(slot=ROW_BREAK, index=index)

add_slot(name='Slot', *, slot=None, width=None, height=None, index=-1)

Adds a new slot to the layout.

Parameters:

Name Type Description Default
name str

The name of the slot. Used for display purposes.

'Slot'
slot Slot | None

An already instantiated Slot instance. If this is given, the additional width & height arguments will be ignored.

None
width Dimension | int | float | None

The width for the new slot. See below for special types.

None
height Dimension | int | float | None

The height for the new slot. See below for special types.

None
index int

The index to add the new slot to.

-1

Returns:

Type Description
Slot

The just-added slot.

When defining dimensions, either width or height, some special value types can be given: - Dimension: Passed directly to the new slot. - None: An Auto dimension is created with no value. - int: A Static dimension is created with the given value. - float: A Relative dimension is created with the given value as its scale. Its bound attribute will default to the relevant part of the terminal's size.

Source code in pytermgui/window_manager/layouts.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def add_slot(
    self,
    name: str = "Slot",
    *,
    slot: Slot | None = None,
    width: Dimension | int | float | None = None,
    height: Dimension | int | float | None = None,
    index: int = -1,
) -> Slot:
    """Adds a new slot to the layout.

    Args:
        name: The name of the slot. Used for display purposes.
        slot: An already instantiated `Slot` instance. If this is given,
            the additional width & height arguments will be ignored.
        width: The width for the new slot. See below for special types.
        height: The height for the new slot. See below for special types.
        index: The index to add the new slot to.

    Returns:
        The just-added slot.

    When defining dimensions, either width or height, some special value
    types can be given:
    - `Dimension`: Passed directly to the new slot.
    - `None`: An `Auto` dimension is created with no value.
    - `int`: A `Static` dimension is created with the given value.
    - `float`: A `Relative` dimension is created with the given value as its
        scale. Its `bound` attribute will default to the relevant part of the
        terminal's size.
    """

    if slot is None:
        if width is None:
            width = Auto()

        elif isinstance(width, int):
            width = Static(width)

        elif isinstance(width, float):
            width = Relative(width, bound=lambda: self.terminal.width)

        if height is None:
            height = Auto()

        elif isinstance(height, int):
            height = Static(height)

        elif isinstance(height, float):
            height = Relative(height, bound=lambda: self.terminal.height)

        slot = Slot(name, width=width, height=height)

    if index == -1:
        self.slots.append(slot)
        return slot

    self.slots.insert(index, slot)

    return slot

apply()

Applies the layout to each slot.

Source code in pytermgui/window_manager/layouts.py
362
363
364
365
366
367
368
369
370
371
372
373
374
def apply(self) -> None:
    """Applies the layout to each slot."""

    position = list(self.terminal.origin)
    for row in self.build_rows():
        position[0] = 1

        for slot in row:
            slot.apply((position[0], position[1]))

            position[0] += slot.width.value

        position[1] += max(slot.height.value for slot in row)

assign(widget, *, index=-1, apply=True)

Assigns a widget to the slot at the specified index.

Parameters:

Name Type Description Default
widget Widget

The widget to assign.

required
index int

The target slot's index.

-1
apply bool

If set, apply will be called once the widget has been assigned.

True
Source code in pytermgui/window_manager/layouts.py
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
def assign(self, widget: Widget, *, index: int = -1, apply: bool = True) -> None:
    """Assigns a widget to the slot at the specified index.

    Args:
        widget: The widget to assign.
        index: The target slot's index.
        apply: If set, `apply` will be called once the widget has been assigned.
    """

    slots = [slot for slot in self.slots if slot is not ROW_BREAK]
    if index > len(slots) - 1:
        return

    slot = slots[index]

    slot.content = widget

    if apply:
        self.apply()

build_rows()

Builds a list of slot rows, breaking them & applying automatic dimensions.

Returns:

Type Description
list[list[Slot]]

A list[list[Slot]], aka. a list of slot-rows.

Source code in pytermgui/window_manager/layouts.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def build_rows(self) -> list[list[Slot]]:
    """Builds a list of slot rows, breaking them & applying automatic dimensions.

    Returns:
        A list[list[Slot]], aka. a list of slot-rows.
    """

    def _get_height(row: list[Slot]) -> int:
        defined = list(filter(lambda slot: not isinstance(slot.height, Auto), row))

        if len(defined) > 0:
            return max(slot.height.value for slot in defined)

        return 0

    def _calculate_widths(row: list[Slot]) -> tuple[int, int]:
        defined: list[Slot] = list(
            filter(lambda slt: not isinstance(slt.width, Auto), row)
        )
        undefined = list(filter(lambda slt: slt not in defined, row))

        available = self.terminal.width - sum(slot.width.value for slot in defined)

        return divmod(available, len(undefined) or 1)

    rows = self._to_rows()
    heights = [_get_height(row) for row in rows]

    occupied = sum(heights)
    auto_height, extra_height = divmod(
        self.terminal.height - occupied, heights.count(0) or 1
    )

    for row, height in zip(rows, heights):
        height = height or auto_height

        auto_width, extra_width = _calculate_widths(row)
        for slot in row:
            width = auto_width if isinstance(slot.width, Auto) else slot.width.value

            if isinstance(slot.height, Auto):
                slot.height.value = height + extra_height
                extra_height = 0

            if isinstance(slot.width, Auto):
                slot.width.value = width + extra_width
                extra_width = 0

    return rows

terminal() property

Returns the current global terminal instance.

Source code in pytermgui/window_manager/layouts.py
185
186
187
188
189
@property
def terminal(self) -> Terminal:
    """Returns the current global terminal instance."""

    return get_terminal()

Relative dataclass

Bases: Dimension

A relative dimension.

This dimension has a scale attribute and bound method. Every time the value is queried, int(self.bound() * self.scale) is returned.

When instantiated through Layout.add_slot, bound will default to either the terminal's width or height, depending on which attribute it is applied to.

Source code in pytermgui/window_manager/layouts.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@dataclass(repr=False)
class Relative(Dimension):
    """A relative dimension.

    This dimension has a scale attribute and bound method. Every time  the `value`
    is queried, `int(self.bound() * self.scale)` is returned.

    When instantiated through `Layout.add_slot`, `bound` will default to either
    the terminal's width or height, depending on which attribute it is applied to.
    """

    _value = 0
    scale: float
    bound: Callable[[], int]

    @property
    def value(self) -> int:
        """Calculates the new value for the dimension."""

        return int(self.bound() * self.scale)

    @value.setter
    def value(self, new: int) -> None:
        """Disallows setting the value.

        We can't inherit and then override a set-get property with a get one, so this
        kind of patches that issue up.
        """

        raise TypeError

    def __repr__(self) -> str:
        scale = self.scale
        bound = self.bound

        original = super().__repr__()
        return original[:-1] + f", {scale=}, {bound=}" + original[-1]

value() property writable

Calculates the new value for the dimension.

Source code in pytermgui/window_manager/layouts.py
70
71
72
73
74
@property
def value(self) -> int:
    """Calculates the new value for the dimension."""

    return int(self.bound() * self.scale)

Slot dataclass

A slot within a layout.

A slot has a name, width & height, as well as some content. It's apply method can be called to apply the slot's position & dimensions to its content.

Source code in pytermgui/window_manager/layouts.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
@dataclass
class Slot:
    """A slot within a layout.

    A slot has a name, width & height, as well as some content. It's `apply` method
    can be called to apply the slot's position & dimensions to its content.
    """

    name: str
    width: Dimension
    height: Dimension

    content: Widget | None = None

    _restore_data: tuple[int, int, tuple[int, int]] | None = None

    def apply(self, position: tuple[int, int]) -> None:
        """Applies the given position & dimension to the content.

        Args:
            position: The position that this object resides in. Set as its content's `pos`.
        """

        if self.content is None or self.width is None or self.height is None:
            return

        if self._restore_data is None:
            self._restore_data = (
                self.content.width,
                self.content.height,
                self.content.pos,
            )

        self.content.height = self.height.value
        self.content.width = self.width.value
        self.content.pos = position

    def detach_content(self) -> None:
        """Detaches content & restores its original state."""

        content = self.content
        if content is None:
            raise AttributeError(f"No content to detach in {self!r}.")

        assert self._restore_data is not None

        content.width, content.height, content.pos = self._restore_data

        self.content = None
        self._restore_data = None

apply(position)

Applies the given position & dimension to the content.

Parameters:

Name Type Description Default
position tuple[int, int]

The position that this object resides in. Set as its content's pos.

required
Source code in pytermgui/window_manager/layouts.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def apply(self, position: tuple[int, int]) -> None:
    """Applies the given position & dimension to the content.

    Args:
        position: The position that this object resides in. Set as its content's `pos`.
    """

    if self.content is None or self.width is None or self.height is None:
        return

    if self._restore_data is None:
        self._restore_data = (
            self.content.width,
            self.content.height,
            self.content.pos,
        )

    self.content.height = self.height.value
    self.content.width = self.width.value
    self.content.pos = position

detach_content()

Detaches content & restores its original state.

Source code in pytermgui/window_manager/layouts.py
152
153
154
155
156
157
158
159
160
161
162
163
164
def detach_content(self) -> None:
    """Detaches content & restores its original state."""

    content = self.content
    if content is None:
        raise AttributeError(f"No content to detach in {self!r}.")

    assert self._restore_data is not None

    content.width, content.height, content.pos = self._restore_data

    self.content = None
    self._restore_data = None

Static dataclass

Bases: Dimension

A static dimension.

This dimension is immutable, and the Layout will always leave it unchanged.

Source code in pytermgui/window_manager/layouts.py
45
46
47
48
49
50
51
52
@dataclass(repr=False, frozen=True)
class Static(Dimension):
    """A static dimension.

    This dimension is immutable, and the Layout will always leave it unchanged.
    """

    _value: int = 0