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/console.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""The platform's console: raw mode, byte input, and size.
|
|
2
|
+
|
|
3
|
+
Modern Windows consoles speak the same ANSI/VT protocol POSIX terminals do once
|
|
4
|
+
virtual-terminal mode is on, so the decoder, the renderer, and the widget layer
|
|
5
|
+
need no Windows branch at all. What differs is only how raw mode is entered, how
|
|
6
|
+
a read waits with a deadline, and how the size is asked for — which is what this
|
|
7
|
+
module isolates behind :class:`Console`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import select
|
|
14
|
+
import shutil
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from typing import Any, TextIO
|
|
17
|
+
|
|
18
|
+
#: Smallest size a terminal may report, so a broken probe cannot shrink the app.
|
|
19
|
+
MIN_COLUMNS = 20
|
|
20
|
+
MIN_ROWS = 8
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Console(ABC):
|
|
24
|
+
"""Raw-mode ownership and byte input for one platform."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, input_fd: int, output: TextIO) -> None:
|
|
27
|
+
"""Bind the console to the file descriptor it reads and the stream it writes."""
|
|
28
|
+
self.input_fd = input_fd
|
|
29
|
+
self.output = output
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def enter(self) -> None:
|
|
33
|
+
"""Put the terminal in raw mode, remembering what to restore."""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def leave(self) -> None:
|
|
37
|
+
"""Restore what :meth:`enter` changed, if anything."""
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def wait_readable(self, timeout: float) -> bool:
|
|
41
|
+
"""Wait up to ``timeout`` seconds for input, and report whether any arrived."""
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def watches_descriptor(self) -> bool:
|
|
45
|
+
"""Whether the event loop itself can watch the descriptor.
|
|
46
|
+
|
|
47
|
+
POSIX loops can (``add_reader``); a Windows console handle is not a
|
|
48
|
+
socket the proactor loop can watch, so a thread does the blocking read
|
|
49
|
+
there instead.
|
|
50
|
+
"""
|
|
51
|
+
return os.name == "posix"
|
|
52
|
+
|
|
53
|
+
def read(self, size: int = 65536) -> bytes:
|
|
54
|
+
"""Return one chunk of input bytes, blocking until some are ready."""
|
|
55
|
+
return os.read(self.input_fd, size)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def size(self) -> tuple[int, int]:
|
|
59
|
+
"""Return the current size, clamped to something usable."""
|
|
60
|
+
found = shutil.get_terminal_size((80, 24))
|
|
61
|
+
return max(MIN_COLUMNS, found.columns), max(MIN_ROWS, found.lines)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PosixConsole(Console):
|
|
65
|
+
"""Raw mode through ``termios``, input through a descriptor the loop can watch."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, input_fd: int, output: TextIO) -> None:
|
|
68
|
+
"""Remember the descriptor and start with no saved attributes."""
|
|
69
|
+
super().__init__(input_fd, output)
|
|
70
|
+
self._attributes: list[Any] | None = None
|
|
71
|
+
|
|
72
|
+
def enter(self) -> None:
|
|
73
|
+
"""Take the descriptor out of cooked mode and into raw mode."""
|
|
74
|
+
import termios
|
|
75
|
+
import tty
|
|
76
|
+
|
|
77
|
+
if not os.isatty(self.input_fd) or not self.output.isatty():
|
|
78
|
+
raise RuntimeError("ZettCode TUI requires an interactive terminal")
|
|
79
|
+
self._attributes = termios.tcgetattr(self.input_fd)
|
|
80
|
+
tty.setraw(self.input_fd)
|
|
81
|
+
|
|
82
|
+
def leave(self) -> None:
|
|
83
|
+
"""Restore the attributes the terminal had before."""
|
|
84
|
+
if self._attributes is None:
|
|
85
|
+
return
|
|
86
|
+
import termios
|
|
87
|
+
|
|
88
|
+
termios.tcsetattr(self.input_fd, termios.TCSADRAIN, self._attributes)
|
|
89
|
+
self._attributes = None
|
|
90
|
+
|
|
91
|
+
def wait_readable(self, timeout: float) -> bool:
|
|
92
|
+
"""Wait on the descriptor with ``select``."""
|
|
93
|
+
return bool(select.select([self.input_fd], [], [], timeout)[0])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class WindowsConsole(Console):
|
|
97
|
+
"""Raw mode through the Win32 console, with VT input and output switched on.
|
|
98
|
+
|
|
99
|
+
A Windows console starts in cooked, non-ANSI mode. Clearing
|
|
100
|
+
``ENABLE_LINE_INPUT`` and ``ENABLE_ECHO_INPUT`` makes a key arrive as soon as
|
|
101
|
+
it is pressed, and ``ENABLE_VIRTUAL_TERMINAL_INPUT`` turns the console's
|
|
102
|
+
input into the same escape sequences a POSIX terminal sends, so the decoder
|
|
103
|
+
needs no special case. ``ENABLE_VIRTUAL_TERMINAL_PROCESSING`` does the same
|
|
104
|
+
for output, which is what lets the renderer write ANSI to it.
|
|
105
|
+
|
|
106
|
+
Every handle is opened through ``ctypes`` on first use, so importing this
|
|
107
|
+
module stays possible on a machine without a console (a service, a test).
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
#: Console mode bits, from ``wincon.h``.
|
|
111
|
+
ENABLE_PROCESSED_INPUT = 0x0001
|
|
112
|
+
ENABLE_LINE_INPUT = 0x0002
|
|
113
|
+
ENABLE_ECHO_INPUT = 0x0004
|
|
114
|
+
ENABLE_WINDOW_INPUT = 0x0008
|
|
115
|
+
ENABLE_MOUSE_INPUT = 0x0010
|
|
116
|
+
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
|
|
117
|
+
ENABLE_PROCESSED_OUTPUT = 0x0001
|
|
118
|
+
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
|
|
119
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
120
|
+
|
|
121
|
+
STD_INPUT_HANDLE = -10
|
|
122
|
+
STD_OUTPUT_HANDLE = -11
|
|
123
|
+
WAIT_OBJECT_0 = 0x00000000
|
|
124
|
+
|
|
125
|
+
def __init__(self, input_fd: int, output: TextIO) -> None:
|
|
126
|
+
"""Remember the descriptor and start with nothing saved."""
|
|
127
|
+
super().__init__(input_fd, output)
|
|
128
|
+
self._input_mode: int | None = None
|
|
129
|
+
self._output_mode: int | None = None
|
|
130
|
+
|
|
131
|
+
# -- the Win32 calls, in one place so a test can stand in for them --------
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _kernel32():
|
|
135
|
+
"""Return ``kernel32``, importing ctypes only where it exists."""
|
|
136
|
+
import ctypes
|
|
137
|
+
|
|
138
|
+
return ctypes.windll.kernel32
|
|
139
|
+
|
|
140
|
+
def _handle(self, which: int) -> int:
|
|
141
|
+
"""Return one standard handle."""
|
|
142
|
+
return self._kernel32().GetStdHandle(which)
|
|
143
|
+
|
|
144
|
+
def enter(self) -> None:
|
|
145
|
+
"""Clear the cooked input flags and turn on VT for input and output."""
|
|
146
|
+
import ctypes
|
|
147
|
+
|
|
148
|
+
kernel32 = self._kernel32()
|
|
149
|
+
stdin = self._handle(self.STD_INPUT_HANDLE)
|
|
150
|
+
mode = ctypes.c_uint()
|
|
151
|
+
if not kernel32.GetConsoleMode(stdin, ctypes.byref(mode)):
|
|
152
|
+
raise RuntimeError("ZettCode TUI requires a Windows console")
|
|
153
|
+
self._input_mode = mode.value
|
|
154
|
+
raw = mode.value & ~self.ENABLE_LINE_INPUT & ~self.ENABLE_ECHO_INPUT & ~self.ENABLE_PROCESSED_INPUT
|
|
155
|
+
raw |= self.ENABLE_VIRTUAL_TERMINAL_INPUT | self.ENABLE_MOUSE_INPUT | self.ENABLE_WINDOW_INPUT
|
|
156
|
+
self._set_input_mode(raw)
|
|
157
|
+
stdout = self._handle(self.STD_OUTPUT_HANDLE)
|
|
158
|
+
out_mode = ctypes.c_uint()
|
|
159
|
+
if kernel32.GetConsoleMode(stdout, ctypes.byref(out_mode)):
|
|
160
|
+
self._output_mode = out_mode.value
|
|
161
|
+
self._set_output_mode(
|
|
162
|
+
out_mode.value
|
|
163
|
+
| self.ENABLE_PROCESSED_OUTPUT
|
|
164
|
+
| self.ENABLE_WRAP_AT_EOL_OUTPUT
|
|
165
|
+
| self.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _set_input_mode(self, mode: int) -> None:
|
|
169
|
+
"""Apply one input mode, reporting a console that refuses it."""
|
|
170
|
+
if not self._kernel32().SetConsoleMode(self._handle(self.STD_INPUT_HANDLE), mode):
|
|
171
|
+
raise RuntimeError("ZettCode TUI requires a Windows console")
|
|
172
|
+
|
|
173
|
+
def _set_output_mode(self, mode: int) -> None:
|
|
174
|
+
"""Apply one output mode."""
|
|
175
|
+
self._kernel32().SetConsoleMode(self._handle(self.STD_OUTPUT_HANDLE), mode)
|
|
176
|
+
|
|
177
|
+
def leave(self) -> None:
|
|
178
|
+
"""Give the console back the modes it had."""
|
|
179
|
+
if self._input_mode is not None:
|
|
180
|
+
self._set_input_mode(self._input_mode)
|
|
181
|
+
self._input_mode = None
|
|
182
|
+
if self._output_mode is not None:
|
|
183
|
+
self._set_output_mode(self._output_mode)
|
|
184
|
+
self._output_mode = None
|
|
185
|
+
|
|
186
|
+
def wait_readable(self, timeout: float) -> bool:
|
|
187
|
+
"""Wait on the console handle itself, in milliseconds."""
|
|
188
|
+
handle = self._handle(self.STD_INPUT_HANDLE)
|
|
189
|
+
milliseconds = max(0, int(timeout * 1000))
|
|
190
|
+
return self._kernel32().WaitForSingleObject(handle, milliseconds) == self.WAIT_OBJECT_0
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def size(self) -> tuple[int, int]:
|
|
194
|
+
"""Return the visible window size the console reports."""
|
|
195
|
+
import ctypes
|
|
196
|
+
|
|
197
|
+
class Coord(ctypes.Structure):
|
|
198
|
+
_fields_ = [("X", ctypes.c_short), ("Y", ctypes.c_short)]
|
|
199
|
+
|
|
200
|
+
class SmallRect(ctypes.Structure):
|
|
201
|
+
_fields_ = [
|
|
202
|
+
("Left", ctypes.c_short),
|
|
203
|
+
("Top", ctypes.c_short),
|
|
204
|
+
("Right", ctypes.c_short),
|
|
205
|
+
("Bottom", ctypes.c_short),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
class Info(ctypes.Structure):
|
|
209
|
+
_fields_ = [
|
|
210
|
+
("dwSize", Coord),
|
|
211
|
+
("dwCursorPosition", Coord),
|
|
212
|
+
("wAttributes", ctypes.c_ushort),
|
|
213
|
+
("srWindow", SmallRect),
|
|
214
|
+
("dwMaximumWindowSize", Coord),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
info = Info()
|
|
218
|
+
if not self._kernel32().GetConsoleScreenBufferInfo(self._handle(self.STD_OUTPUT_HANDLE), ctypes.byref(info)):
|
|
219
|
+
return super().size
|
|
220
|
+
columns = info.srWindow.Right - info.srWindow.Left + 1
|
|
221
|
+
rows = info.srWindow.Bottom - info.srWindow.Top + 1
|
|
222
|
+
return max(MIN_COLUMNS, columns), max(MIN_ROWS, rows)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def console_for(input_fd: int, output: TextIO) -> Console:
|
|
226
|
+
"""Return the console this platform offers."""
|
|
227
|
+
if os.name == "nt":
|
|
228
|
+
return WindowsConsole(input_fd, output)
|
|
229
|
+
return PosixConsole(input_fd, output)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Framework runtime: contracts, focus, screens, keymaps, and the app loop."""
|
|
2
|
+
|
|
3
|
+
from .app import TuiApp
|
|
4
|
+
from .events import (
|
|
5
|
+
AnyEvent,
|
|
6
|
+
Event,
|
|
7
|
+
EventKind,
|
|
8
|
+
FocusEvent,
|
|
9
|
+
KeyEvent,
|
|
10
|
+
MouseAction,
|
|
11
|
+
MouseEvent,
|
|
12
|
+
PasteEvent,
|
|
13
|
+
ResizeEvent,
|
|
14
|
+
TextEvent,
|
|
15
|
+
)
|
|
16
|
+
from .focus import FocusManager, walk
|
|
17
|
+
from .geometry import Constraints, EdgeInsets, Point, Rect, Size
|
|
18
|
+
from .host import Host
|
|
19
|
+
from .keymap import Binding, Command, CommandRegistry, Keymap, event_key, key_id, normalize_key
|
|
20
|
+
from .scheduler import Scheduler
|
|
21
|
+
from .screen import Screen, ScreenStack
|
|
22
|
+
from .theme import DARK, LIGHT, Theme, ToolTheme, scheme_named, theme_named, theme_names
|
|
23
|
+
from .widget import Widget
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AnyEvent",
|
|
27
|
+
"TuiApp",
|
|
28
|
+
"Binding",
|
|
29
|
+
"Command",
|
|
30
|
+
"CommandRegistry",
|
|
31
|
+
"Constraints",
|
|
32
|
+
"DARK",
|
|
33
|
+
"EdgeInsets",
|
|
34
|
+
"Event",
|
|
35
|
+
"EventKind",
|
|
36
|
+
"FocusEvent",
|
|
37
|
+
"FocusManager",
|
|
38
|
+
"Host",
|
|
39
|
+
"KeyEvent",
|
|
40
|
+
"Keymap",
|
|
41
|
+
"LIGHT",
|
|
42
|
+
"MouseAction",
|
|
43
|
+
"MouseEvent",
|
|
44
|
+
"PasteEvent",
|
|
45
|
+
"Point",
|
|
46
|
+
"Rect",
|
|
47
|
+
"ResizeEvent",
|
|
48
|
+
"Scheduler",
|
|
49
|
+
"Screen",
|
|
50
|
+
"ScreenStack",
|
|
51
|
+
"Size",
|
|
52
|
+
"Theme",
|
|
53
|
+
"TextEvent",
|
|
54
|
+
"Widget",
|
|
55
|
+
"event_key",
|
|
56
|
+
"key_id",
|
|
57
|
+
"normalize_key",
|
|
58
|
+
"theme_named",
|
|
59
|
+
"scheme_named",
|
|
60
|
+
"ToolTheme",
|
|
61
|
+
"theme_names",
|
|
62
|
+
"walk",
|
|
63
|
+
]
|