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
zettcode/tui/__init__.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""ZettCode's internally implemented terminal UI framework."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
9
|
+
_EXPORTS = {
|
|
10
|
+
"ARROW_DOWN": ".glyphs",
|
|
11
|
+
"ARROW_UP": ".glyphs",
|
|
12
|
+
"Anchor": ".layout",
|
|
13
|
+
"AnyEvent": ".core",
|
|
14
|
+
"AsyncInput": ".input",
|
|
15
|
+
"BAR_EMPTY": ".glyphs",
|
|
16
|
+
"BAR_FULL": ".glyphs",
|
|
17
|
+
"Binding": ".core",
|
|
18
|
+
"Border": ".layout",
|
|
19
|
+
"AnsiBuffer": ".render",
|
|
20
|
+
"Canvas": ".render",
|
|
21
|
+
"Collapsible": ".widgets",
|
|
22
|
+
"ColorDepth": ".render",
|
|
23
|
+
"Column": ".widgets",
|
|
24
|
+
"Command": ".core",
|
|
25
|
+
"CommandRegistry": ".core",
|
|
26
|
+
"Completer": ".widgets",
|
|
27
|
+
"CompletionItem": ".widgets",
|
|
28
|
+
"CompletionPopup": ".widgets",
|
|
29
|
+
"ConsolePanel": ".widgets",
|
|
30
|
+
"Constraints": ".core",
|
|
31
|
+
"DARK": ".core",
|
|
32
|
+
"DONE": ".glyphs",
|
|
33
|
+
"Dialog": ".widgets",
|
|
34
|
+
"DialogAction": ".widgets",
|
|
35
|
+
"DiffLine": ".widgets",
|
|
36
|
+
"DiffSegment": ".widgets",
|
|
37
|
+
"DiffView": ".widgets",
|
|
38
|
+
"DifferentialRenderer": ".render",
|
|
39
|
+
"ELLIPSIS": ".glyphs",
|
|
40
|
+
"EXAMPLE": ".theme_file",
|
|
41
|
+
"EXPANDED": ".glyphs",
|
|
42
|
+
"EdgeInsets": ".core",
|
|
43
|
+
"Event": ".core",
|
|
44
|
+
"EventKind": ".core",
|
|
45
|
+
"EventType": ".input",
|
|
46
|
+
"FAILED": ".glyphs",
|
|
47
|
+
"FocusEvent": ".core",
|
|
48
|
+
"FocusManager": ".core",
|
|
49
|
+
"HBox": ".layout",
|
|
50
|
+
"HEADER": ".glyphs",
|
|
51
|
+
"Host": ".core",
|
|
52
|
+
"InputDecoder": ".input",
|
|
53
|
+
"InputEvent": ".input",
|
|
54
|
+
"KeyEvent": ".core",
|
|
55
|
+
"Keymap": ".core",
|
|
56
|
+
"LIGHT": ".core",
|
|
57
|
+
"LineSource": ".layout",
|
|
58
|
+
"ListItem": ".widgets",
|
|
59
|
+
"ListPage": ".widgets",
|
|
60
|
+
"ListView": ".widgets",
|
|
61
|
+
"MARKER": ".glyphs",
|
|
62
|
+
"Markdown": ".widgets",
|
|
63
|
+
"MarkdownSource": ".widgets",
|
|
64
|
+
"MarkdownView": ".widgets",
|
|
65
|
+
"MouseAction": ".core",
|
|
66
|
+
"MouseEvent": ".core",
|
|
67
|
+
"Overlay": ".layout",
|
|
68
|
+
"OverlaySlot": ".layout",
|
|
69
|
+
"PENDING": ".glyphs",
|
|
70
|
+
"PROMPT": ".glyphs",
|
|
71
|
+
"Padding": ".layout",
|
|
72
|
+
"PasteEvent": ".core",
|
|
73
|
+
"Point": ".core",
|
|
74
|
+
"ProgressBar": ".widgets",
|
|
75
|
+
"RULE": ".glyphs",
|
|
76
|
+
"RUNNING": ".glyphs",
|
|
77
|
+
"Rect": ".core",
|
|
78
|
+
"ResizeEvent": ".core",
|
|
79
|
+
"RichText": ".widgets",
|
|
80
|
+
"Rule": ".widgets",
|
|
81
|
+
"SEPARATOR": ".glyphs",
|
|
82
|
+
"OPTION": ".capabilities",
|
|
83
|
+
"SKIPPED": ".glyphs",
|
|
84
|
+
"SPINNER": ".glyphs",
|
|
85
|
+
"STATUS": ".glyphs",
|
|
86
|
+
"STEERING": ".glyphs",
|
|
87
|
+
"Scheduler": ".core",
|
|
88
|
+
"Screen": ".core",
|
|
89
|
+
"ScreenStack": ".core",
|
|
90
|
+
"ScrollView": ".layout",
|
|
91
|
+
"Size": ".core",
|
|
92
|
+
"Slot": ".layout",
|
|
93
|
+
"Span": ".render",
|
|
94
|
+
"Spinner": ".widgets",
|
|
95
|
+
"StaticLines": ".layout",
|
|
96
|
+
"StatusBar": ".widgets",
|
|
97
|
+
"Style": ".render",
|
|
98
|
+
"Table": ".widgets",
|
|
99
|
+
"TaskPanel": ".widgets",
|
|
100
|
+
"Terminal": ".terminal",
|
|
101
|
+
"TerminalCapabilities": ".capabilities",
|
|
102
|
+
"TerminalRunner": ".runner",
|
|
103
|
+
"Text": ".widgets",
|
|
104
|
+
"TextArea": ".widgets",
|
|
105
|
+
"TextEvent": ".core",
|
|
106
|
+
"TextLine": ".render",
|
|
107
|
+
"Theme": ".core",
|
|
108
|
+
"ThemeFileError": ".theme_file",
|
|
109
|
+
"Toast": ".widgets",
|
|
110
|
+
"ToolTheme": ".core",
|
|
111
|
+
"Track": ".layout",
|
|
112
|
+
"TuiApp": ".core",
|
|
113
|
+
"VBox": ".layout",
|
|
114
|
+
"Widget": ".core",
|
|
115
|
+
"build_unified": ".widgets",
|
|
116
|
+
"centered": ".layout",
|
|
117
|
+
"detect_capabilities": ".capabilities",
|
|
118
|
+
"detect_reduced_motion": ".capabilities",
|
|
119
|
+
"bundled_themes": ".theme_file",
|
|
120
|
+
"file_theme": ".theme_file",
|
|
121
|
+
"ThemeCatalog": ".theme_file",
|
|
122
|
+
"ThemeSource": ".theme_file",
|
|
123
|
+
"RESERVED_THEMES": ".theme_file",
|
|
124
|
+
"THEME_NAME": ".theme_file",
|
|
125
|
+
"event_key": ".core",
|
|
126
|
+
"key_id": ".core",
|
|
127
|
+
"load_theme": ".theme_file",
|
|
128
|
+
"normalize_key": ".core",
|
|
129
|
+
"parse_background": ".terminal",
|
|
130
|
+
"parse_unified": ".widgets",
|
|
131
|
+
"resolve_tracks": ".layout",
|
|
132
|
+
"run_app": ".runner",
|
|
133
|
+
"scheme_named": ".core",
|
|
134
|
+
"theme_from_toml": ".theme_file",
|
|
135
|
+
"theme_named": ".core",
|
|
136
|
+
"theme_names": ".core",
|
|
137
|
+
"title_sequence": ".terminal",
|
|
138
|
+
"walk": ".core",
|
|
139
|
+
"word_diff": ".widgets",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
|
|
144
|
+
from .capabilities import OPTION, TerminalCapabilities, detect_capabilities, detect_reduced_motion
|
|
145
|
+
from .core import (
|
|
146
|
+
DARK,
|
|
147
|
+
LIGHT,
|
|
148
|
+
AnyEvent,
|
|
149
|
+
Binding,
|
|
150
|
+
Command,
|
|
151
|
+
CommandRegistry,
|
|
152
|
+
Constraints,
|
|
153
|
+
EdgeInsets,
|
|
154
|
+
Event,
|
|
155
|
+
EventKind,
|
|
156
|
+
FocusEvent,
|
|
157
|
+
FocusManager,
|
|
158
|
+
Host,
|
|
159
|
+
KeyEvent,
|
|
160
|
+
Keymap,
|
|
161
|
+
MouseAction,
|
|
162
|
+
MouseEvent,
|
|
163
|
+
PasteEvent,
|
|
164
|
+
Point,
|
|
165
|
+
Rect,
|
|
166
|
+
ResizeEvent,
|
|
167
|
+
Scheduler,
|
|
168
|
+
Screen,
|
|
169
|
+
ScreenStack,
|
|
170
|
+
Size,
|
|
171
|
+
TextEvent,
|
|
172
|
+
Theme,
|
|
173
|
+
ToolTheme,
|
|
174
|
+
TuiApp,
|
|
175
|
+
Widget,
|
|
176
|
+
event_key,
|
|
177
|
+
key_id,
|
|
178
|
+
normalize_key,
|
|
179
|
+
scheme_named,
|
|
180
|
+
theme_named,
|
|
181
|
+
theme_names,
|
|
182
|
+
walk,
|
|
183
|
+
)
|
|
184
|
+
from .glyphs import (
|
|
185
|
+
ARROW_DOWN,
|
|
186
|
+
ARROW_UP,
|
|
187
|
+
BAR_EMPTY,
|
|
188
|
+
BAR_FULL,
|
|
189
|
+
DONE,
|
|
190
|
+
ELLIPSIS,
|
|
191
|
+
EXPANDED,
|
|
192
|
+
FAILED,
|
|
193
|
+
HEADER,
|
|
194
|
+
MARKER,
|
|
195
|
+
PENDING,
|
|
196
|
+
PROMPT,
|
|
197
|
+
RULE,
|
|
198
|
+
RUNNING,
|
|
199
|
+
SEPARATOR,
|
|
200
|
+
SKIPPED,
|
|
201
|
+
SPINNER,
|
|
202
|
+
STATUS,
|
|
203
|
+
STEERING,
|
|
204
|
+
)
|
|
205
|
+
from .input import AsyncInput, EventType, InputDecoder, InputEvent
|
|
206
|
+
from .layout import (
|
|
207
|
+
Anchor,
|
|
208
|
+
Border,
|
|
209
|
+
HBox,
|
|
210
|
+
LineSource,
|
|
211
|
+
Overlay,
|
|
212
|
+
OverlaySlot,
|
|
213
|
+
Padding,
|
|
214
|
+
ScrollView,
|
|
215
|
+
Slot,
|
|
216
|
+
StaticLines,
|
|
217
|
+
Track,
|
|
218
|
+
VBox,
|
|
219
|
+
centered,
|
|
220
|
+
resolve_tracks,
|
|
221
|
+
)
|
|
222
|
+
from .render import AnsiBuffer, Canvas, ColorDepth, DifferentialRenderer, Span, Style, TextLine
|
|
223
|
+
from .runner import TerminalRunner, run_app
|
|
224
|
+
from .terminal import Terminal, parse_background, title_sequence
|
|
225
|
+
from .theme_file import (
|
|
226
|
+
EXAMPLE,
|
|
227
|
+
RESERVED_THEMES,
|
|
228
|
+
THEME_NAME,
|
|
229
|
+
ThemeCatalog,
|
|
230
|
+
ThemeFileError,
|
|
231
|
+
ThemeSource,
|
|
232
|
+
bundled_themes,
|
|
233
|
+
file_theme,
|
|
234
|
+
load_theme,
|
|
235
|
+
theme_from_toml,
|
|
236
|
+
)
|
|
237
|
+
from .widgets import (
|
|
238
|
+
Collapsible,
|
|
239
|
+
Column,
|
|
240
|
+
Completer,
|
|
241
|
+
CompletionItem,
|
|
242
|
+
CompletionPopup,
|
|
243
|
+
ConsolePanel,
|
|
244
|
+
Dialog,
|
|
245
|
+
DialogAction,
|
|
246
|
+
DiffLine,
|
|
247
|
+
DiffSegment,
|
|
248
|
+
DiffView,
|
|
249
|
+
ListItem,
|
|
250
|
+
ListPage,
|
|
251
|
+
ListView,
|
|
252
|
+
Markdown,
|
|
253
|
+
MarkdownSource,
|
|
254
|
+
MarkdownView,
|
|
255
|
+
ProgressBar,
|
|
256
|
+
RichText,
|
|
257
|
+
Rule,
|
|
258
|
+
Spinner,
|
|
259
|
+
StatusBar,
|
|
260
|
+
Table,
|
|
261
|
+
TaskPanel,
|
|
262
|
+
Text,
|
|
263
|
+
TextArea,
|
|
264
|
+
Toast,
|
|
265
|
+
build_unified,
|
|
266
|
+
parse_unified,
|
|
267
|
+
word_diff,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def __getattr__(name: str) -> object:
|
|
272
|
+
"""Import the module a public name lives in, the first time it is asked for.
|
|
273
|
+
|
|
274
|
+
This package is a facade over several subpackages, and a caller that wants
|
|
275
|
+
one name should not pay for the rest of them — the TUI alone holds the
|
|
276
|
+
widget library, the Markdown parser, the diff viewer, and the syntax
|
|
277
|
+
scanners. ``__all__`` is still the public surface.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
module = _EXPORTS[name]
|
|
281
|
+
except KeyError:
|
|
282
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
283
|
+
return getattr(importlib.import_module(module, __name__), name)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def __dir__() -> list[str]:
|
|
287
|
+
"""Return the names this package publishes."""
|
|
288
|
+
return sorted(__all__)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
__all__ = [
|
|
292
|
+
"AnyEvent",
|
|
293
|
+
"ARROW_DOWN",
|
|
294
|
+
"ARROW_UP",
|
|
295
|
+
"BAR_EMPTY",
|
|
296
|
+
"BAR_FULL",
|
|
297
|
+
"DONE",
|
|
298
|
+
"ELLIPSIS",
|
|
299
|
+
"EXPANDED",
|
|
300
|
+
"FAILED",
|
|
301
|
+
"HEADER",
|
|
302
|
+
"MARKER",
|
|
303
|
+
"OPTION",
|
|
304
|
+
"PENDING",
|
|
305
|
+
"PROMPT",
|
|
306
|
+
"RULE",
|
|
307
|
+
"RUNNING",
|
|
308
|
+
"SEPARATOR",
|
|
309
|
+
"SKIPPED",
|
|
310
|
+
"SPINNER",
|
|
311
|
+
"STATUS",
|
|
312
|
+
"STEERING",
|
|
313
|
+
"Anchor",
|
|
314
|
+
"AsyncInput",
|
|
315
|
+
"TuiApp",
|
|
316
|
+
"Binding",
|
|
317
|
+
"Border",
|
|
318
|
+
"AnsiBuffer",
|
|
319
|
+
"Canvas",
|
|
320
|
+
"Collapsible",
|
|
321
|
+
"ColorDepth",
|
|
322
|
+
"Column",
|
|
323
|
+
"Command",
|
|
324
|
+
"CommandRegistry",
|
|
325
|
+
"Completer",
|
|
326
|
+
"CompletionItem",
|
|
327
|
+
"CompletionPopup",
|
|
328
|
+
"ConsolePanel",
|
|
329
|
+
"Constraints",
|
|
330
|
+
"DARK",
|
|
331
|
+
"DifferentialRenderer",
|
|
332
|
+
"Dialog",
|
|
333
|
+
"DialogAction",
|
|
334
|
+
"DiffLine",
|
|
335
|
+
"DiffSegment",
|
|
336
|
+
"DiffView",
|
|
337
|
+
"EdgeInsets",
|
|
338
|
+
"Event",
|
|
339
|
+
"EventKind",
|
|
340
|
+
"EventType",
|
|
341
|
+
"FocusEvent",
|
|
342
|
+
"FocusManager",
|
|
343
|
+
"HBox",
|
|
344
|
+
"Host",
|
|
345
|
+
"InputDecoder",
|
|
346
|
+
"InputEvent",
|
|
347
|
+
"KeyEvent",
|
|
348
|
+
"Keymap",
|
|
349
|
+
"LIGHT",
|
|
350
|
+
"LineSource",
|
|
351
|
+
"ListItem",
|
|
352
|
+
"ListPage",
|
|
353
|
+
"ListView",
|
|
354
|
+
"Markdown",
|
|
355
|
+
"MarkdownSource",
|
|
356
|
+
"MarkdownView",
|
|
357
|
+
"MouseAction",
|
|
358
|
+
"MouseEvent",
|
|
359
|
+
"Overlay",
|
|
360
|
+
"OverlaySlot",
|
|
361
|
+
"Padding",
|
|
362
|
+
"PasteEvent",
|
|
363
|
+
"Point",
|
|
364
|
+
"ProgressBar",
|
|
365
|
+
"RichText",
|
|
366
|
+
"Rect",
|
|
367
|
+
"ResizeEvent",
|
|
368
|
+
"Rule",
|
|
369
|
+
"Scheduler",
|
|
370
|
+
"Screen",
|
|
371
|
+
"ScreenStack",
|
|
372
|
+
"ScrollView",
|
|
373
|
+
"Size",
|
|
374
|
+
"Slot",
|
|
375
|
+
"Span",
|
|
376
|
+
"Spinner",
|
|
377
|
+
"StaticLines",
|
|
378
|
+
"StatusBar",
|
|
379
|
+
"Style",
|
|
380
|
+
"Table",
|
|
381
|
+
"TaskPanel",
|
|
382
|
+
"Terminal",
|
|
383
|
+
"TerminalRunner",
|
|
384
|
+
"TerminalCapabilities",
|
|
385
|
+
"Text",
|
|
386
|
+
"TextArea",
|
|
387
|
+
"TextEvent",
|
|
388
|
+
"TextLine",
|
|
389
|
+
"Theme",
|
|
390
|
+
"ThemeFileError",
|
|
391
|
+
"Toast",
|
|
392
|
+
"Track",
|
|
393
|
+
"VBox",
|
|
394
|
+
"Widget",
|
|
395
|
+
"build_unified",
|
|
396
|
+
"EXAMPLE",
|
|
397
|
+
"detect_capabilities",
|
|
398
|
+
"bundled_themes",
|
|
399
|
+
"file_theme",
|
|
400
|
+
"RESERVED_THEMES",
|
|
401
|
+
"THEME_NAME",
|
|
402
|
+
"ThemeCatalog",
|
|
403
|
+
"ThemeSource",
|
|
404
|
+
"detect_reduced_motion",
|
|
405
|
+
"centered",
|
|
406
|
+
"event_key",
|
|
407
|
+
"key_id",
|
|
408
|
+
"load_theme",
|
|
409
|
+
"normalize_key",
|
|
410
|
+
"parse_unified",
|
|
411
|
+
"resolve_tracks",
|
|
412
|
+
"run_app",
|
|
413
|
+
"theme_named",
|
|
414
|
+
"title_sequence",
|
|
415
|
+
"scheme_named",
|
|
416
|
+
"parse_background",
|
|
417
|
+
"ToolTheme",
|
|
418
|
+
"theme_names",
|
|
419
|
+
"theme_from_toml",
|
|
420
|
+
"walk",
|
|
421
|
+
"word_diff",
|
|
422
|
+
]
|
zettcode/tui/_compat.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""The two version shims the framework needs, so `zettcode-tui` depends on nothing of ours.
|
|
2
|
+
|
|
3
|
+
The release notes of the framework say what it is: a widget toolkit that knows nothing of the agent. It needs the
|
|
4
|
+
string-valued enum (the standard library's is 3.11's, and its spelling is what the rows and the theme files rely on) and
|
|
5
|
+
a TOML reader for a theme file. Both are ten lines here rather than an import of another distribution, which would put
|
|
6
|
+
`zett-agent` behind a widget.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from enum import Enum
|
|
13
|
+
|
|
14
|
+
if sys.version_info >= (3, 11):
|
|
15
|
+
import tomllib
|
|
16
|
+
else: # pragma: no cover - the branch only the 3.10 runtime takes
|
|
17
|
+
import tomli as tomllib
|
|
18
|
+
|
|
19
|
+
__all__ = ["StrEnum", "tomllib"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StrEnum(str, Enum):
|
|
23
|
+
"""A string-valued enum, for the versions before ``enum.StrEnum``.
|
|
24
|
+
|
|
25
|
+
The standard library's version formats as its value, so ``f"{member}"`` is ``"ready"`` rather than
|
|
26
|
+
``"Activity.READY"``; the rows and the theme files rely on that, so every interpreter uses this class instead of the
|
|
27
|
+
standard one and the wording cannot drift with the version.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
"""Return the member's value, as ``enum.StrEnum`` does."""
|
|
32
|
+
return str(self.value)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Terminal capability detection used to pick conservative rendering options."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from .render.color import ColorDepth
|
|
11
|
+
|
|
12
|
+
#: What the modifier that sends ``alt``-prefixed keys is called on this machine: the Option key on a Mac, Alt
|
|
13
|
+
#: elsewhere. The same physical key sends the same bytes; only its name differs, and a hint that says "alt" to
|
|
14
|
+
#: someone looking at a key marked "option" is a hint they have to translate.
|
|
15
|
+
OPTION = "option" if sys.platform == "darwin" else "alt"
|
|
16
|
+
|
|
17
|
+
_TRUECOLOR_COLORTERM = {"truecolor", "24bit"}
|
|
18
|
+
_MOTION_VARIABLES = ("ZETTCODE_REDUCED_MOTION", "NO_MOTION", "REDUCED_MOTION")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class TerminalCapabilities:
|
|
23
|
+
"""What the attached terminal can be trusted to render.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
color_depth: Safest depth the environment advertises.
|
|
27
|
+
unicode: The locale looks like it can encode UTF-8. Advisory for now: no
|
|
28
|
+
widget switches to an ASCII glyph set when it is False.
|
|
29
|
+
mouse: The terminal accepts mouse reporting. Advisory, like ``unicode``.
|
|
30
|
+
bracketed_paste: The terminal wraps pastes in ``ESC[200~`` guards.
|
|
31
|
+
Advisory as well; the decoder accepts them either way.
|
|
32
|
+
width: Initial width guess in cells, replaced on the first resize.
|
|
33
|
+
height: Initial height guess in cells.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
color_depth: ColorDepth = ColorDepth.ANSI16
|
|
37
|
+
unicode: bool = True
|
|
38
|
+
mouse: bool = True
|
|
39
|
+
bracketed_paste: bool = True
|
|
40
|
+
width: int = 80
|
|
41
|
+
height: int = 24
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def truecolor(self) -> bool:
|
|
45
|
+
"""Return whether the terminal can be trusted with 24-bit colour."""
|
|
46
|
+
return self.color_depth is ColorDepth.TRUECOLOR
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def detect_capabilities(environ: Mapping[str, str] | None = None) -> TerminalCapabilities:
|
|
50
|
+
"""Derive capabilities from environment variables.
|
|
51
|
+
|
|
52
|
+
Everything here is a heuristic, so each default errs toward the option that
|
|
53
|
+
still looks correct on a weaker terminal.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
environ: Environment to read; defaults to ``os.environ`` and is
|
|
57
|
+
injectable so tests can describe a terminal without owning one.
|
|
58
|
+
"""
|
|
59
|
+
env = os.environ if environ is None else environ
|
|
60
|
+
return TerminalCapabilities(
|
|
61
|
+
color_depth=detect_color_depth(env),
|
|
62
|
+
unicode=detect_unicode(env),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def detect_color_depth(environ: Mapping[str, str] | None = None) -> ColorDepth:
|
|
67
|
+
"""Return the safest color depth the environment advertises.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
environ: Environment to read; defaults to ``os.environ``.
|
|
71
|
+
"""
|
|
72
|
+
env = os.environ if environ is None else environ
|
|
73
|
+
term = env.get("TERM", "")
|
|
74
|
+
if env.get("NO_COLOR") or term == "dumb":
|
|
75
|
+
return ColorDepth.MONO
|
|
76
|
+
colorterm = env.get("COLORTERM", "").lower()
|
|
77
|
+
if colorterm in _TRUECOLOR_COLORTERM or "truecolor" in term or "direct" in term:
|
|
78
|
+
return ColorDepth.TRUECOLOR
|
|
79
|
+
if "256" in term or colorterm:
|
|
80
|
+
return ColorDepth.ANSI256
|
|
81
|
+
return ColorDepth.ANSI16
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def detect_unicode(environ: Mapping[str, str] | None = None) -> bool:
|
|
85
|
+
"""Return whether the environment looks like it can encode UTF-8.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
environ: Environment to read; defaults to ``os.environ``. An unset
|
|
89
|
+
locale is treated as UTF-8 capable, because that is the common case
|
|
90
|
+
on modern systems.
|
|
91
|
+
"""
|
|
92
|
+
env = os.environ if environ is None else environ
|
|
93
|
+
locale = env.get("LC_ALL") or env.get("LC_CTYPE") or env.get("LANG", "")
|
|
94
|
+
if locale:
|
|
95
|
+
return "utf" in locale.lower()
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def detect_reduced_motion(environ: Mapping[str, str] | None = None) -> bool:
|
|
100
|
+
"""Return whether decorative animation should be suppressed.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
environ: Environment to read; defaults to ``os.environ``. Any of
|
|
104
|
+
``_MOTION_VARIABLES`` being set to a non-empty value wins.
|
|
105
|
+
"""
|
|
106
|
+
env = os.environ if environ is None else environ
|
|
107
|
+
return any(env.get(name) for name in _MOTION_VARIABLES)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Where a copy goes: the machine's own clipboard, or the terminal's.
|
|
2
|
+
|
|
3
|
+
A program writes to *a* clipboard, and which one the reader means depends on where the
|
|
4
|
+
program is running. On the reader's own machine the two are the same thing. Over ``ssh`` they
|
|
5
|
+
are not: the program runs on the server, the reader sits at a terminal on another machine, and
|
|
6
|
+
the clipboard they will paste from is that machine's. A system clipboard tool on the server
|
|
7
|
+
reaches the server's clipboard — if it reaches anything at all — and the reader finds nothing
|
|
8
|
+
when they paste.
|
|
9
|
+
|
|
10
|
+
The only way across the connection is the terminal itself. OSC 52 (``ESC ] 52 ; c ; <base64>
|
|
11
|
+
BEL``) asks the terminal to put text on *its* clipboard, and it travels over the same byte
|
|
12
|
+
stream as everything else the program prints, so it arrives wherever the reader is. That is
|
|
13
|
+
why it is the route over ``ssh`` and only a fallback on a local machine: where a system tool
|
|
14
|
+
works, using it never needs the terminal's permission — several terminals ask the reader, or
|
|
15
|
+
refuse by default, before letting a program write their clipboard.
|
|
16
|
+
|
|
17
|
+
This module only decides and encodes. Writing is :func:`zettcode.tui.runner.clipboard_writer`'s
|
|
18
|
+
job, and the shell reads :func:`copy_route` to say truthfully where the text went.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import base64
|
|
24
|
+
import os
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
from typing import Literal
|
|
27
|
+
|
|
28
|
+
#: Environment variable that overrides the choice: ``system``, ``terminal``, or ``auto``.
|
|
29
|
+
#: Detection reads the environment, and the environment can be wrong — ``sudo`` and some
|
|
30
|
+
#: multiplexers drop the variables, and a forwarded X display makes the server's clipboard
|
|
31
|
+
#: the right one — so the reader can say.
|
|
32
|
+
CLIPBOARD_ENV = "ZETTCODE_CLIPBOARD"
|
|
33
|
+
|
|
34
|
+
#: Variables the SSH server sets for the session it starts; any one of them means the program
|
|
35
|
+
#: is running on the far side of a connection.
|
|
36
|
+
SSH_VARIABLES = ("SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY")
|
|
37
|
+
|
|
38
|
+
CopyRoute = Literal["system", "terminal"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_remote(environ: Mapping[str, str] | None = None) -> bool:
|
|
42
|
+
"""Return whether this process was started through an SSH session.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
environ: Environment to read; defaults to ``os.environ``.
|
|
46
|
+
"""
|
|
47
|
+
env = os.environ if environ is None else environ
|
|
48
|
+
return any(env.get(name) for name in SSH_VARIABLES)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def copy_route(environ: Mapping[str, str] | None = None) -> CopyRoute:
|
|
52
|
+
"""Return which clipboard a copy should reach.
|
|
53
|
+
|
|
54
|
+
``terminal`` over ``ssh``, ``system`` otherwise, unless :data:`CLIPBOARD_ENV` says
|
|
55
|
+
which. A value that is neither ``system`` nor ``terminal`` — ``auto``, or a typo — leaves
|
|
56
|
+
the detection in charge, because a mistyped override that silently disabled copying would
|
|
57
|
+
be worse than one that is ignored.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
environ: Environment to read; defaults to ``os.environ``.
|
|
61
|
+
"""
|
|
62
|
+
env = os.environ if environ is None else environ
|
|
63
|
+
override = env.get(CLIPBOARD_ENV, "").strip().lower()
|
|
64
|
+
if override == "system":
|
|
65
|
+
return "system"
|
|
66
|
+
if override == "terminal":
|
|
67
|
+
return "terminal"
|
|
68
|
+
return "terminal" if is_remote(env) else "system"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def osc52(text: str, environ: Mapping[str, str] | None = None) -> str:
|
|
72
|
+
"""Return the escape sequence that asks the terminal to copy ``text``.
|
|
73
|
+
|
|
74
|
+
Inside tmux the sequence is sent twice: bare, which tmux honours when its
|
|
75
|
+
``set-clipboard`` option is ``on``, and wrapped in tmux's passthrough, which it forwards to
|
|
76
|
+
the outer terminal when ``allow-passthrough`` is on. Which of the two a given tmux accepts
|
|
77
|
+
is a setting the program cannot see, and both carry the same text, so a copy that lands
|
|
78
|
+
twice is the same copy.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
text: What to put on the clipboard.
|
|
82
|
+
environ: Environment to read; defaults to ``os.environ``.
|
|
83
|
+
"""
|
|
84
|
+
env = os.environ if environ is None else environ
|
|
85
|
+
payload = base64.b64encode(text.encode()).decode()
|
|
86
|
+
sequence = f"\x1b]52;c;{payload}\x07"
|
|
87
|
+
if env.get("TMUX"):
|
|
88
|
+
# DCS tmux ; <sequence with every ESC doubled> ST: the doubling is how a passthrough
|
|
89
|
+
# tells the escapes it carries from the one that ends it.
|
|
90
|
+
return sequence + "\x1bPtmux;" + sequence.replace("\x1b", "\x1b\x1b") + "\x1b\\"
|
|
91
|
+
return sequence
|