termwright 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {termwright-0.2.0 → termwright-0.3.0}/PKG-INFO +99 -47
- {termwright-0.2.0 → termwright-0.3.0}/README.md +96 -44
- {termwright-0.2.0 → termwright-0.3.0}/pyproject.toml +3 -3
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/__init__.py +51 -7
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/client.py +78 -127
- termwright-0.3.0/src/termwright/evidence.py +403 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/limits.py +1 -1
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/marker.py +4 -5
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/messages.py +51 -78
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/roles.py +15 -4
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/textual.py +1 -1
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/tree.py +154 -30
- termwright-0.3.0/src/termwright/validate.py +1043 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright_probe/__init__.py +41 -3
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright_probe/__main__.py +0 -1
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright_probe/bootstrap.py +99 -17
- termwright-0.3.0/src/termwright_probe/session.py +247 -0
- termwright-0.3.0/src/termwright_probe/textual_probe.py +500 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright_probe/textual_tree.py +149 -109
- termwright-0.3.0/tests/conftest.py +69 -0
- termwright-0.3.0/tests/fixtures/vanilla_textual_app.py +52 -0
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_client.py +25 -119
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_debug.py +0 -16
- termwright-0.3.0/tests/test_evidence.py +184 -0
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_logs.py +6 -3
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_messages.py +45 -34
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_observation_vectors.py +3 -2
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_probe_bootstrap.py +248 -3
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_probe_golden.py +67 -29
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_probe_session.py +238 -99
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_probe_tree.py +112 -41
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_textual_annotations.py +14 -17
- termwright-0.3.0/tests/test_textual_probe_hook.py +445 -0
- termwright-0.3.0/tests/test_validate.py +212 -0
- {termwright-0.2.0 → termwright-0.3.0}/uv.lock +3 -3
- termwright-0.2.0/src/termwright/diffing.py +0 -118
- termwright-0.2.0/src/termwright/validate.py +0 -846
- termwright-0.2.0/src/termwright_probe/session.py +0 -197
- termwright-0.2.0/src/termwright_probe/textual_probe.py +0 -239
- termwright-0.2.0/tests/conftest.py +0 -32
- termwright-0.2.0/tests/fixtures/vanilla_textual_app.py +0 -32
- termwright-0.2.0/tests/test_deltas.py +0 -117
- termwright-0.2.0/tests/test_diffing.py +0 -233
- termwright-0.2.0/tests/test_textual_probe_hook.py +0 -54
- termwright-0.2.0/tests/test_validate.py +0 -142
- {termwright-0.2.0 → termwright-0.3.0}/.gitignore +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/examples/permission_app.py +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/debug.py +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/errors.py +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/framing.py +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/logging_bridge.py +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright/logs.py +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/src/termwright_probe/defer.py +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_framing.py +0 -0
- {termwright-0.2.0 → termwright-0.3.0}/tests/test_marker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: termwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Semantic side-channel client, automatic Textual probe, and annotation SDK for Termwright
|
|
5
5
|
Project-URL: Homepage, https://github.com/gorce-ai/termwright
|
|
6
6
|
Project-URL: Source, https://github.com/gorce-ai/termwright/tree/main/clients/python
|
|
@@ -17,9 +17,9 @@ Requires-Python: >=3.9
|
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
19
19
|
Requires-Dist: pytest>=7; extra == 'dev'
|
|
20
|
-
Requires-Dist: textual>=
|
|
20
|
+
Requires-Dist: textual>=8.2.8; extra == 'dev'
|
|
21
21
|
Provides-Extra: textual
|
|
22
|
-
Requires-Dist: textual>=
|
|
22
|
+
Requires-Dist: textual>=8.2.8; extra == 'textual'
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
25
25
|
# termwright (Python)
|
|
@@ -28,8 +28,12 @@ Semantic side-channel client, automatic probe and optional annotation SDK for
|
|
|
28
28
|
[Textual](https://textual.textualize.io).
|
|
29
29
|
|
|
30
30
|
An instrumented app publishes its widget tree over a unix socket and commits
|
|
31
|
-
each render with a signed OSC marker, so the driver can assert on
|
|
32
|
-
|
|
31
|
+
each render with a signed OSC marker, so the driver can assert on _roles and
|
|
32
|
+
names_ instead of screen-scraping cells.
|
|
33
|
+
|
|
34
|
+
The protocol client speaks `termwright/2`. Every published semantic revision
|
|
35
|
+
is a complete v2 snapshot with evidence-qualified geometry and pointer
|
|
36
|
+
observations.
|
|
33
37
|
|
|
34
38
|
**Dormant rule.** Without `TERMWRIGHT_ENDPOINT` and `TERMWRIGHT_TOKEN` the
|
|
35
39
|
probe installs nothing, opens no socket, writes no marker, and renders exactly
|
|
@@ -42,7 +46,10 @@ pip install termwright # protocol client + probe + annotation SDK
|
|
|
42
46
|
pip install "termwright[textual]" # + Textual itself
|
|
43
47
|
```
|
|
44
48
|
|
|
45
|
-
Requires Python 3.9+. The
|
|
49
|
+
Requires Python 3.9+. The Textual extra accepts Textual 8.2.8 or newer as an
|
|
50
|
+
advisory packaging floor. Runtime capability checks and behavioral
|
|
51
|
+
conformance, not the version string, decide whether strong semantics engage;
|
|
52
|
+
the protocol modules have no third-party dependencies.
|
|
46
53
|
|
|
47
54
|
## Automatic Textual semantics
|
|
48
55
|
|
|
@@ -70,15 +77,15 @@ useful for a custom runner, is:
|
|
|
70
77
|
python -m termwright_probe -- python app.py
|
|
71
78
|
```
|
|
72
79
|
|
|
73
|
-
Under the driver this publishes
|
|
80
|
+
Under the driver this publishes after every causally ordered frame enqueue:
|
|
74
81
|
|
|
75
82
|
```
|
|
76
83
|
application "PermissionApp"
|
|
77
|
-
region
|
|
78
|
-
text "Allow bash to run?"
|
|
79
|
-
button "Approve"
|
|
80
|
-
button "Reject"
|
|
81
|
-
textbox "Reason"
|
|
84
|
+
region visibleRect=(0,0,80,24)
|
|
85
|
+
text "Allow bash to run?" visibleRect=(0,0,80,1) testId=prompt
|
|
86
|
+
button "Approve" visibleRect=(1,0,80,3) testId=approve [focused]
|
|
87
|
+
button "Reject" visibleRect=(4,0,80,3) testId=reject
|
|
88
|
+
textbox "Reason" visibleRect=(7,0,80,3) testId=reason
|
|
82
89
|
```
|
|
83
90
|
|
|
84
91
|
### Roles and names
|
|
@@ -131,7 +138,7 @@ recreate. `actions` uses the protocol's closed descriptive vocabulary; it never
|
|
|
131
138
|
registers an out-of-band callback, and interaction still becomes real PTY
|
|
132
139
|
input.
|
|
133
140
|
|
|
134
|
-
The API intentionally has no
|
|
141
|
+
The API intentionally has no geometry, focus, visibility, rendered-text or
|
|
135
142
|
portable-state arguments. Those physical facts remain probe-owned, and merge
|
|
136
143
|
tests enforce that the annotation cannot replace them.
|
|
137
144
|
|
|
@@ -147,7 +154,22 @@ Any TUI can drive the client directly. You own the render; the client owns the
|
|
|
147
154
|
revision numbers and hands you the marker to write after the render's last byte.
|
|
148
155
|
|
|
149
156
|
```python
|
|
150
|
-
from termwright import
|
|
157
|
+
from termwright import (
|
|
158
|
+
NodeGeometryObservations,
|
|
159
|
+
Observation,
|
|
160
|
+
Rect,
|
|
161
|
+
SemanticNode,
|
|
162
|
+
SemanticSnapshot,
|
|
163
|
+
client_from_env,
|
|
164
|
+
framework_evidence,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def geometry(rect: Rect) -> NodeGeometryObservations:
|
|
168
|
+
return NodeGeometryObservations(
|
|
169
|
+
displayed=Observation("known", True, evidence=framework_evidence("my-adapter")),
|
|
170
|
+
intendedRect=Observation("known", rect, evidence=framework_evidence("my-adapter")),
|
|
171
|
+
visibleRect=Observation("known", rect, evidence=framework_evidence("my-adapter")),
|
|
172
|
+
)
|
|
151
173
|
|
|
152
174
|
client = client_from_env(adapter_name="my-tui", adapter_version="1.0.0")
|
|
153
175
|
if client is not None and await client.start():
|
|
@@ -156,10 +178,14 @@ if client is not None and await client.start():
|
|
|
156
178
|
sessionId="", revision=0, columns=80, rows=24, # both are overwritten
|
|
157
179
|
rootIds=["root"],
|
|
158
180
|
nodes=[
|
|
159
|
-
SemanticNode(id="root", role="dialog", name="Permission"
|
|
181
|
+
SemanticNode(id="root", role="dialog", name="Permission",
|
|
182
|
+
geometry=geometry(Rect(0, 0, 80, 24))),
|
|
160
183
|
SemanticNode(id="ok", parentId="root", role="button", name="Approve",
|
|
161
|
-
|
|
184
|
+
geometry=geometry(Rect(row=1, column=2, width=9, height=1))),
|
|
162
185
|
],
|
|
186
|
+
coordinateSpace=Observation("known", "viewport-cells", evidence=framework_evidence("my-adapter")),
|
|
187
|
+
hitGrid=Observation("unsupported", capability="pointer-hit-grid",
|
|
188
|
+
reason="framework-unobservable"),
|
|
163
189
|
)
|
|
164
190
|
)
|
|
165
191
|
sys.stdout.write(marker) # only after the render is fully written
|
|
@@ -208,8 +234,8 @@ or, on a session that came up:
|
|
|
208
234
|
|
|
209
235
|
```text
|
|
210
236
|
tw:sem [p41207] 0.002s dial unix:/tmp/tw-8f21/s timeout=5000ms
|
|
211
|
-
tw:sem [p41207] 0.003s hello sent adapter=textual/1.0.0 caps=tree,
|
|
212
|
-
tw:sem [3f9c1a04] 0.011s hello-ack session=3f9c1a04… marker=on subscribe=
|
|
237
|
+
tw:sem [p41207] 0.003s hello sent adapter=textual/1.0.0 caps=tree,states,actions,render-revisions,intended-geometry,clipped-geometry,pointer-hit-grid
|
|
238
|
+
tw:sem [3f9c1a04] 0.011s hello-ack session=3f9c1a04… marker=on subscribe=snapshots logs=off
|
|
213
239
|
tw:io [3f9c1a04] 0.048s r1 snapshot nodes=17
|
|
214
240
|
```
|
|
215
241
|
|
|
@@ -241,8 +267,16 @@ python -m termwright_probe -- python app.py
|
|
|
241
267
|
`app.py` imports no termwright, calls nothing of ours, and is not edited. The
|
|
242
268
|
launcher puts a generated `sitecustomize.py` on `PYTHONPATH`; CPython imports
|
|
243
269
|
it during startup, before the script's own directory reaches `sys.path`; the
|
|
244
|
-
probe waits there until the application imports Textual and
|
|
245
|
-
`App.
|
|
270
|
+
probe waits there until the application imports Textual and observes the
|
|
271
|
+
capability-checked `App._display` / driver enqueue / `post_display_hook`
|
|
272
|
+
boundary. The
|
|
273
|
+
marker is appended without waiting to the same WriterThread FIFO, after the
|
|
274
|
+
frame; a full queue fails the semantic channel instead of blocking Textual's
|
|
275
|
+
event loop. No Textual version is allowlisted. Strong probing accepts only a
|
|
276
|
+
live built-in `LinuxDriver` or `WindowsDriver` whose concrete WriterThread and
|
|
277
|
+
bounded FIFO pass the runtime checks; custom
|
|
278
|
+
`driver_class` values and inline mode fail the semantic channel explicitly
|
|
279
|
+
rather than publishing an unprovable commit. A driver that already sets `TERMWRIGHT_ENDPOINT` and
|
|
246
280
|
`TERMWRIGHT_TOKEN` can compose the same thing itself:
|
|
247
281
|
|
|
248
282
|
```python
|
|
@@ -265,21 +299,22 @@ the claim that the probe observes rather than redraws.
|
|
|
265
299
|
|
|
266
300
|
**What it reports automatically.**
|
|
267
301
|
|
|
268
|
-
| Fact
|
|
269
|
-
|
|
270
|
-
|
|
|
271
|
-
|
|
|
272
|
-
| roles for your own widget classes
|
|
273
|
-
| `frameworkType` on anything unrecognised
|
|
302
|
+
| Fact | Where it comes from |
|
|
303
|
+
| ----------------------------------------- | ---------------------------------------------------------------------- |
|
|
304
|
+
| intended and visible rectangles | `MapGeometry.region` and `MapGeometry.visible_region` |
|
|
305
|
+
| exact pointer recipient | `Screen.get_widget_at`, compressed into the snapshot hit grid |
|
|
306
|
+
| roles for your own widget classes | the MRO, so `SaveButton(Button)` is a button with no registration |
|
|
307
|
+
| `frameworkType` on anything unrecognised | the widget's class name |
|
|
274
308
|
| scrolled out of view vs `display = False` | both `hidden`; the first also `state.offscreen`, with a zero-area rect |
|
|
275
309
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
cells are covered.
|
|
310
|
+
The driver allows pointer actions only when the snapshot's hit grid names the
|
|
311
|
+
node as the recipient at the target cell. Paint order alone is not sufficient.
|
|
279
312
|
|
|
280
313
|
**Where the injection reaches**, measured on CPython 3.12 (see
|
|
281
314
|
`docs/architecture/audit/textual.md` for the full table): a plain script,
|
|
282
|
-
`-m`, `-c`, a console-script entry point and `
|
|
315
|
+
`-m`, `-c`, a console-script entry point, `uv run` and `poetry run` all work. The
|
|
316
|
+
Python 3.12 CI lane installs both environment managers and executes these real
|
|
317
|
+
subprocess cases. `python -S`
|
|
283
318
|
and `python -E` do not, and are not meant to — the first disables `site`
|
|
284
319
|
entirely and the second makes the interpreter ignore `PYTHONPATH`. Both are
|
|
285
320
|
the person running the interpreter opting out.
|
|
@@ -301,28 +336,45 @@ not listed here follows them.
|
|
|
301
336
|
type for that reason and for no other — no other state here is inferred.
|
|
302
337
|
- **Widgets on an inactive screen are absent, not `hidden`** (rule 4). The
|
|
303
338
|
probe walks `app.screen`, so a pushed-over screen's widgets are not in the
|
|
304
|
-
tree at all. A widget hidden on the
|
|
339
|
+
tree at all. A widget hidden on the _active_ screen (`display = False`) does
|
|
305
340
|
publish `hidden: true`. Textual owns the screen stack; reaching into it would
|
|
306
|
-
mean publishing widgets that no longer receive events.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
- **The probe does not report `frame-begin`** (probe capability).
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
341
|
+
mean publishing widgets that no longer receive events. The handshake records
|
|
342
|
+
this narrower limitation as the machine-readable
|
|
343
|
+
`inactive-screen-tree` degradation.
|
|
344
|
+
- **Ownership is process-local and one-shot.** The first application
|
|
345
|
+
interpreter atomically owns the generated bootstrap. Before the
|
|
346
|
+
application's first line, the probe captures its credentials privately and
|
|
347
|
+
removes the endpoint, token, owner marker, and bootstrap path from the
|
|
348
|
+
inherited environment. Python children and grandchildren therefore cannot
|
|
349
|
+
attach to the parent's semantic session. `poetry run` receives a one-hop
|
|
350
|
+
launcher marker so ownership is claimed by its target interpreter, not by
|
|
351
|
+
Poetry's own console process.
|
|
352
|
+
- **The probe does not report `frame-begin`** (probe capability). A frame is
|
|
353
|
+
accepted only when the same `_display` attempt successfully enqueued output
|
|
354
|
+
through a capability-checked non-headless built-in driver before
|
|
355
|
+
`post_display_hook`. The writer is preflighted before snapshot publication,
|
|
356
|
+
then its marker is appended to that same FIFO. A full or replaced writer
|
|
357
|
+
fails closed without blocking on queue capacity. This exposes no instant the
|
|
358
|
+
probe could honestly call the start of a frame. Consumers must not read a
|
|
359
|
+
missing `frame-begin` as "no frame in progress".
|
|
322
360
|
- **A `Static` subclass with a custom `render()` is named by its `content`**
|
|
323
361
|
(rule 2), which is the markup it was given rather than what it draws. Textual
|
|
324
362
|
renders to a strip of segments with no text handle the adapter can read.
|
|
325
363
|
|
|
364
|
+
## Application evidence providers
|
|
365
|
+
|
|
366
|
+
`termwright.evidence` exposes closed pointer, focus, scroll, paint,
|
|
367
|
+
terminal-input-mode, and action-strategy provider families.
|
|
368
|
+
`ApplicationFocusEvidenceProvider.observe` returns a semantic
|
|
369
|
+
recipient mapping or authoritative `None`; the wire keeps `focused` and `none`
|
|
370
|
+
distinct from an unnegotiated provider. Registration must happen before the
|
|
371
|
+
session freeze, and providers publish evidence/recipes only—physical input is
|
|
372
|
+
still sent through the PTY.
|
|
373
|
+
|
|
374
|
+
`ApplicationTerminalInputModeEvidenceProvider.observe` reports the production
|
|
375
|
+
parser's exact mouse and focus modes. It is authoritative evidence for hidden
|
|
376
|
+
ConPTY state, not a callback or a request to synthesize terminal modes.
|
|
377
|
+
|
|
326
378
|
## Conformance
|
|
327
379
|
|
|
328
380
|
`tests/` runs against `clients/test-vectors/`, which is generated from the
|
|
@@ -4,8 +4,12 @@ Semantic side-channel client, automatic probe and optional annotation SDK for
|
|
|
4
4
|
[Textual](https://textual.textualize.io).
|
|
5
5
|
|
|
6
6
|
An instrumented app publishes its widget tree over a unix socket and commits
|
|
7
|
-
each render with a signed OSC marker, so the driver can assert on
|
|
8
|
-
|
|
7
|
+
each render with a signed OSC marker, so the driver can assert on _roles and
|
|
8
|
+
names_ instead of screen-scraping cells.
|
|
9
|
+
|
|
10
|
+
The protocol client speaks `termwright/2`. Every published semantic revision
|
|
11
|
+
is a complete v2 snapshot with evidence-qualified geometry and pointer
|
|
12
|
+
observations.
|
|
9
13
|
|
|
10
14
|
**Dormant rule.** Without `TERMWRIGHT_ENDPOINT` and `TERMWRIGHT_TOKEN` the
|
|
11
15
|
probe installs nothing, opens no socket, writes no marker, and renders exactly
|
|
@@ -18,7 +22,10 @@ pip install termwright # protocol client + probe + annotation SDK
|
|
|
18
22
|
pip install "termwright[textual]" # + Textual itself
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
Requires Python 3.9+. The
|
|
25
|
+
Requires Python 3.9+. The Textual extra accepts Textual 8.2.8 or newer as an
|
|
26
|
+
advisory packaging floor. Runtime capability checks and behavioral
|
|
27
|
+
conformance, not the version string, decide whether strong semantics engage;
|
|
28
|
+
the protocol modules have no third-party dependencies.
|
|
22
29
|
|
|
23
30
|
## Automatic Textual semantics
|
|
24
31
|
|
|
@@ -46,15 +53,15 @@ useful for a custom runner, is:
|
|
|
46
53
|
python -m termwright_probe -- python app.py
|
|
47
54
|
```
|
|
48
55
|
|
|
49
|
-
Under the driver this publishes
|
|
56
|
+
Under the driver this publishes after every causally ordered frame enqueue:
|
|
50
57
|
|
|
51
58
|
```
|
|
52
59
|
application "PermissionApp"
|
|
53
|
-
region
|
|
54
|
-
text "Allow bash to run?"
|
|
55
|
-
button "Approve"
|
|
56
|
-
button "Reject"
|
|
57
|
-
textbox "Reason"
|
|
60
|
+
region visibleRect=(0,0,80,24)
|
|
61
|
+
text "Allow bash to run?" visibleRect=(0,0,80,1) testId=prompt
|
|
62
|
+
button "Approve" visibleRect=(1,0,80,3) testId=approve [focused]
|
|
63
|
+
button "Reject" visibleRect=(4,0,80,3) testId=reject
|
|
64
|
+
textbox "Reason" visibleRect=(7,0,80,3) testId=reason
|
|
58
65
|
```
|
|
59
66
|
|
|
60
67
|
### Roles and names
|
|
@@ -107,7 +114,7 @@ recreate. `actions` uses the protocol's closed descriptive vocabulary; it never
|
|
|
107
114
|
registers an out-of-band callback, and interaction still becomes real PTY
|
|
108
115
|
input.
|
|
109
116
|
|
|
110
|
-
The API intentionally has no
|
|
117
|
+
The API intentionally has no geometry, focus, visibility, rendered-text or
|
|
111
118
|
portable-state arguments. Those physical facts remain probe-owned, and merge
|
|
112
119
|
tests enforce that the annotation cannot replace them.
|
|
113
120
|
|
|
@@ -123,7 +130,22 @@ Any TUI can drive the client directly. You own the render; the client owns the
|
|
|
123
130
|
revision numbers and hands you the marker to write after the render's last byte.
|
|
124
131
|
|
|
125
132
|
```python
|
|
126
|
-
from termwright import
|
|
133
|
+
from termwright import (
|
|
134
|
+
NodeGeometryObservations,
|
|
135
|
+
Observation,
|
|
136
|
+
Rect,
|
|
137
|
+
SemanticNode,
|
|
138
|
+
SemanticSnapshot,
|
|
139
|
+
client_from_env,
|
|
140
|
+
framework_evidence,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def geometry(rect: Rect) -> NodeGeometryObservations:
|
|
144
|
+
return NodeGeometryObservations(
|
|
145
|
+
displayed=Observation("known", True, evidence=framework_evidence("my-adapter")),
|
|
146
|
+
intendedRect=Observation("known", rect, evidence=framework_evidence("my-adapter")),
|
|
147
|
+
visibleRect=Observation("known", rect, evidence=framework_evidence("my-adapter")),
|
|
148
|
+
)
|
|
127
149
|
|
|
128
150
|
client = client_from_env(adapter_name="my-tui", adapter_version="1.0.0")
|
|
129
151
|
if client is not None and await client.start():
|
|
@@ -132,10 +154,14 @@ if client is not None and await client.start():
|
|
|
132
154
|
sessionId="", revision=0, columns=80, rows=24, # both are overwritten
|
|
133
155
|
rootIds=["root"],
|
|
134
156
|
nodes=[
|
|
135
|
-
SemanticNode(id="root", role="dialog", name="Permission"
|
|
157
|
+
SemanticNode(id="root", role="dialog", name="Permission",
|
|
158
|
+
geometry=geometry(Rect(0, 0, 80, 24))),
|
|
136
159
|
SemanticNode(id="ok", parentId="root", role="button", name="Approve",
|
|
137
|
-
|
|
160
|
+
geometry=geometry(Rect(row=1, column=2, width=9, height=1))),
|
|
138
161
|
],
|
|
162
|
+
coordinateSpace=Observation("known", "viewport-cells", evidence=framework_evidence("my-adapter")),
|
|
163
|
+
hitGrid=Observation("unsupported", capability="pointer-hit-grid",
|
|
164
|
+
reason="framework-unobservable"),
|
|
139
165
|
)
|
|
140
166
|
)
|
|
141
167
|
sys.stdout.write(marker) # only after the render is fully written
|
|
@@ -184,8 +210,8 @@ or, on a session that came up:
|
|
|
184
210
|
|
|
185
211
|
```text
|
|
186
212
|
tw:sem [p41207] 0.002s dial unix:/tmp/tw-8f21/s timeout=5000ms
|
|
187
|
-
tw:sem [p41207] 0.003s hello sent adapter=textual/1.0.0 caps=tree,
|
|
188
|
-
tw:sem [3f9c1a04] 0.011s hello-ack session=3f9c1a04… marker=on subscribe=
|
|
213
|
+
tw:sem [p41207] 0.003s hello sent adapter=textual/1.0.0 caps=tree,states,actions,render-revisions,intended-geometry,clipped-geometry,pointer-hit-grid
|
|
214
|
+
tw:sem [3f9c1a04] 0.011s hello-ack session=3f9c1a04… marker=on subscribe=snapshots logs=off
|
|
189
215
|
tw:io [3f9c1a04] 0.048s r1 snapshot nodes=17
|
|
190
216
|
```
|
|
191
217
|
|
|
@@ -217,8 +243,16 @@ python -m termwright_probe -- python app.py
|
|
|
217
243
|
`app.py` imports no termwright, calls nothing of ours, and is not edited. The
|
|
218
244
|
launcher puts a generated `sitecustomize.py` on `PYTHONPATH`; CPython imports
|
|
219
245
|
it during startup, before the script's own directory reaches `sys.path`; the
|
|
220
|
-
probe waits there until the application imports Textual and
|
|
221
|
-
`App.
|
|
246
|
+
probe waits there until the application imports Textual and observes the
|
|
247
|
+
capability-checked `App._display` / driver enqueue / `post_display_hook`
|
|
248
|
+
boundary. The
|
|
249
|
+
marker is appended without waiting to the same WriterThread FIFO, after the
|
|
250
|
+
frame; a full queue fails the semantic channel instead of blocking Textual's
|
|
251
|
+
event loop. No Textual version is allowlisted. Strong probing accepts only a
|
|
252
|
+
live built-in `LinuxDriver` or `WindowsDriver` whose concrete WriterThread and
|
|
253
|
+
bounded FIFO pass the runtime checks; custom
|
|
254
|
+
`driver_class` values and inline mode fail the semantic channel explicitly
|
|
255
|
+
rather than publishing an unprovable commit. A driver that already sets `TERMWRIGHT_ENDPOINT` and
|
|
222
256
|
`TERMWRIGHT_TOKEN` can compose the same thing itself:
|
|
223
257
|
|
|
224
258
|
```python
|
|
@@ -241,21 +275,22 @@ the claim that the probe observes rather than redraws.
|
|
|
241
275
|
|
|
242
276
|
**What it reports automatically.**
|
|
243
277
|
|
|
244
|
-
| Fact
|
|
245
|
-
|
|
246
|
-
|
|
|
247
|
-
|
|
|
248
|
-
| roles for your own widget classes
|
|
249
|
-
| `frameworkType` on anything unrecognised
|
|
278
|
+
| Fact | Where it comes from |
|
|
279
|
+
| ----------------------------------------- | ---------------------------------------------------------------------- |
|
|
280
|
+
| intended and visible rectangles | `MapGeometry.region` and `MapGeometry.visible_region` |
|
|
281
|
+
| exact pointer recipient | `Screen.get_widget_at`, compressed into the snapshot hit grid |
|
|
282
|
+
| roles for your own widget classes | the MRO, so `SaveButton(Button)` is a button with no registration |
|
|
283
|
+
| `frameworkType` on anything unrecognised | the widget's class name |
|
|
250
284
|
| scrolled out of view vs `display = False` | both `hidden`; the first also `state.offscreen`, with a zero-area rect |
|
|
251
285
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
cells are covered.
|
|
286
|
+
The driver allows pointer actions only when the snapshot's hit grid names the
|
|
287
|
+
node as the recipient at the target cell. Paint order alone is not sufficient.
|
|
255
288
|
|
|
256
289
|
**Where the injection reaches**, measured on CPython 3.12 (see
|
|
257
290
|
`docs/architecture/audit/textual.md` for the full table): a plain script,
|
|
258
|
-
`-m`, `-c`, a console-script entry point and `
|
|
291
|
+
`-m`, `-c`, a console-script entry point, `uv run` and `poetry run` all work. The
|
|
292
|
+
Python 3.12 CI lane installs both environment managers and executes these real
|
|
293
|
+
subprocess cases. `python -S`
|
|
259
294
|
and `python -E` do not, and are not meant to — the first disables `site`
|
|
260
295
|
entirely and the second makes the interpreter ignore `PYTHONPATH`. Both are
|
|
261
296
|
the person running the interpreter opting out.
|
|
@@ -277,28 +312,45 @@ not listed here follows them.
|
|
|
277
312
|
type for that reason and for no other — no other state here is inferred.
|
|
278
313
|
- **Widgets on an inactive screen are absent, not `hidden`** (rule 4). The
|
|
279
314
|
probe walks `app.screen`, so a pushed-over screen's widgets are not in the
|
|
280
|
-
tree at all. A widget hidden on the
|
|
315
|
+
tree at all. A widget hidden on the _active_ screen (`display = False`) does
|
|
281
316
|
publish `hidden: true`. Textual owns the screen stack; reaching into it would
|
|
282
|
-
mean publishing widgets that no longer receive events.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
- **The probe does not report `frame-begin`** (probe capability).
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
317
|
+
mean publishing widgets that no longer receive events. The handshake records
|
|
318
|
+
this narrower limitation as the machine-readable
|
|
319
|
+
`inactive-screen-tree` degradation.
|
|
320
|
+
- **Ownership is process-local and one-shot.** The first application
|
|
321
|
+
interpreter atomically owns the generated bootstrap. Before the
|
|
322
|
+
application's first line, the probe captures its credentials privately and
|
|
323
|
+
removes the endpoint, token, owner marker, and bootstrap path from the
|
|
324
|
+
inherited environment. Python children and grandchildren therefore cannot
|
|
325
|
+
attach to the parent's semantic session. `poetry run` receives a one-hop
|
|
326
|
+
launcher marker so ownership is claimed by its target interpreter, not by
|
|
327
|
+
Poetry's own console process.
|
|
328
|
+
- **The probe does not report `frame-begin`** (probe capability). A frame is
|
|
329
|
+
accepted only when the same `_display` attempt successfully enqueued output
|
|
330
|
+
through a capability-checked non-headless built-in driver before
|
|
331
|
+
`post_display_hook`. The writer is preflighted before snapshot publication,
|
|
332
|
+
then its marker is appended to that same FIFO. A full or replaced writer
|
|
333
|
+
fails closed without blocking on queue capacity. This exposes no instant the
|
|
334
|
+
probe could honestly call the start of a frame. Consumers must not read a
|
|
335
|
+
missing `frame-begin` as "no frame in progress".
|
|
298
336
|
- **A `Static` subclass with a custom `render()` is named by its `content`**
|
|
299
337
|
(rule 2), which is the markup it was given rather than what it draws. Textual
|
|
300
338
|
renders to a strip of segments with no text handle the adapter can read.
|
|
301
339
|
|
|
340
|
+
## Application evidence providers
|
|
341
|
+
|
|
342
|
+
`termwright.evidence` exposes closed pointer, focus, scroll, paint,
|
|
343
|
+
terminal-input-mode, and action-strategy provider families.
|
|
344
|
+
`ApplicationFocusEvidenceProvider.observe` returns a semantic
|
|
345
|
+
recipient mapping or authoritative `None`; the wire keeps `focused` and `none`
|
|
346
|
+
distinct from an unnegotiated provider. Registration must happen before the
|
|
347
|
+
session freeze, and providers publish evidence/recipes only—physical input is
|
|
348
|
+
still sent through the PTY.
|
|
349
|
+
|
|
350
|
+
`ApplicationTerminalInputModeEvidenceProvider.observe` reports the production
|
|
351
|
+
parser's exact mouse and focus modes. It is authoritative evidence for hidden
|
|
352
|
+
ConPTY state, not a callback or a request to synthesize terminal modes.
|
|
353
|
+
|
|
302
354
|
## Conformance
|
|
303
355
|
|
|
304
356
|
`tests/` runs against `clients/test-vectors/`, which is generated from the
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "termwright"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Semantic side-channel client, automatic Textual probe, and annotation SDK for Termwright"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -22,8 +22,8 @@ classifiers = [
|
|
|
22
22
|
dependencies = []
|
|
23
23
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
|
-
textual = ["textual>=
|
|
26
|
-
dev = ["pytest>=7", "pytest-asyncio>=0.21", "textual>=
|
|
25
|
+
textual = ["textual>=8.2.8"]
|
|
26
|
+
dev = ["pytest>=7", "pytest-asyncio>=0.21", "textual>=8.2.8"]
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
Homepage = "https://github.com/gorce-ai/termwright"
|
|
@@ -20,13 +20,29 @@ from .client import (
|
|
|
20
20
|
CAPABILITIES_WITH_LOGS,
|
|
21
21
|
DEFAULT_CAPABILITIES,
|
|
22
22
|
ENV_ENDPOINT,
|
|
23
|
-
ENV_PROTOCOL,
|
|
24
23
|
ENV_TOKEN,
|
|
25
24
|
SemanticClient,
|
|
26
25
|
client_from_env,
|
|
27
26
|
)
|
|
28
27
|
from .debug import ENV_DEBUG, ENV_DEBUG_FILE, DebugLog, debug_path
|
|
29
28
|
from .errors import ProtocolViolation, TermwrightError
|
|
29
|
+
from .evidence import (
|
|
30
|
+
ApplicationActionStrategyProvider,
|
|
31
|
+
ApplicationFocusEvidenceProvider,
|
|
32
|
+
ApplicationScrollEvidenceProvider,
|
|
33
|
+
ApplicationPaintEvidenceProvider,
|
|
34
|
+
ApplicationTerminalInputModeEvidenceProvider,
|
|
35
|
+
EvidenceProviderLifecycleError,
|
|
36
|
+
EvidenceProviderRegistry,
|
|
37
|
+
EvidenceProviderRegistration,
|
|
38
|
+
EvidenceRevisionContext,
|
|
39
|
+
default_evidence_provider_registry,
|
|
40
|
+
register_action_strategy_provider,
|
|
41
|
+
register_focus_evidence_provider,
|
|
42
|
+
register_scroll_evidence_provider,
|
|
43
|
+
register_paint_evidence_provider,
|
|
44
|
+
register_terminal_input_mode_evidence_provider,
|
|
45
|
+
)
|
|
30
46
|
from .framing import FRAME_HEADER_BYTES, FrameDecoder, encode_frame, project_dto
|
|
31
47
|
from .limits import ABSOLUTE_LIMITS, DEFAULT_LIMITS, ProtocolLimits
|
|
32
48
|
from .logs import (
|
|
@@ -53,20 +69,30 @@ from .messages import (
|
|
|
53
69
|
parse_adapter_message,
|
|
54
70
|
parse_driver_message,
|
|
55
71
|
)
|
|
56
|
-
from .roles import
|
|
72
|
+
from .roles import (
|
|
73
|
+
ADAPTER_CAPABILITIES,
|
|
74
|
+
EVIDENCE_PROVIDER_CAPABILITIES,
|
|
75
|
+
SEMANTIC_ACTIONS,
|
|
76
|
+
SEMANTIC_ROLES,
|
|
77
|
+
)
|
|
57
78
|
from .tree import (
|
|
79
|
+
EvidenceProvenance,
|
|
58
80
|
CursorInfo,
|
|
59
81
|
NodeGeometryObservations,
|
|
60
82
|
Observation,
|
|
83
|
+
framework_evidence,
|
|
61
84
|
Rect,
|
|
62
85
|
SemanticNode,
|
|
63
86
|
SemanticSnapshot,
|
|
87
|
+
SemanticScrollState,
|
|
88
|
+
SemanticPaintedRegion,
|
|
64
89
|
SemanticState,
|
|
65
90
|
SemanticTextRange,
|
|
91
|
+
SemanticValueObservation,
|
|
66
92
|
)
|
|
67
|
-
from .validate import ValidationResult,
|
|
93
|
+
from .validate import ValidationResult, validate_snapshot
|
|
68
94
|
|
|
69
|
-
__version__ = "0.
|
|
95
|
+
__version__ = "0.3.0"
|
|
70
96
|
|
|
71
97
|
__all__ = [
|
|
72
98
|
"ABSOLUTE_LIMITS",
|
|
@@ -79,14 +105,23 @@ __all__ = [
|
|
|
79
105
|
"flatten_attrs",
|
|
80
106
|
"validate_log_record",
|
|
81
107
|
"ADAPTER_CAPABILITIES",
|
|
108
|
+
"ApplicationActionStrategyProvider",
|
|
109
|
+
"ApplicationFocusEvidenceProvider",
|
|
110
|
+
"ApplicationScrollEvidenceProvider",
|
|
111
|
+
"ApplicationPaintEvidenceProvider",
|
|
112
|
+
"ApplicationTerminalInputModeEvidenceProvider",
|
|
82
113
|
"DEFAULT_CAPABILITIES",
|
|
83
114
|
"DEFAULT_LIMITS",
|
|
84
115
|
"DebugLog",
|
|
85
116
|
"ENV_DEBUG",
|
|
86
117
|
"ENV_DEBUG_FILE",
|
|
87
118
|
"ENV_ENDPOINT",
|
|
88
|
-
"ENV_PROTOCOL",
|
|
89
119
|
"ENV_TOKEN",
|
|
120
|
+
"EvidenceProviderLifecycleError",
|
|
121
|
+
"EvidenceProviderRegistry",
|
|
122
|
+
"EvidenceProviderRegistration",
|
|
123
|
+
"EvidenceRevisionContext",
|
|
124
|
+
"EVIDENCE_PROVIDER_CAPABILITIES",
|
|
90
125
|
"FRAME_HEADER_BYTES",
|
|
91
126
|
"FrameDecoder",
|
|
92
127
|
"MARKER_MAC_BYTES",
|
|
@@ -99,27 +134,36 @@ __all__ = [
|
|
|
99
134
|
"ProtocolViolation",
|
|
100
135
|
"RenderMarker",
|
|
101
136
|
"CursorInfo",
|
|
137
|
+
"EvidenceProvenance",
|
|
102
138
|
"NodeGeometryObservations",
|
|
103
139
|
"Observation",
|
|
140
|
+
"framework_evidence",
|
|
104
141
|
"Rect",
|
|
105
142
|
"SEMANTIC_ACTIONS",
|
|
106
143
|
"SEMANTIC_ROLES",
|
|
107
144
|
"SemanticClient",
|
|
108
145
|
"SemanticNode",
|
|
109
146
|
"SemanticSnapshot",
|
|
147
|
+
"SemanticScrollState",
|
|
148
|
+
"SemanticPaintedRegion",
|
|
110
149
|
"SemanticState",
|
|
111
150
|
"SemanticTextRange",
|
|
151
|
+
"SemanticValueObservation",
|
|
112
152
|
"TermwrightError",
|
|
113
153
|
"ValidationResult",
|
|
114
154
|
"client_from_env",
|
|
115
155
|
"debug_path",
|
|
156
|
+
"default_evidence_provider_registry",
|
|
116
157
|
"encode_frame",
|
|
117
158
|
"encode_marker",
|
|
118
159
|
"parse_adapter_message",
|
|
119
160
|
"parse_driver_message",
|
|
120
161
|
"project_dto",
|
|
121
|
-
"
|
|
162
|
+
"register_action_strategy_provider",
|
|
163
|
+
"register_focus_evidence_provider",
|
|
164
|
+
"register_scroll_evidence_provider",
|
|
165
|
+
"register_paint_evidence_provider",
|
|
166
|
+
"register_terminal_input_mode_evidence_provider",
|
|
122
167
|
"validate_snapshot",
|
|
123
|
-
"validate_tree_delta",
|
|
124
168
|
"verify_marker_payload",
|
|
125
169
|
]
|