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,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
+ ]