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.
Files changed (83) hide show
  1. zettcode/tui/__init__.py +422 -0
  2. zettcode/tui/_compat.py +32 -0
  3. zettcode/tui/capabilities.py +107 -0
  4. zettcode/tui/clipboard.py +91 -0
  5. zettcode/tui/console.py +229 -0
  6. zettcode/tui/core/__init__.py +63 -0
  7. zettcode/tui/core/app.py +658 -0
  8. zettcode/tui/core/events.py +134 -0
  9. zettcode/tui/core/focus.py +85 -0
  10. zettcode/tui/core/geometry.py +212 -0
  11. zettcode/tui/core/host.py +66 -0
  12. zettcode/tui/core/keymap.py +218 -0
  13. zettcode/tui/core/scheduler.py +105 -0
  14. zettcode/tui/core/screen.py +80 -0
  15. zettcode/tui/core/theme.py +248 -0
  16. zettcode/tui/core/widget.py +204 -0
  17. zettcode/tui/gallery.py +588 -0
  18. zettcode/tui/glyphs.py +73 -0
  19. zettcode/tui/input/__init__.py +18 -0
  20. zettcode/tui/input/bridge.py +46 -0
  21. zettcode/tui/input/decoder.py +234 -0
  22. zettcode/tui/input/events.py +64 -0
  23. zettcode/tui/input/keys.py +289 -0
  24. zettcode/tui/input/reader.py +147 -0
  25. zettcode/tui/layout/__init__.py +24 -0
  26. zettcode/tui/layout/box.py +153 -0
  27. zettcode/tui/layout/overlay.py +164 -0
  28. zettcode/tui/layout/scroll.py +477 -0
  29. zettcode/tui/layout/solver.py +95 -0
  30. zettcode/tui/layout/spacing.py +137 -0
  31. zettcode/tui/py.typed +0 -0
  32. zettcode/tui/render/__init__.py +71 -0
  33. zettcode/tui/render/ansi.py +371 -0
  34. zettcode/tui/render/canvas.py +153 -0
  35. zettcode/tui/render/code.py +456 -0
  36. zettcode/tui/render/color.py +177 -0
  37. zettcode/tui/render/renderer.py +151 -0
  38. zettcode/tui/render/rich_text.py +80 -0
  39. zettcode/tui/render/style.py +121 -0
  40. zettcode/tui/render/sweep.py +87 -0
  41. zettcode/tui/render/text.py +319 -0
  42. zettcode/tui/runner.py +316 -0
  43. zettcode/tui/terminal.py +222 -0
  44. zettcode/tui/testing/__init__.py +6 -0
  45. zettcode/tui/testing/harness.py +220 -0
  46. zettcode/tui/testing/snapshot.py +64 -0
  47. zettcode/tui/theme_file.py +300 -0
  48. zettcode/tui/themes/ayu-mirage.toml +44 -0
  49. zettcode/tui/themes/catppuccin-latte.toml +44 -0
  50. zettcode/tui/themes/catppuccin-mocha.toml +44 -0
  51. zettcode/tui/themes/dracula.toml +44 -0
  52. zettcode/tui/themes/github-dimmed.toml +44 -0
  53. zettcode/tui/themes/github-light.toml +44 -0
  54. zettcode/tui/themes/gruvbox-dark.toml +44 -0
  55. zettcode/tui/themes/gruvbox-light.toml +44 -0
  56. zettcode/tui/themes/kanagawa.toml +44 -0
  57. zettcode/tui/themes/nord.toml +44 -0
  58. zettcode/tui/themes/one-dark.toml +44 -0
  59. zettcode/tui/themes/rose-pine-dawn.toml +44 -0
  60. zettcode/tui/themes/rose-pine.toml +44 -0
  61. zettcode/tui/themes/solarized-dark.toml +44 -0
  62. zettcode/tui/themes/solarized-light.toml +44 -0
  63. zettcode/tui/themes/tokyo-night.toml +44 -0
  64. zettcode/tui/widgets/__init__.py +121 -0
  65. zettcode/tui/widgets/collapsible.py +116 -0
  66. zettcode/tui/widgets/completion.py +211 -0
  67. zettcode/tui/widgets/console.py +442 -0
  68. zettcode/tui/widgets/dialog.py +165 -0
  69. zettcode/tui/widgets/diff.py +236 -0
  70. zettcode/tui/widgets/list.py +293 -0
  71. zettcode/tui/widgets/list_page.py +121 -0
  72. zettcode/tui/widgets/markdown.py +657 -0
  73. zettcode/tui/widgets/progress.py +166 -0
  74. zettcode/tui/widgets/rich_text.py +84 -0
  75. zettcode/tui/widgets/status.py +106 -0
  76. zettcode/tui/widgets/table.py +101 -0
  77. zettcode/tui/widgets/tasks.py +130 -0
  78. zettcode/tui/widgets/text.py +111 -0
  79. zettcode/tui/widgets/textarea.py +671 -0
  80. zettcode/tui/widgets/toast.py +137 -0
  81. zettcode_tui-0.4.0.dist-info/METADATA +20 -0
  82. zettcode_tui-0.4.0.dist-info/RECORD +83 -0
  83. 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