ghostty-textual 0.1.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.
- ghostty_textual/__init__.py +69 -0
- ghostty_textual/_native.py +277 -0
- ghostty_textual/_render.py +440 -0
- ghostty_textual/cells.py +111 -0
- ghostty_textual/emulator.py +832 -0
- ghostty_textual/keys.py +51 -0
- ghostty_textual/py.typed +1 -0
- ghostty_textual/theme.py +68 -0
- ghostty_textual/widget.py +682 -0
- ghostty_textual-0.1.0.dist-info/METADATA +210 -0
- ghostty_textual-0.1.0.dist-info/RECORD +13 -0
- ghostty_textual-0.1.0.dist-info/WHEEL +4 -0
- ghostty_textual-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Textual terminal widget backed by libghostty-vt.
|
|
2
|
+
|
|
3
|
+
Importing this package never loads the native library -- see spec v2 §7.
|
|
4
|
+
`GhosttyUnavailable` is raised when a `Terminal` is constructed, so docs, type
|
|
5
|
+
checking, and applications with optional terminal features stay importable on
|
|
6
|
+
platforms with no wheel.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import version as _version
|
|
12
|
+
|
|
13
|
+
from ghostty_textual._native import GhosttyError, GhosttyUnavailable
|
|
14
|
+
from ghostty_textual._render import CursorState, RowPatch, ViewportState
|
|
15
|
+
from ghostty_textual.cells import Cell, CellStyle
|
|
16
|
+
from ghostty_textual.emulator import (
|
|
17
|
+
BellRang,
|
|
18
|
+
ClipboardPolicy,
|
|
19
|
+
ClipboardWritten,
|
|
20
|
+
Frame,
|
|
21
|
+
ProcessingError,
|
|
22
|
+
ResourceLimits,
|
|
23
|
+
ScrollBottom,
|
|
24
|
+
ScrollDelta,
|
|
25
|
+
ScrollTop,
|
|
26
|
+
ScrollToRow,
|
|
27
|
+
Terminal,
|
|
28
|
+
TerminalEffects,
|
|
29
|
+
TerminalModes,
|
|
30
|
+
TerminalNotification,
|
|
31
|
+
TitleChanged,
|
|
32
|
+
)
|
|
33
|
+
from ghostty_textual.keys import KeyEvent, MouseEvent
|
|
34
|
+
from ghostty_textual.theme import TerminalTheme
|
|
35
|
+
from ghostty_textual.widget import PasteRejected, TerminalView
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Cell",
|
|
39
|
+
"CellStyle",
|
|
40
|
+
"BellRang",
|
|
41
|
+
"ClipboardPolicy",
|
|
42
|
+
"ClipboardWritten",
|
|
43
|
+
"CursorState",
|
|
44
|
+
"Frame",
|
|
45
|
+
"GhosttyError",
|
|
46
|
+
"GhosttyUnavailable",
|
|
47
|
+
"KeyEvent",
|
|
48
|
+
"MouseEvent",
|
|
49
|
+
"PasteRejected",
|
|
50
|
+
"ProcessingError",
|
|
51
|
+
"ResourceLimits",
|
|
52
|
+
"RowPatch",
|
|
53
|
+
"ScrollBottom",
|
|
54
|
+
"ScrollDelta",
|
|
55
|
+
"ScrollToRow",
|
|
56
|
+
"ScrollTop",
|
|
57
|
+
"Terminal",
|
|
58
|
+
"TerminalEffects",
|
|
59
|
+
"TerminalModes",
|
|
60
|
+
"TerminalNotification",
|
|
61
|
+
"TerminalTheme",
|
|
62
|
+
"TerminalView",
|
|
63
|
+
"TitleChanged",
|
|
64
|
+
"ViewportState",
|
|
65
|
+
"__version__",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Build metadata is generated from [project].version in pyproject.toml.
|
|
69
|
+
__version__ = _version("ghostty-textual")
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""The native boundary: loader, ABI verification, and union-by-value shims.
|
|
2
|
+
|
|
3
|
+
This is the only module that knows `pyghostty` exists. Everything above it sees
|
|
4
|
+
`Native` and the exceptions defined here.
|
|
5
|
+
|
|
6
|
+
`pyghostty._ffi` and `pyghostty._cdef` are **private** modules of a pinned
|
|
7
|
+
dependency (see `pyproject.toml`). A nominally compatible release may change
|
|
8
|
+
them without deprecation, so `load()` verifies every symbol and struct layout it
|
|
9
|
+
depends on before returning.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import functools
|
|
15
|
+
import platform
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
__all__ = ["GhosttyError", "GhosttyUnavailable", "Native", "load"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GhosttyUnavailable(RuntimeError):
|
|
23
|
+
"""libghostty-vt could not be loaded, or its ABI is not the one we pinned."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GhosttyError(RuntimeError):
|
|
27
|
+
"""A libghostty-vt call failed, or the binding itself misbehaved.
|
|
28
|
+
|
|
29
|
+
Always fatal. `ghostty_terminal_vt_write` is deliberately non-failing for
|
|
30
|
+
untrusted input, so this never means "the remote sent bad bytes" -- it means
|
|
31
|
+
a wrapper bug, ABI mismatch, or lifecycle violation. See spec v2 §3.6.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
SUPPORTED_WHEELS = (
|
|
36
|
+
"macosx_13_0_arm64",
|
|
37
|
+
"macosx_13_0_x86_64",
|
|
38
|
+
"manylinux_2_27_aarch64.manylinux_2_28_aarch64",
|
|
39
|
+
"manylinux_2_27_x86_64.manylinux_2_28_x86_64",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
#: Symbols we call. Verified at load so a pyghostty change fails loudly here
|
|
43
|
+
#: rather than as an AttributeError deep inside a render loop.
|
|
44
|
+
REQUIRED_SYMBOLS = (
|
|
45
|
+
# lifecycle
|
|
46
|
+
"ghostty_terminal_new",
|
|
47
|
+
"ghostty_terminal_free",
|
|
48
|
+
"ghostty_terminal_reset",
|
|
49
|
+
"ghostty_terminal_resize",
|
|
50
|
+
"ghostty_terminal_set",
|
|
51
|
+
"ghostty_terminal_get",
|
|
52
|
+
"ghostty_terminal_vt_write",
|
|
53
|
+
"ghostty_terminal_scroll_viewport",
|
|
54
|
+
"ghostty_terminal_grid_ref",
|
|
55
|
+
# render state
|
|
56
|
+
"ghostty_render_state_new",
|
|
57
|
+
"ghostty_render_state_free",
|
|
58
|
+
"ghostty_render_state_update",
|
|
59
|
+
"ghostty_render_state_begin_update",
|
|
60
|
+
"ghostty_render_state_end_update",
|
|
61
|
+
"ghostty_render_state_get",
|
|
62
|
+
"ghostty_render_state_set",
|
|
63
|
+
"ghostty_render_state_row_iterator_new",
|
|
64
|
+
"ghostty_render_state_row_iterator_next",
|
|
65
|
+
"ghostty_render_state_row_iterator_free",
|
|
66
|
+
"ghostty_render_state_row_get",
|
|
67
|
+
"ghostty_render_state_row_set",
|
|
68
|
+
"ghostty_render_state_row_cells_new",
|
|
69
|
+
"ghostty_render_state_row_cells_next",
|
|
70
|
+
"ghostty_render_state_row_cells_select",
|
|
71
|
+
"ghostty_render_state_row_cells_get",
|
|
72
|
+
"ghostty_render_state_row_cells_get_multi",
|
|
73
|
+
"ghostty_render_state_row_cells_free",
|
|
74
|
+
"ghostty_cell_get",
|
|
75
|
+
"ghostty_cell_get_multi",
|
|
76
|
+
"ghostty_row_get",
|
|
77
|
+
# encoders
|
|
78
|
+
"ghostty_key_encoder_new",
|
|
79
|
+
"ghostty_key_encoder_free",
|
|
80
|
+
"ghostty_key_encoder_encode",
|
|
81
|
+
"ghostty_key_encoder_setopt_from_terminal",
|
|
82
|
+
"ghostty_key_event_new",
|
|
83
|
+
"ghostty_key_event_free",
|
|
84
|
+
"ghostty_key_event_set_action",
|
|
85
|
+
"ghostty_key_event_set_key",
|
|
86
|
+
"ghostty_key_event_set_mods",
|
|
87
|
+
"ghostty_key_event_set_utf8",
|
|
88
|
+
"ghostty_focus_encode",
|
|
89
|
+
"ghostty_paste_is_safe",
|
|
90
|
+
"ghostty_paste_encode",
|
|
91
|
+
"ghostty_mouse_event_new",
|
|
92
|
+
"ghostty_mouse_event_free",
|
|
93
|
+
"ghostty_mouse_event_set_action",
|
|
94
|
+
"ghostty_mouse_event_set_button",
|
|
95
|
+
"ghostty_mouse_event_clear_button",
|
|
96
|
+
"ghostty_mouse_event_set_mods",
|
|
97
|
+
"ghostty_mouse_event_set_position",
|
|
98
|
+
"ghostty_mouse_encoder_new",
|
|
99
|
+
"ghostty_mouse_encoder_free",
|
|
100
|
+
"ghostty_mouse_encoder_setopt_from_terminal",
|
|
101
|
+
"ghostty_mouse_encoder_encode",
|
|
102
|
+
"ghostty_grid_ref_hyperlink_uri",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# ABI-mode cffi cannot pass unions by value. pyghostty ships a layout-identical
|
|
106
|
+
# struct twin for GhosttyPoint and notes that GhosttyTerminalScrollViewport
|
|
107
|
+
# needs the same treatment "later"; as of pyghostty 0.1.4 that twin does not
|
|
108
|
+
# exist, so we carry it.
|
|
109
|
+
#
|
|
110
|
+
# The union is {intptr_t delta | size_t row | uint64_t _padding[2]} = 16 bytes,
|
|
111
|
+
# making the struct tag(4) + pad(4) + 16 = 24 bytes. Over the 16-byte threshold,
|
|
112
|
+
# so arm64 AAPCS64 passes it indirectly and x86-64 SysV passes it in stack
|
|
113
|
+
# memory -- in both cases identically to a struct of the same layout.
|
|
114
|
+
_SCROLL_TWIN_CDEF = """
|
|
115
|
+
typedef struct { uint64_t a; uint64_t b; } GhosttyTerminalScrollViewportValueS;
|
|
116
|
+
typedef struct {
|
|
117
|
+
GhosttyTerminalScrollViewportTag tag;
|
|
118
|
+
GhosttyTerminalScrollViewportValueS value;
|
|
119
|
+
} GhosttyTerminalScrollViewportS;
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
_SCROLL_VIEWPORT_SIG = "void(*)(GhosttyTerminal, GhosttyTerminalScrollViewportS)"
|
|
123
|
+
_GRID_REF_SIG = "GhosttyResult(*)(GhosttyTerminal, GhosttyPointS, GhosttyGridRef*)"
|
|
124
|
+
|
|
125
|
+
#: (twin, real) pairs whose size and alignment must match exactly.
|
|
126
|
+
_TWIN_PAIRS = (
|
|
127
|
+
("GhosttyPointS", "GhosttyPoint"),
|
|
128
|
+
("GhosttyTerminalScrollViewportS", "GhosttyTerminalScrollViewport"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True, slots=True)
|
|
133
|
+
class Native:
|
|
134
|
+
"""A verified handle on libghostty-vt."""
|
|
135
|
+
|
|
136
|
+
ffi: Any
|
|
137
|
+
lib: Any
|
|
138
|
+
|
|
139
|
+
def check(self, result: int, what: str) -> None:
|
|
140
|
+
"""Raise `GhosttyError` unless `result` is GHOSTTY_SUCCESS (0)."""
|
|
141
|
+
if result:
|
|
142
|
+
raise GhosttyError(f"{what} failed: {result}")
|
|
143
|
+
|
|
144
|
+
def get_bool(self, getter: Any, handle: Any, key: int, what: str) -> bool:
|
|
145
|
+
out = self.ffi.new("bool*")
|
|
146
|
+
self.check(getter(handle, key, out), what)
|
|
147
|
+
return bool(out[0])
|
|
148
|
+
|
|
149
|
+
def get_enum(self, getter: Any, handle: Any, key: int, what: str) -> int:
|
|
150
|
+
out = self.ffi.new("int*")
|
|
151
|
+
self.check(getter(handle, key, out), what)
|
|
152
|
+
return int(out[0])
|
|
153
|
+
|
|
154
|
+
def get_u16(self, getter: Any, handle: Any, key: int, what: str) -> int:
|
|
155
|
+
out = self.ffi.new("uint16_t*")
|
|
156
|
+
self.check(getter(handle, key, out), what)
|
|
157
|
+
return int(out[0])
|
|
158
|
+
|
|
159
|
+
def get_u32(self, getter: Any, handle: Any, key: int, what: str) -> int:
|
|
160
|
+
out = self.ffi.new("uint32_t*")
|
|
161
|
+
self.check(getter(handle, key, out), what)
|
|
162
|
+
return int(out[0])
|
|
163
|
+
|
|
164
|
+
def get_u64(self, getter: Any, handle: Any, key: int, what: str) -> int:
|
|
165
|
+
out = self.ffi.new("uint64_t*")
|
|
166
|
+
self.check(getter(handle, key, out), what)
|
|
167
|
+
return int(out[0])
|
|
168
|
+
|
|
169
|
+
def get_struct(self, getter: Any, handle: Any, key: int, ctype: str, what: str) -> Any:
|
|
170
|
+
out = self.ffi.new(f"{ctype}*")
|
|
171
|
+
if hasattr(out[0], "size"):
|
|
172
|
+
out[0].size = self.ffi.sizeof(ctype)
|
|
173
|
+
self.check(getter(handle, key, out), what)
|
|
174
|
+
return out[0]
|
|
175
|
+
|
|
176
|
+
def scroll_viewport(self, terminal: Any, tag: int, value: int = 0) -> None:
|
|
177
|
+
"""Call `ghostty_terminal_scroll_viewport` through the struct twin."""
|
|
178
|
+
fn = self._union_fn("ghostty_terminal_scroll_viewport", _SCROLL_VIEWPORT_SIG)
|
|
179
|
+
behavior = self.ffi.new("GhosttyTerminalScrollViewportS*")
|
|
180
|
+
behavior.tag = tag
|
|
181
|
+
behavior.value.a = value & 0xFFFFFFFFFFFFFFFF
|
|
182
|
+
behavior.value.b = 0
|
|
183
|
+
fn(terminal, behavior[0])
|
|
184
|
+
|
|
185
|
+
def grid_ref_buffers(self) -> tuple[Any, Any]:
|
|
186
|
+
"""Allocate reusable point-twin and grid-reference output buffers."""
|
|
187
|
+
point = self.ffi.new("GhosttyPointS*")
|
|
188
|
+
out = self.ffi.new("GhosttyGridRef*")
|
|
189
|
+
out.size = self.ffi.sizeof("GhosttyGridRef")
|
|
190
|
+
return point, out
|
|
191
|
+
|
|
192
|
+
def grid_ref(self, terminal: Any, x: int, y: int, *, point: Any = None, out: Any = None) -> Any:
|
|
193
|
+
"""Return a viewport reference, optionally reusing ``grid_ref_buffers()``.
|
|
194
|
+
|
|
195
|
+
The returned output is overwritten on the next call using the same buffers.
|
|
196
|
+
"""
|
|
197
|
+
fn = self._union_fn("ghostty_terminal_grid_ref", _GRID_REF_SIG)
|
|
198
|
+
if point is None:
|
|
199
|
+
point = self.ffi.new("GhosttyPointS*")
|
|
200
|
+
point.tag = self.lib.GHOSTTY_POINT_TAG_VIEWPORT
|
|
201
|
+
point.value.x = x
|
|
202
|
+
point.value.y = y
|
|
203
|
+
if out is None:
|
|
204
|
+
out = self.ffi.new("GhosttyGridRef*")
|
|
205
|
+
out.size = self.ffi.sizeof("GhosttyGridRef")
|
|
206
|
+
self.check(fn(terminal, point[0], out), "terminal_grid_ref")
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
def _union_fn(self, name: str, sig: str) -> Any:
|
|
210
|
+
from pyghostty._ffi import union_fn
|
|
211
|
+
|
|
212
|
+
return union_fn(name, sig)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@functools.cache
|
|
216
|
+
def load() -> Native:
|
|
217
|
+
"""Load and verify libghostty-vt.
|
|
218
|
+
|
|
219
|
+
Cached, so the cdef extension and layout assertions run exactly once.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
GhosttyUnavailable: the library is missing, or its ABI is not ours.
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
from pyghostty._ffi import ffi, lib
|
|
226
|
+
except (ImportError, OSError) as exc:
|
|
227
|
+
raise GhosttyUnavailable(
|
|
228
|
+
f"libghostty-vt is unavailable on {platform.system()}/{platform.machine()}. "
|
|
229
|
+
f"Prebuilt wheels exist for: {', '.join(SUPPORTED_WHEELS)}. "
|
|
230
|
+
f"Cause: {exc}"
|
|
231
|
+
) from exc
|
|
232
|
+
|
|
233
|
+
_declare_twins(ffi)
|
|
234
|
+
_verify_symbols(lib)
|
|
235
|
+
_verify_layouts(ffi)
|
|
236
|
+
return Native(ffi=ffi, lib=lib)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _declare_twins(ffi: Any) -> None:
|
|
240
|
+
"""Add our struct twin to pyghostty's shared FFI, tolerating a future upstream fix."""
|
|
241
|
+
try:
|
|
242
|
+
ffi.cdef(_SCROLL_TWIN_CDEF)
|
|
243
|
+
except Exception:
|
|
244
|
+
# Already declared -- either by a re-entrant call (load() is cached, but
|
|
245
|
+
# a caller may hold a stale ffi) or by a pyghostty release that finally
|
|
246
|
+
# shipped the twin. Layout verification below is the real gate.
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _verify_symbols(lib: Any) -> None:
|
|
251
|
+
missing = []
|
|
252
|
+
for name in REQUIRED_SYMBOLS:
|
|
253
|
+
try:
|
|
254
|
+
getattr(lib, name)
|
|
255
|
+
except AttributeError:
|
|
256
|
+
missing.append(name)
|
|
257
|
+
if missing:
|
|
258
|
+
raise GhosttyUnavailable(
|
|
259
|
+
"pyghostty's ABI is missing symbols ghostty-textual requires "
|
|
260
|
+
f"({len(missing)}): {', '.join(missing)}. "
|
|
261
|
+
"This build of pyghostty is not the pinned one."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _verify_layouts(ffi: Any) -> None:
|
|
266
|
+
for twin, real in _TWIN_PAIRS:
|
|
267
|
+
try:
|
|
268
|
+
got = (ffi.sizeof(twin), ffi.alignof(twin))
|
|
269
|
+
want = (ffi.sizeof(real), ffi.alignof(real))
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
raise GhosttyUnavailable(f"cannot measure {twin}/{real}: {exc}") from exc
|
|
272
|
+
if got != want:
|
|
273
|
+
raise GhosttyUnavailable(
|
|
274
|
+
f"union twin {twin} does not match {real}: "
|
|
275
|
+
f"size/align {got} != {want}. Passing it by value would corrupt "
|
|
276
|
+
f"the stack on this ABI."
|
|
277
|
+
)
|