@termwright/probe-tview 0.2.0 → 0.3.0

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.
package/README.md CHANGED
@@ -1,116 +1,126 @@
1
1
  # @termwright/probe-tview
2
2
 
3
- Semantics from a [tview](https://github.com/rivo/tview) application that
4
- **imports nothing of ours**.
3
+ Capability-driven semantic probing for
4
+ [tview](https://github.com/rivo/tview), without editing or copying upstream
5
+ source files.
5
6
 
6
- The application is built through an ephemeral Go workspace that redirects
7
- `github.com/rivo/tview` to an instrumented copy. Nothing is written into the
8
- project: its `go.mod`, its `go.sum` and any `go.work` of its own come out of the
9
- build byte-identical.
7
+ Termwright controls the Go build and uses the official `-toolexec` hook to add
8
+ owned compilation units to the tview package and, on Windows, tcell. The
9
+ compiler checks every private-field assumption. A new upstream version is
10
+ accepted by compilation plus behavioral conformance, not by matching a source
11
+ digest.
10
12
 
11
- ## Install
13
+ ## Install and use
12
14
 
13
15
  ```sh
14
16
  npm install --save-dev @termwright/probe-tview
15
17
  ```
16
18
 
17
- Requires the Go toolchain and `git` (which the toolchain needs anyway). Node >= 22.
19
+ The application opts in with one line immediately before `Run`:
18
20
 
19
- ## Usage
20
-
21
- One call prepares the build; the launcher owns everything else.
22
-
23
- ```ts
24
- import {prepareInstrumentedBuild} from '@termwright/probe-tview';
25
-
26
- const build = await prepareInstrumentedBuild({moduleDir: 'path/to/app'});
27
-
28
- // build.env carries GOWORK; the project's own files are untouched.
29
- await execFile('go', ['build', '-o', 'app-binary', '.'], {cwd: 'path/to/app', env: build.env});
21
+ ```go
22
+ import "github.com/gorce-ai/termwright/clients/go/tviewprobe"
30
23
 
31
- await launchTerminal({command: ['./app-binary']});
24
+ app.SetRoot(root, true)
25
+ defer tviewprobe.Attach(app, root)()
26
+ if err := app.Run(); err != nil {
27
+ panic(err)
28
+ }
32
29
  ```
33
30
 
34
- The framework version is read from the module, the instrumented copy is cached,
35
- and a second call with the same inputs reuses it.
36
-
37
- ## What it gives you
38
-
39
- Being inside the package is the point. A `tview.Grid` exposes no accessor for
40
- its children at all, so an out-of-package adapter has to be handed a callback;
41
- here it is a field read that also carries whether the item was drawn. A widget
42
- on a `Pages` page that is not shown reports as **hidden** rather than going
43
- missing, so a test can tell "not on screen" from "not there".
31
+ Prepare the controlled build and pass the returned Go arguments to the build
32
+ or test command:
44
33
 
45
- Identity is the primitive's pointer: tview retains its widget tree, so the same
46
- `*Button` is the same button across frames. The handshake therefore reports
47
- `identityKind: 'stable'` and only the probe capabilities it earns:
48
- `stable-identity` and `annotations`. Its `frameworkVersion` is the exact
49
- version selected by the verified patch set, not the Go runtime version.
50
-
51
- ## Describing what the probe cannot see
34
+ ```ts
35
+ import { execFile } from 'node:child_process';
36
+ import { prepareInstrumentedBuild } from '@termwright/probe-tview';
37
+
38
+ const build = await prepareInstrumentedBuild({ moduleDir: 'path/to/app' });
39
+ await execFile('go', ['build', ...build.goArgs, '-o', 'app-binary', '.'], {
40
+ cwd: build.moduleDir,
41
+ env: build.env,
42
+ });
43
+ ```
52
44
 
53
- Zero-config means the probe reads facts. It cannot read intent — which button
54
- is the destructive one, which list is the inbox, what "overdue" means here. For
55
- that, and only for that, an application may import
56
- `github.com/gorce-ai/termwright/clients/go/annotate`:
45
+ The mechanism works with ordinary module-cache dependencies, local
46
+ replacements, workspaces and vendored applications. It does not modify the
47
+ application's `go.mod`, `go.sum`, `go.work`, vendor tree, or upstream module
48
+ bytes.
49
+
50
+ ## Intervention tiers
51
+
52
+ - T0 public tview/tcell APIs provide most widget hierarchy, state, geometry
53
+ and the `Screen.Show` output boundary.
54
+ - A T1 add-only tview unit exposes sealed root, Grid, Modal, DropDown and other
55
+ rendered state. Private-field drift is a compile error.
56
+ - On Windows a second T1 tcell unit writes the authenticated frame marker
57
+ through the console handle used by `Show`. Unix uses the public `Tty()`
58
+ writer.
59
+
60
+ The application screen is decorated before `Run`, and its existing public
61
+ before/after-draw hooks are chained. Those hooks arm exactly tview's final
62
+ `Show`; intermediate `Show` calls made by custom primitives or application
63
+ hooks still flush normally but cannot publish a partial semantic frame. After
64
+ the armed underlying `Show` completes, the probe reads the current
65
+ `Application.root`, admits one complete snapshot to the bounded publication
66
+ queue and writes its marker through the same screen sink. This also covers
67
+ tview's before-draw short-circuit and roots changed through `SetRoot`. It never
68
+ calls `Show` itself, never holds a process-global render mutex and never
69
+ performs socket I/O on the render goroutine. Runtime displacement of either
70
+ composed hook fails semantics closed.
71
+
72
+ A queue refusal, re-entrant `Show`, missing writer, partial marker, worker
73
+ failure or missing injected unit closes semantic publication with a typed
74
+ diagnostic. It is never hidden by a timeout increase, retry, quiet window or
75
+ stale-tree fallback.
76
+
77
+ ## Optional semantics
78
+
79
+ The automatic tree does not require wrappers or per-widget annotations. An
80
+ application may add intent with the framework-neutral Go SDK:
57
81
 
58
82
  ```go
59
- import (
60
- "github.com/gorce-ai/termwright/clients/go/annotate"
61
- "github.com/gorce-ai/termwright/clients/go/protocol"
62
- )
63
-
64
- annotate.Tag(label, annotate.Semantics{Key: "unread-label"})
65
83
  annotate.Tag(unreadBadge, annotate.Semantics{
66
- Role: "status", Name: "Unread messages", TestID: "unread-badge",
67
- Actions: []protocol.Action{protocol.ActionFocus, protocol.ActionActivate},
68
- LabelledBy: []annotate.SemanticKey{"unread-label"},
84
+ Key: "unread-badge",
85
+ Role: "status",
86
+ Name: "Unread messages",
87
+ TestID: "unread-badge",
88
+ Actions: []protocol.Action{protocol.ActionFocus},
69
89
  })
70
90
  ```
71
91
 
72
- The probe merges this with what it observed: the wording is the author's, the
73
- bounds and the focus stay the probe's. `Semantics` has no field for bounds,
74
- focus, visibility, value, rendered text or framework state not by convention
75
- but structurally, so an annotation cannot go stale against the screen. Actions
76
- come from the protocol's closed descriptive set and never install callbacks.
77
- Tagging retains nothing; the entry is released with the widget.
78
-
79
- `LabelledBy` and `DescribedBy` use framework-neutral `SemanticKey` strings, so
80
- one annotation does not retain its targets. The probe resolves them after the
81
- whole tree has been walked. Missing or duplicate keys are omitted instead of
82
- becoming dangling or arbitrary node references. Pointer identity remains the
83
- stable node id; a key is only the relation target for tview. Primary framework
84
- provenance is reported in `p`, with recognizer and annotation exceptions in
85
- `px`.
86
-
87
- This is the one import that makes an application no longer zero-config, which
88
- is why it is optional and why the two example fixtures in this package are kept
89
- apart.
90
-
91
- ## Dormant without instrumentation
92
-
93
- Without `TERMWRIGHT_ENDPOINT` and `TERMWRIGHT_TOKEN` the instrumented copy opens
94
- no socket, writes no marker and renders exactly what upstream renders. That is
95
- measured, not asserted: the test suite builds the same application twice, once
96
- against untouched tview and once against the copy, and requires the two screens
97
- to be byte-identical.
98
-
99
- ## When it refuses
100
-
101
- - `-mod=vendor` in `GOFLAGS` is reported by name rather than overridden;
102
- workspace mode is incompatible with it, and overriding would change what
103
- compiles.
104
- - A framework version with no patch set is named as such — "this is not
105
- tview v0.42.0" — instead of failing somewhere inside a diff.
92
+ Annotations can describe roles, names, ids, relationships and closed actions.
93
+ They cannot override geometry, focus, visibility, value, rendered text or
94
+ framework state. They are held in a side table and do not require replacing
95
+ tview constructors or fluent widget types.
96
+
97
+ ## Dormant and unsupported modes
98
+
99
+ Without both `TERMWRIGHT_ENDPOINT` and `TERMWRIGHT_TOKEN`, `Attach` returns
100
+ before creating a client, channel, goroutine, socket or framework hook. The
101
+ instrumented and ordinary builds are required to render byte-identical output
102
+ in this mode.
103
+
104
+ A prebuilt binary cannot receive T1 units and therefore runs as explicitly
105
+ reported raw PTY. A controlled build whose capability unit fails to compile is
106
+ rejected loudly; Termwright does not create an exact-version patch profile or
107
+ silently downgrade its semantic tree.
108
+
109
+ Known declared limitations are clipped geometry for some tview containers and
110
+ enumeration of application-defined custom container children. Such nodes stay
111
+ visible as generic/opaque nodes and the reduced capability is present in run
112
+ metadata.
106
113
 
107
114
  ## Development
108
115
 
109
116
  ```sh
110
- pnpm build && pnpm typecheck && pnpm test
117
+ pnpm build
118
+ pnpm typecheck
119
+ pnpm test
120
+ cd ../../clients/go && go test -race -count=1 ./...
111
121
  ```
112
122
 
113
- The suites that need Go or a pseudo-terminal skip themselves where either is
114
- missing, and say so in a test named for it. `TERMWRIGHT_SKIP_GO=1` and
115
- `TERMWRIGHT_SKIP_PTY=1` force it. Implementation notes, including the traps
116
- that cost time, are in [`NOTES.md`](NOTES.md).
123
+ The native Linux and Windows certification rows require the relevant
124
+ toolchain and real PTY/ConPTY rather than treating their absence as a green
125
+ adapter result. Implementation invariants are recorded in
126
+ [`NOTES.md`](NOTES.md).
@@ -0,0 +1,80 @@
1
+ //go:build windows
2
+
3
+ package tcell
4
+
5
+ // This add-only companion joins the tcell package only for a Windows-targeted
6
+ // Termwright build. Windows' cScreen has no Tty(), but it owns the console
7
+ // handle that Show uses synchronously. Exposing one narrow marker operation
8
+ // preserves the same-handle commit boundary without modifying tcell's renderer.
9
+
10
+ import (
11
+ "errors"
12
+ "io"
13
+ "syscall"
14
+ "unicode/utf16"
15
+ "unsafe"
16
+ )
17
+
18
+ var (
19
+ termwrightKernel32 = syscall.NewLazyDLL("kernel32.dll")
20
+ termwrightGetConsoleMode = termwrightKernel32.NewProc("GetConsoleMode")
21
+ termwrightSetConsoleMode = termwrightKernel32.NewProc("SetConsoleMode")
22
+ )
23
+
24
+ const termwrightMarkerOutputMode = uint32(0x0001 | 0x0004) // processed output + VT processing
25
+
26
+ // TermwrightWriteMarker is defined on baseScreen because NewConsoleScreen
27
+ // returns *baseScreen, whose embedded screenImpl interface otherwise hides
28
+ // methods implemented only by the concrete *cScreen.
29
+ func (b *baseScreen) TermwrightWriteMarker(marker string) (resultErr error) {
30
+ s, ok := b.screenImpl.(*cScreen)
31
+ if !ok {
32
+ return errors.New("tcell: screen is not the Windows console implementation")
33
+ }
34
+ s.Lock()
35
+ defer s.Unlock()
36
+ if s.fini {
37
+ return errors.New("tcell: screen is finalized")
38
+ }
39
+ encoded := utf16.Encode([]rune(marker))
40
+ if len(encoded) == 0 {
41
+ return nil
42
+ }
43
+ var originalMode uint32
44
+ if ok, _, err := termwrightGetConsoleMode.Call(uintptr(s.out), uintptr(unsafe.Pointer(&originalMode))); ok == 0 {
45
+ return err
46
+ }
47
+ // Inspect the real writer capability instead of tcell's historical `vten`
48
+ // implementation field. tcell <= 2.11 used that field to select a legacy
49
+ // renderer, while >= 2.12 requires VT during screen initialization and no
50
+ // longer declares it. Vendored ordered-passthrough ConPTY serializes both
51
+ // legacy Console API output and VT on this handle, so a legacy frame can be
52
+ // followed by a temporarily VT-enabled marker without weakening ordering.
53
+ if originalMode&termwrightMarkerOutputMode != termwrightMarkerOutputMode {
54
+ if ok, _, err := termwrightSetConsoleMode.Call(uintptr(s.out), uintptr(originalMode|termwrightMarkerOutputMode)); ok == 0 {
55
+ return err
56
+ }
57
+ defer func() {
58
+ restored, _, restoreErr := termwrightSetConsoleMode.Call(uintptr(s.out), uintptr(originalMode))
59
+ if restored == 0 && resultErr == nil {
60
+ resultErr = restoreErr
61
+ }
62
+ }()
63
+ var activeMode uint32
64
+ if ok, _, err := termwrightGetConsoleMode.Call(uintptr(s.out), uintptr(unsafe.Pointer(&activeMode))); ok == 0 {
65
+ return err
66
+ }
67
+ if activeMode&termwrightMarkerOutputMode != termwrightMarkerOutputMode {
68
+ return errors.New("tcell: active Windows console output could not enable VT processing")
69
+ }
70
+ }
71
+ var written uint32
72
+ writeErr := syscall.WriteConsole(s.out, &encoded[0], uint32(len(encoded)), &written, nil)
73
+ if writeErr != nil {
74
+ return writeErr
75
+ }
76
+ if written != uint32(len(encoded)) {
77
+ return io.ErrShortWrite
78
+ }
79
+ return nil
80
+ }