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.
@@ -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
+ )