zettcode-tui 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zettcode/tui/__init__.py +422 -0
- zettcode/tui/_compat.py +32 -0
- zettcode/tui/capabilities.py +107 -0
- zettcode/tui/clipboard.py +91 -0
- zettcode/tui/console.py +229 -0
- zettcode/tui/core/__init__.py +63 -0
- zettcode/tui/core/app.py +658 -0
- zettcode/tui/core/events.py +134 -0
- zettcode/tui/core/focus.py +85 -0
- zettcode/tui/core/geometry.py +212 -0
- zettcode/tui/core/host.py +66 -0
- zettcode/tui/core/keymap.py +218 -0
- zettcode/tui/core/scheduler.py +105 -0
- zettcode/tui/core/screen.py +80 -0
- zettcode/tui/core/theme.py +248 -0
- zettcode/tui/core/widget.py +204 -0
- zettcode/tui/gallery.py +588 -0
- zettcode/tui/glyphs.py +73 -0
- zettcode/tui/input/__init__.py +18 -0
- zettcode/tui/input/bridge.py +46 -0
- zettcode/tui/input/decoder.py +234 -0
- zettcode/tui/input/events.py +64 -0
- zettcode/tui/input/keys.py +289 -0
- zettcode/tui/input/reader.py +147 -0
- zettcode/tui/layout/__init__.py +24 -0
- zettcode/tui/layout/box.py +153 -0
- zettcode/tui/layout/overlay.py +164 -0
- zettcode/tui/layout/scroll.py +477 -0
- zettcode/tui/layout/solver.py +95 -0
- zettcode/tui/layout/spacing.py +137 -0
- zettcode/tui/py.typed +0 -0
- zettcode/tui/render/__init__.py +71 -0
- zettcode/tui/render/ansi.py +371 -0
- zettcode/tui/render/canvas.py +153 -0
- zettcode/tui/render/code.py +456 -0
- zettcode/tui/render/color.py +177 -0
- zettcode/tui/render/renderer.py +151 -0
- zettcode/tui/render/rich_text.py +80 -0
- zettcode/tui/render/style.py +121 -0
- zettcode/tui/render/sweep.py +87 -0
- zettcode/tui/render/text.py +319 -0
- zettcode/tui/runner.py +316 -0
- zettcode/tui/terminal.py +222 -0
- zettcode/tui/testing/__init__.py +6 -0
- zettcode/tui/testing/harness.py +220 -0
- zettcode/tui/testing/snapshot.py +64 -0
- zettcode/tui/theme_file.py +300 -0
- zettcode/tui/themes/ayu-mirage.toml +44 -0
- zettcode/tui/themes/catppuccin-latte.toml +44 -0
- zettcode/tui/themes/catppuccin-mocha.toml +44 -0
- zettcode/tui/themes/dracula.toml +44 -0
- zettcode/tui/themes/github-dimmed.toml +44 -0
- zettcode/tui/themes/github-light.toml +44 -0
- zettcode/tui/themes/gruvbox-dark.toml +44 -0
- zettcode/tui/themes/gruvbox-light.toml +44 -0
- zettcode/tui/themes/kanagawa.toml +44 -0
- zettcode/tui/themes/nord.toml +44 -0
- zettcode/tui/themes/one-dark.toml +44 -0
- zettcode/tui/themes/rose-pine-dawn.toml +44 -0
- zettcode/tui/themes/rose-pine.toml +44 -0
- zettcode/tui/themes/solarized-dark.toml +44 -0
- zettcode/tui/themes/solarized-light.toml +44 -0
- zettcode/tui/themes/tokyo-night.toml +44 -0
- zettcode/tui/widgets/__init__.py +121 -0
- zettcode/tui/widgets/collapsible.py +116 -0
- zettcode/tui/widgets/completion.py +211 -0
- zettcode/tui/widgets/console.py +442 -0
- zettcode/tui/widgets/dialog.py +165 -0
- zettcode/tui/widgets/diff.py +236 -0
- zettcode/tui/widgets/list.py +293 -0
- zettcode/tui/widgets/list_page.py +121 -0
- zettcode/tui/widgets/markdown.py +657 -0
- zettcode/tui/widgets/progress.py +166 -0
- zettcode/tui/widgets/rich_text.py +84 -0
- zettcode/tui/widgets/status.py +106 -0
- zettcode/tui/widgets/table.py +101 -0
- zettcode/tui/widgets/tasks.py +130 -0
- zettcode/tui/widgets/text.py +111 -0
- zettcode/tui/widgets/textarea.py +671 -0
- zettcode/tui/widgets/toast.py +137 -0
- zettcode_tui-0.4.0.dist-info/METADATA +20 -0
- zettcode_tui-0.4.0.dist-info/RECORD +83 -0
- zettcode_tui-0.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""The retained widget contract every framework component implements."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ..render import Canvas
|
|
9
|
+
from .events import AnyEvent
|
|
10
|
+
from .geometry import Constraints, Point, Rect, Size
|
|
11
|
+
from .host import Host
|
|
12
|
+
from .theme import DARK, Theme
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .app import TuiApp
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Widget:
|
|
19
|
+
"""Base class for the retained widget tree.
|
|
20
|
+
|
|
21
|
+
Subclasses override the small contract below while the framework owns
|
|
22
|
+
layout order, event delivery, and painting. Nothing in this module knows
|
|
23
|
+
about the coding agent; a widget only reacts to input and draws cells.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Start detached, unfocused, and due for a first measure and paint."""
|
|
28
|
+
self.rect = Rect(0, 0, 0, 0)
|
|
29
|
+
self.parent: Widget | None = None
|
|
30
|
+
self.app: TuiApp | None = None
|
|
31
|
+
self.focused = False
|
|
32
|
+
self._mounted = False
|
|
33
|
+
self._needs_layout = True
|
|
34
|
+
self._needs_paint = True
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def theme(self) -> Theme:
|
|
38
|
+
"""Return the active theme, or the default before this tree is mounted."""
|
|
39
|
+
return self.app.theme if self.app is not None else DARK
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def focusable(self) -> bool:
|
|
43
|
+
"""Return whether Tab traversal may land on this widget."""
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def selects_text(self) -> bool:
|
|
48
|
+
"""Return whether a drag over this widget selects its own text.
|
|
49
|
+
|
|
50
|
+
A widget that does not — a list, a panel, the header — hands the drag
|
|
51
|
+
to the application's screen selection instead, which copies the cells
|
|
52
|
+
the frame actually paints.
|
|
53
|
+
"""
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
# -- tree ---------------------------------------------------------------
|
|
57
|
+
@property
|
|
58
|
+
def children(self) -> Sequence[Widget]:
|
|
59
|
+
"""Return the child widgets in paint order, if this widget has any."""
|
|
60
|
+
return ()
|
|
61
|
+
|
|
62
|
+
def mount(self) -> None:
|
|
63
|
+
"""Attach this subtree and run mount hooks exactly once."""
|
|
64
|
+
if self._mounted:
|
|
65
|
+
return
|
|
66
|
+
self._mounted = True
|
|
67
|
+
for child in self.children:
|
|
68
|
+
child.parent = self
|
|
69
|
+
child.mount()
|
|
70
|
+
self.on_mount()
|
|
71
|
+
|
|
72
|
+
def unmount(self) -> None:
|
|
73
|
+
"""Detach this subtree and run unmount hooks exactly once."""
|
|
74
|
+
if not self._mounted:
|
|
75
|
+
return
|
|
76
|
+
for child in self.children:
|
|
77
|
+
child.unmount()
|
|
78
|
+
self.on_unmount()
|
|
79
|
+
self._mounted = False
|
|
80
|
+
|
|
81
|
+
# -- contract -----------------------------------------------------------
|
|
82
|
+
def measure(self, constraints: Constraints) -> Size:
|
|
83
|
+
"""Return the size this widget wants within the given constraints.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
constraints: Size range the parent allows; a result outside it is
|
|
87
|
+
clamped by ``Constraints.constrain`` in the caller.
|
|
88
|
+
"""
|
|
89
|
+
return constraints.constrain(Size(0, 0))
|
|
90
|
+
|
|
91
|
+
def layout(self, rect: Rect) -> None:
|
|
92
|
+
"""Assign the final rectangle, notifying the widget when it changed.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
rect: Absolute cell rectangle; children receive rectangles derived
|
|
96
|
+
from it, so a widget must not assume its own position is (0, 0).
|
|
97
|
+
"""
|
|
98
|
+
changed = rect != self.rect
|
|
99
|
+
self.rect = rect
|
|
100
|
+
self._needs_layout = False
|
|
101
|
+
self._needs_paint = True
|
|
102
|
+
if changed:
|
|
103
|
+
self.on_resize(rect)
|
|
104
|
+
|
|
105
|
+
def render(self, canvas: Canvas) -> None:
|
|
106
|
+
"""Paint this widget into the shared canvas.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
canvas: Frame buffer shared by every screen; drawing outside
|
|
110
|
+
``self.rect`` is allowed but clipped to the canvas.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def handle(self, event: AnyEvent, host: Host) -> bool:
|
|
114
|
+
"""React to one input event and return whether it was consumed.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
event: The event routed to this widget, already past the capture phase.
|
|
118
|
+
host: Services such as focus and repaint; a widget never reaches for
|
|
119
|
+
the app directly.
|
|
120
|
+
"""
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def capture_event(self, event: AnyEvent, host: Host) -> bool:
|
|
124
|
+
"""React during the root-to-target phase, before the target runs.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
event: The event travelling down the tree.
|
|
128
|
+
host: Services such as focus and repaint.
|
|
129
|
+
"""
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def bubble_event(self, event: AnyEvent, host: Host) -> bool:
|
|
133
|
+
"""React during the target-to-root phase, after the target declines.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
event: The event travelling back up the tree.
|
|
137
|
+
host: Services such as focus and repaint.
|
|
138
|
+
"""
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def cursor(self) -> Point | None:
|
|
142
|
+
"""Return the terminal cursor this widget wants, if any."""
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def interrupt(self) -> bool:
|
|
146
|
+
"""Stop what this widget is doing because the reader pressed Ctrl-C; return whether there was anything.
|
|
147
|
+
|
|
148
|
+
A page that runs something of its own — a command, a download — answers ``True`` after stopping it, so
|
|
149
|
+
the key does not also close the page. The default has nothing to stop.
|
|
150
|
+
"""
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
# -- lifecycle hooks ----------------------------------------------------
|
|
154
|
+
def on_mount(self) -> None:
|
|
155
|
+
"""Run once after the subtree is attached."""
|
|
156
|
+
|
|
157
|
+
def on_unmount(self) -> None:
|
|
158
|
+
"""Run once before the subtree is detached."""
|
|
159
|
+
|
|
160
|
+
def on_resize(self, rect: Rect) -> None:
|
|
161
|
+
"""Run after the assigned rectangle actually changed."""
|
|
162
|
+
|
|
163
|
+
def on_focus(self) -> None:
|
|
164
|
+
"""Run when this widget becomes the focus target."""
|
|
165
|
+
|
|
166
|
+
def on_blur(self) -> None:
|
|
167
|
+
"""Run when this widget stops being the focus target."""
|
|
168
|
+
|
|
169
|
+
def on_tick(self) -> bool | None:
|
|
170
|
+
"""Run before each frame so a widget can react to the clock.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
``False`` when nothing the reader can see is different from the last frame, which
|
|
174
|
+
is what the default does because it does nothing. ``True`` says something is.
|
|
175
|
+
``None`` — what a subclass that overrides this without returning anything gives —
|
|
176
|
+
is the cautious answer, "it may be", and is treated as ``True``: a widget has to
|
|
177
|
+
say *no* to let a frame be skipped, so forgetting to cannot freeze the screen.
|
|
178
|
+
"""
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
# -- invalidation -------------------------------------------------------
|
|
182
|
+
def invalidate(self) -> None:
|
|
183
|
+
"""Mark this widget as needing a repaint on the next frame."""
|
|
184
|
+
self._needs_paint = True
|
|
185
|
+
|
|
186
|
+
def request_layout(self) -> None:
|
|
187
|
+
"""Mark this widget as needing a fresh measure and repaint."""
|
|
188
|
+
self._needs_layout = True
|
|
189
|
+
self._needs_paint = True
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def needs_layout(self) -> bool:
|
|
193
|
+
"""Return whether the next frame must measure this widget again."""
|
|
194
|
+
return self._needs_layout
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def needs_paint(self) -> bool:
|
|
198
|
+
"""Return whether the next frame must repaint this widget."""
|
|
199
|
+
return self._needs_paint
|
|
200
|
+
|
|
201
|
+
def clear_dirty(self) -> None:
|
|
202
|
+
"""Record that the current frame already satisfied the dirty flags."""
|
|
203
|
+
self._needs_layout = False
|
|
204
|
+
self._needs_paint = False
|