Skip to content

manager

The WindowManager class, whos job it is to move, control and update windows, while letting Compositor draw them.

Edge

Bases: Enum

Enum for window edges.

Source code in pytermgui/window_manager/manager.py
35
36
37
38
39
40
41
class Edge(Enum):
    """Enum for window edges."""

    LEFT = _auto()
    TOP = _auto()
    RIGHT = _auto()
    BOTTOM = _auto()

WindowManager

Bases: Widget

The manager of windows.

This class can be used, or even subclassed in order to create full-screen applications, using the pytermgui.window_manager.window.Window class and the general Widget API.

Source code in pytermgui/window_manager/manager.py
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
165
166
167
168
169
170
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
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
class WindowManager(Widget):  # pylint: disable=too-many-instance-attributes
    """The manager of windows.

    This class can be used, or even subclassed in order to create full-screen applications,
    using the `pytermgui.window_manager.window.Window` class and the general Widget API.
    """

    focusing_actions = (MouseAction.LEFT_CLICK, MouseAction.RIGHT_CLICK)
    """These mouse actions will focus the window they are acted upon."""

    autorun = True

    def __init__(
        self,
        *,
        layout_type: Type[Layout] = Layout,
        framerate: int = 60,
        autorun: bool | None = None,
    ) -> None:
        """Initialize the manager."""

        super().__init__()

        self._is_running = False
        self._windows: list[Window] = []
        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}

        self.focused: Window | None = None

        if autorun is not None:
            self.autorun = autorun

        self.layout = layout_type()
        self.compositor = Compositor(self._windows, framerate=framerate)
        self.mouse_translator: MouseTranslator | None = None

        self._mouse_target: Window | None = None
        self._drag_offsets: tuple[int, int] = (0, 0)
        self._drag_target: tuple[Window, Edge] | None = None

        # This isn't quite implemented at the moment.
        self.restrict_within_bounds = True

        terminal.subscribe(terminal.RESIZE, self.on_resize)

    def __iadd__(self, other: object) -> WindowManager:
        """Adds a window to the manager."""

        if not isinstance(other, Window):
            raise ValueError("You may only add windows to a WindowManager.")

        return self.add(other)

    def __isub__(self, other: object) -> WindowManager:
        """Removes a window from the manager."""

        if not isinstance(other, Window):
            raise ValueError("You may only add windows to a WindowManager.")

        return self.remove(other)

    def __enter__(self) -> WindowManager:
        """Starts context manager."""

        return self

    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
        """Ends context manager."""

        # Run the manager if it hasnt been run before.
        if self.autorun and exception is None and self.mouse_translator is None:
            self.run()

        if exception is not None:
            self.stop()
            raise exception

        return True

    def __iter__(self) -> Iterator[Window]:
        """Iterates this manager's windows."""

        return iter(self._windows)

    def _run_input_loop(self) -> None:
        """The main input loop of the WindowManager."""

        while self._is_running:
            key = getch(interrupts=False)

            if key == chr(3):
                self.stop()
                break

            if self.handle_key(key):
                continue

            self.process_mouse(key)

    def get_lines(self) -> list[str]:
        """Gets the empty list."""

        # TODO: Allow using WindowManager as a widget.

        return []

    def clear_cache(self, window: Window) -> None:
        """Clears the compositor's cache related to the given window."""

        self.compositor.clear_cache(window)

    def on_resize(self, size: tuple[int, int]) -> None:
        """Correctly updates window positions & prints when terminal gets resized.

        Args:
            size: The new terminal size.
        """

        width, height = size

        for window in self._windows:
            newx = max(0, min(window.pos[0], width - window.width))
            newy = max(0, min(window.pos[1], height - window.height + 1))

            window.pos = (newx, newy)

        self.layout.apply()
        self.compositor.redraw()

    def run(self, mouse_events: list[str] | None = None) -> None:
        """Starts the WindowManager.

        Args:
            mouse_events: A list of mouse event types to listen to. See
                `pytermgui.ansi_interface.report_mouse` for more information.
                Defaults to `["press_hold", "hover"]`.

        Returns:
            The WindowManager's compositor instance.
        """

        self._is_running = True

        if mouse_events is None:
            mouse_events = ["press_hold", "hover"]

        with alt_buffer(cursor=False, echo=False):
            with mouse_handler(mouse_events, "decimal_xterm") as translate:
                self.mouse_translator = translate
                self.compositor.run()

                self._run_input_loop()

    def stop(self) -> None:
        """Stops the WindowManager and its compositor."""

        self.compositor.stop()
        self._is_running = False

    def add(
        self, window: Window, assign: str | bool = True, animate: bool = True
    ) -> WindowManager:
        """Adds a window to the manager.

        Args:
            window: The window to add.
            assign: The name of the slot the new window should be assigned to, or a
                boolean. If it is given a str, it is treated as the name of a slot. When
                given True, the next non-filled slot will be assigned, and when given
                False no assignment will be done.
            animate: If set, an animation will be played on the window once it's added.
        """

        self._windows.insert(0, window)
        window.manager = self

        if assign:
            if isinstance(assign, str):
                getattr(self.layout, assign).content = window

            elif len(self._windows) <= len(self.layout.slots):
                self.layout.assign(window, index=len(self._windows) - 1)

            self.layout.apply()

        # New windows take focus-precedence over already
        # existing ones, even if they are modal.
        self.focus(window)

        if not animate:
            return self

        if window.height > 1:
            animator.animate_attr(
                target=window,
                attr="height",
                start=0,
                end=window.height,
                duration=300,
                on_step=_center_during_animation,
            )

        return self

    def remove(
        self,
        window: Window,
        autostop: bool = True,
        animate: bool = True,
    ) -> WindowManager:
        """Removes a window from the manager.

        Args:
            window: The window to remove.
            autostop: If set, the manager will be stopped if the length of its windows
                hits 0.
        """

        def _on_finish(_: AttrAnimation | None) -> bool:
            self._windows.remove(window)

            if autostop and len(self._windows) == 0:
                self.stop()
            else:
                self.focus(self._windows[0])

            return True

        if not animate:
            _on_finish(None)
            return self

        animator.animate_attr(
            target=window,
            attr="height",
            end=0,
            duration=300,
            on_step=_center_during_animation,
            on_finish=_on_finish,
        )

        return self

    def focus(self, window: Window | None) -> None:
        """Focuses a window by moving it to the first index in _windows."""

        if self.focused is not None:
            self.focused.blur()

        self.focused = window

        if window is not None:
            self._windows.remove(window)
            self._windows.insert(0, window)

            window.focus()

    def focus_next(self) -> Window | None:
        """Focuses the next window in focus order, looping to first at the end."""

        if self.focused is None:
            self.focus(self._windows[0])
            return self.focused

        index = self._windows.index(self.focused)
        if index == len(self._windows) - 1:
            index = 0

        window = self._windows[index]
        traversed = 0
        while window.is_persistent or window is self.focused:
            if index >= len(self._windows):
                index = 0

            window = self._windows[index]

            index += 1
            traversed += 1
            if traversed >= len(self._windows):
                return self.focused

        self.focus(self._windows[index])

        return self.focused

    def handle_key(self, key: str) -> bool:
        """Processes a keypress.

        Args:
            key: The key to handle.

        Returns:
            True if the given key could be processed, False otherwise.
        """

        # Apply WindowManager bindings
        if self.execute_binding(key):
            return True

        # Apply focused window binding, or send to InputField
        if self.focused is not None:
            if self.focused.execute_binding(key):
                return True

            if self.focused.handle_key(key):
                return True

        return False

    # I prefer having the _click, _drag and _release helpers within this function, for
    # easier readability.
    def process_mouse(self, key: str) -> None:  # pylint: disable=too-many-statements
        """Processes (potential) mouse input.

        Args:
            key: Input to handle.
        """

        window: Window

        def _clamp_pos(pos: tuple[int, int], index: int) -> int:
            """Clamp a value using index to address x/y & width/height"""

            offset = self._drag_offsets[index]

            # TODO: This -2 is a very magical number. Not good.
            maximum = terminal.size[index] - ((window.width, window.height)[index] - 2)

            start_margin_index = abs(index - 1)

            if self.restrict_within_bounds:
                return max(
                    index + terminal.margins[start_margin_index],
                    min(
                        pos[index] - offset,
                        maximum
                        - terminal.margins[start_margin_index + 2]
                        - terminal.origin[index],
                    ),
                )

            return pos[index] - offset

        def _click(pos: tuple[int, int], window: Window) -> bool:
            """Process clicking a window."""

            left, top, right, bottom = window.rect
            borders = window.chars.get("border", [" "] * 4)

            if real_length(borders[1]) > 0 and pos[1] == top and left <= pos[0] < right:
                self._drag_target = (window, Edge.TOP)

            elif (
                real_length(borders[3]) > 0
                and pos[1] == bottom - 1
                and left <= pos[0] < right
            ):
                self._drag_target = (window, Edge.BOTTOM)

            elif (
                real_length(borders[0]) > 0
                and pos[0] == left
                and top <= pos[1] < bottom
            ):
                self._drag_target = (window, Edge.LEFT)

            elif (
                real_length(borders[2]) > 0
                and pos[0] == right - 1
                and top <= pos[1] < bottom
            ):
                self._drag_target = (window, Edge.RIGHT)

            else:
                return False

            self._drag_offsets = (
                pos[0] - window.pos[0],
                pos[1] - window.pos[1],
            )

            return True

        def _drag(pos: tuple[int, int], window: Window) -> bool:
            """Process dragging a window"""

            if self._drag_target is None:
                return False

            target_window, edge = self._drag_target
            handled = False

            if window is not target_window:
                return False

            left, top, right, bottom = window.rect

            if not window.is_static and edge is Edge.TOP:
                window.pos = (
                    _clamp_pos(pos, 0),
                    _clamp_pos(pos, 1),
                )

                handled = True

            # TODO: Why are all these arbitrary offsets needed?
            elif not window.is_noresize:
                if edge is Edge.RIGHT:
                    window.rect = (left, top, pos[0] + 1, bottom)
                    handled = True

                elif edge is Edge.LEFT:
                    window.rect = (pos[0], top, right, bottom)
                    handled = True

                elif edge is Edge.BOTTOM:
                    window.rect = (left, top, right, pos[1] + 1)
                    handled = True

            if handled:
                window.is_dirty = True
                self.compositor.set_redraw()

            return handled

        def _release(_: tuple[int, int], __: Window) -> bool:
            """Process release of key"""

            self._drag_target = None

            # This return False so Window can handle the mouse action as well,
            # as not much is done in this callback.
            return False

        handlers = {
            MouseAction.LEFT_CLICK: _click,
            MouseAction.LEFT_DRAG: _drag,
            MouseAction.RELEASE: _release,
        }

        translate = self.mouse_translator
        event_list = None if translate is None else translate(key)

        if event_list is None:
            return

        for event in event_list:
            # Ignore null-events
            if event is None:
                continue

            for window in self._windows:
                contains = window.contains(event.position)

                if event.action in self.focusing_actions:
                    self.focus(window)

                if event.action in handlers and handlers[event.action](
                    event.position, window
                ):
                    break

                if contains:
                    if self._mouse_target is not None:
                        self._mouse_target.handle_mouse(
                            MouseEvent(MouseAction.RELEASE, event.position)
                        )

                    self._mouse_target = window
                    window.handle_mouse(event)
                    break

                if window.is_modal:
                    break

            # Unset drag_target if no windows received the input
            else:
                self._drag_target = None
                if self._mouse_target is not None:
                    self._mouse_target.handle_mouse(
                        MouseEvent(MouseAction.RELEASE, event.position)
                    )

                self._mouse_target = None

    def screenshot(self, title: str, filename: str = "screenshot.svg") -> None:
        """Takes a screenshot of the current state.

        See `pytermgui.exporters.to_svg` for more information.

        Args:
            filename: The name of the file.
        """

        self.compositor.capture(title=title, filename=filename)

    def show_positions(self) -> None:
        """Shows the positions of each Window's widgets."""

        def _show_positions(widget, color_base: int = 60) -> None:
            """Show positions of widget."""

            if isinstance(widget, Container):
                for i, subwidget in enumerate(widget):
                    _show_positions(subwidget, color_base + i)

                return

            if not widget.is_selectable:
                return

            debug = widget.debug()
            color = str_to_color(f"@{color_base}")
            buff = color(" ", reset=False)

            for i in range(min(widget.width, real_length(debug)) - 1):
                buff += debug[i]

            self.terminal.write(buff, pos=widget.pos)

        for widget in self._windows:
            _show_positions(widget)
        self.terminal.flush()

        getch()

    def alert(self, *items: Any, center: bool = True, **attributes: Any) -> Window:
        """Creates a modal popup of the given elements and attributes.

        Args:
            *items: All widget-convertable objects passed as children of the new window.
            center: If set, `pytermgui.window_manager.window.center` is called on the window.
            **attributes: kwargs passed as the new window's attributes.
        """

        window = Window(*items, is_modal=True, **attributes)

        if center:
            window.center()

        self.add(window, assign=False)

        return window

    def toast(
        self,
        *items: Any,
        offset: int = 0,
        duration: int = 300,
        delay: int = 1000,
        **attributes: Any,
    ) -> Window:
        """Creates a Material UI-inspired toast window of the given elements and attributes.

        Args:
            *items: All widget-convertable objects passed as children of the new window.
            delay: The amount of time before the window will start animating out.
            **attributes: kwargs passed as the new window's attributes.
        """

        # pylint: disable=no-value-for-parameter

        toast = Window(*items, is_noblur=True, **attributes)

        target_height = toast.height
        toast.overflow = Overflow.HIDE

        def _finish(_: Animation) -> None:
            self.remove(toast, animate=False)

        def _progressively_show(anim: Animation, invert: bool = False) -> bool:
            height = int(anim.state * target_height)

            toast.center()

            if invert:
                toast.height = target_height - 1 - height
                toast.pos = (
                    toast.pos[0],
                    self.terminal.height - toast.height + 1 - offset,
                )
                return False

            toast.height = height
            toast.pos = (toast.pos[0], self.terminal.height - toast.height + 1 - offset)

            return False

        def _animate_toast_out(_: Animation) -> None:
            animator.schedule(
                FloatAnimation(
                    delay,
                    on_finish=lambda *_: animator.schedule(
                        FloatAnimation(
                            duration,
                            on_step=lambda anim: _progressively_show(anim, invert=True),
                            on_finish=_finish,
                        )
                    ),
                )
            )

        leadup = FloatAnimation(
            duration, on_step=_progressively_show, on_finish=_animate_toast_out
        )

        # pylint: enable=no-value-for-parameter

        self.add(toast.center(), animate=False, assign=False)
        self.focus(toast)
        animator.schedule(leadup)

        return toast

focusing_actions = (MouseAction.LEFT_CLICK, MouseAction.RIGHT_CLICK) class-attribute

These mouse actions will focus the window they are acted upon.

__enter__()

Starts context manager.

Source code in pytermgui/window_manager/manager.py
105
106
107
108
def __enter__(self) -> WindowManager:
    """Starts context manager."""

    return self

__exit__(_, exception, __)

Ends context manager.

Source code in pytermgui/window_manager/manager.py
110
111
112
113
114
115
116
117
118
119
120
121
def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
    """Ends context manager."""

    # Run the manager if it hasnt been run before.
    if self.autorun and exception is None and self.mouse_translator is None:
        self.run()

    if exception is not None:
        self.stop()
        raise exception

    return True

__iadd__(other)

Adds a window to the manager.

Source code in pytermgui/window_manager/manager.py
89
90
91
92
93
94
95
def __iadd__(self, other: object) -> WindowManager:
    """Adds a window to the manager."""

    if not isinstance(other, Window):
        raise ValueError("You may only add windows to a WindowManager.")

    return self.add(other)

__init__(*, layout_type=Layout, framerate=60, autorun=None)

Initialize the manager.

Source code in pytermgui/window_manager/manager.py
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
def __init__(
    self,
    *,
    layout_type: Type[Layout] = Layout,
    framerate: int = 60,
    autorun: bool | None = None,
) -> None:
    """Initialize the manager."""

    super().__init__()

    self._is_running = False
    self._windows: list[Window] = []
    self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}

    self.focused: Window | None = None

    if autorun is not None:
        self.autorun = autorun

    self.layout = layout_type()
    self.compositor = Compositor(self._windows, framerate=framerate)
    self.mouse_translator: MouseTranslator | None = None

    self._mouse_target: Window | None = None
    self._drag_offsets: tuple[int, int] = (0, 0)
    self._drag_target: tuple[Window, Edge] | None = None

    # This isn't quite implemented at the moment.
    self.restrict_within_bounds = True

    terminal.subscribe(terminal.RESIZE, self.on_resize)

__isub__(other)

Removes a window from the manager.

Source code in pytermgui/window_manager/manager.py
 97
 98
 99
100
101
102
103
def __isub__(self, other: object) -> WindowManager:
    """Removes a window from the manager."""

    if not isinstance(other, Window):
        raise ValueError("You may only add windows to a WindowManager.")

    return self.remove(other)

__iter__()

Iterates this manager's windows.

Source code in pytermgui/window_manager/manager.py
123
124
125
126
def __iter__(self) -> Iterator[Window]:
    """Iterates this manager's windows."""

    return iter(self._windows)

add(window, assign=True, animate=True)

Adds a window to the manager.

Parameters:

Name Type Description Default
window Window

The window to add.

required
assign str | bool

The name of the slot the new window should be assigned to, or a boolean. If it is given a str, it is treated as the name of a slot. When given True, the next non-filled slot will be assigned, and when given False no assignment will be done.

True
animate bool

If set, an animation will be played on the window once it's added.

True
Source code in pytermgui/window_manager/manager.py
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
def add(
    self, window: Window, assign: str | bool = True, animate: bool = True
) -> WindowManager:
    """Adds a window to the manager.

    Args:
        window: The window to add.
        assign: The name of the slot the new window should be assigned to, or a
            boolean. If it is given a str, it is treated as the name of a slot. When
            given True, the next non-filled slot will be assigned, and when given
            False no assignment will be done.
        animate: If set, an animation will be played on the window once it's added.
    """

    self._windows.insert(0, window)
    window.manager = self

    if assign:
        if isinstance(assign, str):
            getattr(self.layout, assign).content = window

        elif len(self._windows) <= len(self.layout.slots):
            self.layout.assign(window, index=len(self._windows) - 1)

        self.layout.apply()

    # New windows take focus-precedence over already
    # existing ones, even if they are modal.
    self.focus(window)

    if not animate:
        return self

    if window.height > 1:
        animator.animate_attr(
            target=window,
            attr="height",
            start=0,
            end=window.height,
            duration=300,
            on_step=_center_during_animation,
        )

    return self

alert(*items, center=True, **attributes)

Creates a modal popup of the given elements and attributes.

Parameters:

Name Type Description Default
*items Any

All widget-convertable objects passed as children of the new window.

()
center bool

If set, pytermgui.window_manager.window.center is called on the window.

True
**attributes Any

kwargs passed as the new window's attributes.

{}
Source code in pytermgui/window_manager/manager.py
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
def alert(self, *items: Any, center: bool = True, **attributes: Any) -> Window:
    """Creates a modal popup of the given elements and attributes.

    Args:
        *items: All widget-convertable objects passed as children of the new window.
        center: If set, `pytermgui.window_manager.window.center` is called on the window.
        **attributes: kwargs passed as the new window's attributes.
    """

    window = Window(*items, is_modal=True, **attributes)

    if center:
        window.center()

    self.add(window, assign=False)

    return window

clear_cache(window)

Clears the compositor's cache related to the given window.

Source code in pytermgui/window_manager/manager.py
150
151
152
153
def clear_cache(self, window: Window) -> None:
    """Clears the compositor's cache related to the given window."""

    self.compositor.clear_cache(window)

focus(window)

Focuses a window by moving it to the first index in _windows.

Source code in pytermgui/window_manager/manager.py
287
288
289
290
291
292
293
294
295
296
297
298
299
def focus(self, window: Window | None) -> None:
    """Focuses a window by moving it to the first index in _windows."""

    if self.focused is not None:
        self.focused.blur()

    self.focused = window

    if window is not None:
        self._windows.remove(window)
        self._windows.insert(0, window)

        window.focus()

focus_next()

Focuses the next window in focus order, looping to first at the end.

Source code in pytermgui/window_manager/manager.py
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
def focus_next(self) -> Window | None:
    """Focuses the next window in focus order, looping to first at the end."""

    if self.focused is None:
        self.focus(self._windows[0])
        return self.focused

    index = self._windows.index(self.focused)
    if index == len(self._windows) - 1:
        index = 0

    window = self._windows[index]
    traversed = 0
    while window.is_persistent or window is self.focused:
        if index >= len(self._windows):
            index = 0

        window = self._windows[index]

        index += 1
        traversed += 1
        if traversed >= len(self._windows):
            return self.focused

    self.focus(self._windows[index])

    return self.focused

get_lines()

Gets the empty list.

Source code in pytermgui/window_manager/manager.py
143
144
145
146
147
148
def get_lines(self) -> list[str]:
    """Gets the empty list."""

    # TODO: Allow using WindowManager as a widget.

    return []

handle_key(key)

Processes a keypress.

Parameters:

Name Type Description Default
key str

The key to handle.

required

Returns:

Type Description
bool

True if the given key could be processed, False otherwise.

Source code in pytermgui/window_manager/manager.py
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
def handle_key(self, key: str) -> bool:
    """Processes a keypress.

    Args:
        key: The key to handle.

    Returns:
        True if the given key could be processed, False otherwise.
    """

    # Apply WindowManager bindings
    if self.execute_binding(key):
        return True

    # Apply focused window binding, or send to InputField
    if self.focused is not None:
        if self.focused.execute_binding(key):
            return True

        if self.focused.handle_key(key):
            return True

    return False

on_resize(size)

Correctly updates window positions & prints when terminal gets resized.

Parameters:

Name Type Description Default
size tuple[int, int]

The new terminal size.

required
Source code in pytermgui/window_manager/manager.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def on_resize(self, size: tuple[int, int]) -> None:
    """Correctly updates window positions & prints when terminal gets resized.

    Args:
        size: The new terminal size.
    """

    width, height = size

    for window in self._windows:
        newx = max(0, min(window.pos[0], width - window.width))
        newy = max(0, min(window.pos[1], height - window.height + 1))

        window.pos = (newx, newy)

    self.layout.apply()
    self.compositor.redraw()

process_mouse(key)

Processes (potential) mouse input.

Parameters:

Name Type Description Default
key str

Input to handle.

required
Source code in pytermgui/window_manager/manager.py
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
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
def process_mouse(self, key: str) -> None:  # pylint: disable=too-many-statements
    """Processes (potential) mouse input.

    Args:
        key: Input to handle.
    """

    window: Window

    def _clamp_pos(pos: tuple[int, int], index: int) -> int:
        """Clamp a value using index to address x/y & width/height"""

        offset = self._drag_offsets[index]

        # TODO: This -2 is a very magical number. Not good.
        maximum = terminal.size[index] - ((window.width, window.height)[index] - 2)

        start_margin_index = abs(index - 1)

        if self.restrict_within_bounds:
            return max(
                index + terminal.margins[start_margin_index],
                min(
                    pos[index] - offset,
                    maximum
                    - terminal.margins[start_margin_index + 2]
                    - terminal.origin[index],
                ),
            )

        return pos[index] - offset

    def _click(pos: tuple[int, int], window: Window) -> bool:
        """Process clicking a window."""

        left, top, right, bottom = window.rect
        borders = window.chars.get("border", [" "] * 4)

        if real_length(borders[1]) > 0 and pos[1] == top and left <= pos[0] < right:
            self._drag_target = (window, Edge.TOP)

        elif (
            real_length(borders[3]) > 0
            and pos[1] == bottom - 1
            and left <= pos[0] < right
        ):
            self._drag_target = (window, Edge.BOTTOM)

        elif (
            real_length(borders[0]) > 0
            and pos[0] == left
            and top <= pos[1] < bottom
        ):
            self._drag_target = (window, Edge.LEFT)

        elif (
            real_length(borders[2]) > 0
            and pos[0] == right - 1
            and top <= pos[1] < bottom
        ):
            self._drag_target = (window, Edge.RIGHT)

        else:
            return False

        self._drag_offsets = (
            pos[0] - window.pos[0],
            pos[1] - window.pos[1],
        )

        return True

    def _drag(pos: tuple[int, int], window: Window) -> bool:
        """Process dragging a window"""

        if self._drag_target is None:
            return False

        target_window, edge = self._drag_target
        handled = False

        if window is not target_window:
            return False

        left, top, right, bottom = window.rect

        if not window.is_static and edge is Edge.TOP:
            window.pos = (
                _clamp_pos(pos, 0),
                _clamp_pos(pos, 1),
            )

            handled = True

        # TODO: Why are all these arbitrary offsets needed?
        elif not window.is_noresize:
            if edge is Edge.RIGHT:
                window.rect = (left, top, pos[0] + 1, bottom)
                handled = True

            elif edge is Edge.LEFT:
                window.rect = (pos[0], top, right, bottom)
                handled = True

            elif edge is Edge.BOTTOM:
                window.rect = (left, top, right, pos[1] + 1)
                handled = True

        if handled:
            window.is_dirty = True
            self.compositor.set_redraw()

        return handled

    def _release(_: tuple[int, int], __: Window) -> bool:
        """Process release of key"""

        self._drag_target = None

        # This return False so Window can handle the mouse action as well,
        # as not much is done in this callback.
        return False

    handlers = {
        MouseAction.LEFT_CLICK: _click,
        MouseAction.LEFT_DRAG: _drag,
        MouseAction.RELEASE: _release,
    }

    translate = self.mouse_translator
    event_list = None if translate is None else translate(key)

    if event_list is None:
        return

    for event in event_list:
        # Ignore null-events
        if event is None:
            continue

        for window in self._windows:
            contains = window.contains(event.position)

            if event.action in self.focusing_actions:
                self.focus(window)

            if event.action in handlers and handlers[event.action](
                event.position, window
            ):
                break

            if contains:
                if self._mouse_target is not None:
                    self._mouse_target.handle_mouse(
                        MouseEvent(MouseAction.RELEASE, event.position)
                    )

                self._mouse_target = window
                window.handle_mouse(event)
                break

            if window.is_modal:
                break

        # Unset drag_target if no windows received the input
        else:
            self._drag_target = None
            if self._mouse_target is not None:
                self._mouse_target.handle_mouse(
                    MouseEvent(MouseAction.RELEASE, event.position)
                )

            self._mouse_target = None

remove(window, autostop=True, animate=True)

Removes a window from the manager.

Parameters:

Name Type Description Default
window Window

The window to remove.

required
autostop bool

If set, the manager will be stopped if the length of its windows hits 0.

True
Source code in pytermgui/window_manager/manager.py
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
def remove(
    self,
    window: Window,
    autostop: bool = True,
    animate: bool = True,
) -> WindowManager:
    """Removes a window from the manager.

    Args:
        window: The window to remove.
        autostop: If set, the manager will be stopped if the length of its windows
            hits 0.
    """

    def _on_finish(_: AttrAnimation | None) -> bool:
        self._windows.remove(window)

        if autostop and len(self._windows) == 0:
            self.stop()
        else:
            self.focus(self._windows[0])

        return True

    if not animate:
        _on_finish(None)
        return self

    animator.animate_attr(
        target=window,
        attr="height",
        end=0,
        duration=300,
        on_step=_center_during_animation,
        on_finish=_on_finish,
    )

    return self

run(mouse_events=None)

Starts the WindowManager.

Parameters:

Name Type Description Default
mouse_events list[str] | None

A list of mouse event types to listen to. See pytermgui.ansi_interface.report_mouse for more information. Defaults to ["press_hold", "hover"].

None

Returns:

Type Description
None

The WindowManager's compositor instance.

Source code in pytermgui/window_manager/manager.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def run(self, mouse_events: list[str] | None = None) -> None:
    """Starts the WindowManager.

    Args:
        mouse_events: A list of mouse event types to listen to. See
            `pytermgui.ansi_interface.report_mouse` for more information.
            Defaults to `["press_hold", "hover"]`.

    Returns:
        The WindowManager's compositor instance.
    """

    self._is_running = True

    if mouse_events is None:
        mouse_events = ["press_hold", "hover"]

    with alt_buffer(cursor=False, echo=False):
        with mouse_handler(mouse_events, "decimal_xterm") as translate:
            self.mouse_translator = translate
            self.compositor.run()

            self._run_input_loop()

screenshot(title, filename='screenshot.svg')

Takes a screenshot of the current state.

See pytermgui.exporters.to_svg for more information.

Parameters:

Name Type Description Default
filename str

The name of the file.

'screenshot.svg'
Source code in pytermgui/window_manager/manager.py
529
530
531
532
533
534
535
536
537
538
def screenshot(self, title: str, filename: str = "screenshot.svg") -> None:
    """Takes a screenshot of the current state.

    See `pytermgui.exporters.to_svg` for more information.

    Args:
        filename: The name of the file.
    """

    self.compositor.capture(title=title, filename=filename)

show_positions()

Shows the positions of each Window's widgets.

Source code in pytermgui/window_manager/manager.py
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
def show_positions(self) -> None:
    """Shows the positions of each Window's widgets."""

    def _show_positions(widget, color_base: int = 60) -> None:
        """Show positions of widget."""

        if isinstance(widget, Container):
            for i, subwidget in enumerate(widget):
                _show_positions(subwidget, color_base + i)

            return

        if not widget.is_selectable:
            return

        debug = widget.debug()
        color = str_to_color(f"@{color_base}")
        buff = color(" ", reset=False)

        for i in range(min(widget.width, real_length(debug)) - 1):
            buff += debug[i]

        self.terminal.write(buff, pos=widget.pos)

    for widget in self._windows:
        _show_positions(widget)
    self.terminal.flush()

    getch()

stop()

Stops the WindowManager and its compositor.

Source code in pytermgui/window_manager/manager.py
197
198
199
200
201
def stop(self) -> None:
    """Stops the WindowManager and its compositor."""

    self.compositor.stop()
    self._is_running = False

toast(*items, offset=0, duration=300, delay=1000, **attributes)

Creates a Material UI-inspired toast window of the given elements and attributes.

Parameters:

Name Type Description Default
*items Any

All widget-convertable objects passed as children of the new window.

()
delay int

The amount of time before the window will start animating out.

1000
**attributes Any

kwargs passed as the new window's attributes.

{}
Source code in pytermgui/window_manager/manager.py
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
def toast(
    self,
    *items: Any,
    offset: int = 0,
    duration: int = 300,
    delay: int = 1000,
    **attributes: Any,
) -> Window:
    """Creates a Material UI-inspired toast window of the given elements and attributes.

    Args:
        *items: All widget-convertable objects passed as children of the new window.
        delay: The amount of time before the window will start animating out.
        **attributes: kwargs passed as the new window's attributes.
    """

    # pylint: disable=no-value-for-parameter

    toast = Window(*items, is_noblur=True, **attributes)

    target_height = toast.height
    toast.overflow = Overflow.HIDE

    def _finish(_: Animation) -> None:
        self.remove(toast, animate=False)

    def _progressively_show(anim: Animation, invert: bool = False) -> bool:
        height = int(anim.state * target_height)

        toast.center()

        if invert:
            toast.height = target_height - 1 - height
            toast.pos = (
                toast.pos[0],
                self.terminal.height - toast.height + 1 - offset,
            )
            return False

        toast.height = height
        toast.pos = (toast.pos[0], self.terminal.height - toast.height + 1 - offset)

        return False

    def _animate_toast_out(_: Animation) -> None:
        animator.schedule(
            FloatAnimation(
                delay,
                on_finish=lambda *_: animator.schedule(
                    FloatAnimation(
                        duration,
                        on_step=lambda anim: _progressively_show(anim, invert=True),
                        on_finish=_finish,
                    )
                ),
            )
        )

    leadup = FloatAnimation(
        duration, on_step=_progressively_show, on_finish=_animate_toast_out
    )

    # pylint: enable=no-value-for-parameter

    self.add(toast.center(), animate=False, assign=False)
    self.focus(toast)
    animator.schedule(leadup)

    return toast